Babel,是一个 JavaScript 的编译工具,它能够将 es6+语法的代码,转换为浏览器兼容的低版本的代码。它简直就是一个神兵利器,前端工程师拥有了它,就能够在项目中使用一些较新的 es 语法。笔者决定弄懂它,并实现一个本身的 Babel 插件。javascript
Babel 的工做原理,能够用以下公式表述。它实际上就是接受输入的源代码,而后对它作一些处理和转换,最后输出为目标版本的代码。前端
const babel = sourceCode => distCode
复制代码
在将输入的源码作处理或者转换时,这就须要用到了它的插件系统。一个插件只负责处理一件事,好比@babel/plugin-transform-arrow-functions
,就是负责将箭头函数转换为普通函数的插件。Babel 提供了很是多的插件,这样就足以保证能够将新的 es 语法转换为旧版本的代码形式。想了解详细的 Babel 插件系统,能够查看Babel#Plugins。java
若是不配置任何的插件,Babel 将不会对源码作任何的处理,它只会照原样输出。下面举个例子,node
const babel = require("@babel/core")
const code = ` const a = () => { console.log(1); } `
// 没有配置任何plugin,那么转换以后的code将没有任何变化
babel.transform(code, undefined, (err, result) => {
if (err) {
throw err
}
console.log(result.code)
})
复制代码
咱们将箭头函数使用 Babel 来转换,可是没有配置任何的插件,最后转换以后的结果将和输入的代码一摸同样。git
➜ babel node scripts/index.ts
const a = () => {
console.log(1);
};
复制代码
若是配置了@babel/plugin-transform-arrow-functions
,Babel 就能正常将咱们的箭头函数转换为普通函数的形式了。以下,es6
// 配置@babel/plugin-transform-arrow-functions
babel.transform(
code,
{ plugins: ["@babel/plugin-transform-arrow-functions"] },
(err, result) => {
if (err) {
throw err
}
console.log(result.code)
}
)
复制代码
转换以后的代码以下,github
➜ babel node scripts/index.ts
const a = function () {
console.log(1);
};
复制代码
对于其余的语法形式的转换,能够添加其余的插件。若是仅仅这样,对于一个实际项目代码的转换,将要配置很是多的插件。为了简化这种形式,Babel 又提供了 Presets,简单的说,就是将不少个插件集合从新命名为一个新名称。这样,只须要配置了这个 Presets,那么就相对于配置它所包含的全部的插件。Babel 定义了经常使用的 Presets,详细能够查看Babel#Presets。express
经过加入插件处理的方式,Babel 将会有很是好的可扩展性和可插拔性,好比 esNext 中又添加了一个新的语法糖,那么 Babel 只须要单独提供这个新语法处理的插件,并将它配置进去就能够了。对于前端工程师们,也能够根据实际业务需求,写本身的插件,将输入的源码处理成本身想要的输出。api
为了能写出本身的 Babel 插件,咱们就须要知道 Babel 将输入的代码转换成什么样子,插件接受的参数又是什么样子,最后须要返回的值是什么样子。数组
Babel 会将输入的源代码先转换成 AST(Abstract Syntax Tree),而后将 AST 做为参数传给插件,插件将在 AST 上作处理,能够添加,删除或者改变节点。例如,咱们上面例子中箭头函数 a,生成的 AST 大体结构以下,
"program": {
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "ArrowFunctionExpression",
"params": [],
"body": {
"type": "BlockStatement",
"body": [
{ ↔ }
],
}
}
}
],
"kind": "const"
}
],
},
复制代码
AST 能够当作一棵树,它包含了不少的节点,每一个节点都会包含一个 type 字段,这个 type 字段就是用来代表当前节点的类型,好比上面的Identifier
代表是标识符,ArrowFunctionExpression
代表是箭头函数表达式。想详细了解 AST 结构,能够查看astexplorer。
要处理 AST 树,就得遍历这颗树,找到咱们要处理的节点位置。对于一棵树的遍历,有 DFS(深度优先搜索)和 BFS(广度优先搜索)两种方式。对于 AST 的遍历,使用的是 DFS 方式。Babel 提供了@babel/traverse
来遍历它,能够很方便的找到须要处理的节点位置。例如上面的例子,咱们能够像下面这样找到console.log(1)
中1
这个节点位置,
const babel = require("@babel/core")
const traverse = require("@babel/traverse")
const code = ` const a = () => { console.log(1); } `
babel.parse(code, null, (err, ast) => {
if (err) {
throw err
}
traverse(ast, {
NumericLiteral(path) {
console.log(JSON.stringify(path.node, null, 4))
},
})
})
复制代码
因为console.log(1)
接受的参数是一个数字字面量,因此它对应的type
就是NumericLiteral
。最后找到这个节点的信息以下,
➜ babel node scripts/index.ts
{
"type": "NumericLiteral",
"start": 37,
"end": 38,
"loc": {
"start": {
"line": 3,
"column": 16
},
"end": {
"line": 3,
"column": 17
}
},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
复制代码
能够看到,节点包含了 type,loc 信息,以及 value 等信息。更多关于 traverse 的使用,能够查看这里Babel#traverse。
根据上面的思路,咱们能够得出以下结论,
Babel 中插件接受 AST 做为参数,而后能够在 AST 上作一些自定义的处理,最后返回处理以后的 AST。
为了验证这个结论正确性,咱们来看看官方的@babel/plugin-transform-arrow-functions
的源码,源码只有 28 行代码,我贴出来,并作一些本身的注释,一块儿看看。
import { declare } from "@babel/helper-plugin-utils";
import type NodePath from "@babel/traverse";
export default function declare((api, options) {
// 判断当前Babel版本是不是v7.x
api.assertVersion(7);
// 接受咱们传入的参数
const { spec } = options;
// 返回一个对象
return {
name: "transform-arrow-functions",
visitor: {
ArrowFunctionExpression(
path: NodePath<BabelNodeArrowFunctionExpression>,
) {
// 先判断是否是箭头函数表达式,不是就直接返回
if (!path.isArrowFunctionExpression()) return;
// 将箭头函数转为函数表达式
path.arrowFunctionToExpression({
allowInsertArrow: false,
specCompliant: !!spec,
});
},
},
};
});
复制代码
从源码能够看出,它返回一个declare
函数。这个函数接受两个参数,一个api
,一个是options
。函数处理步骤以下,
name
和visitor
;其中,visitor
又是一个对象,它才真正包含对肩头函数表达式的处理。实际上,path.arrowFunctionToExpression
就是使用@babel/types
中arrowfunctionexpression
,详细能够查看babel-types#arrowfunctionexpression。
跟咱们猜测的 Babel 插件样子有点出入,可是它包含了咱们猜测的内容。最后,咱们能够总结出写一个 Babel 插件的样子应该是这样的,
export default function declare(api, options) {
// api能够作一些版本兼容性判断,或者缓存相关的。
// options就是咱们配置插件时,传入的参数,这里插件内部就能够使用了
return {
name: "my-custorm-plugin",
visitor: {
// 遍历AST作处理
},
}
}
复制代码
清楚了 Babel 插件的模版形式,就能够按照这个模版写咱们自定义的功能插件。假设,咱们要写的一个 Babel 插件,就是去掉全部的console.log
相关调试信息的代码。
// 源代码
const a = () => {
console.log(1)
}
复制代码
例如上面的代码通过咱们的 Babel 插件处理以后,输出的代码应该是一个空的箭头函数 a,
// 转换以后
const a = () => {}
复制代码
根据 Babel 插件模版代码,咱们能够这样实现以下,
// plugins/remove-console-log.js
const types = require("@babel/types")
module.exports = function declare(api, options) {
api.assertVersion(7)
return {
name: "remove-console-log",
visitor: {
ExpressionStatement(path) {
const expression = path.node.expression
if (types.isCallExpression(expression)) {
const callee = expression.callee
if (types.isMemberExpression(callee)) {
const objName = callee.object.name
const methodName = callee.property.name
if (objName === "console" && methodName === "log") {
path.remove()
}
}
}
},
},
}
}
复制代码
而后在 babel.config.js 中配置以下,
module.exports = {
plugins: ["./plugins/remove-console-log.js"],
}
复制代码
最后,咱们经过 Babel 转换以后就能够获得咱们指望的结果了。
经过本身实现一个 Babel 插件,而后贯穿整个过程把 Babel 原理弄清楚。上面其实还有一个小知识点,就是 Babel 怎么将源码转换成 AST 的。其实,它的过程也不难理解,只是在转换为 AST 以前,须要先进行词法分析,把源码字符串转换成 Token 数组;而后根据词法分析获得的结果,转换成 AST。完整的 Babel 原理过程能够简单的表述为以下,
let tokens = tokenizer(input) // 词法分析
let ast = parser(tokens) // 转换为AST
let newAst = transformer(ast) // 调用插件,进行转换
let output = codeGenerator(newAst) // 最后,生成新的目标代码
复制代码
若是想更加详细研究 Babel 的过程,能够看看这个简易的编译器the-super-tiny-compiler,它实现了完整的流程过程,代码也很是简单易懂。