如果我们需要在js
中对js代码
进行修改的时候,@babel/core
是个全能的选项。
在@babel/core
中,已经集成了
ast
树构建(parse)ast
树遍历和改造(traverse)ast
节点类型和创建(types)ast
输出代码(transfromFromAst)
等等几个常用功能,并以插件模式来实现业务逻辑解耦。
除此之外,@babel/generator
是个单独的ast
代码生成工具。它与transfromFromAst
不同的是:
generator | transfrom |
---|---|
可以基于任何类型ast 节点输出代码 | 只能基于完整ast 树 |
代码压缩、引号选择等简单配置 | 完整代码配置,支持插件 |
除了@babel/core
,还有个轻量级的选项是acorn
,而且babel的AST解析部分也是用的acorn
。
体积更小的acorn
,压缩后在80kb左右,可以在web中使用;但功能也相对单一,仅能实现ast
解析。对ast
树的遍历需要单独实现,修改则是直接操作ast
树。
理解AST语法树
首先来看一下ast树的样子:
const { parse, traverse, types, transformFromAstSync } = require('@babel/core')
const ast = parse('const a = 1')
console.log(JSON.stringify(ast, null, ' '))
为了方便观看,我删掉了代码行列数、起止位置等信息。
{
"type": "File",
"errors": [],
"program": {
"type": "Program",
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"identifierName": "a"
},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
}
],
"kind": "const"
}
],
"directives": []
},
"comments": []
}
所有节点都具备一个type
属性,不同的type
有不同的节点结构。
其中,File
-> Program
,是完整ast
文档的固有信息,我们可以先暂时不管;从Program.body
开始,是代码正文,下面用伪代码描述一下:
VariableDeclaration
开始声明变量,类型(kind
)为const
,包含以下几个变量VariableDeclarator
id
变量名为a
(id.name)init
有初始值(init不是null),值类型为number
(NumericLiteral)NumericLiteral
value
值为1
VariableDeclarator
VariableDeclarator
- (本次声明的其他变量 ... )
从AST转为代码
因为拿到的是完整的ast
文档,所以我们可以直接用transfrom
来进行代码生成:
const ast = parse('const a = 1')
const { code } = transformFromAstSync(ast)
console.log(code)
输出
% node babel.js
const a = 1;
给加了个分号。
AST遍历
当我们需要收集代码信息,比如分析依赖做tree-shaking
,或者要更改代码,比如const/let
改为var
,都要遍历AST,查找和操作节点。
const ast = parse('const a = 1')
traverse(ast, {
enter(nodePath) {
console.log(nodePath.type)
}
})
输出
% node babel.js
Program
VariableDeclaration
VariableDeclarator
Identifier
NumericLiteral
节点类型方法
代码中,traverse
的第二个参数,是各种节点操作方法的map:按照节点类型命名一个方法,对应类型的节点就会进入这个方法。
比较特殊的是enter
方法,所有节点都会先走进这个方法,然后再执行自身类型对应的方法。
traverse(ast, {
enter(nodePath) {
console.log(1, nodePath.type)
},
// 节点类型命名方法
Identifier(nodePath) {
console.log(2, nodePath.type)
}
})
输出
% node babel.js
1 Program
1 VariableDeclaration
1 VariableDeclarator
1 Identifier
# 节点类型方法在enter方法之后执行
2 Identifier
1 NumericLiteral
NodePath
NodePath是用来描述节点关系的结构,通过traverse
中的节点方法暴露出来。
常用的属性/方法包括:
- NodePath.node 节点本身
- NodePath.parentPath 父级节点
- NodePath.scope 语法上下文,能得到全局变量等信息
- NodePath.traverse() 自遍历
- NodePath.replaceWith() 节点替换
- NodePath.remove() 节点删除
AST修改
基于traverse
和暴露出的NodePath
,我们可以对AST进行修改操作。
直接修改Node
Node节点作为NodePath中的一个引用值属性,直接操作Node就会影响最终的代码生成。比如我们把变量a
更名为A
:
const ast = parse('const a = 1')
traverse(ast, {
Identifier(nodePath) {
const { node } = nodePath
node.name = 'A'
}
})
运行
% node babel.js
const A = 1;
甚至删掉节点
traverse(ast, {
Identifier(nodePath) {
const { node } = nodePath
node.name = 'A'
},
VariableDeclarator({ node }) {
delete node.init // node.init = null 也可以
},
VariableDeclaration({ node }) {
node.kind = 'var'
}
})
// 输出 'var A;'
根据实际经验,直接操作节点,尤其是复杂节点的时候,会对遍历过程造成影响,引发一些不可预期的问题,所以建议在操作复杂节点时
使用NodePath修改
我们使用NodePath提供的方法,实现上面的效果:
traverse(ast, {
Identifier(nodePath) {
const { node } = nodePath
if(node.name === 'a') {
const newId = types.identifier('A')
nodePath.replaceWith(newId)
}
},
VariableDeclaration(nodePath) {
const { node } = nodePath
const { declarations, kind } = node
if(kind === 'const') {
const newNode = types.variableDeclaration('var', declarations)
nodePath.replaceWith(newNode)
}
},
NumericLiteral(nodePath) {
if(nodePath.parentPath && nodePath.parentPath.type === 'VariableDeclarator') {
nodePath.remove()
}
}
})
看起来更严谨、更优雅。
使用types创建AST节点
在types
后面.
,第一个字母小写,对应节点类型的创建方法;第一个字母大写,对应相应节点类型定义。@babel/core
对类型、属性提示做得非常友好,会自动补全。
// types.Identifier
types.identifier('A')
// types.VariableDeclaration
types.variableDeclaration('var', declarations)
必要的条件判断
注意这里每个节点方法中,都增加了修改条件判断。
因为每次新增、替换的节点,也都会进入traverse
过程,不加条件判断会陷入死循环。
Maximum call stack size exceeded
插件
写一个Babel插件
插件是一个函数,这个函数需要返回一个结构:
function plugin() {
return {
visitor: {
// ...
}
}
}
这个visitor
,就是traverse
的第二个参数。
const plugin = () => {
return {
visitor: {
Identifier(nodePath) {
const { node } = nodePath
if(node.name === 'a') {
const newId = types.identifier('A')
nodePath.replaceWith(newId)
}
},
VariableDeclaration(nodePath) {
const { node } = nodePath
const { declarations, kind } = node
if(kind === 'const') {
const newNode = types.variableDeclaration('var', declarations)
nodePath.replaceWith(newNode)
}
},
NumericLiteral(nodePath) {
if(
nodePath.parentPath
&& nodePath.parentPath.type === 'VariableDeclarator'
) {
nodePath.remove()
}
}
}
}
}
插件调用
const ast = parse('const a = 1')
const { code } = transformFromAstSync(
ast,
null,
{ plugins: [ plugin ] } // 这里
)
console.log(code)
运行
% node babel.js
var A;
Babel插件技能get。
官方插件
就常用的代码修改,官方已发布很多插件,除了特定的场景,一般对脚手架之类应用,基本够用:
实践
@babel/plugin-transform-async-to-generator
安装
# https://babeljs.io/docs/en/babel-plugin-transform-async-to-generator
npm i -D @babel/plugin-transform-async-to-generator
代码
const ast = parse('const fn = async () => { }')
const { code } = transformFromAstSync(
ast,
null,
{ plugins: ['@babel/plugin-transform-async-to-generator'] } // 这里,字符串调用。
)
console.log(code)
输出
% node babel.js
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
const fn = /*#__PURE__*/function () {
var _ref = _asyncToGenerator(function* () {});
return function fn() {
return _ref.apply(this, arguments);
};
}();
以上