# 前端如何管理 API (上)

管理 API 可以說是前後端一直以來的難題之一。

後端要實作正確的 API,並且針對不同的格式做處理,同時也要防範 CORS 以及 DDoS 等等;而前端管理 API,要如何管理參數的正確性,endpoint 如何有效地整合在一起,如果請求有錯誤,又要怎麼處理,才不會讓整個 UI 機制都垮掉,都是與後端串接 API 時需要考慮的地方。

在實作 API 的時候,有幾件事情要注意,不光是只是 call API 而已:

  1. 如果 API 過久都沒有回應,或是後端不小心寫了一個很爛的 SQL query 導致請求時間過長。這時應該怎麼辦?如果沒有考量到這一步,你的使用者可能看著 loading UI 很久都沒有回應,如果連 loading UI 都沒有實作的話就更糟糕了
  2. 如果 API 回傳錯誤怎麼辦?400, 404, 500 又代表什麼意思呢?
  3. 使用者開心地寫完一篇文章,卻因為網路不穩定,導致文章無法成功送出,而且要重打一遍,應該如何處理?
  4. 使用者不小心離線了,如何避免無可挽回的操作?使用者從離線回到連線,如何讓 API 在連線時繼續運作?

以下我們來探討這些常見的問題。

# 1. API 超時、沒有回應

這個情況可以透過 1. nginx 設定請求時間 2. 應用層將每個請求都建立超時時間。

如果很不幸與你合作的後端沒有實作這些功能,剛好 SQL 又寫得很爛的話,你可能需要處理 API 超時的狀況,以免讓使用者等太久。

# AJAX

在 ajax 當中,我們可以用 xhr.abort() 來取消一個請求,最簡單的方法就是 setTimeout(() => xhr.abort(), REQUEST_TIMEOUT) 放棄請求。另外,也需要在 UI 上提示使用者,這次的請求不成功,並顯示相關的原因。

# Fetch

fetch 在 chrome69 中推出了 AbortController,詳細可以參考我之前寫的文章 (opens new window)

以前 fetch 無法直接取消請求,不過現在有了 abortController,就可以取消請求了。

在實作這類型的功能時,你或許會需要提示使用者,現在這個請求超時,並提供使用者幾個選項:1. 錯誤訊息提示 2. 提供重試按鈕

特別要提一下的是 fetch() 第二個參數中的 headers 可以透過 Headers() 介面來實作。雖然用物件也可以啦。

還有 body 在傳入 application/json 要記得使用 JSON.stringify

這邊要注意的是如果 fetch 回傳像是 404 等錯誤代碼,是不會進入 catch 的,也就是這個 Promise 還是會直接 resolved,只有在 network error 的時候 fetch 才會失敗。

像是下面的範例中:

fetch('/not-exist-api-path')
  .then(console.log) // 會執行
  .catch(console.warn); // 就算回傳 404 也不會 catch

如果出現錯誤,一樣會繼續執行 then,所以如果你希望將 4xx, 5xx 等系列的狀態碼當作錯誤的話,需要再另外判斷:

fetch('/not-exist-api-path')
  .then(res => {
    if (res.ok) { // 可以透過 res.ok 來判斷回應是否正常
      // res.status: 404
      // res.statusText: Not Found
      return res.json()
    }
    throw res; // 把 res 丟出去
  });

或者直接套用成熟的函式庫來減少 API 管理的複雜度。目前比較熱門的套件是 axios (opens new window),除了 API 很簡潔之外,也很容易做客製化的設定。

# 2. API 回傳錯誤狀態代碼

HTTP 狀態碼是用來規範伺服器狀態的,以 3 位數的數字表示對應的訊息。

本篇幅只介紹錯誤狀態的部分。

錯誤狀態代碼很多,不過簡單可以區分為 4xx 系列與 5xx 系列。通常 5xx 系列代表後端的程式碼有問題或伺服器有問題導致的錯誤,;而 4xx 代表你的請求有問題,導致伺服器不接受請求。

