iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Modern Web

一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年系列 第 19

Day 19|一個 182 行的檔案,決定了全站每一次請求的命運

  • 分享至 

  • xImage
  •  

模組四|核心系統改寫(Day 17–21)

舊系統的請求封裝是一個檔案、182 行、兩個攔截器、16 個 if 分支。

它負責的事情包括:帶 token、設定語系、決定逾時、判斷狀態碼、顯示錯誤訊息、控制載入動畫、還有決定「這支 API 要打哪一套後端」。

新系統把它拆成 七個檔案。

今天講為什麼要拆、拆成什麼,以及一個我覺得比「功能完整」更重要的判準。

先看舊的那個檔案做了什麼

它把 HTTP 500 當成成功

第一個讓我停下來的是這一行:

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

翻成白話:

  1. 後端回傳的 code 如果是 0,把它改成 200
  2. 然後自己加一個 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 講的「技術債往下游流」的另一個實例——前端拆得再漂亮,也改不了上游的約定。

帶走什麼

  1. 封裝的品質,看它讓你「不用它」的時候有多容易,不是看它處理了多少情況。
  2. 在攔截器裡改寫後端回傳的資料,會讓兩份文件對不上。 要改就要在 API 文件層級講清楚,不要偷偷做。
  3. 「每加一個例外就改那個檔案」是設計問題,不是紀律問題。 你要提供插座,而不是要求大家不要敲牆。
  4. 會觸發全域副作用的錯誤碼(例如自動登出),一定要寫進文件。 拆不掉沒關係,但要讓下一個人查得到。
  5. 用錯誤訊息的文字內容當判斷條件,永遠是個定時炸彈。 不管那段文字是中文還是英文。

明天 Day 20 講表格。Day 2 量到舊系統有 3,354 個手寫的表格欄位,而新系統把它變成了資料——但我也會給一個很反直覺的數字:框架附了一整套表格機制,我們的業務頁面用了幾次?


上一篇
Day 18|同一件事有兩套做法,比沒做還貴
系列文
一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言