引文
最近刷到回顾 babel 6和7,来预测下 babel 8一文,作者从Babel6、7的发展演进,到优化Babel7遗留的问题,预测了Babel8的新特性,,笔者深有感触,裂墙推荐
,遂有此文。
现在的企业级项目,几乎都离不开Babel的支持。或直接使用或被间接集成到了脚手架中。但许多人都只会基本的配置,对相关原理毫不了解(包括笔者)。如果您也是如此,那这篇文章很适合您。
Babel is a JavaScript compiler for use next generation JavaScript today.
ES不断发展,一年一个大版本,新的标准/提案和新的特性层出不穷。然而各种浏览器对新语言特性的实现都会滞后,并且实现程度也不一致。这导致开发者不敢轻易在实际生产项目中使用高级语言特性,而Babel 从诞生之初就是为了解决此类问题。
JS新的语法和 API 进入 ES 标准是有个过程的,这个过程分为以下几个阶段:
- 阶段 0 – Strawman: 只是一个想法,可能用 Babel plugin 实现
- 阶段 1 – Proposal: 值得继续的建议
- 阶段 2 – Draft: 建立 spec
- 阶段 3 – Candidate: 完成 spec 并且在浏览器实现
- 阶段 4 – Finished: 会加入到下一年的 es20xx spec
Babel 是一下一代的JavaScript编译器,将新的语法转化为能够兼容宿主环境的语法。其中包括转换新的语法和为新的API提供polyfill(垫片)。
Plugin && Preset
preset是插件的集合,plugin才是真正发挥作用的实体。Babel7 只提供了四种官方preset。
- preset-env(将ES next 转换为兼容目标环境的JavaScript)
- preset-react(转换React)
- preset-typescript(转换TypeScript)
- preset-flow(转换Flow)
开发者可以自由组合preset和plugin生成自定义的preset
// .babelrc.js
module.exports = {
presets: [
require("@babel/preset-react")
],
plugins: [
require("@babel/plugin-transform-arrow-functions")
]
};
复制代码
执行顺序
plugin按照从左到右,从上到下的顺序执行,preset和plugin执行正好相反。并且plugin会在preset之前执行。
Babel配置
文件类型
- Project-wide configuration(项目范围的配置)
- babel.config.json
- babel.config.js
- babel.config.cjs
- babel.config.mjs
项目范围配置对于必须广泛应用配置的项目非常理想,是monorepo风格
项目的首选。
- File-relative configuration(相对文件的配置)
- .babalrc
- .babelrc.json
- .babelrc.js
- .babelrc.cjs
- .babelrc.mjc
- 在package.json 的babel字段添加配置
在使用相对文件配置的时候,有两个边界情况需要注意:
- 一旦发现了package.json 将会停止搜索,因此相对文件配置只适用于单个package项目,monorepo 风格项目则不适合。
- 正在编译的文件不在babel的
根目录
将会被忽略(相对文件配置不可跨package边界
),除非手动配置babelrcRoots
。
当项目使用原生ECMAScript模块,及package.json包含{ type: 'module' }时,`.js`文件与`.mjs`文件等价,否则与`.cjs`文件等价
复制代码
优先级
babel.config.json < .babelrc < programmatic options from @babel/cli
复制代码
动态|静态配置文件
.js(cjs|mjs)文件是动态的,建议只在需要根据条件或者运行时环境动态生成配置时使用。因为动态就意味着难以静态分析,因此在缓存
、linting
、IDE提示
等方面有着负面影响。如果必须使用动态配置文件,可以配合Babel的API使用。
// babel.config.js
module.exports = function (api) {
api.cache.using(() => process.env.NODE_ENV);
// 如果() => process.env.NODE_ENV 返回结果与上一次计算值不同,就会重新调用获取配置信息
return {
presets: [],
plugins: []
}
}
复制代码
.json文件是静态的,允许其他使用Babel的工具去安全的缓存Babel的结果,显著提升构建性能。
合并策略
Babel对除了presets、plugins外的配置项使用Object.assign进行合并,presets、plugins配置项则使用Array.concat 进行合并。
const config = {
plugins: [["plugin-1a", { loose: true }], "plugin-1b"],
presets: ["preset-1a"],
sourceType: "script"
};
const newConfigItem = {
plugins: [["plugin-1a", { loose: false }], "plugin-2b"],
presets: ["preset-1a", "preset-2a"],
sourceType: "module"
};
BabelConfigMerge(config, newConfigItem);
// returns
({
plugins: [
["plugin-1a", { loose: true }],
"plugin-1b",
["plugin-1a", { loose: false }],
"plugin-2b"
], // new plugins by config.plugins.concat(newConfigItem.plugins)
presets: [
"preset-1a",
"preset-1a",
"preset-2b"
], // new presets by config.presets.concat(newConfigItem.presets)
sourceType: "module" // sourceType: "script" is overwritten
})
复制代码
Babel处理流程
Babel 的处理流程主要分为三步:第一步通过parser将代码解析为AST,第二步遍历AST对代码进行转换,最后将转换后的AST生成新的代码及sourceMap输出。
解析(parse)
解析过程接收字符串格式代码,并输出 AST。 解析过程又被分为两个阶段:词法分析(Lexical Analysis)
和 语法分析(Syntactic Analysis)
阶段。
词法分析(Lexical Analysis)
const greet = name => {
return 'hello ' + name;
};
复制代码
词法分析阶段把原始代码通tokenizer
转换为令牌流(tokens)
。
你可以把令牌看作是一个扁平的语法片段数组:
[
{ type: { ... }, value: "const", start: 0, end: 5, loc: { ... } },
{ type: { ... }, value: "greet", start: 6, end: 11, loc: { ... } },
{ type: { ... }, value: "=", start: 12, end: 13, loc: { ... } },
{ typw: { ... }, value: 'name', start: 14, end: 18, loc: { ... } },
...
]
复制代码
语法分析(Syntactic Analysis)
该阶段会使用tokens中的信息把它们转换成一个 AST 的表述结构和添加一些方法,这样更易于后续的操作。可以通过AST explorer来查看生成的AST结构。
转换(transform)
转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作,也可以做代码压缩等操作。
生成(generate)
代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(Source Map)。
Babel 插件生效流程
Babel在对代码完成parse生成AST之后,会调用transformFile(file, pluginPasses),传入所有的插件。该方法分为四个阶段:
- 遍历插件执行插件的pre方法
- 将所有插件的visitor合并为一个单一的visitor
- 遍历AST,访问指定Node节点时,调用visitor中的visit方法
- 遍历插件执行插件的post方法
function transformFile (file, pluginPasses) {
for (const pluginPairs of pluginPasses) {
const passPairs = [];
const passes = [];
const visitors = [];
// 执行插件的pre方法
for (const [plugin, pass] of passPairs) {
const fn = plugin.pre;
if (fn) {
const result = fn.call(pass, file);
...
}
}
// 插件合并,生成单一的visitor
// visitor = { key: [fun1, fun2, ...] } key是节点类型,如ArrowFunctionExpression、Identifier
const visitor = traverse.visitors.merge(
visitors,
passes,
file.opts.wrapPluginVisitorMethod,
};
// 执行插件 visitor 中定义的方法
traverse(file.ast, visitor, file.scope);
// 执行插件的post方法
for (const [plugin, pass] of passPairs) {
const fn = plugin.post;
if (fn) {
const result = fn.call(pass, file);
...
}
}
}
}
复制代码
在traverse的过程中,采取深度优先搜索算法(DFS)
搜索AST,在遇到插件对该类型节点有修改或者有子节点的情况,进行递归遍历。
function traverse (ast, visitor) {
// 格式化各种形式的visitor
visitors.explode(visitor);
// 递归遍历节点
traverse.node(ast, visitor);
}
复制代码
traverse.node = function (node, visitors, scope) {
// keys like ['Program', 'ArrowFunctionExpression', 'Identifier']
const keys = t.VISITOR_KEYS[node.type];
const context = new TraversalContext(scope, visitors);
for (const key of keys) {
if (context.visit(node, key)) return;
}
}
复制代码
上述traverse.node 方法中的context.visit方法是关键。会依次执行插件的enter方法,递归调用traverse.node处理子节点,最后执行插件的exit方法。
// 插件(访问者)调用visit方法访问AST(被访问者|被访问对象)
function visit () {
// 执行插件 enter 方法
if (this.shouldSkip || this.call("enter") || this.shouldSkip) {
return this.shouldStop;
}
// 递归调用 traverse.node
traverse.node(
this.node,
this.opts,
this.scope,
this.state,
this,
this.skipKeys,
);
// 执行插件 exit 方法
this.call("exit");
return this.shouldStop;
}
复制代码
写一个简单插件
访问者模式(Visitor Pattern)
在写插件之前,需要了解一个重要的概念:访问者模式
。
因为Babel插件的设计遵循访问者模式,该模式适用于数据结构不变
,而数据操作多变
的情况,其能将数据结构与对数据的操作进行解耦。
回到Babel,AST的结构基本上是不变的,而插件对AST的操作是多种多样的。此时就很适合使用访问者模式。Babel中每个插件就是访问者,而AST就是被访问者,每当访问插件关心的节点时,插件内访问者的方法就会被调用,此时可以新增、替换、删除节点,达到转换AST的目的。
转换箭头函数插件
const code = `
const greet = name => {
return 'hello ' + name;
};
`;
// Code Parsing
const ast = parser.parse(code);
// Code Transformation
// 定义访问者
const visitor = {
ArrowFunctionExpression(path) {
if (!path.isArrowFunctionExpression()) return;
// 调用parse阶段添加的方法,对AST进行处理、转换
path.arrowFunctionToExpression({
allowInsertArrow: false
});
}
}
traverse(ast, visitor);
// Code Generation
const { code: outputCode, map } = generate(ast, {}, code); // code 用于生成sourceMap
console.log(outputCode, map);
复制代码
总结
本文从介绍plugin和preset等基本概念开始,到罗列Babel的各种配置方式,以及Babel的处理流程,再从源码角度剖析插件是如何生效的,最后手写了一个将箭头函数转换为普通函数的Babel插件,详尽的介绍了项目中的普遍会用到的基础类库Babel。希望对您有所启发,欢迎点赞?。