常見的狀況有:

  • 400: bad request,可能是參數有誤,或是必要的參數未填,或是請求有誤等等,可以參考後端提供了 error 欄位來查看。這時你可能需要提示使用者,哪裡出錯了,並且最好能夠保留使用者的輸入,以便他們修改。
  • 401: Unauthorized,你的請求不被允許,最常見的情況就是沒有登入,或是你本來就沒有權限,也有可能是你忘記送出 cookie 或 token。不妨確認一下登入狀態,並且告知使用者發生了什麼事。這類型的錯誤通常是權限不足,直接將使用者導到正確的頁面(例如登入頁面)也行。 另外,如果 API 的網域跟前端的頁面不同,可能需要考慮到 CORS 的問題,也可能會因為標頭沒有正確設定而導致 cookie 無法送出。
  • 403: Forbidden,你不允許訪問這個資源。
  • 429: Too many requests: 表示撞到 rate limit
  • 500: Internal Server Error 伺服器遇到不明確的錯誤,所以無法完成請求。通常是某段程式碼導致例外而沒有被正確處理。

理論上我們可以將所有的回應都回傳 200,再從 response 當中判斷錯誤,像是 GraphQL 就是統一一個 endpoint 再處理。

不過透過錯誤狀態代碼我們可以更清楚知道發生什麼事,瀏覽器也可以針對狀態代碼顯示不同的錯誤訊息,而且這是經過標準化後的代號。

# 3. 使用者因為網路問題無法送出 API

如果你的應用程式(像部落格文章等),可能會因為無法送出 API 而導致文章沒有送出,結果要重打一遍的情況。

要實作即時儲存機制有兩種辦法,一種是直接存在瀏覽器端的 localStorge,並且在送出的時候刪掉;另外一種則是每隔一段時間就自動打 API 到後端幫使用者儲存。

在這種情況下,你可能需要讓使用者感受到時時刻刻都有儲存的感覺,像是 google drive 的:

螢幕快照 2019-04-04 下午2.49.42

或是 medium 的即時儲存:

# 4. 使用者離線

如果使用者在離線時(可能因為網路問題斷線)做了一些必須連線才可以做的操作,我們可能需要將使用者的「操作行為」記錄下來,並且在 online 恢復連線的時候,一一將使用者的操作送出。

此時你可能需要 localStorge 或是 sessionStorge 來記錄,這種類型的方式適合用物件來描述行為,因為很容易做序列化。

同時可能需要監聽 navigator.onLine 或是查看 navigator.online 這個值。如果偵測到目前沒有連線,可以顯示對應的訊息。

online 的時候,可能需要通知使用者,目前連線狀態已恢復,是否要再次送出。

螢幕快照 2019-04-04 下午2.54.49

# 5. 重試

有時候 API 無效可能只是伺服器太忙碌或是其他因素,對於這類型的 API,我們可以使用 retry 的方式。

retry 又可以分為兩種:1. 提供重試選項(按鈕)讓使用者觸發。 2. 自動在 retry,超過一定次數後再通知使用者。

如果是第 2 種方式,通常會使用 exponential backoff,也就是指數退後的方式。什麼是指數退後呢?

一般來說,如果 API 失敗我們想要重試的話,會每隔一段時間再重試一次,這個區間如果太小,可能會讓伺服器負擔太大;如果區間太大,又會讓使用者等太久。因此指數退後就是以指數函數的方式,先從間隔較短開始,1 秒、2 秒、4 秒、8 秒逐漸增加。

# 其他

最近 GraphQL 盛行,大幅度減緩了前端管理 API 的難題,我們可以預測回傳的欄位及型別,也不需要用狀態碼來判斷錯誤(是好是壞呢?),而是統一一個 endpoint,並且由回傳的欄位查看是否有錯誤。

關於 GraphQL,我們會在其他章節討論。

# 小結

設計、使用 API 時有以下情形需要考慮:

  • ajax 跟 fetch 處理錯誤代碼的方式不同
  • ajax 超時、沒有回應,可自動取消,或讓使用者再次重試
  • 回傳錯誤狀態碼:根據狀態碼不同妥善處理錯誤,並且通知使用者哪裡出錯了
  • 無法成功送出 API:可以將行為序列化在 storage 裡頭,連上網路時再一併將請求送出。
  • 重試:可以使用指數退後的方式,或是提供重試選項給使用者

另外在這篇文章中我們只講到概念上要考慮哪些事情,但並沒有真正實作,等到後面進階篇時會再搭配 RxJS 一起介紹。