前言
项目开发过程中,前端开发需要依赖后台的环境数据,而后台人员通常是无法立即提供数据给到前端。为了使前后端开发互不影响,后台一般会先定义好接口文档并提供给前端,前端就可以通过接口文档进行本地数据模拟后台环境数据,实现前后端并行开发,前端也可以根据假数据进行开发自测,做到缺陷前移。
本文提供一种约定式的本地数据模拟方案的实现,该方案在现在看来可能不是最佳的方案,但是方案的实现过程还是值得拎出来讲一讲,学习一下其中的实现思路的。
实现效果如下:
在api目录下新建api文件,将mock数据与api请求的配置写在一起,项目启动时扫描api目录下所有文件,读取api请求及其mock配置,生成mock数据。
// @file src/api/vm.js
import defineRequest from 'xxx';
export default {
fetchVmData: defineRequest({
url: '/api/vm',
type: 'GET', // 一般都是模拟get请求数据
params (params) {
return params;
},
mock: {
'data|1-5': [{
name: '@cname',
id: '@uuid',
status: '@boolean',
}],
},
}),
};
复制代码
具体实现过程
1. 递归扫描api目录
第一步,递归扫描api目录下的所有文件,返回文件数组。
const scanApi = (baseDir) => {
const result = [];
const files = fs.readFileSync(baseDir);
for (const file of files) {
const current = path.join(basePath, file);
const stat = fs.statSync(current);
const isFile = stat.isFile();
const isDir = stat.isDirectory();
if (isFile) {
result.push(current);
} else if (isDir) {
result = result.concat(scanApi(current));
}
}
return result;
};
复制代码
第二步,读取文件数组中每个文件的内容
const readFileContent = (files) => {
return files.map(currentFile => fs.readFileSync(currentFile).toString());
};
复制代码
2. 提取mock配置
第三步,从文件内容中提取我们需要的mock配置,那如何提取mock配置呢?写一个正则提取吗?也是可行的,只不过需要我们去维护多个复杂的正则表达式,除了正则提取,我们还可以借助jscodeshift
神器进行提取。jscodeshift
是将js、ts代码解析为抽象语法树,并提供一系列用于访问和修改抽象语法树的操作接口(抽象语法树是以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构,推荐一个AST可视化工具)。
从上图的ast分析可知,我们要先找到defineRequest
的调用处,然后从调用处获取arguments[0]
来读取mock相关配置。
const jAst = require('jscodeshift');
// 获取项目里所有的defineRequest
const getDefineRequests = (contents) => {
return contents.reduce((total, content) => {
const ast = jAst(content); // 字符串转抽象语法树
const arr = ast.find(jAst.CallExpression, {
callee: {
// 找到节点类型为函数调用且名字为defineRequest
name: 'defineRequest',
},
}).map(item => item.parent); // 单文件可以有多个defineRequest
total = total.concat(arr);
return total;
}, []);
};
复制代码
const getConfigMap = (defineRequests) => {
const result = {};
defineRequests.forEach(({ value: item }) => {
const name = item.key.name;
const properties = item.value.arguments[0].properties;
// 存放每个defineRequest的'url', 'type', 'mock'配置
const cfgMap = ['url', 'type', 'mock'].reduce((map, keyName) => {
const target = properties.find(prop => prop.key.name === keyName);
let value;
if (target?.value) {
// 给value赋值
eval(`value = ${jAst(target.value).toSource()}`);
}
map[keyName] = value;
return map;
}, {});
result[name] = cfgMap;
});
return result;
};
复制代码
// map转数组
const getConfigList = (map) => {
return Object.entries(map).map(([key, value]) => {
return {
name: key,
url,
type: value.type || 'get',
response: value.mock,
};
}).filter(obj => obj.response); // 过滤掉没有配置mock的
};
复制代码
至此,我们就拿到了所有的mock配置,mock字段的内容可以传给mockjs进行假数据生成,项目如果是使用webpack
构建的话,我们可以在webpack-dev-server
(v4.7.x)的setupMiddlewares
钩子方法里,拿到devServer.app
,通过匹配路由响应相关的mock数据了(本文重点在于讲基于ast实现本地数据模拟,匹配路由响应数据的代码实现就略过)。
遇到的问题和应对措施
方案是实现出来了,但是还是存在一些问题的,接下来讲下遇到的问题以及如何解决。
1. 不支持解析ts
如果你在api文件中使用ts写代码,是会报错的,因为jscodeshift
默认是不解析typescript
的,我们需要给jscodeshift
传一个配置
const jAst = require('jscodeshift');
const parser = require('jscodeshift/parser/ts')();
const ast = jAst(content); // old
const ast = jAst(content, { parser }); // new
复制代码
2. 不支持解析export let xx = defineRequest(...)
上文我们是针对export default { x: defineRequest(...) }
的抽象语法树进行操作,如果是export let x = defineRequest(...)
,这个时候它的抽象语法树就变得不一样了,我们需要额外写代码处理下这种情况。
// old
const name = item.key.name;
const properties = item.value.arguments[0].properties;
// new
const { name, properties } = ((item) => {
if (item.type === 'VariableDeclarator') {
// 解析let x = defineRequest(...)
return {
name: item.id.name,
properties: item.init.arguments[0].properties,
};
}
// 解析export default { x: defineRequest(...) }
return {
name: item.key.name,
properties: item.value.arguments[0].properties,
};
})(item);
复制代码
3. 不支持在api文件使用花式写法
如果你的api文件是像下面这么写的,那么是会解析报错的
const mock = {
'data|1-5': [{
name: '@cname',
id: '@uuid',
status: '@boolean',
}],
};
export default {
fetchVmData: defineRequest({
url: '/api/vm',
type: 'GET', // 一般都是模拟get请求数据
params (params) {
return params;
},
mock,
}),
fetchHostData: defineRequest({
url: '/api/host',
type: 'GET', // 一般都是模拟get请求数据
params (params) {
return params;
},
mock,
}),
};
复制代码
这种写法导致抽象语法树又发生了变化,获取不到mock的配置了,也就是说,本文的方案是一种约定式的本地数据模拟方案,不支持开发者在api文件里进行各种花式写法,最好的写法就是按照方案里支持的写法进行去写代码。值得提一下的是,我们可以基于AST去对老代码进行重构,而基于AST实现的难点就在于代码的各种花式写法太多了,我们很容易就遗漏某些骚写法,或者难以针对某种骚写法访问修改它的AST。
4. mock代码会被打包到生产环境
我们把mock代码传给了defineRequest
,mock代码与api请求代码耦合在了一起,项目打包的时候,也会把mock代码打包到生产环境中。我们还需要写一个删除mock代码的插件,在打包时把mock代码删掉。
jAst(content).find(jAst.Property, {
key: {
type: 'Identifier',
name: 'mock',
},
}).filter(p => {
const node = p.parent.parent.node;
if (node.type === 'CallExpression') {
const callee = node.callee;
return callee.type === 'Identifier' && callee.name === 'defineRequest';
}
return false;
}).remove().toSource();
复制代码
虽然mock代码与api请求代码耦合在了一起,但是也不完全是一件坏事。当你的api请求代码废弃时,将api请求代码删除的同时也会把mock代码也给删掉,如果把mock代码抽出去,很多开发者在删除废弃api请求代码时,总是会忘记把mock代码也删掉。
通篇写下来,基于AST实现本地数据模拟方案的方式存在着许多局限性,显然不是最佳的本地数据模拟方案实现。我在很久很久以前,曾经参考umijs实现过另外一套本地数据模拟方案:sxf-dev-mock。可以将这两套方案实现进行对比,我觉得还是umijs的方案可用性会好很多。
最后的最后,感谢我的导师锐哥、同事炜哥提供的技术方案、技术难点攻克。