Babel中文站点:www.babeljs.cn/
Babel外文站点:babeljs.io/
什么是Babel
Babel取自人类语言最早的传说,Tower of Babel。 上帝摧毁了巴别塔上说着共同语言的我们,又被Babel重新带了回来。 如果说巴别塔是人们对混乱到秩序的向往,那Babel在前端领域就带给了工程师放飞语法的所有畅想。
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
以上是官网对于Babel的解释,究其起源是由于前端的宿主容器无法实时的迎合各种语法进化而导致的兼容性问题,当我们无法改变如浏览器的宿主容器的时,Babel可以为我们提供以下服务:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的功能
- 源码转换(codemods)
Babel提供的服务:
语法转换
一般代指高级语言特性的降级
Polyfill
也叫垫片、补丁(通过引入第三方 polyfill 模块,例如 core-js)
源码转换
JSX等
Babel工程化的概念:
-
可插拔(Pluggable) 这就要求Babel需要有一套灵活的插件机制,满足第三方开发的入驻和使用场景。
-
可调式(Debuggable) 在Babel编译过程中,需要提供一套Source Map,来帮助使用Babel的开发找到编译前后的映射关系,从而方便前端开发中的调试。
-
基于协定(Compact) 需要实现灵活的配置方式,比如你熟悉的 Babelloose 模式,Babel 提供 loose 选项,帮助开发者在"尽量还原规范"和"更小的编译产出体积"之间,找到平衡。
总结
Babel的核心目标:编译
自身实现基于编译原理,深入AST(抽象语法树)来生成目标代码。同时,Babel 需要工程化协作,需要和各种工具(如 Webpack)相互配合,因此 Babel 一定是庞大复杂的。
Babel Monorepo架构体系
@babel/core
@babel/core 是 Babel 实现转换的核心
它可以根据配置,进行源码的编译转换:
js
var babel = require("@babel/core");
babel.transform(code, options, function(err, result) {
result; // => { code, map, ast }
});
@babel/cli
@babel/cli 是 Babel 提供的命令行,它可以在终端中通过命令行方式运行,编译文件或目录。 我们简单说一下它的实现原理: @babel/cli 使用了 commander 库搭建基本的命令行开发。
以编译文件为例,其关键部分源码如下:
js
import * as util from "./util";
const results = await Promise.all(
_filenames.map(async function (filename: string): Promise<Object> {
let sourceFilename = filename;
if (cliOptions.outFile) {
sourceFilename = path.relative(
path.dirname(cliOptions.outFile),
sourceFilename,
);
}
// 获取文件名
sourceFilename = slash(sourceFilename);
try {
return await util.compile(filename, {
...babelOptions,
sourceFileName: sourceFilename,
// 获取 sourceMaps 配置项
sourceMaps:
babelOptions.sourceMaps === "inline"
? true
: babelOptions.sourceMaps,
});
} catch (err) {
if (!cliOptions.watch) {
throw err;
}
console.error(err);
return null;
}
}),
);
util.compile
在上述代码中,@babel/cli 使用了util.compile方法执行关键的编译操作,该方法定义在 babel-cli/src/babel/util.js
babel-cli/src/babel/util.js代码解析:
js
import * as babel from "@babel/core";
// 核心编译方法
export function compile(
filename: string,
opts: Object | Function,
): Promise<Object> {
// 编译配置
opts = {
...opts,
caller: CALLER,
};
return new Promise((resolve, reject) => {
// 调用 transformFile 方法执行编译过程
babel.transformFile(filename, opts, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
由此可见,@babel/cli 负责获取配置内容,并最终依赖了 @babel/core 完成编译。
也可以在 @babel/cli 的 package.json 中找到线索:
cpp
"peerDependencies": {
"@babel/core": "^7.0.0-0"
},
@babel/core 是 @babel/cli 的关键依赖,@babel/core 提供了基础的编译能力。
@babel/standalone
可以在非 Node.js 环境(比如浏览器环境)自动编译含有 text/babel 或 text/jsx 的 type 值的 script标签,并进行编译
如下面代码:
cpp
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const getMessage = () => "Hello World";
document.getElementById('output').innerHTML = getMessage();
</script>
@babel/standalone的核心代码与@babel/core的关系:
js
import {
transformFromAst as babelTransformFromAst,
transform as babelTransform,
buildExternalHelpers as babelBuildExternalHelpers,
} from "@babel/core";
所以,@babel/standalone也是基于@babel/core而产生的应用。@babel/standalone可以在浏览器中直接执行,因此这个包对于浏览器环境动态插入高级语言特性的脚本、在线自动解析编译非常有意义。我们知道的 Babel官网也用到了这个包,JSFiddle、JS Bin 也都是 @babel/standalone 的受益者。
以上介绍的@babel/standalone、@babel/cli可以说都是基于@babel/core的能力而衍生的应用,那@babel/core又基于哪些babel基础包搭建的核心能力呢?
@babel/core的组成
- @babel/parser
- @babel/code-frame
- @babel/generator
- @babel/traverse
- @babel/types
- ...(等包提供基础的AST处理能力)
@babel/parser
@babel/parser 的实现主要依赖并参考了 acorn 和 acorn-jsx
典型用例:
js
require("@babel/parser").parse("code", {
sourceType: "module",
plugins: [
"jsx",
"flow"
]
});
parse源码分析:
js
export function parse(input: string, options?: Options): File {
if (options?.sourceType === "unambiguous") {
options = {
...options,
};
try {
options.sourceType = "module";
// 获取相应的编译器
const parser = getParser(options, input);
// 使用编译器将源代码转为 ast
const ast = parser.parse();
if (parser.sawUnambiguousESM) {
return ast;
}
if (parser.ambiguousScriptDifferentAst) {
try {
options.sourceType = "script";
return getParser(options, input).parse();
} catch {}
} else {
ast.program.sourceType = "script";
}
return ast;
} catch (moduleError) {
try {
options.sourceType = "script";
return getParser(options, input).parse();
} catch {}
throw moduleError;
}
} else {
return getParser(options, input).parse();
}
}
所以说,require("@babel/parser").parse()方法可以返回一个针对源码编译得到的 AST,但是这里的AST仍然需要经过我们的修改,才能产生编译后的代码。如何对AST进行二次修改和聚合,@babel/traverse、@babel/types、@babel/generator带给了我们这些。
@babel/traverse、@babel/types
提供了对具体的AST节点的修改能力。
cpp
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
}
});
@babel/generator
对新的 AST 进行聚合并生成 js 代码
cpp
const output = generate(
ast,
{
/* options */
},
code
);
Babel整体编译底层流程总结
- @babel/core利用自身能力将source code解析成AST
- @babel/traverse、@babel/types利用自身能力将旧AST转换为新AST
- @babel/generator利用自身能力将新AST生成output code