說到前端開發中的mock,即假資料,我們都知道它是用來解決前端開發的閉環除錯或測試用的一種技術手段,就是當真正的服務端api沒有開發完成,只有介面文件的情況下前端可以先行完成功能開發和測試。mock方案既可以放在服務端,也可以放在本地。
服務端的mock通常需要專門開啟一個mock服務,可以進行介面配置和跨域等。
本地的mock方案則相對靈活很多,最簡單的方式是將mock資料直接注入接受請求返回值的變數中,但是它的缺點很大,侵入性太強,不夠靈活,甚至可以說汙染了業務邏輯,完全是給後續挖坑的方案。稍微複雜一點的方案比如mock.js, 通過修改XmlHttpRequest
的預設行為可以攔截ajax,解決了構造隨機mock以及侵入太強的問題,但是它的缺點同樣明顯,真實連調時需要移除相關程式碼,並且它對於fetch請求素手無策。
要做到業務程式碼無侵入的mock,也很簡單。現在的前端專案都是工程化的,大部分都會用到一個本地node服務,比如webpack-dev-server
等,我們在和服務端連調之前除錯網頁,看到的所有內容都是本地服務提供的靜態響應,api完成之前即便使用代理,繞過跨域也無法有效自測,我們可以從這個本地服務去入手,通過實現一箇中介軟體去攔截http請求,然後對API介面型別的請求做自定義的響應處理來實現mock。這樣的mock方案可以做到業務程式碼零侵入,連調時只要關閉mock的開關或者移除中介軟體就可以。
大多數前端專案本地服務都是依賴express,以它為例,實現一箇中介軟體要如何做呢。這裡的關鍵問題在於URL如何去對映,有兩種方案:
檔案對映
檔案對映是指對請求url中的“/”替換,比如api/a/b
這樣一個請求,可以對映成api_a_b
這樣命名的檔案,中介軟體通過讀取檔案來響應請求。
map對映
map對映是指不必按照嚴格的格式去建立對映檔案,而是通過配置一個json
檔案去處理,key
表示url
,value
為待返回的響應內容。
檔案對映的優點是不必進行專門配置,對新請求只要建立新的對映檔案就好了,但是缺點也很明顯,由於檔名是靜態的,這種方式難以處理類似api/a/b/10
這樣帶有pathParam
的get
請求,還有就是檔案無法複用,即便兩個不同請求返回的結果是相同的,也需要分別建立各自的mock
檔案。相比之下,map對映的方式除了必須的路由配置外,實現可以相當靈活,可以複用mock
。
下面介紹一下自己寫的一箇中介軟體dynamic-mock-express,它具備如下功能和特性:
1.可以友好地支援Restful API
2.可以自定義url過濾規則,確定哪些請求不需要mock
3.支援將map對映的value設定為函式,並且可以通過引數接受相對應請求的query,params和body
4.支援將返回值的任意屬性或巢狀屬性設定為函式,同樣可以接受上述引數
5.配置無快取,修改後不需要重啟啟動專案就可以生效
6.函式通過接受store物件可以實現動態的、響應式的mock,可以簡單模擬真實後端服務
使用方法
1.安裝:
npm i dynamic-mock-express -D
複製程式碼
2.根目錄建立mock資料夾,在該資料夾下面建立index.js
,即配置檔案,例如:
// index.js
module.exports = {
needMock: true, // 是否開啟mock,預設true,連調時設為false 即可,不必重啟專案
prefix: "api", //需要mock的url字首
tip: true, //為true時匹配不到url時控制檯會警告,預設為true
ignore: (url, method) => {
// 可以接受url和請求的method,返回是true的將被過濾,不會被mock處理
},
routes: {
"GET:a/c": require("./mock_1"),
"GET:a/b/:id": (data) => { // 可以接受params引數,data共有四個屬性:params、query、body、store
return {
data: "mock_2",
params: data.params,
};
},
"GET:b/:id/:code": ({params}) => { //將請求內容原樣返回
return {
id: params.id,
code: params.code
}
},
"POST:b/c": { // 可以直接將value定義為一個json物件
a:1
},
"POST:a/b/c": data => { // 通過data.body可以將post資料本身作為響應
return {
status: true,
body: data.body
};
},
"DELETE:a/b/:id": (data) => { // 支援Restful api
return {
id: data.params.id
};
},
"DELETE:a/b/c/:id": { // 支援屬性或巢狀屬性定義為函式
a: {
b: 1,
c: (data) => {
return {
id: data.params.id
}
}
}
}
}
};
複製程式碼
3.掛載中介軟體
express服務
const app = express();
const mock = require("dynamic-mock-express");
app.use(
mock({
mockDir: path.resolve(__dirname, "../mock")
})
);
複製程式碼
封裝的express服務,如webpack-dev-server(vue-cli或者create-react-app等)
const mock = require("dynamic-mock-express");
new WebpackDevServer(compiler, {
...
setup: (app) => {
app.use(
mock({
mockDir: path.resolve(__dirname, "../mock"), // mock資料夾目錄
entry: "index.js" // mock資料夾入口,如果配置檔案就叫index.js,可以不配置
})
);
}
}
複製程式碼
假如發出一個api/a/b/10
的get
請求,mock將會按照如上配置響應如下結果:
{
data: "mock_2",
params: {
id: "10"
}
}
複製程式碼
4.響應式的動態mock
配置storePath
屬性可以讓mock
具備響應式能力,如下:
const path = require("path");
module.exports = {
needMock: true,
prefix: "api",
storePath: Path.reslove(__dirname, "store"), // 必須是絕對路徑
tip: true,
routes: {
"GET:a/b/:id": ({store, params}) => {
return store.data.find(item => {return item.id == params.id});
},
"POST:a/b": ({store, body}) => {
store.data.find(item => {
return item.id == body.id
}).name = body.name;
return {
status: true
};
}
}
}
複製程式碼
mock/store.js
module.exports = {
data: [
{
id: 10,
name: "zhangsan"
},
{
id: 11,
name: "lisi"
}
]
}
複製程式碼
用虛擬碼的形式去發起如下請求:
// pseudocode
get("api/a/b/10").then(res => {
console.log(res) // {id: 10, name: "zhangsan"}
post("api/a/b")
.send({id: 10, name: "wangwu"})
.then(res => {
get("api/a/b/10").then(res =>{
console.log(res) // {id: 10, name: "wangwu"}
})
})
})
複製程式碼
可以發現post修改資料後再次get的時候結果已經更新,這樣我們就可以通過簡單的一些邏輯程式碼模擬一些呼叫真實api介面的互動而不用手動去修改mock。
專案地址github.com/silentport/…,歡迎大家提issue。