注意! 廣告警告! 廣告警告! 廣告警告!
在一個web應用的開發週期中, 一般前端與後端都是並行開發的, 各自完成自己的開發工作後進行聯調, 聯調通過再進行提測/釋出.
開發過程中, 前端都會以後端提供的 api 文件作為標準, mock 模擬 api 返回資料, 以確保在開發中就保證功能的完整性.
而關於如何更好的進行 mock, 業界/開源社群可謂有相當多質量上乘的解決方案, 如easy-mock, yapi等.
但是越是大而全的工具很多時候功能會超越需求非常多, 要簡單實現 mock api 的需求其實也有非常多小而美工具庫可以使用.
而本文主要介紹mock-server這個工具的使用
選用mock-server的主要原因除了是我開發的使用比較簡單之外, 更多的是滿足了下文提到的一些開發需求, 如果你也有同樣的需求而還沒找到解決方案的話, 不妨試用一下.
安裝 & 基本配置
可選全域性安裝, 安裝完成過後, 就可以通過mock
命令啟動來 mock server
npm install -g mock-server-local
mock -h # 即安裝成功
# 用 -d 指定mock資料配置目錄, 為空時預設為當前目錄 `.`
mock -d ./mock
# 用 -p 指定server的埠, 預設為8888, 如果8888被佔用會埠號+1, 直至埠可用
# 注意如果指定了埠號, 但是埠號被佔用的話, 會丟擲錯誤
mock -d ./mock -p 8080
複製程式碼
個人比較習慣在專案中進行安裝, 並通過npm script
啟動, 而 mock 資料也存放在專案當中, 通過 git 等版本管理工具在專案成員當中共享, 假設專案目錄為proj
// proj/package.json
{
// ...
"script": {
"mock": "mock -d ./mock -p 8080"
}
// ...
}
複製程式碼
# 本地安裝
npm install mock-server-local --save-dev
# 啟動mock server
npm run mock
> mock@1.8.9 mock /path/to/proj
> mock -d ./mock -p 8080
you can access mock server:
http://127.0.0.1:8080
http://ww.xx.yy.zz:8080
you can access mock server view:
http://127.0.0.1:8080/view
http://ww.xx.yy.zz:8080/view
複製程式碼
就這樣 mock server 就已經啟動了, 訪問127.0.0.1:8080/view
即可看到 mock server 的控制頁面
就下來就是調整代理, 把應用的請求轉發到 mock server 進行處理
如果你使用webpack
來構建你的專案, 那你只需要改動一下webpack.devServer
的配置即可
假設我們的業務域名為target.mock.com
, 而介面基本都是target.mock.com/api/**
, 可以這樣進行配置
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8080', // mock server
headers: {
host: 'target.mock.com' // 業務域名
},
onProxyReq: function(proxyReq, req, res) {
proxyReq.setheader('host', 'target.mock.com'); // 業務域名
}
}
}
}
複製程式碼
接著在開發中, 啟動 webpack 之後, 發出的請求/api/**
都會被轉發到
而如果應用本身不使用 webpack 或其他帶 server 功能的打包工具, 可以使用代理工具進行請求轉發
如果是用 Chrome 瀏覽器除錯應用, 可以下載SwitchyOmega
一類可配置, 把特定域名的請求進行轉發
使用微信開發者工具的話, 可直接設定代理, 設定
-> 代理設定
-> 勾選手動設定代理
-> 填寫代理配置
推薦使用 whistle, fiddler 一類功能完整代理工具, 類似配置如下
target.mock.com/api 127.0.0.1:8080 # 介面請求轉發到mock server
target.mock.com www.xx.yy.zz # 頁面從正常的開發測試機ip中獲取, 或本地除錯伺服器
複製程式碼
mock 資料配置
mock server 的配置是根據 mock 目錄的目錄結構生成的, 假設需要進行 mock 的 api 介面完整的 url 為target.mock.com/api/login
而且需要模擬以下三種情況的資料返回
- 登入失敗, 返回錯誤碼-1 及錯誤資訊
- 登入成功, 返回錯誤碼 0 和使用者資訊, 且要帶上登入態 cookie
- 請求時間超過 8 秒, 導致前端請求超時
那麼目錄結構與資料配置檔案應該如下所示
|- proj
|- mock
|- target.mock.com
|- api
|- login
|- 登入成功
|- 登入失敗
|- 請求超時
// 登入失敗
// proj/mock/target.mock.com/api/login/登入失敗/data.js
module.exports = {
code: -1,
msg: '登入失敗!'
};
// 登入成功
// proj/mock/target.mock.com/api/login/登入成功/data.js
module.exports = {
code: 0,
msg: '登入成功',
username: 'ahui'
};
// proj/mock/target.mock.com/api/login/登入成功/http.js
module.exports = {
header: {
'Set-Cookie': 'token=123456789;'
}
};
// 請求超時
// proj/mock/target.mock.com/api/login/請求超時/data.js
module.exports = {};
// proj/mock/target.mock.com/api/login/請求超時/http.js
module.exports = {
delay: 8
};
複製程式碼
根據上面目錄配置, 訪問 mock server 頁面的 mock 皮膚127.0.0.1:8080/view/mocks
, 就可以看到以下頁面
現在我們只需要勾選其中一個狀態, 然後發出請求即可
mock完成之後就可以愉快編寫業務邏輯了
mock 配置詳解
看了上面的例子應該也就大致可以瞭解到, data.js
裡面定義介面返回的資料. 而http.js
, 顧名思義就是定義 http 請求相關行為的, 例如可以定義響應頭, http 狀態碼, 請求耗時等.
data 同時也支援使用 json 檔案, 非 js/json 檔案的一律當 json 格式處理, 而 http 則只支援通過 js 檔案定義
http.js
可選配置如下
module.exports = {
delay: 8, // 耗時8秒
status: 200 // http狀態碼
header: {
// ... http響應頭
}
}
複製程式碼
而data.js
除了可以簡單定義返回資料之外, 還可以直接返回模板, 如
module.exports = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
這個是由mock server返回的模板
</body>
</html>
`;
複製程式碼
並且可以返回一個方法, 通過指令碼邏輯來處理最終要返回的資料
// ctx: https://koajs.com/#context
module.exports = function(ctx) {
// 邏輯處理
return {
data: {}, // 響應資料
header: {}, // 如有需要可以配置, 同http.js#header
delay: 0.1, // 如有需要可以配置, 同http.js#delay
status: 200 // 如有需要可以配置, 同http.js#status
};
};
// 如果當中有非同步邏輯, 請返回promise, 或直接使用async方法
module.exports = async function(ctx) {
// 非同步邏輯處理
await asyncFn();
return {
data: {}, // 響應資料
header: {}, // 如有需要可以配置, 同http.js#header
delay: 0.1, // 如有需要可以配置, 同http.js#delay
status: 200 // 如有需要可以配置, 同http.js#status
};
};
複製程式碼
代理線上資料
在開發過程可能出現這樣的場景, 一期專案已經開發完了, 現在進行二期迭代的開發工作, 這個時候由於之前的介面後臺已經都實現, 二期開發中只想對新增的 api 進行 mock
這個時候可以修改一下代理工具的配置, 把不同介面的請求轉發到不同的伺服器
# 新增介面轉發至mock server
target.mock.com/api/new1 127.0.0.1:8080
target.mock.com/api/new2 127.0.0.1:8080
# 其餘介面直接使用線上/測試機資料
target.mock.com ww.xx.yy.zz
複製程式碼
又或者可以直接使用 mock server 提供的簡單的代理功能, 只需要在 mock 目錄個目錄下新建proxy.js
檔案
|- proj
|- mock
|- proxy.js
// proj/mock/proxy.js
module.exports = {
target.mock.com: 'https://ww.xx.yy.zz' // 這裡可以指定ip也可以指定域名, 但是需要注意協議型別是必須要帶上的
}
複製程式碼
這樣配置之後, 在代理工具中就可以直接把所有的target.mock.com
的請求都直接轉發到 mock server
當對應請求的 url 並沒有勾選任何一個返回狀態, 或根本沒有配置對應的 url 時, mock server 都會幫助我們把請求轉發到目標 ip
假設沒有配置proxy.js
的話, 對於沒有命中的 url 請求, 會根據 host 直接請求線上的資源或介面
模板介面除錯 & 微信登入支援
在非前後端分離的架構中, 很常會出現這樣的需求, 應用的入口即是後端介面, 後端會進行鑑權, 拼接模板內容和資料, 然後直接返回頁面給到前端進行展示.
這樣的場景 mock server 可以很簡單通過data.js
中匯出方法的方式來處理
const fs = require('fs');
module.exports = async ctx => {
let html = '';
let template = fs.readFileSync('path/to/html/template/index.ejs'); // 舉例ejs, 實際可以處理任何模板引擎
// 這裡處理模板的語法
// 1. 處理類似include的拼接模板的語法
// 2. 處理類似<%= =>插入變數/資料的語法
// 3. 等等等等....
html = processedHtml;
return {
data: html,
header: {
'Set-Cookie': 'SESSIONID=123123123123;'
};
};
};
複製程式碼
這樣子我們就可以進行模板介面的除錯了. 再回到我們的上一個例子
我們希望可以使用線上已有介面和資料狀態(如開戶資料)
也希望使用後端的登入態(這樣後續的介面呼叫也能通過鑑權), 但也同時希望可以除錯本地模板呢?
比較直觀的方式是, 本地修改模板然後把模板改動上傳到開發伺服器, 然後直接請求開發伺服器進行除錯
但是改動比較多, 需要頻繁除錯的話, 或許使用 mock server 也是一個不錯的選擇. 更進一步, 如果是微信 h5 且後端的登入鑑權接入了微信登入呢?
我們來分析一下如何使用 mock server 滿足這樣的除錯述求, h5 微信登入基本的流程如下
- 請求線上/開發測試伺服器介面
- 介面返回 http 狀態碼 302 並帶上 Location 頭, 跳轉到微信 url
- 請求微信 url 會返回 301 再回跳我們的業務域名
- 回跳我們的業務域名時, 即再次請求伺服器介面, 獲取微信登入 code 進行業務登入
- 返回登入態及 html 頁面
上面的流程中, 其實需要介入只有最後一步而已, 就是獲取到登入態並返回需要除錯的 html 模板內容即可
而前面的步驟, 完全可以通過在data.js
中實現簡單的代理完成
// 微信登入/data.js
const httpProxy = require("http-proxy");
const fs = require("fs");
const path = require("path");
proxy = httpProxy.createServer({
secure: false
});
async function req({ req, res }) {
proxy.web(req, res, {
target: {
protocol: "https:",
host: "ww.xx.yy.zz", // 目標伺服器
port: 443,
pfx: fs.readFileSync(path.resolve(process.cwd(), "cert/cert.p12")), // 如果伺服器是https需要生成證書
passphrase: "password"
},
selfHandleResponse: true
});
return new Promise((resolve, reject) => {
proxy.once("proxyRes", function(proxyRes, req, res) {
let body = [];
let size = 0;
function onData(chunk) {
body.push(chunk);
size += chunk.length;
}
proxyRes.on("data", onData).once("end", () => {
proxyRes.off("data", onData);
body = Buffer.concat(body, size);
resolve({
header: proxyRes.headers,
data: body,
status: proxyRes.statusCode
});
});
});
});
}
module.exports = async function(ctx) {
// 登入態
const res = await req(ctx);
const header = res.header;
res.header = Object.keys(header).reduce((c, k) => {
let nk = k
.split("-")
.map(v => v.charAt(0).toUpperCase() + v.slice(1))
.join("-");
c[nk] = header[k];
return c;
}, {});
if (res.header["Set-Cookie"]) {
// 如果有Set-Cookie header, 則要處理返回本地模板
// 這裡處理模板的語法
// 1. 處理類似include的拼接模板的語法
// 2. 處理類似<%= =>插入變數/資料的語法
// 3. 等等等等....
res.data = template;
// 這裡需要注意, 目標伺服器可能會返回gzip過後的資料
// 如果不對Content-Encoding和Content-Length進行處理的話
// 會導致響應中Content-Length和實際內榮長度不一致而出錯
res.header["Content-Encoding"] = "identity";
delete res.header["Content-Length"];
}
return res;
};
複製程式碼
這樣我們就可以對具體的介面模板進行除錯了
寫在最後
重複造輪子不易, 且造且珍惜
如果大家有mock api的需求的話, 不妨也試用一下mock-server
如果覺得mock-server還不錯, 或者解決了mock的一些痛點, 不妨賞個star
最後, 用得不爽或發現bug, 懇請提issue!