为了避免后端接口开发进度影响前端开发,一般和后端约定好接口后前端可以使用本地mock数据的方式进行开发
本文会演示如何搭建一个mock服务器demo地址。
说明一下,本文搭建的mock服务器的优点:
1、mock文件可拆分(不需要在任何地方引入)
2、mock数据可动态修改(不需要重启服务器)
3、完全模拟后端接口请求,可模拟接口错误、接口延迟、接口参数等
4、支持文件上传接口
5、可自定义接口返回数据逻辑
优点:简单
缺点:直接通过前端写死数据,这种方式侵入性比较大,后面替换正式接口改动较多,模拟效果不好
优点:代码侵入性较小
缺点:mockjs拦截的是ajax请求,所以对使用fetch无效;network看不到发送的请求;对代码还是有侵入性
const mock = Mock = require('mockjs')
Mock.mock('/api/sign/out', 'get', (options) => {
console.log(options);
return {
data: true,
code: 0,
message:'success'
}
})
// 请求代码如下
axios.get('/api/sign/out')
.then(function (response) {
console.log(response);
})
优点:无代码侵入性
缺点:配置麻烦、mock数据不方便、需要后端支持
优点:无代码侵入性
缺点:暂无
在介绍如何搭建mock服务器时,先看看最终搭建mock服务器后,前端是如何使用mock服务器的
在mock文件夹下创建mock数据即可
/**
* feat 1.支持 mock 文件拆分:mock/ 文件夹下面的所有 js 都会被框架自动加载,因此你可以把接口合理的拆分到多个 mock 文件中
* feat 2.支持模拟延迟(setTimeout 返回)
* feat 3.支持 mock 动态数据
*
*/
const Mock = require("mockjs");
// 项目接口 本地 mock 数据
module.exports = {
// 支持值为 Object 和 Array
"GET /api/users": { users: [1, 2] },
// GET 可忽略
"/api/users/1": { id: 1422 },
"/api/error": (req, res) => {
setTimeout(() => {
// 模拟接口报错
res.status(400).json({
error: "模拟接口报错",
});
}, 1000);
},
// 支持mockjs和function,req和res参考express
'GET /api/list': (req, res) => {
const { pageSize = 10, pageIndex = 1 } = req.query;
const startIndex = (pageIndex - 1) * pageSize + 1;
setTimeout(() => {
res.send(
Mock.mock({
success: 444,
data: {
[`data|${pageSize}`]: [
{
'id|+1': startIndex,
name: '@name',
'number|+1': startIndex,
title: '@title',
'price|1000-4000': 400,
city: '@city',
'age|20-30': 22,
address: `@county(true)`,
link: 'www.baidu.com',
column: 'column',
'status|+1': ['unpack', 'mailed', 'sending', 'received'],
purchasePerson: '@csentence',
time: '@date',
},
],
total: 100,
},
})
);
}, 1000);
},
};
fetch("/api/list?pageSize=20", { method: "get" })
.then((res) => {
return res.json();
})
.then((res) => {
console.log("res: ", res);
});
}
创建mock文件后不需做任何处理,直接可以使用mock服务了。
我们项目本地开发时一般都会有一个服务器,比如webpack的webpack-dev-server,所以我们不需要单独的再开发一个node服务器,而直接使用webpack-dev-server编写mock服务器即可
devServer: {
before(app) {
app.get('/test', function (req, res) {
res.json({
msg: '接口成功',
success: true,
code: 0,
});
});
app.post('/test2', function (req, res) {
res.json({
data: [{ name: 1 }],
msg: '接口成功',
success: true,
code: 0,
});
});
},
},
webpack-dev-server提供在服务器内部的所有其他中间件之前执行自定义中间件的能力。before配置(webpack5.0是onBeforeSetupMiddleware配置)
我们可以在这里自定义我们要mock的接口即可。但是要考虑新的问题:
1、每次新增或修改mock接口时都要重新去修改webpack配置文件吗(webpack配置文件修改是需要重新启动的)
2、mock数据和webpack配置耦合
解决方法:
我们可以在项目根目录下创建mock文件夹
然后使用node递归遍历mock文件夹生成mock数据map
function getFilesSync(filePath, result) {
try {
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
const filePaths = fs.readdirSync(filePath);
filePaths.forEach((item) => {
const itemFilePath = path.join(filePath, item);
getFilesSync(itemFilePath, result);
});
} else {
delete require.cache[require.resolve(filePath)];
const file = require(filePath);
Object.assign(result, file);
}
} catch (error) {
console.log("error: ", error);
}
}
devServer: {
before(app) {
const apiMap = {};
getFilesSync(path.join(process.cwd(), "mock"), apiMap);
for (const [key, handle] of Object.entries(apiMap)) {
const arr = key.split(" ").reverse();
const api = arr[0];
const method = arr[1] ? arr[1].toLowerCase() : "get";
app[method](
api,
typeof handle === "function"
? handle
: (req, res) => {
res.send(handle);
}
);
}
},
},
上面会在webpack-dev-server启动时自动遍历mock文件夹并注册好所有mock接口。但这里只是解决了自动注册mock接口,mock接口修改时,还是需要手动重启服务器。如何解决?
我的想法是监听mock文件夹,当mock文件夹修改时自动重启服务器
webpack-dev-server内部也是用chokidar这个库来监听的文件变化的,这里我们也是用chokidar
// server.js
const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.config.js');
const proxy = require('./proxyConfig');
const chokidar = require('chokidar');
const compiler = Webpack(webpackConfig);
const server = new WebpackDevServer({
...webpackConfig.devServer,
proxy
}, compiler);
function createServer() {
const server = new WebpackDevServer({
...webpackConfig.devServer,
proxy
}, compiler);
server.startCallback(() => {
const watcher = chokidar.watch('./proxyConfig.js').on('change', () => {
console.log("checked dev-server proxy changes, restarting server");
server.stopCallback(() => {
watcher.close();
createServer();
});
});
});
}
createServer();
在 WDS 提供的 Nodejs API 的基础上,我们利用startCallback回调注册chokidar监听文件的程序,当检测到proxy配置的文件变化后,stop WDS 并重启一个新的 WDS 实例。
到此我们就基本完成了一个mock服务器。但是上述mock服务器还是有一个非常大的缺点。
每次修改mock接口,比如新增一个mock接口都需要重启服务器,虽然重启操作是自动完成的,但我还是需要等待重启。
理想情况下是我对mock数据修改后服务器无需重启就能生效。但是修改接口是需要重新注册接口的,重新注册接口就必须要重启服务器,似乎陷入了一个死循环。
换个思路,既然每添加一个接口路由都需要重新注册,那我们直接代理所有路由,然后根据接口路径返回不同数据
devServer: {
before(app) {
app.use('*', function (req, res) {
const method = req.method.toLowerCase();
const baseUrl = req.baseUrl;
if (method === 'get') {
if (baseUrl === '/test') {
res.send({
msg: '接口成功',
success: true,
code: 0,
});
}
}
if (method === 'post') {
if (baseUrl === '/test2') {
res.send({
data: [{ name: 1 }],
msg: '接口成功',
success: true,
code: 0,
});
}
}
});
},
},
当然返回值也是动态取的,上面只是演示原理。不过又带了了新问题:因为代理了所有接口,我们项目获取html的接口实际上也被代理了,但这是并没有返回html所以页面都打不开。我第一想法是所有不匹配的路由直接返回html,但是webpack-dev-server打包的文件都存放在内存中,我通过node获取不到webpack-dev-server打包的文件,所以无法返回。如何解决?gg
最后我想到使用express中间件,当没有匹配的接口路由时,直接调用next() 交个webpack-dev-server自己处理
最终代码:
// mockMiddleware.js
function getFilesSync(filePath, result) {
try {
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
const filePaths = fs.readdirSync(filePath);
filePaths.forEach((item) => {
const itemFilePath = path.join(filePath, item);
getFilesSync(itemFilePath, result);
});
} else {
delete require.cache[require.resolve(filePath)];
const file = require(filePath);
Object.assign(result, file);
}
} catch (error) {
// console.log('error: ', error);
}
}
module.exports = function mockMiddleware() {
return function (req, res, next) {
const method = req.method.toLowerCase();
const baseUrl = req.baseUrl;
const apiMap = {};
getFilesSync(path.join(process.cwd(), 'mock'), apiMap);
for (const [key, handle] of Object.entries(apiMap)) {
const arr = key.split(' ').reverse();
const api = arr[0];
const _method = arr[1] ? arr[1].toLowerCase() : 'get';
if (_method === method && api === baseUrl) {
if (typeof handle === 'function') {
handle(req, res);
} else {
res.send(handle);
}
return;
}
}
next();
};
};
// webpack配置
devServer: {
contentBase: 'static',
before(app) {
app.use('*', mockMiddleware());
},
},