说到前端开发中的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。