前言
在面向全球用戶的平台系統中,API 被濫用幾乎是必然會遇到的問題——無論是攻擊者暴力嘗試登入、
爬蟲高頻抓取資料,還是用戶無意間重複提交表單。
本文分享一個我們實際在生產環境中使用的 Redis-based API Rate Limiting 架構,
涵蓋兩種策略:滑動視窗(Sliding Window) 和 滑動視窗 + 封鎖鎖定(Sliding Window + Block Lock),
並支援以 Session ID、IP、Member Code 等多種身份維度進行限流。
大多數 Vue 專案如果需要 Server Side Rendering,通常會選擇 Nuxt.js 作為全端框架。但我們的情境不一樣——後端是既有的 ASP.NET MVC 應用,負責身份驗證、Session 管理、多語系路由等,不可能整個換成 Node.js。
因此我們設計了一套特殊的整合架構:ASP.NET 只負責輸出 HTML Shell(_Layout.cshtml),Vue 3 SPA 由 Rsbuild 獨立建置,兩者透過一份 manifest.json 在 Debug 和 Release 模式下各自連到對應的前端資源。
這套架構讓前後端可以獨立演進——前端換打包工具(Webpack → Rsbuild)、改 chunk 策略、升級 Vue 版本,都不需要動後端程式碼。
突然有感而發 想寫下部門之前用了很多年的 GitFlow 以及它們會遇到的問題,以及他們解決什麼問題
最主要觀念是
正常功能開發以 main branch 為主體(所有的分支都由這裡出發) ,分別 Merge 到 QAT 避免同步大家開發多個功能,
有互相的影響以及將不必要的程式碼帶到正式環境
緊急修正的情況可以直接根據 main branch 拉出分支修正 避免其他影響 然後部屬到 Pre-prod 測試
首先我們先定義好 Git Branch 以及對應的環境
我們這裡以 main 當作我們的主要分支 所有 branch 都是以 main 為主
| Branch | Env | Branch Remark |
|---|---|---|
| qat | QAT | QA Testing |
| main | Pre-Prod, Prod | 正式環境 |
| feature/xxx | 開發功能分支 | |
| hotfix/xxx | 緊急功能修正 | |
| fix/xxx | 排程功能部屬 |
我們環境實際上重要的有三種環境
| Environment | remark |
|---|---|
| QAT | 給 QA 做測試的環境(所有的 Feature 都會先進到 QAT 給 QA 測試 測試完成的才會 Deploy to Pre-prod) |
| Pre-prod | 上線前給 QA 做最後測試的環境 |
| Prod | 正式環境 |
那我們可以依照幾種情境講述會遇到的問題
開發功能 照順序進到 Prod
緊急修正問題 直接推到 Prod
開發功能 時程需要拖很長(歷時一兩個月)
以下是大致上的 Gitflow 流程圖
我們會一個一個介紹
首先我們先介紹我們有幾個 branch 以及大概的功能敘述
branch 流程順序如下:
如圖片黑框的部分 這樣有可能會發生衝突
所以這是第一個衝突點,Routing 檔案衝突
接著測試到一個段落 QA 也認為 1 跟 2. 的 branch 差不多可以進 Prod 了,我們會透過剛剛提到的 5.
fix/Week1 將大家要進的程式碼統一 Merge 到此 branch 然後 merge to main ,部屬到 Pre-Prod 環境給 QA 做最後檢查
branch 流程順序如下:
這樣好處是 Merge prod 只會有一個點 到時候要退版也會好退
如圖片黑框的部分 fix/week1 是從上方這個 main branch 開的 然後 merge 進去這樣有可能會發生衝突
這樣就完成照順序進到 Prod 的功能了
想當然 我們在開發的過程中 有可能緊急有發生 Prod 有問題 需要緊急修正的問題
branch:
hotfix/RegisterIssue : 註冊問題修正的 branch
branch 流程順序如下:
為什麼要做 3 是因為要避免 qat 以及 main branch 有一些落差,理論上 qat 會比較新 但是功能應該要跟 main 相同
因為要先知道 如果一個 feature branch 存活的週期大於一個月以上一定要定時做一件事情
**定時 sync main to feature branch **
其餘流程要照著正常 feature 推動 這樣才不會有到時候要 Merge main 一堆衝突的困擾
不管使用什麼 gitflow 都是要避免衝突,盡量要跟衝突的檔案的作者溝通 看上線時程,以及 sync main branch.
如果真有遇到 Merge 到錯誤的 branch (最好是先 force reset 到錯誤的地方開始解決 千萬不要再修改檔案避免造成更多錯誤)
先說明這個問題主要是 通常在滾動頁面的時候 會有兩個需求
往下滑動就要隱藏 Header
開始上滑就要顯示 Header
但是問題來了,iOS 有一個橡皮筋的功能叫做 Elastic Scroll ,他會造成你滑動到最上方以後 會再反彈
如果沒有寫好的話,明明就滑動到最上方還是隱藏 Header (明明 Chrome 跟其他瀏覽器不會)….
為了兼容這個問題 我們以 Vue Use 為例子做一個範例
裡面其實只有 directions 跟 y 最重要
因為我們判斷它是否為回彈事件為 判斷他 nextY ===0 代表他就在最上方
不用管他是什麼事件,這樣就可以解決了
這裡說明一下 為什麼使用這方法而不用 touchmove preventDefault 是因為 有可能舊版本的 safari 沒有這個 event…
也不想要考慮太多相容性 就先保留這個行為 但是讓結果相同
1 | import { useScroll } from '@vueuse/core'; |
這個問題主要要考慮的是 iOS 以及 safari 版本不同 有可能有不同的事件 所以最好是找一個比較符合的解決方法
touchmove prevent
使用 debounce 限制 scroll top 抓取事件 delay 50 ms 之類 然後判斷高度 可以使用 lodash.throttle 套件
此篇文章是介紹 SEO Prerender,可能很多人會大概會想說聽過 Server Side Render 以及 Client Side Render ,
Prerender 簡單來說就是預先將網頁 snap shot 起來,等到搜尋引擎來的時候 就不需要當場在渲染頁面
以下是這三種方法的介紹以及優缺點
我這裡總結一下
就是 Prerender 以及 Server Side Render 都是後端產生的 html 差異在
Prerender 是預先拿取 snapshot 的檔案回來,效能比較好但是比較不即時
Server Side Render 是當下的 Request 才開始渲染網頁,比較消耗效能 但是比較即時
所以就看大家怎麼選擇
我們這裡會稍微介紹一下 怎麼使用 Asp.net 實作一個 Prerender 檢查的 Dll ,
首先我們要先知道在 Request 的什麼階段比較適合將 Snap Shot 的檔案回傳給搜尋引擎
那以下是我們的介紹
我們先來介紹當一個 Request 進到 IIS Server 中的生命週期
簡單來說 IIS 上面有一些預先檢查 EX: URL Rerewrite . 當這些檢查如果都不符合的時候
就會開始進到 Asp.net 生命週期,所以在做 Prerender 的檢查應該使用 Application Module
在 Request 還沒有進到 Asp.net 驗證以及 Routing 之前判斷,如果根據某一些 User Agent 就應該將預設好的 html
回給 search engine
sequenceDiagram
User->> IIS: Request
alt Rewrite not Match
IIS->> Application Module: Request
else Rewrite Match
IIS->> User: Return the destination
end
alt Check User Agent
Application Module->> Route: Enter into Routing
else User Agent Match with Bot And Static File Exist
Application Module->> User: Return the static file To User
end
這是 ASP.NET 應用程式生命週期流程圖 這裡不多加詳述
剛剛說的檢查是否為 Search Engine 應該在 BeginRequest 就檢查,避免做太多不必要的檢查之類
因為看到 MapRequestHandler 代表就是已經進到 Route Config 裡面了
graph LR
A(BeginRequest) --> B(AuthenticateRequest)
B --> C(AuthorizeRequest)
C --> D(ResolveRequestCache)
D --> E(MapRequestHandler)
E --> F(.....)
1 |
|
上述的例子是直接在 global.asax 裡面直接判斷,也可以使用 modules 來達到這件事情。
下面是常見的搜尋引擎的 UserAgent
| 搜索引擎 | User-Agent |
|---|---|
| Googlebot/2.1 (+http://www.google.com/bot.html) | |
| Bing | Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) |
| Baidu | Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html) |
| Yahoo | Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp) |
| Yandex | Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots) |
| DuckDuckGo | DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html) |
| Sogou | Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07) |
使用 Prerender 好處在於說可以自己決定要回傳什麼時候的 SnapShot 檔案給搜尋引擎使用,
但是反之也要注意更新檔案的頻率。
在 Composition Api 使用 Pinia 做狀態管理的時候,官方網站上面有寫說
寫一陣子後才想到有遇過幾個問題
怎麼樣算是沒有 refs or reactive , 並且怎樣寫才是正確的呢?
先看一下我們定義的 State
以 number , array ,object 為例子來講解一下我們使用的方法
1 | import { defineStore } from 'pinia'; |
Sample
我們用三個層面來探討
1 | import { useCounter } from './stores/counter'; |
1 | const { |
1 | const { n, numbers, data, double } = storeToRefs(counter); |
這個最不需要討論 因為建立實體後 可以調用所有的 method 屬性跟 Getter 都會是雙向綁定的
只是缺點是 每次使用都要 counter.xxx counter.xxx 有點麻煩
1 | import { useCounter } from './stores/counter'; |
因為在 ES6 上面建立的解構的方法 所以一開始大家都會想要用這種方法片段的取出值
1 | const { |
大家可以看到 標黑色的部分 因為單純的 number or string 這些都是 call by value 所以透過解構的方法並不會雙向綁定
但是物件跟陣列是會的
為了避免混用 我們還是少用這種方法
這是官方推薦的方法
1 | const { n, numbers, data, double } = storeToRefs(counter); |
這樣就可以把 n 直接做雙向綁定 只要操作的時候使用 n.value ++就可以
但是要注意一件事情. storeToRefs 出來的值 如果再將其中的子屬性 assign 出去也不會有 two way binding 的效果
1 | const userName = data.value.user; |
那如果我們還是希望可以有子屬性有 two way binding 的效果可以參考以下範例
ex: 使用者的資料
1 | data: { |
因為有可能不是每次都要把所有使用者資料撈出來,會遇到一些寫法問題 請參考範例
Pinia 存取的結構:
這裡有發現有幾種寫法就算 Pinia 狀態有改變,畫面上面的值沒有跟著連動
Not Working Sample
rootStore.data.Status
直接把 rootStore 的屬性取出是無法做 reactive
data.value.Profile.Phone
透過 storeToRefs 拉出的屬性值注意只有屬性值不會 reactive
rootStore.phoneNumber
直接把 rootStore 的 Getter 取出是無法做 reactive
後面有回頭去看 Vue 3 reactive 用法
其實 reactive 是使用 proxy 做的
其實他文件上面有提到兩點很重要的
以下是 working 的 sample
1 | const { data, phoneNumber } = storeToRefs(rootStore); |
為什麼要特地說明這個呢? 因為在 vue 3 中 會很常使用 composition api, 並且把一些共用的邏輯抽成 composeble 的 code
相對 Pinia 也是 可能會需要單獨取某一些值出來,所以特地記錄這塊