Babel的原理及简单实现
# 什么是 Babel
Babel 是一个 JavaScript 编译器。Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
# babel 处理过程
Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),⽣成(generate)。词法分析阶段把字符串形式的代码转换令牌 (tokens) 流。
babel 针对处理时采用的不同的⼯具,最终也有不同的效果:
- @babel/parser : 转化为 AST 抽象语法树;
- @babel/traverse 对 AST 节点进⾏递归遍历;
- @babel/types 对具体的 AST 节点进⾏进⾏修改;
- @babel/generator : AST 抽象语法树⽣成为新的代 码;
接下来一起写一个简单的 demo 来了解其做的事, 为了方便理解,推荐打开下面两个网站。
- JSON 格式转换 - 将我们的结果复制上去查看规整的结构 (opens new window)
- AST 树转换 - 查看 Babel 转换最后生成的 AST 树是怎么样的 (opens new window)
# 解析
解析做的事就是将我们普通的代码解析成抽象语法树(AST),下面我们就用一个简短的字串来进行演示
假设我们有这样的一段代码:
(add 10 (subtract 40 2))
我们需要将其转换成这样的代码:
add(10, subtract(40, 2));
# 词法分析
首先我们需要进行词法分析(分词),把我们平时输入的代码看作是一串普通的字符或是文本,我们通过分析它的类型将其解析出来。下面我们将(add 10 (subtract 40 2))按类别切割开来
function generateToken(data) {
let current = 0;
const tokens = [];
// 将字串从头到尾遍历,并为其附上类型值
while (current < data.length) {
let char = data[current];
if (char === "(" || char === ")") {
tokens.push({
type: "paren",
value: char,
});
current++;
continue;
}
// 针对字串,当碰到字母时,我们认为就是碰到了函数名,循环将其全称解析出来
if (/[a-zA-Z\$\_]/.test(char)) {
let strValue = "";
while (/[a-zA-Z0-9\$\_]/.test(char)) {
strValue += char;
char = data[++current];
}
tokens.push({
type: "name",
value: strValue,
});
continue;
}
// 数字也同理
if (/[0-9]/.test(char)) {
let strValue = "";
while (/[0-9]/.test(char)) {
strValue += char;
char = data[++current];
}
tokens.push({
type: "number",
value: strValue,
});
continue;
}
if (/\s/.test(char)) {
current++;
continue;
}
// 如果碰到我们词法分析以外的字符,则报错
throw new TypeError("I dont know what this character is: " + char);
}
}
const parser = (data) => {
const tokens = generateToken(data);
return tokens;
};
console.dir(parser("(add 10 (subtract 40 2))"));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
结果如下,可以看到, 是一个扁平的带有每个字符含义的数组:
[
{ "type": "paren", "value": "(" },
{ "type": "name", "value": "add" },
{ "type": "number", "value": "10" },
{ "type": "paren", "value": "(" },
{ "type": "name", "value": "subtract" },
{ "type": "number", "value": "40" },
{ "type": "number", "value": "2" },
{ "type": "paren", "value": ")" },
{ "type": "paren", "value": ")" }
]
2
3
4
5
6
7
8
9
10
11
接下来我们就要对其进行语法分析
# 语法分析
我们将通过上一轮的分词获得的 tokens 数组提供的信息来分析出代码之间的逻辑关系,只有经过词法分析 token 流才能成为有结构的抽象语法树.babel 的语法分析遵循estree 规范 (opens new window)。我们需要分析词法生成的数组内的每一个元素属于声明、表达式或是语句,以及他们之间作用域的关系,这也是语法分析具有难度的一部分,在 babel 的原理中这里代码占有的比重巨大,这里我只是针对 demo 简单粗暴的写了对应的处理。
function generateTokens(ast) {...}
/**
* 生成AST树
* 将扁平化的数组转换为有层次结构的多叉树状
* has : type , start ,end ,expression, callee,arguments, optional
* 带有父子级关系的层级结构,CallExpression类型为调用的函数,入参则会被加入到其arguments中
*/
function generateAST(tokens) {
let current = 0;
// 根
let ast = {
type: "Program",
body: [],
};
function walk() {
let token = tokens[current];
if (token.type === "number") {
current++;
return {
type: "NumberLiteral",
value: token.value,
};
}
if (token.type === "paren" || token.value === "(") {
// ‘(’后面的第一位是执行的函数名,记录下来,并不断将其参数打入params里
token = tokens[++current];
let node = {
type: "CallExpression",
name: token.value,
params: [],
};
token = tokens[++current];
while (
token.type !== "paren" ||
(token.type === "paren" && token.value !== ")")
) {
// 同步将函数调用的入参传入
node.params.push(walk());
// 重新定位同步后的token值
token = tokens[current];
}
current++;
return node;
}
}
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
const parser = (data) => {
const tokens = generateToken(data);
return generateAST(tokens);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
最后我们可以得出这样的一层结构
{
"type": "Program",
"body": [
{
"type": "CallExpression",
"name": "add",
"params": [
{ "type": "NumberLiteral", "value": "10" },
{
"type": "CallExpression",
"name": "subtract",
"params": [
{ "type": "NumberLiteral", "value": "40" },
{ "type": "NumberLiteral", "value": "2" }
]
}
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
我们把想要生成的 add(10, subtract(40, 2)); 的代码放到 ast 生成器的网站上看一下, 对比我们的代码 ,是不是愈来愈像啦?到这里我们就完成了代码的简单解析 parser
{
"type": "Program",
"start": 0,
"end": 25,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 25,
"expression": {
"type": "CallExpression",
"start": 0,
"end": 24,
"callee": {
"type": "Identifier",
"start": 0,
"end": 3,
"name": "add"
},
"arguments": [
{
"type": "Literal",
"start": 4,
"end": 6,
"value": 10,
"raw": "10"
},
{
"type": "CallExpression",
"start": 8,
"end": 23,
"callee": {
"type": "Identifier",
"start": 8,
"end": 16,
"name": "subtract"
},
"arguments": [
{
"type": "Literal",
"start": 17,
"end": 19,
"value": 40,
"raw": "40"
},
{
"type": "Literal",
"start": 21,
"end": 22,
"value": 2,
"raw": "2"
}
],
"optional": false
}
],
"optional": false
}
}
],
"sourceType": "module"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 转换
在 Babel 中我们最常使用的就是代码转换,我们常用的 Babel 插件就是定义代码转换规则而生的,而代码解析和生成这一头一尾都主要是 Babel 负责。即通过 babel 生成的抽象语法树,将原本的抽象语法树生成符合目标规则的新的抽象语法树,babel 最后根据新的抽象语法树生成代码。
// 首先, 我们需要一个遍历器 transformer转化 ast 到新的 ast 建立父子级关系 - 深度优先遍历
function transformer(ast) {
let newAst = {
type: "Program",
body: [],
};
// 将父级的引用都存放在ast._context上,保证子集能知道父级是谁
ast._context = newAst.body;
DFS(ast, {
Literal: {
enter(node, parent) {
parent._context.push({
type: "Literal",
value: node.value,
});
},
},
CallExpression: {
enter(node, parent) {
let expression = {
type: "CallExpression",
callee: {
type: "Identifier",
name: node.name,
},
arguments: [],
};
// 为新层级创建_context
node._context = expression.arguments;
if (parent.type !== "CallExpression") {
expression = {
type: "ExpressionStatement",
expression: expression,
};
}
parent._context.push(expression);
},
},
});
return newAst;
}
/**
*
* @param ast
* @param visitor 本质是挂载不同方法的 JavaScript 对象,类似生命周期,在指定的时机匹配执行
* 它会访问 ast 上每个节点,然后根据针对不同节点用相应的方法做出不同的转换.
*/
function DFS(ast, visitor) {
function traversArray(children, parent) {
children.forEach((child) => traverseNode(child, parent));
}
function traverseNode(node, parent) {
let methods = visitor[node.type];
// enter生命周期
if (methods && methods.enter) {
methods.enter(node, parent);
}
switch (node.type) {
case "Program":
traversArray(node.body, node);
break;
case "CallExpression":
traversArray(node.params, node);
break;
case "Literal":
break;
}
// 同理, exit生命周期
// if (methods && methods.exit) {
// methods.exit(node, parent);
// }
}
return traverseNode(ast, null);
}
const parser = (input) => {
const tokens = generateTokens(input);
const ast = generateAST(tokens);
const newAst = transformer(ast);
return newAst;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
生成代码如下:
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": { "type": "Identifier", "name": "add" },
"arguments": [
{ "type": "Literal", "value": "2" },
{
"type": "CallExpression",
"callee": { "type": "Identifier", "name": "subtract" },
"arguments": [
{ "type": "Literal", "value": "40" },
{ "type": "Literal", "value": "2" }
]
}
]
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在真实的 babel 插件、visitor 对象中,每次访问节点方法时,都会传入一个 node 参数,这个 node 参数中包含了节点的信息以及节点和所在的位置,以供对特定节点进行操作。具体来说 node 是表示两个节点之间连接的对象。这个对象不仅包含了当前节点的信息,也有当前节点的父节点的信息,同时也包含了添加、更新、移动和删除节点有关的其他很多方法。具体地,node 对象包含的属性和方法主要如下:
── 属性
- node 当前节点
- parent 父节点
- parentPath 父 path
- scope 作用域
- context 上下文
- ... ── 方法
- get 当前节点
- findParent 向父节点搜寻节点
- getSibling 获取兄弟节点
- replaceWith 用 AST 节点替换该节点
- replaceWithMultiple 用多个 AST 节点替换该节点
- insertBefore 在节点前插入节点
- insertAfter 在节点后插入节点
- remove 删除节点
- ...
# 生成
babel 的最后一步,就是将我们转换后的 AST 抽象语法树生成新的代码
function generate(ast) {
switch (ast.type) {
case "Identifier":
return ast.name;
case "Literal":
return ast.value;
case "Program":
return ast.body.map((childAST) => generate(childAST).join("\n"));
// 每一个ExpressionStatement 就是一段执行语句
case "ExpressionStatement":
return generate(ast.expression) + ";";
case "CallExpression":
return (
generate(ast.callee) +
"(" +
ast.arguments.map((arg) => generate(arg)).join(", ") +
")"
);
}
}
const parser = (input) => {
const tokens = generateTokens(input);
const ast = generateAST(tokens);
const newAst = transformer(ast);
return generate(newAst);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
最后,我们成功的将(add 10 (subtract 40 2)) 转化为了 add(10, subtract(40, 2)) , 从而通过这个 babel 的 demo 了解 babel 的工作原理, 下面我们来简单的写一个 babel 插件玩玩
# 写一个 babel 插件
插件的本质就是一个 JS 程序, 指示着 Babel 如何对代码进行转换。
首先我们先创建目录并安装核心依赖
- @babel/core: Babel 的核心模块.
- @babel/cli: 终端运行工具, 内置的插件,运行你从终端使用 babel 的工具.
以及后面需要用到的很方便的库
- @babel-types
npm init -y
npm i --save-dev @babel/cli @babel/core
2
然后我们创建一个.babelrc 文件作为 babel 的配置文件
{
"presets": [...],
"plugins": [...]
}
2
3
4
我们引入 babel 的插件时,也可以通过相对路径引入
{
"plugins": [ // 想传入其他参数配置时,plugin内的插件采用数组的形式
["./plugin", {
"operator": "*"
}]
]
}
2
3
4
5
6
7
# Bable 变形概念
visiter(访问者):以访问者的形式获取节点信息,visiter 是一个对象,包含各种类型节点的访问函 数,我们修改节点必不可少
Path:具体来说 Path 是表示两个节点之间连接的对象。这个对象不仅包含了当前节点的信息,也有 当前节点的父节点的信息,同时也包含了添加、更新、移动和删除节点有关的其他很多方法。
在平时编写组件库的按需引入时,许多主流的组件库都引入了babel-plugin-import (opens new window) 插件,其原理就是将相对路径引入的文件通过改写其引入的方式将指定的组件文件、css 引入到项目中来。当然这也对组件库的打包方式提出了一定的要求:把每一个组件当作一个模块进行打包,而不是打包成单独的 indexjs
import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);
↓ ↓ ↓ ↓ ↓ ↓
var \_button = require('antd/lib/button');
ReactDOM.render(<\_button>xxxx</\_button>);
2
3
4
5
6
7
import assert from "assert";
import { join } from "path";
import {
addSideEffect,
addDefault,
addNamed,
} from "@babel/helper-module-imports";
export default function ({ types }) {
let plugins = null;
// Only for test
// eslint-disable-next-line no-underscore-dangle
global.__clearBabelAntdPlugin = () => {
plugins = null;
};
function applyInstance(method, args, context) {
// eslint-disable-next-line no-restricted-syntax
for (const plugin of plugins) {
if (plugin[method]) {
plugin[method].apply(plugin, [...args, context]);
}
}
}
const Program = {
enter(path, { opts = {} }) {
// Init plugin instances once.
if (!plugins) {
if (Array.isArray(opts)) {
plugins = opts.map(
(
{
libraryName,
libraryDirectory,
style,
styleLibraryDirectory,
customStyleName,
camel2DashComponentName,
camel2UnderlineComponentName,
fileName,
customName,
transformToDefaultImport,
},
index
) => {
assert(libraryName, "libraryName should be provided");
return new Plugin(
libraryName,
libraryDirectory,
style,
styleLibraryDirectory,
customStyleName,
camel2DashComponentName,
camel2UnderlineComponentName,
fileName,
customName,
transformToDefaultImport,
types,
index
);
}
);
} else {
assert(opts.libraryName, "libraryName should be provided");
plugins = [
new Plugin(
opts.libraryName,
opts.libraryDirectory,
opts.style,
opts.styleLibraryDirectory,
opts.customStyleName,
opts.camel2DashComponentName,
opts.camel2UnderlineComponentName,
opts.fileName,
opts.customName,
opts.transformToDefaultImport,
types
),
];
}
}
applyInstance("ProgramEnter", arguments, this); // eslint-disable-line
},
exit() {
applyInstance("ProgramExit", arguments, this); // eslint-disable-line
},
};
const methods = [
"ImportDeclaration",
"CallExpression",
"MemberExpression",
"Property",
"VariableDeclarator",
"ArrayExpression",
"LogicalExpression",
"ConditionalExpression",
"IfStatement",
"ExpressionStatement",
"ReturnStatement",
"ExportDefaultDeclaration",
"BinaryExpression",
"NewExpression",
"ClassDeclaration",
"SwitchStatement",
"SwitchCase",
];
const ret = {
visitor: { Program },
};
// eslint-disable-next-line no-restricted-syntax
for (const method of methods) {
ret.visitor[method] = function () {
// eslint-disable-line
applyInstance(method, arguments, ret.visitor); // eslint-disable-line
};
}
return ret;
}
function transCamel(_str, symbol) {
const str = _str[0].toLowerCase() + _str.substr(1);
return str.replace(/([A-Z])/g, ($1) => `${symbol}${$1.toLowerCase()}`);
}
function winPath(path) {
return path.replace(/\\/g, "/");
}
function normalizeCustomName(originCustomName) {
// If set to a string, treat it as a JavaScript source file path.
if (typeof originCustomName === "string") {
// eslint-disable-next-line import/no-dynamic-require
const customNameExports = require(originCustomName);
return typeof customNameExports === "function"
? customNameExports
: customNameExports.default;
}
return originCustomName;
}
class Plugin {
constructor(
libraryName,
libraryDirectory,
style,
styleLibraryDirectory,
customStyleName,
camel2DashComponentName,
camel2UnderlineComponentName,
fileName,
customName,
transformToDefaultImport,
types,
index = 0
) {
this.libraryName = libraryName;
this.libraryDirectory =
typeof libraryDirectory === "undefined" ? "lib" : libraryDirectory;
this.camel2DashComponentName =
typeof camel2DashComponentName === "undefined"
? true
: camel2DashComponentName;
this.camel2UnderlineComponentName = camel2UnderlineComponentName;
this.style = style || false;
this.styleLibraryDirectory = styleLibraryDirectory;
this.customStyleName = normalizeCustomName(customStyleName);
this.fileName = fileName || "";
this.customName = normalizeCustomName(customName);
this.transformToDefaultImport =
typeof transformToDefaultImport === "undefined"
? true
: transformToDefaultImport;
this.types = types;
this.pluginStateKey = `importPluginState${index}`;
}
getPluginState(state) {
if (!state[this.pluginStateKey]) {
state[this.pluginStateKey] = {}; // eslint-disable-line
}
return state[this.pluginStateKey];
}
importMethod(methodName, file, pluginState) {
if (!pluginState.selectedMethods[methodName]) {
const { style, libraryDirectory } = this;
const transformedMethodName = this.camel2UnderlineComponentName // eslint-disable-line
? transCamel(methodName, "_")
: this.camel2DashComponentName
? transCamel(methodName, "-")
: methodName;
const path = winPath(
this.customName
? this.customName(transformedMethodName, file)
: join(
this.libraryName,
libraryDirectory,
transformedMethodName,
this.fileName
) // eslint-disable-line
);
pluginState.selectedMethods[methodName] = this.transformToDefaultImport // eslint-disable-line
? addDefault(file.path, path, { nameHint: methodName })
: addNamed(file.path, methodName, path);
if (this.customStyleName) {
const stylePath = winPath(this.customStyleName(transformedMethodName));
addSideEffect(file.path, `${stylePath}`);
} else if (this.styleLibraryDirectory) {
const stylePath = winPath(
join(
this.libraryName,
this.styleLibraryDirectory,
transformedMethodName,
this.fileName
)
);
addSideEffect(file.path, `${stylePath}`);
} else if (style === true) {
addSideEffect(file.path, `${path}/style`);
} else if (style === "css") {
addSideEffect(file.path, `${path}/style/css`);
} else if (typeof style === "function") {
const stylePath = style(path, file);
if (stylePath) {
addSideEffect(file.path, stylePath);
}
}
}
return { ...pluginState.selectedMethods[methodName] };
}
buildExpressionHandler(node, props, path, state) {
const file = (path && path.hub && path.hub.file) || (state && state.file);
const { types } = this;
const pluginState = this.getPluginState(state);
props.forEach((prop) => {
if (!types.isIdentifier(node[prop])) return;
if (
pluginState.specified[node[prop].name] &&
types.isImportSpecifier(path.scope.getBinding(node[prop].name).path)
) {
node[prop] = this.importMethod(
pluginState.specified[node[prop].name],
file,
pluginState
); // eslint-disable-line
}
});
}
buildDeclaratorHandler(node, prop, path, state) {
const file = (path && path.hub && path.hub.file) || (state && state.file);
const { types } = this;
const pluginState = this.getPluginState(state);
const checkScope = (targetNode) =>
pluginState.specified[targetNode.name] && // eslint-disable-line
path.scope.hasBinding(targetNode.name) && // eslint-disable-line
path.scope.getBinding(targetNode.name).path.type === "ImportSpecifier"; // eslint-disable-line
if (types.isIdentifier(node[prop]) && checkScope(node[prop])) {
node[prop] = this.importMethod(
pluginState.specified[node[prop].name],
file,
pluginState
); // eslint-disable-line
} else if (types.isSequenceExpression(node[prop])) {
node[prop].expressions.forEach((expressionNode, index) => {
if (types.isIdentifier(expressionNode) && checkScope(expressionNode)) {
node[prop].expressions[index] = this.importMethod(
pluginState.specified[expressionNode.name],
file,
pluginState
); // eslint-disable-line
}
});
}
}
ProgramEnter(path, state) {
const pluginState = this.getPluginState(state);
pluginState.specified = Object.create(null);
pluginState.libraryObjs = Object.create(null);
pluginState.selectedMethods = Object.create(null);
pluginState.pathsToRemove = [];
}
ProgramExit(path, state) {
this.getPluginState(state).pathsToRemove.forEach(
(p) => !p.removed && p.remove()
);
}
ImportDeclaration(path, state) {
const { node } = path;
// path maybe removed by prev instances.
if (!node) return;
const { value } = node.source;
const { libraryName } = this;
const { types } = this;
const pluginState = this.getPluginState(state);
if (value === libraryName) {
node.specifiers.forEach((spec) => {
if (types.isImportSpecifier(spec)) {
pluginState.specified[spec.local.name] = spec.imported.name;
} else {
pluginState.libraryObjs[spec.local.name] = true;
}
});
pluginState.pathsToRemove.push(path);
}
}
CallExpression(path, state) {
const { node } = path;
const file = (path && path.hub && path.hub.file) || (state && state.file);
const { name } = node.callee;
const { types } = this;
const pluginState = this.getPluginState(state);
if (types.isIdentifier(node.callee)) {
if (pluginState.specified[name]) {
node.callee = this.importMethod(
pluginState.specified[name],
file,
pluginState
);
}
}
node.arguments = node.arguments.map((arg) => {
const { name: argName } = arg;
if (
pluginState.specified[argName] &&
path.scope.hasBinding(argName) &&
path.scope.getBinding(argName).path.type === "ImportSpecifier"
) {
return this.importMethod(
pluginState.specified[argName],
file,
pluginState
);
}
return arg;
});
}
MemberExpression(path, state) {
const { node } = path;
const file = (path && path.hub && path.hub.file) || (state && state.file);
const pluginState = this.getPluginState(state);
// multiple instance check.
if (!node.object || !node.object.name) return;
if (pluginState.libraryObjs[node.object.name]) {
// antd.Button -> _Button
path.replaceWith(
this.importMethod(node.property.name, file, pluginState)
);
} else if (
pluginState.specified[node.object.name] &&
path.scope.hasBinding(node.object.name)
) {
const { scope } = path.scope.getBinding(node.object.name);
// global variable in file scope
if (scope.path.parent.type === "File") {
node.object = this.importMethod(
pluginState.specified[node.object.name],
file,
pluginState
);
}
}
}
Property(path, state) {
const { node } = path;
this.buildDeclaratorHandler(node, "value", path, state);
}
VariableDeclarator(path, state) {
const { node } = path;
this.buildDeclaratorHandler(node, "init", path, state);
}
ArrayExpression(path, state) {
const { node } = path;
const props = node.elements.map((_, index) => index);
this.buildExpressionHandler(node.elements, props, path, state);
}
LogicalExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["left", "right"], path, state);
}
ConditionalExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(
node,
["test", "consequent", "alternate"],
path,
state
);
}
IfStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["test"], path, state);
this.buildExpressionHandler(node.test, ["left", "right"], path, state);
}
ExpressionStatement(path, state) {
const { node } = path;
const { types } = this;
if (types.isAssignmentExpression(node.expression)) {
this.buildExpressionHandler(node.expression, ["right"], path, state);
}
}
ReturnStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["argument"], path, state);
}
ExportDefaultDeclaration(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["declaration"], path, state);
}
BinaryExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["left", "right"], path, state);
}
NewExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["callee", "arguments"], path, state);
}
SwitchStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["discriminant"], path, state);
}
SwitchCase(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["test"], path, state);
}
ClassDeclaration(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ["superClass"], path, state);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# babel 其他插件的作用
# @babel/preset-env
@babel/preset-env : 可以将 es6 往上的代码向下兼容,也可以兼容到不同版本的浏览器 plugins 是提供的插件, preset 是一组 plugins 的集合.
const presets = [
[
"@babel/env",
// 配置其执行的浏览器版本
{
targets: {
edge: "17",
chrome: "64",
firefox: "60",
safari: "11.1"
}
}
]
]
2
3
4
5
6
7
8
9
10
11
12
13
14
# @babel/types
@babel/types : babel-types 是一个强大的用于处理 AST 节点的工具库,“它包含了构造、验证以及变换 AST 节点的方法。该工具库包含考虑周到的工具方法,对编写处理 AST 逻辑非常有用。我们可以借助他来进行 babel 数据类型的判断等功能,例如:
function plugin() {
return ({ types }) => ({
visitor: {
ImportDeclaration(path, state) {
const specifiers = path.node.specifiers;
specifiers.forEach((specifier) => {
if (
!types.isImportDefaultSpecifier(specifier) && // 通过types判断类型
!types.isImportNamespaceSpecifier(specifier)
) {
// do something
}
});
},
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import traverse from "babel-traverse";
import * as t from "babel-types";
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "n" })) {
path.node.name = "x";
}
},
});
2
3
4
5
6
7
8
9
# @Babel-generator
Babel 的代码生成器,它读取 AST 并将其转换为代码和源码映射(sourcemaps)。
# @Babel-template
一个虽然很小但却非常有用的模块。 它能让你编写字符串形式且带有占位符的代码来代替手动编码, 尤其是生成大规模 AST 的时候。 在计算机科学中,这种能力被称为准引用(quasiquotes)
# @Babel-traverse
负责维护整棵树的状态,并且负责替换、移除和添加节点。
babel-traverse 可以遍历由 Babylon( @babel/parser )生成的抽象语法树,并把抽象语法树的各个节点从拓扑数据结构转化成一颗路径(Path)树,Path 表示两个节点之间连接的响应式(Reactive)对象,它拥有添加、删除、替换节点等方法。当你调用这些修改树的方法之后,路径信息也会被更新。除此之外,Path 还提供了一些操作作用域(Scope) 和标识符绑定(Identifier Binding) 的方法可以去做处理一些更精细复杂的需求。可以说 babel-traverse 是使用 Babel 作为编译器最核心的模块。