很久之前,我曾使用写过一些生产辅助插件,那时候就对如何准确插入新代码产生了浓厚的兴趣,经过对Umi.js的源码解读,终于找到了比较合适的方法
希望你能从这篇文章中得到以下知识:
- 何为babel?babel有什么用?以及怎么用
- 何为AST(抽象语法树)?
- 如何通过babel以及AST实现代码的更新
一.AST(抽象语法树)
It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.
它是一种分层的程序表示,根据编程语言的语法来表示源代码结构,每个AST节点对应一个源代码项。
router.js:
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
routes: [
{
name: '空白页面',
path: '/emptypagetwo',
component: './EmptyPageTwo',
},
],
});
以上为router.js文件,一个标准的js文件,在经过babel的解析后,就会生成以下AST:
{
type: 'File',
start: 0,
end: 483,
loc: SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 29, column: 0 }
},
errors: [],
program: Node {
type: 'Program',
start: 0,
end: 483,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [ [Node], [Node] ],
directives: []
},
comments: []
}
实际上,正真AST每个节点会有更多的信息。但是,这是大体思想。从纯文纯中,我们将得到树形结构的数据。每个条目和树中的节点一一对应。AST的编程语言的开始,当然,我们今天不对其进行更深的展开,基于本文题目,我们想要实现编程的可视化,通过AST我们就可以将新生成的代码插入指定文件的指定位置中,这是编程可视化的核心中的核心!
二.Babel
Babel 是一个 JavaScript 编译器,可以将ES6及跟高版本转换成较低的ES5,让低端运行环境.当然,今天的主角并不是他对ES6的语法转换,而是他的3个函数:parse、traverse、generate.在上文中,我们已经获取到了一个JS文件的AST树,那么,这一步我们就需要通过AST实现生成代码的插入.当然,我们得先了解核心函数:
1.babel/parse
JS的语法解析,通过'babel/parser'可以将JS解析成AST.
import * as parser from '@babel/parser';
import { readFileSync, writeFileSync, existsSync } from 'fs';
let configPath = '../router.js'
function parse(){
const ast = parser.parse(readFileSync(configPath, 'utf-8'), {
sourceType: 'module',
plugins: ['typescript'],
});
console.log(ast)
}
输出如下:
{
type: 'File',
start: 0,
end: 483,
loc: SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 29, column: 0 }
},
errors: [],
program: Node {
type: 'Program',
start: 0,
end: 483,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [ [Node], [Node] ],
directives: []
},
comments: []
}
2.babel/traverse及bable/type
traverse用于遍历ast,type则用于判断节点的类型.只所以将这个函数放一起是因为他们紧密性较强.
import traverse from '@babel/traverse';
import * as t from '@babel/types';
traverse(ast, {
ObjectExpression({ node, parent }) {
// find routes on object, like { routes: [] }
const { properties } = node;
properties.forEach((p) => {
const { key, value } = p;
if (t.isObjectProperty(p) && t.isIdentifier(key) && key.name === 'routes') {
routesNode = value;
}
});
}
})
上面代码中,通过traverse对AST的遍历,筛选出类型为ObjectExpression(对象表达式,like { routes: [] })的节点.
3.babel/generator
parse的对立面,将AST还原成JS.
import generate from '@babel/generator';
import prettier from 'prettier';
const newCode = generate(ast, {}).code;
// prettier用于没话代码
prettier.format(newCode, {
// format same as ant-design-pro
singleQuote: true,
trailingComma: 'es5',
printWidth: 100,
parser: 'typescript',
});
三.实践
通过以上知识点,实现一个对router.js的动态添加.
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import * as t from '@babel/types';
import winPath from './winPath'
import prettier from 'prettier';
import { readFileSync, writeFileSync, existsSync } from 'fs';
let configPath = '../router.js'
let newRouter = {
name: '测试页面2',
path: '/test',
component: './test',
}
/**
* 添加新路由
* @param {*} configPath
* @param {*} newRoute
*/
function writeNewRoute(configPath, newRoute) {
const { code, routesPath } = getNewRouteCode(configPath, newRoute);
writeFileSync(routesPath, code, 'utf-8');
}
/**
* 获取添加后的代码
* @param {*} configPath
* @param {*} newRoute
*/
function getNewRouteCode(configPath, newRoute) {
const ast = parser.parse(readFileSync(configPath, 'utf-8'), {
sourceType: 'module',
plugins: ['typescript'],
});
console.log(ast,'start addNewRouter');
let routesNode = ''
traverse(ast, {
ObjectExpression({ node, parent }) {
// find routes on object, like { routes: [] }
const { properties } = node;
properties.forEach((p) => {
const { key, value } = p;
if (t.isObjectProperty(p) && t.isIdentifier(key) && key.name === 'routes') {
routesNode = value;
}
});
}
})
writeRouteNote(routesNode, newRoute);
const code = generateCode(ast);
return { code, routesPath: configPath };
// writeNewRoute(routesNode, newRouter)
}
/**
* 生成代码
* @param {*} ast
*/
function generateCode(ast) {
console.log('i am generatorcodes ast', ast);
const newCode = generate(ast, {}).code;
return prettier.format(newCode, {
// format same as ant-design-pro
singleQuote: true,
trailingComma: 'es5',
printWidth: 100,
parser: 'typescript',
});
}
/**
* 写入节点
* @param {*} targetNode 找到的节点
* @param {*} newRoute 新的路由配置
*/
function writeRouteNote(targetNode, newRoute) {
const { elements } = targetNode;
const paths = elements.map(ele => {
if (!t.isObjectExpression(ele)) {
return false;
}
const { properties } = ele;
const redirect = properties.find((p) => p.key.name === 'redirect');
if (redirect) {
return false;
}
const pathProp = properties.find((p) => p.key.name === 'path');
if (!pathProp) {
return currentPath;
}
let fullPath = pathProp.value.value;
if (fullPath.indexOf('/') !== 0) {
fullPath = join(currentPath, fullPath);
}
return fullPath;
});
const matchedIndex = paths.findIndex(p => p && newRoute.path.indexOf(winPath(p)) === 0);
const newNode = getNewRouteNode(newRoute);
console.log(matchedIndex, 'matchedIndex');
// 不存在相同节点
if (matchedIndex === -1) {
elements.push(newNode);
// return container for test
return targetNode;
}
}
function getNewRouteNode(newRoute) {
return (parser.parse(`(${JSON.stringify(newRoute)})`).program.body[0]).expression;
}
writeNewRoute(configPath, newRouter)
四.总结
上述实践中,只是使用了核心函数实现了核心的功能点.如果真的要实现严格的代码插入则需要在节点遍历的时候加入更多的验证.例如:子路由等判断.
五.参考文献
Umi.js : github.com/umijs/umi