AST简介
抽象语法树(abstract
syntax
trees
),就是将代码转换成的⼀种抽象的树形结构,通常是 JSON
描述。AST 并不是哪个编程语⾔特有的概念,在前端领域,⽐较常⽤的 AST ⼯具如esprima,babel(babylon
) [中文]的解析模块,其他如 Vue
⾃⼰实现的模板解析器。本⽂主要以 babel
为例对 AST
的原理进⾏浅析,通过实践掌握如何利⽤ AST
掌握代码转换的能⼒。
- 推荐⼯具: AST 在线学习 和 tokens 在线分析
- 插件集合(注意,前端
AST
并不仅针对JavaScript
,CSS
、HTML
⼀样具有相应的解析⼯具,JavaScript
重点关注)
功能 | 插件 | 常用方法 |
---|---|---|
ast 解析 | esprima @babel/parser | recast.parse |
ast 遍历 | estraverse @babel/traverse | recast.visit |
⽣成代码 | escodegen @babel/generator | recast.print recast.prettyPrint |
代码的编译流程
我们把上述过程分为三部分:解析(parse),转换(transform),⽣成(generate),其中 scanner 部分叫做词法(syntax)分析,parser 部分叫做语法(grammar)分析。显然,词法分析的结果是 tokens,语法分析得到的就是 AST。
const esprima = require('esprima');
var program = 'const answer = 42';
//词法分析
const token = esprima.tokenize(program);
console.log(token)
//语法分析
const ast = esprima.parse(program);
console.log(JSON.stringify(ast, null, ' '));
//词法分析输出
[
{ type: 'Keyword', value: 'const' }, // keyword 关键词
{ type: 'Identifier', value: 'answer' }, // Identifier 标识符
{ type: 'Punctuator', value: '=' }, // Punctuator 标点符号
{ type: 'Numeric', value: '42' } // Numeric 数字
]
//语法分析输出
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration", //变量声明
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "Literal", //直译
"value": 42,
"raw": "42"
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
- 可⻅词法分析,旨在将源代码按照⼀定的分隔符(空格/tab/换⾏等)、注释进⾏分割,并将各个部分进⾏分类构造出⼀段
token
流。 - ⽽语法分析,则基于
tokens
将源码语义化、结构化为⼀段JOSN
描述(AST
)。反之,如果给出⼀段代码的描述信息,我们也是可以还原源码的。理论上,描述信息发⽣变化,⽣成的源码的对应信息也会发⽣变化。所以我们可以通过操作AST
达到修改源码信息的⽬的,辅以⽂件的创建接⼝,这也是babel
打包⽣成代码的基本原理。了解到这⼀层,便能想象ES6
=>ES5
、ts
=>js
、uglifyJS
、样式预处理器、eslint
、代码提示等⼯具的⼯作⽅式了。
AST 的节点类型
在操作 AST
过程中,源码部分集中在 Program
对象的 body
属性下,每个节点有着统⼀固定的格式:
@babel/core
依赖了 parser
、traverse
、generator
模块,所以安装 @babel/core
即可。下⽂均以 babel 作为⽰例⼯具,其他⼯具类似,不再赘述。
const babel = require('@babel/core');
const code = `
import React from 'react'; function add(a, b) { return a + b; } let str = 'hello'; `;
const ast = babel.parse(code, {
sourceType: 'module'
});
console.log(ast.program.body);
//输出
[
Node {
type: 'ImportDeclaration',
start: 2,
end: 28,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
specifiers: [ [Node] ],
source: Node {
type: 'StringLiteral',
start: 20,
end: 27,
loc: [SourceLocation],
extra: [Object],
value: 'react'
}
},
Node {
type: 'FunctionDeclaration',
start: 30,
end: 68,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
id: Node {
type: 'Identifier',
start: 39,
end: 42,
loc: [SourceLocation],
name: 'add'
},
generator: false,
async: false,
params: [ [Node], [Node] ],
body: Node {
type: 'BlockStatement',
start: 49,
end: 68,
loc: [SourceLocation],
body: [Array],
directives: []
}
},
Node {
type: 'VariableDeclaration',
start: 70,
end: 88,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
declarations: [ [Node] ],
kind: 'let'
}
]
以上⾯的代码为例,如果想在函数返回语句之前加⼊⼀⾏语句 console.log('函数执⾏完成')
,最朴素的做法是这样:
const babel = require('@babel/core');
const code = `
import React from 'react';
function add(a, b) {
const decrease = () => {
return c;
}
return a + b;
}
let str = 'hello';
`;
const ast = babel.parse(code, {
sourceType: 'module'
});
const CONSOLE_AST = babel.template.ast(`console.log('函数执行完成');`);
function insertConsoleBeforeReturn(body) {
body.forEach(node => {
if (node.type === 'FunctionDeclaration') { // 函数关键字声明形式
console.log(node,node.body.body)
const blockStatementBody = node.body.body;
if (blockStatementBody && blockStatementBody.length) {
const index = blockStatementBody.findIndex(n => n.type === 'ReturnStatement');
if (~index) {
// 函数体存在语句且最后⼀条语句是 return (假设 return 就是最后的语句)
blockStatementBody.splice(index, 0, CONSOLE_AST); // 直接修改 ast, 前插⼀个节 点
}
}
}
});
}
insertConsoleBeforeReturn(ast.program.body)
console.log(babel.transformFromAstSync(ast).code);
//node节点
Node {
type: 'FunctionDeclaration',
start: 32,
end: 125,
loc: SourceLocation {
start: Position { line: 3, column: 2, index: 32 },
end: Position { line: 8, column: 3, index: 125 },
filename: undefined,
identifierName: undefined
},
id: Node {
type: 'Identifier',
start: 41,
end: 44,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: 'add'
},
name: 'add'
},
generator: false,
async: false,
params: [
Node {
type: 'Identifier',
start: 45,
end: 46,
loc: [SourceLocation],
name: 'a'
},
Node {
type: 'Identifier',
start: 48,
end: 49,
loc: [SourceLocation],
name: 'b'
}
],
body: Node {
type: 'BlockStatement',
start: 51,
end: 125,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
body: [ [Node], [Node] ], //包含了返回函数ReturnStatement
directives: []
}
}
//node.body.body
[
Node {
type: 'VariableDeclaration',
start: 57,
end: 103,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
declarations: [ [Node] ],
kind: 'const'
},
Node {
type: 'ReturnStatement',
start: 108,
end: 121,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
argument: Node {
type: 'BinaryExpression',
start: 115,
end: 120,
loc: [SourceLocation],
left: [Node],
operator: '+',
right: [Node]
}
}
]
//转化后的代码
import React from 'react';
function add(a, b) {
const decrease = () => {
return c;
};
console.log('函数执行完成');
return a + b;
}
let str = 'hello';
虽然⼿动操作
AST
满⾜了当前的需求,但是诸如箭头函数,类或对象的⽅法、没有 return 语句或省略return
关键字的函数、表达式声明的函数、IIFE
、语句内又嵌套的函数……上述⽅法都是没有考虑的,所以不推荐⼿动实现。
由此可⻅,处理 AST
的过程就是对不同节点类型遍历和操作的过程,为简化操作,babel
提供了专⻔的接⼝,我们只需要提供相应类型的处理⽅法(visitor
)即可。还是上⾯的需求(好⼀点的是所有的return
语句都会处理,即使是嵌套的函数):
babel.traverse(ast, {
ReturnStatement(path) {
path.insertBefore(CONSOLE_AST);
}
});
//输出
import React from 'react';
function add(a, b) {
const decrease = () => {
console.log('函数执行完成');
return c;
};
console.log('函数执行完成');
return a + b;
}
let str = 'hello';
traverse
⽅法帮我们处理了 ast
的遍历过程,对于不同节点的处理只需要维护⼀份 types 对应的⽅法即可。进⼀步的,构造 CONSOLE_AST
节点也有⼏种⽅式。先使⽤在线⼯具将 console.log('函数执⾏完成')
; 结构化(如果你已经⼗分熟悉这个过程,可以跳过):
{
"type": "Program",
"start": 0,
"end": 22,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 22,
"expression": {
"type": "CallExpression",
"start": 0,
"end": 21,
"callee": {
"type": "MemberExpression",
"start": 0,
"end": 11,
"object": {
"type": "Identifier",
"start": 0,
"end": 7,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 8,
"end": 11,
"name": "log"
},
"computed": false,
"optional": false
},
"arguments": [
{
"type": "Literal",
"start": 12,
"end": 20,
"value": "函数执行完成",
"raw": "'函数执行完成'"
}
],
"optional": false
}
}
],
"sourceType": "module"
}
- 基础⽅式——使⽤
@babel/types
来构造语句
const t = require('@babel/types');
const generate = require('@babel/generator').default;
const CONSOLE_AST = t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier('console'),
t.identifier('log')
),
[t.stringLiteral('函数执行完成')],
)
);
console.log(JSON.stringify(CONSOLE_AST, null, ' '), '\n\n', generate(CONSOLE_AST).code);
//输出
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
},
"computed": false,
"optional": null
},
"arguments": [
{
"type": "StringLiteral",
"value": "函数执行完成"
}
]
}
}
console.log("\u51FD\u6570\u6267\u884C\u5B8C\u6210");
- 终极简化版——模板 API,也是上⾯表格提前给出来的⽅式:
const template = require('@babel/template').default;
// 或 const template = require('@babel/core').template;
const CONSOLE_AST = template.ast(
`console.log('函数执⾏完成')`
);
console.log(CONSOLE_AST);
//输出
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: [Object],
property: [Object],
computed: false,
loc: undefined
},
arguments: [ [Object] ],
loc: undefined
},
loc: undefined
}
AST 与 babel 插件
官⽅插件
随着 ECMAScript 的发展,不断涌出⼀些新的语⾔特性(如管道操作符、可选链操作符、控制合并操作符……),也包括但不限于 JSX 语法等。遇到 babel 本身的解析引擎模块不能识别新特性的问题,可以由插件来处理。
const babel = require('@babel/core')
const code = `
const square = x => x ** 2;
const sum = a => a + 2;
const list = 5 |> square(^^) |> sum(^^);
`;
const ast = babel.parse(code, {
sourceType: 'module',
});
console.log(babel.transform(code).code);
运⾏上⾯的代码会直接报错,源码(第 5 ⾏)使⽤的管道操作符处于提案中,需要借助插件来解析:
@babel/parser
模块 + 内联配置(记得安装@babel/plugin-proposal-pipeline-operator
)解析
const ast = babel.parse(code, {
sourceType: 'module',
plugins:[
["pipelineOperator", {
"proposal": "hack",
"topicToken": "^^"
}]
]
});
2.@babel/core
模块 + ⽂件 babel.config.json
解析(babel
会⾃动到项⽬⽬录查找最近的)babel配置文件
{
"plugins": [
["@babel/plugin-proposal-pipeline-operator", {
"proposal": "hack",
"topicToken": "^^"
}]
]
}
//输出
const square = x => x ** 2;
const sum = a => a + 2;
const list = sum(square(5));
同理,其他插件通过相同的⽅式使⽤。
- 当项⽬需要⽀持的语⾔特性越来越多,
plugins
需要逐⼀添加,为了解决插件的管理与依赖问题,通过提供常⽤的环境配置。因此 总能看到这样的配置 (React
项⽬):
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
"plugin": []
}
- 先执⾏完所有
plugins
,再执⾏presets
。 - 多个
plugins
,按照声明次序顺序执⾏。 - 多个
presets
,按照声明次序逆序执⾏。
自定义插件
以上⾯的源码为例,实现变量标识的重命名,源码及转换逻辑:
//.my-plugin.js
module.exports = function (babel) {
return {
visitor: {
VariableDeclaration(path, state) {
path.node.declarations.forEach(each => {
path.scope.rename(
each.id.name,
path.scope.generateUidIdentifier("uid").name
);
});
}
},
}
}
//babel.config.json
{
"plugins": [
["./my-plugin"],
["@babel/plugin-proposal-pipeline-operator", {
"proposal": "hack",
"topicToken": "^^"
}]
]
}
//输出
const _uid = x => x ** 2;
const _uid2 = a => a + 2;
const _uid3 = _uid2(_uid(5));
自定义try-catch插件
const babel = require('@babel/core')
// babel-loader : test:/\.[tj]sx?/
const code = `
async function Async1() {
await fetch();
}
async function Async2() {
const { data } = await fetch();
return data;
}
const Async3 = async function () {
const { code } = await fetch();
return code;
}`
console.log(babel.transform(code).code);
//try-catch.js
module.exports = function ({ types: t, template }) {
return {
name: 'auto-try-catch',
visitor: {
AwaitExpression(path) {
const shouldSkip = path.findParent(p => p.isTryStatement());
if (!shouldSkip) {
const blockStatement = path.findParent(p => p.isBlockStatement());
if (blockStatement) {
/* blockStatement.replaceWith(
t.blockStatement([
t.tryStatement(
blockStatement.node,
t.catchClause(
t.identifier('err'),
t.blockStatement([
t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier('console'),
t.identifier('error')
),
[t.identifier('err')],
)
)]
)
)
)]
)
) */
blockStatement.replaceWith(
t.blockStatement([
template.ast(`
try ${blockStatement.toString()}
catch (err) {
console.error(err);
}
`)
])
)
}
}
}
}
};
}
//输出
async function Async1() {
try {
await fetch();
} catch (err) {
console.error(err);
}
}
async function Async2() {
try {
const {
data
} = await fetch();
return data;
} catch (err) {
console.error(err);
}
}
const Async3 = async function () {
try {
const {
code
} = await fetch();
return code;
} catch (err) {
console.error(err);
}
};