一、背景
前后端分离,在日常开发中,一直是开发人员的刚需。
作为前端,期望能够在开发过程中,不依赖后台接口的开发进度,直接自己模拟后台返回数据,在没有真实接口和数据的情况下,跑通接口调用逻辑,对返回数据进行处理、或是界面展示。
笔者所在团队的解决方案:
使用内部在线mock平台,由后台同学在平台上定义各个接口名称及其返回的数据结构,前端同学根据平台上给出的接口,进行相应开发,及接口调用逻辑测试。
笔者开发体验:
1.接口名称及返回数据结构,都由后台同学定义给出,按照规范,前端同学除添加数据外,不可以擅自操作mock平台上的接口。(避免造成混乱)因此,mock数据的创建,十分依赖后台同学的工作,前端同学没有自主性。实际开发中,也常出现需要等待mock数据的情况,造成前端接口调用相关逻辑被阻滞、无法流畅开发的问题。
2.在线的mock平台要切换到浏览器界面操作。当需要对mock数据进行编辑,需将页面切换到编辑页、并进行保存等相关操作,效率比较低下。
二、期望开发体验
笔者期望的开发体验是,当我需要调用某个接口的时候,可以自主模拟这样一个接口,以及它返回的数据结构+数据,而不需要依赖后台同学的工作。并且,对接口返回数据的编辑,全部放在我们最熟悉的编辑器里。
【最优体验】
本地mock数据的文件夹层级,与接口url相对应。可以通过文件夹名称,快速定位到指定接口返回的mock数据。
如:接口 /jsonServer/user/getUserName ,对应的mock数据,存放在 mock文件夹下的 jsonServer/user/getUserName.json 中。
有人可能会说,不知道接口的url和返回的数据结构,凭自己的想象模拟,和后台同学实际设计的接口大概率不会吻合,拿到真实接口之后,还需要做修改,这是一种浪费。
我认为,这种浪费,相比被动等待、依赖别人给mock接口,高效太多。况且,很多接口返回的数据结构,是完全可以推测出来的(类似返回列表、详情等),对不上的,可能只是字段名称而已,修改成本很低。
同时,如果有了自主mock的能力,我们甚至可以拿着自己推测的数据结构,找后台同学对接,最起码,这个时候,我们有主动推进的资本,而不是完全被动等待。
三、创建mock-server.js
const jsonServer = require('json-server');
// mark:要提前创建db.js,返回我们的mock数据
const $db = require('./db');
const server = jsonServer.create();
const middlewares = jsonServer.defaults();
const router = jsonServer.router($db);
server.use(router);
// Set default middlewares (logger, static, cors and no-cache)
server.use(middlewares);
// To handle POST, PUT and PATCH you need to use a body-parser
server.use(jsonServer.bodyParser);
server.listen(3001, () => {
console.log('JSON Server is running at 3001');
});
四、整合出db.json
根据json-server的原理,它返回的数据全部存储在db.json这一个json文件中。
而我们开发用的mock数据,显然不能全部手动塞到这一个json中。
为了达到前面说的最优体验,在创建好各个接口对应的json文件后,我们需要用简单的文件操作,将这些json文件合并为db.json。
我的mock目录(mock位于根目录下)结构如下(因为要进行文件操作,所以使用db.js):
db.js如下:
const $fs = require('fs');
const mockData = {
DB: { },
readDir(path) {
const stats = $fs.statSync(path);
if (stats.isDirectory()) {
const files = $fs.readdirSync(path);
files.forEach(file => {
this.readDir(`${path}/${file}`);
});
} else {
const data = $fs.readFileSync(path);
// 把json文件的路径作为key
const key = path.replace('.json', '');
this.DB[key] = JSON.parse(data);
}
}
};
mockData.readDir('mock/data');
module.exports = mockData.DB;
ok,至此,mock-server和mock数据全部准备好了,node mock-server.js,启动试一下。
错误写的很明白,database、也就是db.json的属性里,不能包含字符 / 。
很显然,是 this.DB[key] = JSON.parse(data); 这句话,造成的报错。
怎么办?
key值里面不能有 / ,改造呗。
const key = path.replace('.json', '');
改成
const key = path.replace('.json', '').replace(/\//g, '_');
再启动,不报错。
五、读取db.json的数据
成功生成db.json后,看一下它的结构(浏览器访问:localhost:3001/db):
显然,这时候,当我们想获取某一个接口对应的数据时,需要这样访问:
http://localhost:3001/mock_data_steps_step1
这自然不是我们期望的访问方式。
我们期望的是,访问:http://localhost:3001/steps/step1
可以拿到上图所示的数据。
这才是模拟接口调用该有的样子。
六、做些努力,实现更真实的接口调用方式
1.首先去掉当前key的mock_data_字样,简化key值
const key = path.replace('.json', '').replace(/\//g, '_');
改成
const key = path.replace('.json', '').replace('mock/data/', '').replace(/\//g, '_');
再看一下db.json:
嗯,清爽多了。
2.将接口url映射成db.json的key值
还记得自定义路由吗(参考:json-server全攻略)?说到路由映射,用自定义路由呗
(非最终解决方案。存在bug,笔者未解决,如有方案,欢迎留言讨论!最终解决方案见【七 — 4】)
其中,routeHandler.js需要返回一个json对象,指明路由配置规则。
这里的routeHandler.js:
function routeHandler(db) {
const rewriter = {};
Object.keys(db).forEach(key => {
const routeKey = `/${key.replace(/_/g, '/')}`;
rewriter[routeKey] = `/${key}`;
});
return rewriter;
}
module.exports = routeHandler;
打印一下返回的rewiriter:
访问 http://localhost:3001/steps/step1:
成功拿到所需数据。
但是
但是
但是
从页面发送请求,会有问题!详情见下文。
七、从页面发请求,拉取mock数据
mock服务和数据准备好,接下来自然是要使用了。
同时启动vue-cli-service和mock-server两个服务(一个跑页面,一个跑数据),并在页面发送请求。
1.首先,将所有请求代理到3001端口上:
vue.config.js配置:
devServer: {
port: 3000,
proxy: 'http://localhost:3001'
}
2.简单封装request.js。这里使用axios发送异步请求:
import $axios from 'axios';
function request(options) {
let conf = Object.assign({
url: '',
method: 'get',
responseType: 'text',
ContentType: 'application/json'
}, options);
let pm = $axios(conf).then(xhr => {
console.log('xhr:', xhr);
return Promise.resolve(xhr.data);
}).catch(err => {
console.log('err:', err);
return Promise.reject(new Error('request 抛出错误'));
});
return pm;
}
export default request;
3.页面模拟请求发送:
page.vue:
$request({
url: '/steps/step2'
method: 'get',
params: { name: 'test' }
}).then(rs => {
console.log('rs:', rs);
}).catch(err => {
console.log('err:', err);
}
404,推测两种可能,要么是代理没生效,要么是路由映射出了问题。
将请求的 url: '/steps/step2' 改成 url: '/steps_step2':
成功拿到mock数据。说明3001端口代理生效。
4.放弃自定义路由jsonServer.rewriter,改为手动映射路由(路由问题最终解决方案)
更改mock-server.js:
const jsonServer = require('json-server');
const $db = require('./db');
const server = jsonServer.create();
const middlewares = jsonServer.defaults();
const router = jsonServer.router($db);
// Set default middlewares (logger, static, cors and no-cache)
server.use(middlewares);
// To handle POST, PUT and PATCH you need to use a body-parser
server.use(jsonServer.bodyParser);
// 拦截客户端请求,进行自定义处理
server.use((req, res, next) => {
// 手动映射,更改请求url(/steps/step1 => /steps_step1)
req.url = req.url.replace(/\//g, '_').replace('_', '/');
next();
});
server.use(router);
server.listen(3001, () => {
console.log('JSON Server is running at 3001');
});
再次尝试发送请求:
成功拿到mock数据。
八、优化mock数据设计
经过前面的配置,我们已经可以自由地使用json-server,进行完全由我们自己掌控的mock体验。
但是,后端给我们返回的数据结构,通常如前面例子中所示。往往是一个对象的形式,其中包含code、data,这样两个字段。
这就造成了一些问题:
(1)当接口返回的data是数组数据(通常是列表),由于数组被包在 { code: 0, data: [ xxx ] } 这个数据结构里面,我们就没有办法使用json-server各种便捷的数组过滤功能。
(2)我们无法使用json-server的功能,对db.json的数据进行期望的写入操作。一旦操作,就会破坏掉 { code: 0, data: [ xxx ] } 这个数据结构。
显而易见,在mock数据中直接使用code+data的数据结构,并不合适。
或许你会说,很简单啊,创建mock数据的时候,不按照这种格式,直接假设返回的是data的值,不就可以了吗?
没错,这样是可以完美解决上面两个问题。
但是造成了另一个问题:如何模拟code不为0的返回结果呢?
【解决方案】
笔者想到的解决方案是,在data目录下,创建success和fail文件夹,分别存放模拟成功和失败的数据。
目录结构可参考“四”中截图。
这样,success文件夹下的数据,默认都是 code: 0 的,fail下的数据,依然采用 { code: xxx, data: xxx } 的结构。
优化后的目录结构:
success下的step1.json:
fail下的step1.json:
九、一次开发,无限复用。健壮清爽的mock体验,你值得拥有
最后,总结一下各文件:
1.mock-server.js
const jsonServer = require('json-server');
const $db = require('./db');
const server = jsonServer.create();
const middlewares = jsonServer.defaults();
const router = jsonServer.router($db);
// Set default middlewares (logger, static, cors and no-cache)
server.use(middlewares);
// To handle POST, PUT and PATCH you need to use a body-parser
server.use(jsonServer.bodyParser);
// 拦截客户端请求,进行自定义处理
server.use((req, res, next) => {
// 手动映射,更改请求url(/steps/step1 => /steps_step1)
req.url = req.url.replace(/\//g, '_').replace('_', '/');
next();
});
server.use(router);
server.listen(3001, () => {
console.log('JSON Server is running at 3001');
});
2.db.js
const $fs = require('fs');
const mockData = {
DB: {},
readDir(path) {
const stats = $fs.statSync(path);
if (stats.isDirectory()) {
const files = $fs.readdirSync(path);
files.forEach(file => {
this.readDir(`${path}/${file}`);
});
} else {
const data = $fs.readFileSync(path);
const key = path.replace('.json', '').replace('mock/data/', '').replace(/\//g, '_');
this.DB[key] = JSON.parse(data);
}
}
};
mockData.readDir('mock/data');
module.exports = mockData.DB;
3.request.js
import $axios from 'axios';
function request(options) {
// 是否走mock数据。开发期间手动更改
const isMock = false;
let conf = Object.assign({
url: '',
method: 'get',
responseType: 'text',
ContentType: 'application/json'
}, options);
// 处理mock。默认返回成功数据,如指定mockFail,则返回失败数据
if (options.mockFail) {
conf.url = '/fail' + conf.url;
} else if (isMock) {
conf.url = '/success' + conf.url;
}
let pm = $axios(conf).then(xhr => {
console.log('xhr:', xhr);
return Promise.resolve(xhr.data);
}).catch(err => {
console.log('err:', err);
return Promise.reject(new Error('request 抛出错误'));
});
return pm;
}
export default request;
4.页面请求示例
$request({
url: '/test',
method: 'get',
params: { name: '111' },
mockFail: true
}).then(rs => {
console.log('rs:', rs);
}).catch(err => {
console.log('err:', err);
});
【附:mock方案设计中的其他问题】
db.json是由db.js生成并返回的,因此,每次重启mock-server,都会根据mock文件夹下的json文件重新生成db.json。也就是说,上一次对db.json的任何写入、删除等操作,都不会被保存,只要重启服务,数据就会恢复原样。这里,可以根据自己的需求,自行选择要不要在操作db.json的同时,同步改写mock文件夹保存的json数据。
#菜鸟一枚,如有错误、或更优方案等,诚请指出、指导。另,jsonServer.rewriter未生效问题,求指导。