AC 關鍵字計畫 | API 開發、測試、除錯一次到位!使用 MSW 快速上手 Mock API
本篇文章已刊載在AC Blog。
什麼是 API
在認識 API 以前,我們直接從生活中的例子來認識介面 (Interface)。
忙碌的午後,「小美」想要喝罐可爾必思。於是她前往公司樓下的飲料販賣機,在「操作面板上」投硬幣跟按按鈕,販賣機裡的「系統」判斷金額是否符合,以及是按下哪個飲料的對應按鈕。判斷結束後,就把一罐可爾必思推出來。最後小美從取物口拿出,大口地暢飲。
在上述例子裡,飲料販賣機的控制面板即為人機互動的「介面」,也就是等下待會要講的「API」。而小美呢,則是介面的使用者,對應到軟體開發的領域上是指 API 的使用者,也就是我們這些應用程式的開發者。
API(Application Programming Interface)的字面意思是「應用程式介面」。就像飲料販賣機的介面一樣,API 是應用程式之間一套明確定義的溝通方法和接口。使用 API 的過程中,不需要理解內部複雜的運作,只需要知道要呼叫什麼方法,傳入對應參數,就能取得想要的結果。
Web 應用程式中的 API
在 Web 應用程式的開發情境下的 API 被稱為 Web API,在 Web API 作用時,客戶端(前端)和伺服器端(後端)會透過 HTTP 通訊協定來進行請求與回應。
作為前端的開發者,為了要取得存放在系統的資訊,常常會需要以 Web API 來發送請求與後端溝通。因此通常需要做的事情有以下:
- 閱讀 API 文件
- 如果是第三方開發的應用(例如 Google、Facebook),可能需要註冊、驗證等程序
- 撰寫有關 Web API 串接的函式
- 在適當時機點呼叫對應的串接函式
而後端的開發者,則是將擁有的資料庫開放給第三方使用。因此開發步驟大致會是這樣:
- 撰寫一個明確的 API 文件,清楚定義 API 的使用方式及格式等等。
- 按照文件開發一個 Web API,提供可外部呼叫的方法,用這個方式做為使用介面。
控制 API 的回傳結果,可能嗎?
前端開發時,十之八九都會需要實作 API 的串接。你可能會先建立相關的函式模組,然後在元件或是狀態管理的程式碼中匯入,在適當時機點發出 API 的請求。
問題來了,如果想要測試 API 回傳不同的結果下,元件的操作邏輯是否正常;或是現在正處於雛型(Prototype)開發的階段,在後端的 Web API 還沒完成的情況下,前端要超前部署產出具體的半成品等,前端這邊是不是可以先實作好 API 串接的部分,而且還能隨意控制 API 回傳的結果呢?
舉個簡單的例子,在 api.js
中,定義了所有跟 API 串接的函式模組,其中,isLoggedIn
函式是檢查使用者是否已登入。
- api.js
1 | export async function isLoggedIn() { |
在元件- App.js
中,我們匯入isLoggedIn
函式,並且在適當時機點呼叫它,如果已經登入,執行 A,否則執行 B。
- App.js
1 | import { isLoggedIn } from './api'; |
假設不變動 App.js
中的-
1 | const isLoggedIn = await isLoggedIn(); |
我們有辦法由前端控制 isLoggedIn
變數是 true
還是 false
,來檢查相關的條件判斷嗎?
這個問題,當然是 YES 囉!這就是今天要講的-Mock API。
API 開發、測試、除錯一次到位-Mock API
先來說文解字一下,「Mock」是從單元測試的概念延伸而來的,主要是模擬真實的物件或類別,但是省略內部複雜的行為,讓測試過程可以更專注在本身的測試重點上。
同樣地,比起透過真實的後端 server 取得資料,我們有時候會比較想要自己決定資料的樣子,甚至是定義 API 的入口點(entrypoint)和參數欄位等等,但同時又不想變動前端呼叫 API 串接的部分。可以完成以上種種行為的方式,就是 Mock API。
使用 Mock API 的理由
最普遍的使用需求,應該就是希望在初期的開發階段,前端不需要等後端實作完成後才能接著進行。除了讓開發時程不用花到雙倍的時間,前後端雙方也能透過 Mock API 的過程更具體討論實作細節,達成品質與速度都能雙贏的目的 💪
除此之外,以下的場合也能應用 Mock API 來完成:
- 撰寫測試案例:進行元件測試或 E2E 測試時,可以撰寫各種 HTTP 狀態,或是不同回應資料時的測試案例。
- 開發時的除錯:藉由修改 mocking 的內容,幫助元件或頁面進行除錯。
向 MSW Say Hi!
MSW(Mock Service Worker)是一套可以實現 Mock API 的工具。最大的特色是能在網路層(Network)發出實際的請求(Request),並透過 Service Worker 攔截,回傳已經定義好的資料內容。
另外,MSW 也擁有許多優點,像是:
- 跟許多框架、工具都有很好的整合性,像是 React、Jest、Cypress 等。
- 針對主流的 API 設計-RESTful 和 GraphQL 都有良好的支援。
- 如果專案是以 TypeScript 開發,不須特別設定就能直接使用 TypeScript,享受強型別帶來的便利與好處。
- 清楚好閱讀的官方文件,並且有範例程式,讓開發者能好上手。
因此跟其他相似的工具來比較,像是 MirageJS、json-server,MSW 的下載使用量平均超過它們的兩倍,並且有成長的趨勢。這也表示相關的社群資源和討論聲量也會愈來愈豐富。
在熟悉 MSW 之前,我們先基礎認識它的底層機制-Service Worker,以及知道 RESTful 和 GraphQL 的差別。
Service Worker
Service Worker 是 Web Worker 的一種。除了可以在背景執行緒中運作,不影響使用者的網頁瀏覽體驗以外,最大特色是透過 cache 的機制來打造離線體驗。我們多少會碰到一些網站提供推播通知、暫存資料或是背景中同步等功能,這要多虧有 Service Worker 的實現。
導入 Service Worker 有幾個重點步驟:
- 檢查瀏覽器的支援度:基本上除了IE,大部分瀏覽器在某版本之後都有陸續支援,如果需要,可以加上檢查。
1 | if ('serviceWorker' in navigator) { |
- 註冊:呼叫全域方法
navigator.serviceWorker.register()
進行註冊。由於這是個 promise 函式,可以透過then
跟catch
來做完成後的處理。
1 | navigator.serviceWorker.register('/serviceWorker.js') |
- 安裝:註冊完後,會觸發
serviceWorker.js
中的install
事件,執行瀏覽器的離線快取功能。
1 | this.addEventListener('install', function () { |
- 啟動:註冊完後,會觸發
serviceWorker.js
中的active
事件,正式啟動 Service Worker。
1 | this.addEventListener('active', function () { |
- 存取請求內容:當在這個 Service Worker 的可控範圍內偵測到 HTTP 請求,會觸發
serviceWorker.js
中的fetch
事件實作 cache 的機制。
1 | this.addEventListener('fetch', async function (event) { |
- 收發訊息:觸發
serviceWorker.js
中的message
事件,處理各種請求階段的接收與發送訊息。
1 | this.addEventListener('message', function(e) { |
RESTful vs. GraphQL
RESTful 是一種 API 的設計風格,將有語意性的詞彙加上資料參數形成特定路由(Route),並運用 HTTP 各種定義的請求方式,向後端取得資源或是修改資源。
因此 RESTful 最大的特點是,可以透過路由的結構清楚知道資料欄位跟操作方式,適合用來設計簡單或是結構較為單純的 API。
GraphQL 是由 Facebook 在 2015 年發布的 open source。透過前端定義的資料結構,告訴後端返回相同資料結構的對應資料,來避免後端設計大量的 API route ,或是返回多餘的資料欄位。
不過這種靈活性也增加了複雜性,通常會比較適合用來設計具規模或是結構比較交錯複雜的 API。
MSW 初體驗
如果只是想先體驗看看,不用大費周章建立環境,可以參考 MSW 建立的範例程式,裡面有許多不同的範本專案。切到不同的專案環境後,就可以執行相關指令玩玩看囉😊
- clone 範本專案。執行以下指令,或是使用 GitHub Desktop 的 Clone Repository > URL 輸入以下的網址部分。
1 | git clone https://github.com/mswjs/examples.git |
- 切換到 examples 目錄。
1 | cd examples |
- 安裝所有依賴套件。
1 | yarn |
- 切換到特定目錄,例如:想看使用 React,加上 RESTful 的 API 設計會是什麼樣子,那麼就切換到
rest-react
。
1 | cd examples/rest-react |
- 執行以下指令,在本機建立 live server。
1 | yarn start |
- 打開瀏覽器輸入
localhost:3001
。正常的話,可以看到頁面上有個簡易表單。按 F12 觀察 console,就會出現 [MSW] Mocking enabled.的訊息,表示 MSW 已經在運作囉!
- 可以把 Dev Tool 切到 Network 觀察。在表單欄位隨意輸入後送出,會發現 Network 中發出了一個 login 的請求。
- 再切回 Console,可以看到 MSW 有印出請求的 Request、Reponse 的詳細內容。找到程式碼來對照看看,的確 MSW 發揮了作用,以程式中定義好的資料作為 Response 的資料,magic~
建置與導入 MSW
如果想引入專案環境中,其實非常簡單,基本上安裝 msw 這個套件就可以了。
1 | yarn add -DW msw |
不過不少人,包括我自己是用 webpack 來打包前端程式和建立 live server。因此接下來的建置流程會再多介紹 webpack 的導入方式。
- 安裝 MSW 以及執行 webpack 時所需的 plugins,作為開發用的依賴套件。
1 | yarn add -DW msw copy-webpack-plugin workbox-webpack-plugin |
在專案的根目錄下建立
public
的子目錄,如果已經有的話省略。在
src
的目錄下建立mocks
的子目錄。在
mocks
的目錄下建立兩個檔案-handlers.ts(js)
以及browser.ts(js)
,待會會提到更多這兩個檔案的實作部分。在專案的根目錄下開啟終端機,執行以下指令來建立負責產生 Service Worker 的程式檔
mockServiceWorker.js
。如果有興趣的話,可以從這份檔案來看 MSW 如何設計 Service Worker 的架構與各種生命週期的事件喔!
1 | npx msw init public/ |
- 在
webpack.config.js
或特定的 webpack 設定檔中,在plugins
部分新增跟 Service Worker 相關的設定。CopyWebpackPlugin
- 將mockServiceWorker.js
複製到 live server 中。WorkboxPlugin
- 建立 Service Worker。
1 | const CopyWebpackPlugin = require('copy-webpack-plugin'); |
- 在
index.html
偵測當前瀏覽器有無支援 Service Worker,有的話就以mockServiceWorker.js
來註冊 Service Worker。
1 | <script> |
如果按照以上步驟做完的話,目錄結構應該會長這樣-
1 | -- MyProject/ |
快速上手 MSW
handlers.ts(js)
是負責撰寫 mock API 的實作細節。通常會建立一個陣列變數-handlers
來存放各個 API 串接的實作。
1 | export const handlers = []; |
舉上面範例程式的 login 請求作為例子,分別看看 RESTful 跟 GraphQL 這兩種 API 設計模式,MSW 會如何實作。
RESTful
從 msw 匯入 rest
。rest
提供了各種模擬 HTTP 的請求方式實作-
rest.get()
:對應GET
請求,取得資源。rest.post()
:對應POST
請求,提交指定資源的實體。rest.put()
:對應PUT
請求,上傳或取代指定的資源。rest.patch()
:對應PATCH
請求,指定資源的部份修改。rest.delete()
:對應DELETE
請求,刪除指定資源。rest.options()
:對應OPTIONS
請求,查詢 server 支援的 HTTP 請求方式。
這些方法需要固定傳入兩個參數-
- entrypoint-API的路徑,可以
/:someData
的方式以路徑傳入資料參數。 - resolver function-請求完成後的 resolve 函式。有三個參數可以操作,分別是-
req
:req.body
的物件可以取得 Request body 中的資料參數。res
:執行res()
來回覆請求。ctx
:執行ctx.json({...})
組合回傳的資料,定義想要的格式和內容。
1 | import { rest } from 'msw' |
GraphQL
從 msw 匯入 graphql
。graphql
實作了兩種操作方式:
graphql.query()
:對應 GraphQL 的Query
來完成資源的查詢。graphql.mutation()
:對應 GraphQL 的Mutation
來完成資源新增、刪除、修改等會產生副作用的操作。
這些方法需要固定傳入兩個參數:
- name-已經定義好的 Query 或 Mutation 名稱。
- resolver function-請求完成後的 resolve 函式。有三個參數可以操作,分別是-
req
:req.variables
的物件可以取得 Request 中的資料參數。res
:執行res()
來回覆請求。ctx
:執行ctx.data({...})
組合回傳的資料,根據定義好的欄位格式,設計想要的內容。
1 | import { graphql } from 'msw' |
當我們寫好 handlers 後,在 browser.ts(js)
中就可以新增負責初始化 MSW 的變數,並且傳入 handlers 作為初始化的參數, 讓 MSW 知道遇到哪些請求後要執行 mock API 的動作。
1 | import { setupWorker } from 'msw'; |
通常我們會在本機的開發環境,或是執行測試時,才會希望啟動 MSW。因此我們可以在主程式的進入點加上環境的條件判斷。
1 | import { worker } from './mocks/browser'; |
小結
針對不同專案找出最適合的 API 設計,以及讓前端可以在面對各種資料情況時,都能提供良好的介面與瀏覽體驗。這些事情都可以透過 Mock API 的過程中實現。希望透過今天的介紹,讓大家對於 Mock API 有更深的了解,並且感受 MSW 擁有的魅力與好處🥳
Reference
AC 關鍵字計畫 | API 開發、測試、除錯一次到位!使用 MSW 快速上手 Mock API