iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 12
2
Modern Web

深入現代前端開發系列 第 12

Day12 前端如何管理 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,詳細可以參考我之前寫的文章

以前 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,除了 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 文件中的即時儲存或是 medium 的即時儲存

4. 使用者離線

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

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

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

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

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 一起介紹。


上一篇
Day11 設計 UI 真的很簡單嗎?談 UI 狀態
下一篇
Day13 前端如何管理 API (中)- Cookie、CORS 、CSRF
系列文
深入現代前端開發32

尚未有邦友留言

立即登入留言