需求背景
以往在开发项目时,都是手动维护一张路由表,在低代码平台中,我们希望把这部分工作自动化,因此产生了路由自动生成这个需求。
路由表是一个数组,里面每项都是json对象,生成路由表就是对这个数组进行操作,产出一段路由表代码,最终写入前端页面的过程。
既然要产生代码,那数据库中存的也是代码片段,就不能用简单的JSON对象对其修改,需要用到Babel对代码进行操作,主要有三步:
解析代码(Parse)
使用@babel/parser将代码解析成AST(抽象语法树),语法树是由语法单元(注释、空白、字符串、括号、数字、标识符等)组成的数组,让计算机理解我们的代码
修改代码(Transform)
代码解析后用@babel/traverse遍历每个语法树节点,进行修改、删除、更新操作
生成代码(Generate)
修改后的语法树需要@babel/generator生成代码
假设有个空的路由表routes
const routes = [];
我们要插入如下路由
{
"name": "home",
"path": "/",
"meta": {
"title": "首页"
}
}
然后通过name查找路由,并修改其meta属性中的title为“缺省页”
const routes = [
{
"name": "home",
"path": "/",
"meta": {
"title": "缺省页"
}
}
];
最后将其删除。
本文将详细介绍路由的创建、修改、删除。
技术方案
技术选型
@babel/types:生成语法树节点
@babel/parser:解析代码片段
@babel/template:将代码片段转换成AST(抽象语法树)
@babel/traverse:对AST(抽象语法树)进行遍历
@babel/generator: 将AST(抽象语法树)转换成代码片段
主要流程
安装依赖
yarn install @babel/parser @babel/types @babel/traverse @babel/template @babel/generator
代码结构
const { parse } = require('@babel/parser');
const t = require('@babel/types');
const traverse = require('@babel/traverse').default;
const template = require('@babel/template').default;
const generate = require('@babel/generator').default;
const code = `
// 全局路由表
const routes = [];
`;
const action = ''; // 操作动作:add(更新)/update(修改)/remove(删除)
const name = ''; // 查找路由的name
const payload = ''; // 新增或修改时的值
const ast = parse(code, {
sourceType: 'module',
});
// 生成路由
const genRoutesCode = ({ action, name, payload }) => {
// 遍历器
const visitor = {
// 数组表达式节点
ArrayExpression(path) {},
// 对象表达式节点
ObjectExpression(path) {}
};
// 对 ast 进行深度遍历
traverse(ast, visitor);
return generate(ast, {
jsescOption: { minimal: true } // 防止生成代码时中文变成unicode
}).code;
};
const result = genRoutesCode({
action,
name,
payload
});
console.log(result);
首先看下我们的需求,插入路由要对routes数组进行操作,因此visitor遍历器中要对ArrayExpression类型节点进行遍历,而修改和删除都是对json对象的操作,所以visitor中定义了ObjectExpression。
这里的ArrayExpression和ObjectExpression是语法树中的节点类型,具体有哪些不用记,用的时候到astexplorer.net/ 中去看就可以了
插入路由
如上所述,插入路由需要对语法树中的ArrayExpression即数组表达式节点进行操作,因为我们新建的路由可能存在多级json结构,需要写一个buildAST方法,对每一级节点进行判断,如果当前的值类型为object时,则需递归调用,生成节点我们用了@babel/types这个工具。
// 数组表达式节点
ArrayExpression(path) {
const { elements } = path.node;
// 新增路由
if (action === 'add') {
// 构建语法树
const buildAST = (source, tree) => {
Object.entries(source).forEach(([key, val]) => {
if (typeof val === 'string') {
tree.push(t.objectProperty(t.identifier(key), t.stringLiteral(val)));
}
if (typeof val === 'number') {
tree.push(t.objectProperty(t.identifier(key), t.NumericLiteral(val)));
}
if (typeof val === 'object') {
const child = buildAST(val, []);
tree.push(t.objectProperty(t.identifier(key), child));
}
});
return t.objectExpression(tree);
};
const ast = buildAST(payload, [t.objectProperty(t.identifier('component'), t.identifier('window.sam.PageRender'))]);
elements.push(routeAST);
}
可以看到,每一种类型的值都需要进行判断并生成节点操作,小范围的代码还可以接受,如果要插入一大段代码会很麻烦,好在babel提供了@babel/template解决这个问题,template可以构造不同类型的代码(statement声明/expression表达式/program程序),我们的json可以用expression方法构造,将我们的代码修改下
// 数组表达式节点
ArrayExpression(path) {
const { elements } = path.node;
// 新增路由
if (action === 'add') {
const routeBase = generate(template.expression(`DATA`)({
DATA: JSON.stringify(payload)
})).code;
const routeBuild = template.expression(`{BASE,COMP}`);
const routeAST = routeBuild({
COMP: '"component":window.PageRender',
BASE: routeBase.replace(/^{|}$/g, ''),
});
elements.push(routeAST);
}
}
修改路由
首先我们要通过name在节点树中找到路由对应的节点,然后对其值进行修改。
// 对象表达式节点
ObjectExpression(path) {
let route;
// 查找路由
path.node.properties.forEach(prop => {
const propKey = prop.key.name || prop.key.value;
if (propKey === 'name' && prop.value.value === name) {
route = path;
}
});
// 更新路由
if (action === 'update' && route) {
const modifer = (key, val, prop) => {
const propKey = prop.key.name || prop.key.value;
// 设置节点值
if ((propKey === key) && typeof val !== 'object') {
prop.value.value = val;
}
if ((propKey === key) && typeof val === 'object') {
prop.value.properties.forEach(child => {
const childKey = child.key.name || child.key.value;
modifer(childKey, val[childKey], child);
});
}
};
// 遍历路由节点属性
route.node.properties.forEach(prop => {
Object.entries(payload).forEach(([key, val]) => {
modifer(key, val, prop);
});
});
}
}
删除节点
同上找到节点后,调用path上的remove方法将节点移除。
// 对象表达式节点
ObjectExpression(path) {
let route;
// 查找路由
path.node.properties.forEach(prop => {
const propKey = prop.key.name || prop.key.value;
if (propKey === 'name' && prop.value.value === name) {
route = path;
}
});
// 删除路由
if (action === 'remove' && route) {
route.remove();
}
}
完整示例
const { parse } = require('@babel/parser');
const t = require('@babel/types');
const traverse = require('@babel/traverse').default;
const template = require('@babel/template').default;
const generate = require('@babel/generator').default;
const code = `
// 全局路由表
window.sam.routes = [{
name: 'home',
path: '/',
meta: {
title: '首页'
},
component: window.sam.PageRender
}, {
name: 'index',
path: '/',
meta: {
title: '首页index'
},
component: window.sam.PageRender
}];
`;
const ast = parse(code, {
sourceType: 'module',
});
/* const action = 'add'
const name = 'home';
const payload = {
name: 'detail',
path: '/detail',
meta: {
title: '详情页'
}
}; */
/* const action = 'remove';
const name = 'index';
const payload = {}; */
const action = 'update';
const name = 'home';
const payload = {
name: 'default',
path: '/default',
meta: {
title: '缺省页'
}
};
const transformRoutesAST = ({ action, name, payload }) => {
const visitor = {
ArrayExpression(path) {
const { elements } = path.node;
// 新增路由
if (action === 'add') {
/* const buildAST = (source, tree) => {
Object.entries(source).forEach(([key, val]) => {
if (typeof val === 'string') {
tree.push(t.objectProperty(t.identifier(key), t.stringLiteral(val)));
}
if (typeof val === 'number') {
tree.push(t.objectProperty(t.identifier(key), t.NumericLiteral(val)));
}
if (typeof val === 'object') {
const child = buildAST(val, []);
tree.push(t.objectProperty(t.identifier(key), child));
}
});
return t.objectExpression(tree);
};
const ast = buildAST(payload, [t.objectProperty(t.identifier('component'), t.identifier('window.sam.PageRender'))]);
*/
const routeBase = generate(template.expression(`DATA`)({
DATA: JSON.stringify(payload)
})).code;
const routeBuild = template.expression(`{BASE,COMP}`);
const routeAST = routeBuild({
COMP: '"component":window.sam.PageRender',
BASE: routeBase.replace(/^{|}$/g, ''),
});
elements.push(routeAST);
}
},
ObjectExpression(path) {
let route;
// 查找路由
path.node.properties.forEach(prop => {
const propKey = prop.key.name || prop.key.value;
if (propKey === 'name' && prop.value.value === name) {
route = path;
}
});
// 删除路由
if (action === 'remove' && route) {
route.remove();
}
// 更新路由
if (action === 'update' && route) {
const modifer = (key, val, prop) => {
const propKey = prop.key.name || prop.key.value;
// 设置节点值
if ((propKey === key) && typeof val !== 'object') {
prop.value.value = val;
}
if ((propKey === key) && typeof val === 'object') {
prop.value.properties.forEach(child => {
const childKey = child.key.name || child.key.value;
modifer(childKey, val[childKey], child);
});
}
};
// 遍历路由节点属性
route.node.properties.forEach(prop => {
Object.entries(payload).forEach(([key, val]) => {
modifer(key, val, prop);
});
});
}
}
};
// 对 ast 进行深度遍历
traverse(ast, visitor);
return generate(ast, {
jsescOption: { minimal: true }
}).code;
};
const result = transformRoutesAST({
action,
name,
payload
});
console.log(result);