本文会对babel文档文档从一个推导角度来阐述每个babel模块的作用,尝试理清其中脉络,方便快速理解。
本文不是官网的copyer或者中文翻译
核心
babel的核心功能在@babel/core
包中,核心api为transform
系列函数:
babel.transform(code, options, function(err, result) {
result; // => { code, map, ast }
});
该函数可以将es6+代码转译成es5代码,所以被广泛集成在其他工具里面,完成代码的转译工作,如babel-loader
内部就是调该api。
在babel中,还提供了@babel/cli
和@babel/register
两个工具,前者提供命令行工具函数对文件进行转译;后者提供require
钩子:对node的require函数改造,对后续require
函数在执行时自动对模块进行源码转译后在导入。
babel的目标是对代码进行转译,这个过程可以拆解为:解析源码,遍历ast改造代码,重新生成代码这三个过程。为了提高使用范围,在v7+
版本中,babel将功能拆解出来了多个工具,主要有:
- 解析源码:
@babel/parser
; - 遍历ast改造代码:
@babel/traverse
和@babel/plugin-*
; 解析源码
function square(n) { return n * n; }
解析成:
{ type: "FunctionDeclaration", id: { type: "Identifier", name: "square" }, params: [{ type: "Identifier", name: "n" }], body: { type: "BlockStatement", body: [{ type: "ReturnStatement", argument: { type: "BinaryExpression", operator: "*", left: { type: "Identifier", name: "n" }, right: { type: "Identifier", name: "n" } } }] } }
ast可以简单的理解为源码字符串进行语法分析后的结构化数据,方便后续进行检查或者改造。
ast中的节点一般还会包含坐标位置,如字符串下标,行数,列数等,更多详细内容请参考官方文档。
老版本babel中使用的是 acorn 和 acorn-jsx,在v7
以上时,进行了fork改造为@babel/parser
。
另外@babel/core
也集成了@babel/parser
功能,可以直接从@babel/core
中导出api直接使用:
babel.parse(code: string, options?: Object, callback: Function)
当前的解析器默认只支持最新的es6代码,如果需要兼容一些新语法(非语法糖之类的新特性,新表达式和新操作服,如对象解构,可选表达式,类型等),需要扩展babel语法插件。
很多工具其实只需要解析代码即可,如代码检验,如语法高亮,源码中数据收集。
遍历ast改造代码
讲过解析器已经将源码解析成更好处理的结构化数据ast,如果需求是对代码进行调整,只需对ast数据进行调整,然后使用生成器生成新的代码即可。但整个babel需要解决的是将所有最新的es6+特性转译成向后兼容的浏览器可执行代码(es5),需要处理的情况众多,如果直接对ast进行改造,那么代码将非常臃肿。且es规范还在不停的迭代中,臃肿的代码的对后续维护迭代也带来巨大的挑战。针对这种困境,必须需要进行架构上的调整,使用插件化架构。
babel即是处于这样一个原因,采用了访问者模式。可以简单的理解为,在对ast进行一个遍历时,每次进入一个新的节点或者退出一个节点时,都会拜访每一个插件,咨询它们是否需要对当前的情况进行处理。这钟架构证了性能,也保证了扩展性。
另一种插件化架构,也就是流式架构,如gulp。也就是插件队列依次对上一个插件处理后的ast对象进行更深一层次的改造。但这种架构,需要多次循环ast, 在实际使用中,一般一个生产项目,文件内容巨大,文件数量居多,会导致性能崩溃。
所以babel核心框架中,只包含了访问节点和调用插件的逻辑。实际对ast的改造,全部转交给了插件。这也是为什么babel自带了那么多@babel/plugin-*
的插件。同样社区也拥有非常多的插件,从能能够支持flow, typescipt这些新语法。
插件化的架构,也允许使用者进行拔插式配置,根据当前使用场景进行高度定制。这也就是在配置文件中如babrlrc.js
可以配置插件的原因。
为了支持高度动态配置化来适配复杂的场景,babel会将每个插件负责的功能划分足够小,一般每个插件只会负责一个特性。这会导致使用时,需要去了解每一个插件的作用,然后在配置文件中配置超长的插件列表,带来巨大的心智负担和维护难度。为了解决这个问题,babel提供了预设的机制。简单的理解就是一个babel配置可以继承另一个配置,那么我们只需要继承社区上或者官方专业人员配置的预设即可,如:
@babel/preset-env
@babel/preset-react
@babel/preset-typescript
@babel/preset-flow
为了方便插件中的复用,babel将遍历ast的工具也开放出来为一个单独的模块@babel/traverse
。将节点类型的判断和创建节点的工具库,放在了@babel/types
。
另外对于一些babel中多个模块公用的一些工具,都封装成工具模块,也就是@babel/helper-*
系列模块,如:
@babel/helper-compilation-targets
@babel/helper-module-imports
由于babel自带了那么多插件,所以很多helper其实是插件的辅助工具,如helper-module-imports
就是辅助生成一些导入节点。
在es6+转成es5的过程中,很多语法糖语法(语法上的细微调整),如let
, const
等实现直接用插件调整代码即可解决。但对于其他的需要大端代码才能实现的特性,如Array#includes
,生成器,迭代器,async/await
, promise
等,如果每次都通过代码展开,那么编译后的代码将会巨大。为了解决这个问题,会将includes
的实现放在补丁(polyfill)中,然后直接使用补丁中的实现。如生成器,迭代器,async/await
, promise
等都是通过这种机制支持。
这些的补丁(polyfill)的导入方式也有两种,一种是全量导入,也就是导入@babel/polyfill
模块。一种是按需导入,需要使用预设@babel/preset-env
,根据实际使用情况,在使用的模块中按需导入@babel/runtime
中的补丁(polyfill)。如:
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() {
_classCallCheck(this, Circle);
};
@babel/polyfill
和@babel/runtime
的底层实现都是core-js
。
实际情景下,还是存在插件无法解决的情况:一个无法用老代码补丁实现,也无法使用语法糖替换代码的特性,如Proxy
对象,这种特性一般需要js引擎从底层提供。在使用这些特性时,需要注意浏览器兼容性。
生成代码
使用@babel/generator
即可对一个ast树重新生成为代码。
配置
我们通常见到的babel配置就是就是用于指导babel行为的配置文件,可以简单的理解为@babel/core
中transform
函数的选项支持使用配置文件配置。
更多的配置详细使用等请看官网。
其他官方工具
babel还提供了一些其他工具,用于扩展babel生态链:
@babel/standalone
: 支持浏览器上运行的babel版本,用于一些在线编辑网站,如JS Bin@babel/code-frame
: 代码窗口,用于输出类似这种:1 | class Foo { > 2 | constructor() | ^ 3 | }
@babel/template
: babel插件开发工具,支持根据代码字符串创建ast节点。因为ast节点携带信息较多,且结构较深,在手动创建复杂的代码节点时十分不便。使用官方提供的这个工具,可以快速创建一整段代码节点,并且还支持占位符:const buildRequire = template(` var IMPORT_NAME = require(SOURCE); `); const ast = buildRequire({ IMPORT_NAME: t.identifier("myModule"), SOURCE: t.stringLiteral("my-module"), }); // || || || // \\// \\// \\// const myModule = require("my-module");
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。