webpack和Lint等很多的工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的
抽象语法树用途
- 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
- 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
- IDE的错误提示、格式化、高亮、自动补全等等
- 代码混淆压缩
- UglifyJS2等
- 优化变更代码,改变代码结构使达到想要的结构
- 代码打包工具webpack、rollup等等
- CommonJS、AMD、CMD、UMD等代码规范之间的转化
- CoffeeScript、TypeScript、JSX等转化为原生Javascript
抽象语法树定义
工具的原理都是通过JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这颗树,可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作
Javascript的语法是为了给开发者更好的编程而设计的,但是不适合程序的理解。所以需要转化为AST来使之更适合程序分析,浏览器编译器一般会把源码转化为AST来进行进一步的分析等其他操作。

JavaScript Parser
- JavaScript Parser,把js源码转化为抽象语法树的解析器。
- 浏览器会把js源码通过解析器转为抽象语法树,再进一步转化为字节码或直接生成机器码。
- 每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,firefox的SpiderMonkey引擎等等,MDN提供了详细SpiderMonkey AST format的详细说明,算是业界的标准。
常用的JavaScript Parser有:
- esprima
- traceur
- acorn
- shift
esprima
通过 esprima 把源码转化为AST
AST的可视化工具
esprima
astexplorer
- 解析语法(js) => 语法树
let esprima = require('esprima');
let code = `function ast(){}`
let ast = esprima.parseScript(code);
console.log(ast)
结果
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "ast"
},
"params": [],
"body": {
"type": "BlockStatement",
"body": []
},
"generator": false,
"expression": false,
"async": false
}
],
"sourceType": "script"
}
estraverse
通过 estraverse 遍历并更新AST
let estraverse = require('estraverse');
estraverse.traverse(ast, {
enter(node){ //直接用enter
if (node.type === 'Identifier') {
node.name = 'testAST'
}
console.log('enter', node.type);
},
leave(node) {
console.log('leave', node.type);
}
});
结果
enter Program
enter FunctionDeclaration
enter Identifier
leave Identifier
enter BlockStatement
leave BlockStatement
leave FunctionDeclaration
leave Program
escodegen
通过 escodegen 将AST重新生成源码
let escodegen = require('escodegen');
let r = escodegen.generate(ast)
console.log(r);
结果
function testAST() {
}
箭头函数插件 (ArrowFunctionPlugin)
- 通过@babel/core、@babel/types
- @babel/types
- 生成语法树
- 判断类型
let babel = require('@babel/core');
let t = require('@babel/types');
let code = `let sum = (a, b) => {return a + b}`;
let ArrowPlugin = {
visitor: {
//path 树的路径
ArrowFunctionExpression(path) {
let node = path.node;
let params = node.params;
let body = node.body;
//判断是不是代码块
if (!t.isBlockStatement(body)) {
let returnStatement = t.returnStatement(body)
body = t.blockStatement([returnStatement])
}
//生成函数表达式
let funcs = t.functionExpression(null, params, body, false, false);
path.replaceWith(funcs)
}
}
}
let r = babel.transform(code, {
plugins: [
ArrowPlugin
]
});
console.log(r.code)
类函数插件(ClassFunctionPlugin)
let babel = require('@babel/core');
let t = require('@babel/types');
let code = `
class Person {
constructor(name, age) {
this.name = name;
}
getName(){
return this.name
}
}
`
let ClassPlugin = {
visitor:{
ClassDeclaration(path) {
let node = path.node;
let className = node.id.name; //函数名必须是个标识符
className = t.identifier(className);
let classList = node.body.body;
let funcs = t.functionDeclaration(className, [], t.blockStatement([]), false, false);
path.replaceWith(funcs);
let es5Funcs = []
classList.forEach((item, index) => {
//函数代码体
let body = classList[index].body;
if (item.kind === 'constructor') {
//如果是构造函数就生成新的函数将默认的空函数替换掉
let params = item.params.length ? item.params.map(item => item.name) : [];
params = t.identifier(params.toString());
funcs = t.functionDeclaration(className, [params], body, false, false);
}else{
let protoObj = t.memberExpression(className, t.identifier('prototype'));
let left = t.memberExpression(protoObj, t.identifier(item.key.name));
let right = t.functionExpression(null, [], body, false, false);
let assign = t.assignmentExpression('=', left, right);
//多个原先方法
es5Funcs.push(assign)
}
})
if (es5Funcs.length == 0) {
path.replaceWith(funcs);
}else {
//有原先方法
es5Funcs.push(funcs);
//替换n个节点
path.replaceWithMultiple(es5Funcs)
}
}
}
}
let r = babel.transform(code, {
plugins: [
ClassPlugin
]
});
console.log(r.code)
Tree Shaking (ImportFunctionPlugin)
//实现模块按需 加载
//import {Button} from 'antd';
//babel-plugin-import
//import {Button, Alter} from 'antd';
//import Button from 'antd/lib/button';
//import Alter from 'antd/lib/alter';
let babel = require('@babel/core');
let t = require('@babel/types');
let code = `import {Button, Alter} from 'antd';`
let ImportPlugin = {
visitor:{
ImportDeclaration(path) {
let node = path.node;
let source = node.source.value;
let specifiers = node.specifiers;
if (!t.isImportDefaultSpecifier(specifiers[0])) {
specifiers = specifiers.map(specifier => {
return t.importDeclaration(
[t.importDefaultSpecifier(specifier.local)],
t.stringLiteral(`${source}/lib/${specifier.local.name.toLowerCase()}`)
)
});
path.replaceWithMultiple(specifiers);
}
}
}
}
let r = babel.transform(code, {
plugins: [
ImportPlugin
]
})
console.log(r.code);