背景
在我们前端的日常工作中,难免需要请求后端的接口获取数据来进行页面的渲染,这个时候我们前端静态页面的开发以及相应的逻辑处理都与后端的接口息息相关,所以绝大多数时候在新需求的前期,我们前后端需要统一一份接口文档,以此来作为各自开发的指导和约束。
接口定义好入参、返参之后我们就可以在前端自己mock数据进行相关的开发,比如有一个定义好的查询商品列表的接口,我们很自然的就能依据接口文档写出如下代码
// 定义api
const getProducts = ()=>{
return axios.get('/products')
}
// 组件中使用
getProducts().then(res=>{
this.productList = res.data
})
但是在一开始,后端往往只是定义了一个接口格式,并没有实现其中的业务逻辑,或者是并没有部署,就像下面这种场景
后端:“我在本地调试,接口还不稳定你先写页面,周四给你哈” 此刻前端内心OS:“我去 *** ,周五就提测了,我 ** 怎么联调”
为了让页面有数据方便前端页面功能开发,我们可能会按照以下两种方式进行mock
- 修改定义api的部分
const getProducts = ()=>{
return Promise.resolve([
{id:001,name:'商品1'}
])
}
- 修改调用部分
getProducts().then(res=>{
// this.productList = res.data
}).finally(()=>{
this.productList = [
{id:001,name:'商品1'}
]
})
可以看出无论哪种方式,我们都要修改接口相关处的代码,等到接口真正可用时我们又得删除这一部分无用的mock代码,如果有几十个接口,重复的工作量大不说,而且容易遗漏,接下来我们就一起来尝试如何优雅实现前端mock
实践
因为目前公司项目普遍以vue为主,所以接下来的两个方案都是以vue/cli 4.x
版本构建的项目进行介绍(react同理,配置webpack即可),http请求使用的主流的axios
方案一 利用axios的adpter
axios
作为一款优秀的http库,能兼容在浏览器环境以及node环境发送http请求,核心就是其内置adpter
模块会根据运行环境在浏览器端使用XMLHttpRequest
,在node环境使用其内置http
发起请求,并且支持配置自定义adpter
,方案一就从这里入手
从场景出发,我们在本地开发的时候是否需要使用mock显然是一个需要全局可控的操作,可以简单的进行开关,最合适的方式就是从脚本区分
添加dev:mock
脚本,区分dev
,开启全局mock
// package.json
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev:mock": "cross-env IS_MOCK=true vue-cli-service serve",
},
cross-env
帮我们处理兼容问题,向process.env
中注入IS_MOCK
变量
修改webpack配置,省略其余不相关配置
// vue.config.js
const IS_MOCK = process.env.IS_MOCK === 'true' // 当运行dev:mock时为true
module.exports = {
// 利用webpack definePlugin注入全局变量
chainWebpack: config => {
config.plugin('define').tap(args=>{
args[0].IS_MOCK = IS_MOCK
return args
})
},
}
添加mock配置文件
module.exports = {
'/products':{
code:0,
data:[
{id:001,name:'商品1'}
],
message:'success',
}
}
封装axios实例
import axios from "axios";
// 读取mock配置
const mockMap = require("xxxxxx/mock.js"); // mock配置文件路径
var instance = axios.create({
adapter: (config) => {
// mock配置中匹配到当前url并且开启mock启用自定义adpter
if(mockMap[config.url] && IS_MOCK){
return Promise.resolve({
data: mockMap[config.url],
status: 200,
});
}else{
// 否则调用默认的适配器处理,需要删除自定义适配器,否则会死循环
delete config.adapter;
return axios(config);
}
},
});
export default instance
定义api采用配置后的axios实例
// api.js
import instance from 'xxxx' // axios实例路径
export const getProducts = ()=>{
return instance.get('products')
}
组件中使用
import { getProducts } from 'api.js'
getProducts().then(res=>{
this.productList = res.data
})
使用这种方案后续只需要维护mock.js
文件,把接口文档相应的内容配置上就可以实现本地开发请求的mock,可以运行dev
或者dev:mock
来决定是否启用mock
** 这种模式类似于上面修改定义api的方式实现mock,方便之处在于可以全局统一处理,实际上并不会发送http请求。
方案二 proxy + express
方案一虽然实现了简单的mock功能,但实际上思考一下还是有几处不够合理的地方:
- 并没有实际发送http请求,对请求的整个链路没有做到尽可能真实的模拟
- 依赖于axios库,其它http库能否实现以及实现是否像axios一样轻便没有探究,因此可拓展性较差
- 功能耦合,mock作为一个额外的功能模块,把其嵌入到axios中处理,会使得axios变得略显臃肿
基于以上几个问题,于是有了使用express
启动一个代理服务器,接收我们前端应用的请求来进行mock相关的处理,模拟一个完整的http请求闭环的想法
首先在项目根目录下新建一个server.js
文件,用来写我们代理服务器的代码
const express = require("express");
// 引入mock配置文件
const mockMap = require("./mock");
const app = express();
app.use(express.json());
app.all("*", requestFilter, async (req, res, next) => {
const { path } = req;
res.send(mockMap[path]);
});
// 端口和 webpack prxoy中设置对应上,可以随意设置一个未被占用的端口
app.listen("3306", () => {
console.log("serve is running at port 3306");
});
通熟易懂,就是监听3306端口,拦截所有请求,返回mock数据
修改webpack的proxy配置,使得运行mock脚本时,请求打到我们的代理服务器上,省略其余不相关配置
// vue.config.js
const IS_MOCK = process.env.IS_MOCK === 'true' // 当运行dev:mock时为true
module.exports = {
devServer: {
// ...
proxy: {
// 所有匹配/api的请求会被转发到target
'/api': {
// 开启mock时 设置target为本地代理服务地址,这里端口3306与serve中监听端口保持一致
target: IS_MOCK ? 'http://localhost:3306/' : 'xxxxx',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
},
}
},
}
修改一下上文创建axios实例的代码instance.defaults.baseURL = "/api"
,设置一下所有请求的baseURL
,以此来使proxy配置匹配所有请求
完成上述配置,我们重启前端应用,并且启动我们的服务端代码就可以使用mock功能了 ,我们可以修改一下脚本,方便当我们开启mock功能的时候自动启动serve服务
// package.json
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev:mock": "cross-env IS_MOCK=true vue-cli-service serve & node serve.js",
},
其实到这里,mock的功能已经可用了,我们还可以基于现有的代码结合自身的需求的进行拓展
拓展
我在项目中使用的时候进行了一点点拓展,给大家提供一个拓展的参考
修改mock
配置文件
module.exports = {
'/products':{
code:0,
data:[{
id:1,
name:'商品1'
}],
message:'success',
config:{
method:'GET',
delay: 1000,
validator:(request)=>{
const error = {
status:400,
msg:'请检查参数是否合法!'
}
const success = {
status:200,
msg:'success'
}
// 假设该请求需要一个必传参数 timestamp
return request.query.timestamp ? success : error
},
}
},
}
加了额外的config字段进行一些差异化配置,例如指定响应延时 1000ms 必传参数校验等
修改serve
端代码
const express = require("express");
const mockMap = require("./mock");
const app = express();
app.use(express.json());
// 请求过滤器
const requestFilter = (req, res, next) => {
const { path,method } = req;
// 设置相应头 处理中文乱码
res.set('Content-Type', 'text/plain')
// 404 提前过滤
if (!mockMap[path]) {
res.status(404).end();
}
// 请求方法不匹配提前过滤
if(method !== mockMap[path].config?.method ?? 'GET'){
res.status(405).end('请检查请求方法是否正确!')
}
// 自定义校验规则不匹配过滤
if (mockMap[path].config && mockMap[path].config.validator) {
const data = mockMap[path].config.validator(req);
if (data.status !== 200) {
res.status(data.status).end(data.msg)
}
}
setTimeout(() => {
next();
}, mockMap[path].config?.delay ?? 0);
};
app.all("*", requestFilter, async (req, res, next) => {
const { path } = req;
// 移除config字段
const { code,data,message } = mockMap[path]
res.send({code,data,message});
});
app.listen("3306", () => {
console.log("serve is running at port 3306");
});
其实就是加了个中间件去处理额外的一些逻辑,按需拓展即可
上述就是我介绍的两种前端实现请求Mock的方案,配置简单,新老项目都能轻易的插拔上述Mock功能来提高开发的效率,最后欢迎指正文中错误~