webpack和Lint等很多的工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的
1. 抽象语法树用途
-
代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
- 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误
- IDE 的错误提示、格式化、高亮、自动补全等等
-
代码混淆压缩
- UglifyJS2 等
-
优化变更代码,改变代码结构使达到想要的结构
- 代码打包工具 webpack、rollup 等等
- CommonJS、AMD、CMD、UMD 等代码规范之间的转化
- CoffeeScript、TypeScript、JSX 等转化为原生 Javascript
2. 抽象语法树概念
- 这些工具的原理都是通过
JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作
3. JavaScript Parser
JavaScript Parser是把JavaScript源码转化为抽象语法树的解析器。- 浏览器会把JavaScript源码通过解析器转为抽象语法树,再进一步转化为字节码或直接生成机器码。
- 一般来说每个JavaScript引擎都会有自己的抽象语法树格式,Chrome 的 v8 引擎,firefox 的 SpiderMonkey 引擎等等,MDN 提供了详细SpiderMonkey AST format 的详细说明,算是业界的标准。
3.1 常用的 JavaScript Parser
- esprima
- traceur
- acorn
- shift
3.2 遍历
cnpm i esprima estraverse- S
let esprima = require('esprima');//把JS源代码转成AST语法树
let estraverse = require('estraverse');///遍历语法树,修改树上的节点
let escodegen = require('escodegen');//把AST语法树重新转换成代码
let code = `function ast(){}`;
let ast = esprima.parse(code);
let indent = 0;
const padding = ()=>" ".repeat(indent);
estraverse.traverse(ast,{
enter(node){
console.log(padding()+node.type+'进入');
if(node.type === 'FunctionDeclaration'){
node.id.name = 'newAst';
}
indent+=2;
},
leave(node){
indent-=2;
console.log(padding()+node.type+'离开');
}
});
Program进入
FunctionDeclaration进入
Identifier进入
Identifier离开
BlockStatement进入
BlockStatement离开
FunctionDeclaration离开
Program离开
4. babel
- Babel 能够转译
ECMAScript 2015+的代码,使它在旧的浏览器或者环境中也能够运行 - 工作过程分为三个部人
-
Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
-
Transform(转换) 对抽象语法树进行转换
-
Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
-
4.1 AST遍历
- AST是深度优先遍历
- 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
- Visitor 的对象定义了用于 AST 中获取具体节点的方法
Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法
4.2 babel插件
- @babel/parser 可以把源码转换成AST
- @babel/traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
- @babel/generate 可以把AST生成源码,同时生成sourcemap
- @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用
- @babel/template可以简化AST的创建逻辑
- @babel/code-frame可以打印代码位置
- @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能
- babylon Babel 的解析器,以前叫babel parser,是基于acorn扩展而来,扩展了很多语法,可以支持es2020、jsx、typescript等语法
- babel-types-api
- Babel 插件手册
- babeljs.io babel 可视化编译器
4.2.1 转换箭头函数
//babel插件其实是一个对象,它会有一个visitor访问器
let BabelPluginTransformEs2015ArrowFunctions2 = {
//每个插件都会有自己的访问器
visitor:{
//属性就是节点的类型,babel在遍历到对应类型的节点的时候会调用此函数
ArrowFunctionExpression(nodePath){
//参数是节点的数据
let node = nodePath.node;
//获取 当前路径上的节点
//处理this指针的问题
hoistFunctionEnvironment(nodePath);
node.type = 'FunctionExpression';
}
}
}
5. webpack TreeShaking插件
let babel = require("@babel/core");
let types = require("babel-types");
// 插件是一个visitor
const visitor = {
// 遇到ImportDeclaration
ImportDeclaration: {
// 进入的时候
enter(path, state = { opts }) {
const specifiers = path.node.specifiers;
const source = path.node.source;
if (
state.opts.libraryName == source.value &&
!types.isImportDefaultSpecifier(specifiers[0])
) {
const declarations = specifiers.map((specifier, index) => {
return types.ImportDeclaration(
[types.importDefaultSpecifier(specifier.local)],
types.stringLiteral(`${source.value}/${specifier.local.name}`)
);
});
path.replaceWithMultiple(declarations);
}
},
},
};
module.exports = function (babel) {
return {
visitor,
};
};
// 源代码转成ast
const parser = require("@babel/parser");
// 遍历转换
const traverse = require("@babel/traverse").default;
// 转换后的ast转成源代码
const generate = require("@babel/generator").default;
// 工具方法
const types = require("@babel/types");
const sourceCode = `console.log("hello");`;
const ast = parser.parse(sourceCode, {
sourceType: "module",
plugins: ["jsx"],
});
traverse(ast, {
CallExpression(path) {
if (
types.isMemberExpression(path.node.callee) &&
path.node.callee.object.name === "console" &&
["log", "info", "error", "debug"].includes(path.node.callee.property.name)
) {
const { line, column } = path.node.loc.start;
path.node.arguments.unshift(
types.stringLiteral(`filename: (${line}, ${column})`)
);
}
},
});
const { code } = generate(ast);
console.log(code);