语法转换
基于Babel 的JavaScript 语法树构造与代码转化

浏览器的兼容性问题一直是前端项目开发中的难点之一,往往客户端浏览器的升级无法与语法特性的迭代保持一致;因此我们需要使用大量的垫片flowToDecorator
函数,其能够从
Babel
自
return {
plugins: [
[transformES2015TemplateLiterals, { loose, spec }],
transformES2015Literals,
transformES2015FunctionName,
[transformES2015ArrowFunctions, { spec }],
transformES2015BlockScopedFunctions,
[transformES2015Classes, optsLoose],
transformES2015ObjectSuper,
...(modules === "commonjs" && [transformES2015ModulesCommonJS, optsLoose]),
modules === "systemjs" && [transformES2015ModulesSystemJS, optsLoose],
modules === "amd" && [transformES2015ModulesAMD, optsLoose],
modules === "umd" && [transformES2015ModulesUMD, optsLoose],
[transformRegenerator, { async: false, asyncGenerators: false }],
].filter(Boolean), // filter out falsy values
};
- 在解析步骤中,
Babel 分别使用词法分析(Lexical Analysis) 与语法分析(Syntactic Analysis) 来将输入的代码转化为抽象语法树;其中词法分析步骤会将代码转化为令牌流,而语法分析步骤则是将令牌流转化为语言内置的AST 表示。 - 在转化步骤中,
Babel 会遍历上一步生成的令牌流,根据配置对节点进行添加、更新与移除等操作;Babel 本身并没有进行转化操作,而是依赖于外置的插件进行实际的转化。 - 最后的代码生成则是将上一步中经过转化的抽象语法树重新生成为代码,并且同时创建
SourceMap ;代码生成相较于前两步会简单很多,其核心思想在于深度优先遍历抽象语法树,然后生成对应的代码字符串。
抽象语法树
抽象语法树n * n
,其会被转化为如下数组:
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
其中每个 type
是一系列描述该令牌属性的集合:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
这里的每一个 type
类似于start
、end
、loc
等属性;在实际应用中,譬如对于babylon
解释器生成如下的
// 源代码
(foo, bar) => foo + bar;
// 简化的 AST 表示
{
"program": {
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "ArrowFunctionExpression",
"params": [
{
"type": "Identifier",
"name": "foo"
},
{
"type": "Identifier",
"name": "bar"
}
],
"body": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "foo"
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "bar"
}
}
}
}
]
}
}
我们可以使用 AST Explorer 这个工具进行在线预览与编辑;在上述的operator
属性设置为 +
,而左右两个参数分别挂载于 left
与 right
属性下。在接下来的转化步骤中,我们即是需要对这样的抽象语法树进行转换,该步骤主要由babel-traverse
这个库来辅助进行
// AST shortened for clarity
{
"program": {
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "Literal",
"value": "use strict"
}
},
{
"type": "ExpressionStatement",
"expression": {
"type": "FunctionExpression",
"async": false,
"params": [
{
"type": "Identifier",
"name": "foo"
},
{
"type": "Identifier",
"name": "bar"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "foo"
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "bar"
}
}
}
]
},
"parenthesizedExpression": true
}
}
]
}
}
自定义插件
// Source Code
const func = (foo, bar) => foo + bar;
// Transformed Code
("use strict");
const _func = function (_foo, _bar) {
return _foo + _bar;
};
在上一节中我们对比过转化前后两个函数语法树的差异,这里我们就开始定义转化插件。首先每个插件都是以
// plugin.js 文件,定义插件
import type NodePath from "babel-traverse";
export default function (babel) {
const { types: t } = babel;
return {
name: "ast-transform", // not required
visitor: {
Identifier(path) {
path.node.name = `_${path.node.name}`;
},
ArrowFunctionExpression(
path: NodePath<BabelNodeArrowFunctionExpression>,
state: Object
) {
// In some conversion cases, it may have already been converted to a function while this callback
// was queued up.
if (!path.isArrowFunctionExpression()) return;
path.arrowFunctionToExpression({
// While other utils may be fine inserting other arrows to make more transforms possible,
// the arrow transform itself absolutely cannot insert new arrow functions.
allowInsertArrow: false,
specCompliant: !!state.opts.spec,
});
},
},
};
}
// babel.js 使用插件
const babel = require("babel-core");
const plugin = require("./plugin");
const out = babel.transform(src, {
plugins: [plugin],
});
常用转化操作
遍历
- 获取子节点路径我们可以通过
path.node.{property}
的方式来访问AST 中节点属性:
// the BinaryExpression AST node has properties: `left`, `right`, `operator`
BinaryExpression(path) {
path.node.left;
path.node.right;
path.node.operator;
}
我们也可以使用某个路径对象的 get
方法,通过传入子路径的字符串表示来访问某个属性:
BinaryExpression(path) {
path.get('left');
}
Program(path) {
path.get('body.0');
}
- 判断某个节点是否为指定类型
babel 内置的type 对象提供了许多可以直接用来判断节点类型的工具函数:
BinaryExpression(path) {
if (t.isIdentifier(path.node.left)) {
// ...
}
}
或者同时以浅比较来查看节点属性:
BinaryExpression(path) {
if (t.isIdentifier(path.node.left, { name: "n" })) {
// ...
}
}
// 等价于
BinaryExpression(path) {
if (
path.node.left != null &&
path.node.left.type === "Identifier" &&
path.node.left.name === "n"
) {
// ...
}
}
- 判断某个路径对应的节点是否为指定类型
BinaryExpression(path) {
if (path.get('left').isIdentifier({ name: "n" })) {
// ...
}
}
- 获取指定路径的父节点有时候我们需要从某个指定节点开始向上遍历获取某个父节点,此时我们可以通过传入检测的回调来判断:
path.findParent((path) => path.isObjectExpression());
// 获取最近的函数声明节点
path.getFunctionParent();
- 获取兄弟路径如果某个路径存在于
Function 或者Program 中的类似列表的结构中,那么其可能会包含兄弟路径:
// 源代码
const a = 1; // pathA, path.key = 0
const b = 2; // pathB, path.key = 1
const c = 3; // pathC, path.key = 2
// 插件定义
export default function ({ types: t }) {
return {
visitor: {
VariableDeclaration(path) {
// if the current path is pathA
path.inList; // true
path.listKey; // "body"
path.key; // 0
path.getSibling(0); // pathA
path.getSibling(path.key + 1); // pathB
path.container; // [pathA, pathB, pathC]
},
},
};
}
- 停止遍历部分情况下插件需要停止遍历,我们此时只需要在插件中添加
return 表达式:
BinaryExpression(path) {
if (path.node.operator !== '**') return;
}
我们也可以指定忽略遍历某个子路径:
outerPath.traverse({
Function(innerPath) {
innerPath.skip(); // if checking the children is irrelevant
},
ReferencedIdentifier(innerPath, state) {
state.iife = true;
innerPath.stop(); // if you want to save some state and then stop traversal, or deopt
},
});
操作
- 替换节点
// 插件定义
BinaryExpression(path) {
path.replaceWith(
t.binaryExpression("**", path.node.left, t.numberLiteral(2))
);
}
// 代码结果
function square(n) {
- return n * n;
+ return n ** 2;
}
- 将某个节点替换为多个节点
// 插件定义
ReturnStatement(path) {
path.replaceWithMultiple([
t.expressionStatement(t.stringLiteral("Is this the real life?")),
t.expressionStatement(t.stringLiteral("Is this just fantasy?")),
t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),
]);
}
// 代码结果
function square(n) {
- return n * n;
+ "Is this the real life?";
+ "Is this just fantasy?";
+ "(Enjoy singing the rest of the song in your head)";
}
- 将某个节点替换为源代码字符串
// 插件定义
FunctionDeclaration(path) {
path.replaceWithSourceString(`function add(a, b) {
return a + b;
}`);
}
// 代码结果
- function square(n) {
- return n * n;
+ function add(a, b) {
+ return a + b;
}
- 插入兄弟节点
// 插件定义
FunctionDeclaration(path) {
path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));
path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
}
// 代码结果
+ "Because I'm easy come, easy go.";
function square(n) {
return n * n;
}
+ "A little high, little low.";
- 移除某个节点
// 插件定义
FunctionDeclaration(path) {
path.remove();
}
// 代码结果
- function square(n) {
- return n * n;
- }
- 替换节点
// 插件定义
BinaryExpression(path) {
path.parentPath.replaceWith(
t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me."))
);
}
// 代码结果
function square(n) {
- return n * n;
+ "Anyway the wind blows, doesn't really matter to me, to me.";
}
- 移除某个父节点
// 插件定义
BinaryExpression(path) {
path.parentPath.remove();
}
// 代码结果
function square(n) {
- return n * n;
}
作用域
- 判断某个局部变量是否被绑定:
FunctionDeclaration(path) {
if (path.scope.hasBinding("n")) {
// ...
}
}
FunctionDeclaration(path) {
if (path.scope.hasOwnBinding("n")) {
// ...
}
}
- 创建
UID
FunctionDeclaration(path) {
path.scope.generateUidIdentifier("uid");
// Node { type: "Identifier", name: "_uid" }
path.scope.generateUidIdentifier("uid");
// Node { type: "Identifier", name: "_uid2" }
}
- 将某个变量声明提取到副作用中
// 插件定义
FunctionDeclaration(path) {
const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
path.remove();
path.scope.parent.push({ id, init: path.node });
}
// 代码结果
- function square(n) {
+ const _square = function square(n) {
return n * n;
- }
+ };