模組四|核心系統改寫(Day 17–21)
舊系統的請求封裝是一個檔案、182 行、兩個攔截器、16 個 if 分支。
它負責的事情包括:帶 token、設定語系、決定逾時、判斷狀態碼、顯示錯誤訊息、控制載入動畫、還有決定「這支 API 要打哪一套後端」。
新系統把它拆成 七個檔案。
今天講為什麼要拆、拆成什麼,以及一個我覺得比「功能完整」更重要的判準。
第一個讓我停下來的是這一行:
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500
}
這行的意思是:HTTP 狀態碼 200 到 500 之間,全部視為「請求成功」。
包括 400(參數錯誤)、401(沒登入)、403(沒權限)、500(伺服器爆炸),通通不會走進錯誤處理,全部走成功分支。
為什麼要這樣寫?因為後端把業務錯誤訊息放在回應內容裡,而如果讓請求套件直接判定失敗,前端就拿不到那段訊息了。
所以這是一個有理由的決定。但它的代價是:「伺服器掛了」和「你填的資料不對」,從此在程式碼裡長得一模一樣,兩者都是「成功」,只是內容不同。
判斷它們的責任,就落到後面那一長串 if 上。

第二個更值得看:
res.data.code = res.data.code === 0 ? 200 : res.data.code
res.data.success = res.data.code === 0 || res.data.code === 200
翻成白話:
code 如果是 0,把它改成 200
success 欄位也就是說:前端各處拿到的 code,已經不是後端原本給的那個了。
**這種改寫的問題不是它做錯了什麼,是它讓兩份文件對不上。**後端的 API 文件說「成功回傳 code: 0」,前端工程師實際 console.log 出來看到 200,然後困惑三十分鐘。
而且這個改寫藏在攔截器裡(一個大部分人不會去讀的檔案)。
| 檔案 | 職責 |
|---|---|
| 核心類別 | 只管發請求與生命週期 |
| 轉換層 | 請求前 / 回應後的資料整形 |
| 取消 | 重複請求自動取消 |
| 重試 | 可設定的重試策略 |
| 狀態碼處理 | 一個狀態碼一個分支 |
| 輔助工具 | 共用的小函式 |
| 組裝入口 | 把上面這些接起來,輸出統一的請求方法 |
最後那個「組裝入口」是使用者唯一會碰到的東西。你在頁面裡只會看到:
export function getOrderList(params: OrderListParams) {
return defHttp.get<OrderListResult>({ url: Api.GetOrderList, params })
}
一行。背後那六個檔案你完全不需要知道。
外觀模式(Facade Pattern),名字取得很直白:「facade」就是建築物的正面。
你用 App 叫一台車,只按了一個按鈕。
而背後跑起來的是:媒合附近的司機、規劃路線、估算車資、綁定付款、還有失敗時重新派單。
你不需要認識這五個系統,也不需要知道它們的順序。
但如果你有特殊需求(「我要指定不走高速公路」),那個按鈕旁邊也有地方讓你設定。
defHttp.get() 就是那個按鈕。而背後的取消、重試、狀態碼處理,是那五個系統。

拆成七個檔案,比較好讀,這很直觀。
但我覺得真正的價值在別的地方:每個角色都可以單獨關掉。
實際的場景:
某支 API 需要不要自動取消重複請求(因為它本來就會被連續呼叫)。
某支匯出 API 需要不要走統一的錯誤提示(因為它要自己處理下載失敗)。
某支輪詢 API 需要不要顯示載入動畫(不然畫面會一直閃)。
在舊系統,這三種需求你都得改那個 182 行的檔案:加一個判斷、加一個例外清單、或是在呼叫端傳一個特殊參數然後在攔截器裡撈出來。
每次例外,那個檔案就長胖一點。 16 個 if 就是這樣長出來的。
新系統的做法是:這些行為都是可注入的 hook,總共五個切入點(請求前、回應後、錯誤時、以及兩個原生攔截器)。要加行為就注入一個,要關掉就傳個選項。
開放封閉原則(OCP, Open-Closed Principle):對擴充開放,對修改封閉。
你家裝潢好了,之後要加一台除濕機。
好的設計:牆上有插座。插上去就好,牆不用動。
壞的設計:每加一台電器,就要把牆敲開重新拉一次電線。
而每敲一次牆,旁邊的線都有可能被弄斷。
那 16 個 if 就是「每次都敲牆」。而每敲一次,就有可能弄壞已經在跑的其他 API——這正是為什麼沒有人敢動那個檔案。
所以我後來的判準變成這樣:
封裝的品質,不看它處理了多少情況,看它讓你「不用它」的時候有多容易。
一個什麼都幫你做、但無法退出的封裝,會變成第二個那 182 行的檔案。
拆得再乾淨,還是會留下坑。而我們的坑就寫在錯誤處理裡:
const hasError = code === ResultEnum.ERROR // ERROR = -1
if (hasError) {
userStore.logout(false) // ← 直接登出
}
任何一支 API 回傳 code: -1,使用者就會被登出。
這個設計的原意大概是「-1 代表登入狀態異常」。但實際上 -1 是一個很容易被當成通用錯誤碼的值,只要後端某個人在某支 API 用它表示「參數錯誤」,使用者就會在填錯表單的時候被踢出系統。
這個地雷我們沒有拆,但做了一件事:把它寫進專案守則的地雷清單。新人接手時會看到「注意:這個錯誤碼會觸發全域登出」。
我覺得這是務實的處理。改它要協調所有後端 API 的錯誤碼定義,那是跨團隊的事;而寫下來,至少讓下一個踩到的人知道要往哪裡查。
同一個檔案裡有這樣的判斷:
if (code === 500 && msg.includes('does not have permission')) {
createMessage.error('帳號沒有權限')
return
}
用比對錯誤訊息的英文字串,來判斷這是不是權限問題。
後端哪天把那句訊息改掉,這個判斷就靜靜失效,不會報錯,只是提示訊息會變回原本那句英文。
Day 9 講過舊系統拿「翻譯後的中文」當判斷條件。這是同一個問題,只是換成英文,而且發生在新系統。
重寫不會自動讓你不犯舊的錯。 同一類問題會用新的樣子重新出現,除非有人認得出那個形狀。
一、七個檔案的協作順序變成新的知識。 「轉換層在攔截器之前還是之後」這種問題,舊系統從上到下讀就知道,現在要理解組裝入口。我們用了大量註解補這件事,但那不是解法,只是緩解。
二、可注入的 hook 也可能被濫用。 現在加行為很便宜,而「便宜」正是 Day 4 講的那個問題(沒有摩擦的捷徑會被累積)。目前 hook 數量還好,但沒有機制限制它成長。
三、我們繼承了舊系統的「500 也算成功」。 因為後端的錯誤訊息還是放在回應內容裡,前端不能讓請求直接失敗。這是 Day 13 講的「技術債往下游流」的另一個實例——前端拆得再漂亮,也改不了上游的約定。
明天 Day 20 講表格。Day 2 量到舊系統有 3,354 個手寫的表格欄位,而新系統把它變成了資料——但我也會給一個很反直覺的數字:框架附了一整套表格機制,我們的業務頁面用了幾次?