什么是 Babel
Babel 其实就是一个 JavaScript 的“编译器”,是一个使用 Lerna 构建的 Monorepo 风格的仓库,在其./packages目录下有 140 多个包。Babel 不仅仅是一个工具,更是一个工具链(toolchain),是前端基建中绝对重要的一环。
主要模块
因为前端语言特性和宿主(浏览器/Node.js 等)环境高速发展,但宿主环境对新语言特性的支持无法做到即时,而开发者又需要兼容各种宿主环境,因此语言特性的降级成为刚需,前端各种代码被编译为 JavaScript 的需求成为标配,这就是 Babel 的职责。Babel 在前端中占有举足轻重的历史地位,几乎所有的大型前端应用项目都离不开 Babel 的支持。
@babel/core
是 Babel 实现转换的核心,它可以根据配置,进行源码的编译转换:
var babel = require("@babel/core");
babel.transform(code, options, function(err, result) {
result; // => { code, map, ast }
});
@babel/cli
是 Babel 提供的命令行,它可以在终端中通过命令行方式运行,编译文件或目录。@babel/cli 使用了 commander 库搭建基本的命令行开发。作为 @babel/cli 的关键依赖,@babel/core 提供了基础的编译能力。以编译文件为例,其关键部分源码如下:
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;
}
}),
);
@babel/standalone
这个包对于浏览器环境动态插入高级语言特性的脚本、在线自动解析编译非常有意义。Babel 官网也用到了这个包。它可以在非 Node.js 环境(比如浏览器环境)自动编译含有 text/babel 或 text/jsx 的 type 值的 script 标签,并进行编译,如下面代码:
<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/parser
是 Babel 用来对 JavaScript 语言解析的解析器,源码中的require("@babel/parser").parse() 方法可以返回给我们一个针对源码编译得到的 AST,这里的 AST 符合 Babel AST 格式。
@babel/traverse
有了 AST,我们还需要对 AST 完成修改,才能产出编译后的代码。这就需要对 AST 进行遍历,此时 就需要 @babel/traverse,大概用法如下:
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
}
});
遍历的同时,需要,@babel/types 对具体的 AST 节点进行修改。
@babel/generator
得到了编译后的 AST 之后,最后一步:使用 @babel/generator 对新的 AST 进行聚合并生成 JavaScript 代码:
const output = generate(
ast,
{
/* options */
},
code
);
@babel/preset-env
在工程中运用时,我们很少直接操作 @babel/core、@babel/types 等,而应该对 @babel/preset-env 更加熟悉,毕竟 @babel/preset-env 是直接暴露给开发者在业务中运用的包能力。
在工程中,我们需要 Babel 做到的是编译降级,而这个编译降级一般通过 @babel/preset-env 来配置。@babel/preset-env 允许我们配置需要支持的目标环境(一般是浏览器范围或 Node.js 版本范围),利用 core-js 和 regenerator-runtime, 通过 targets 参数,按照 browserslist 规范筛选出适配环境所需的 polyfills(或 plugins),关键源码如下:
export default declare((api, opts) => {
// 规范参数
const {
bugfixes,
configPath,
debug,
exclude: optionsExclude,
forceAllTransforms,
ignoreBrowserslistConfig,
include: optionsInclude,
loose,
modules,
shippedProposals,
spec,
targets: optionsTargets,
useBuiltIns,
corejs: { version: corejs, proposals },
browserslistEnv,
} = normalizeOptions(opts);
let hasUglifyTarget = false;
// 获取对应 targets
const targets = getTargets(
(optionsTargets: InputTargets),
{ ignoreBrowserslistConfig, configPath, browserslistEnv },
);
const include = transformIncludesAndExcludes(optionsInclude);
const exclude = transformIncludesAndExcludes(optionsExclude);
const transformTargets = forceAllTransforms || hasUglifyTarget ? {} : targets;
// 获取需要兼容的内容
const compatData = getPluginList(shippedProposals, bugfixes);
const modulesPluginNames = getModulesPluginNames({
modules,
transformations: moduleTransformations,
shouldTransformESM: modules !== "auto" || !api.caller?.(supportsStaticESM),
shouldTransformDynamicImport:
modules !== "auto" || !api.caller?.(supportsDynamicImport),
shouldTransformExportNamespaceFrom: !shouldSkipExportNamespaceFrom,
shouldParseTopLevelAwait: !api.caller || api.caller(supportsTopLevelAwait),
});
// 获取目标 plugin 名称
const pluginNames = filterItems(
compatData,
include.plugins,
exclude.plugins,
transformTargets,
modulesPluginNames,
getOptionSpecificExcludesFor({ loose }),
pluginSyntaxMap,
);
removeUnnecessaryItems(pluginNames, overlappingPlugins);
const polyfillPlugins = getPolyfillPlugins({
useBuiltIns,
corejs,
polyfillTargets: targets,
include: include.builtIns,
exclude: exclude.builtIns,
proposals,
shippedProposals,
regenerator: pluginNames.has("transform-regenerator"),
debug,
});
const pluginUseBuiltIns = useBuiltIns !== false;
// 根据 pluginNames,返回一个 plugins 配置列表
const plugins = Array.from(pluginNames)
.map(pluginName => {
if (
pluginName === "proposal-class-properties" ||
pluginName === "proposal-private-methods" ||
pluginName === "proposal-private-property-in-object"
) {
return [
getPlugin(pluginName),
{
loose: loose
? "#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error"
: "#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error",
},
];
}
return [
getPlugin(pluginName),
{ spec, loose, useBuiltIns: pluginUseBuiltIns },
];
})
.concat(polyfillPlugins);
return { plugins };
});
@babel/plugin
是 Babel 插件集合,他的细分内容命名规则如下:
@babel/plugin-syntax- 是 Babel 的语法插件。它的作用是扩展 @babel/parser 的一些能力,提供给工程使用。比如 @babel/plugin-syntax-top-level-await 插件,提供了使用 top level await 新特性的能力。
@babel/plugin-proposal- 用于编译转换在提议阶段的语言特性。
@babel/plugin-transform- 是 Babel 的转换插件。比如简单的 @babel/plugin-transform-react-display-name 插件,可以自动适配 React 组件 DisplayName。
babel-loader
babel-loader 是 Babel 结合 Webpack,融入整个基建环节的例子。在 Webpack 编译生命周期中,babel-loader 作为一个 Webpack loader,承担着文件编译的职责。
总结
Babel 生态和前端工程中的各个环节都是打通开放的。它可以以 babel-loader 的形式和 Webpack 协作,也可以以 @babel/eslint-parser 的方式和 ESLint 合作。现代化的前端工程是一环扣一环的,作为工程链上的任意一环,插件化能力、协作能力将是设计的重点和关键。