Babel在前端项目运动比较广泛,无论前端Web项目,还是跨平台ReactNative Weex等跨平台项目,都会有Babel的身影。
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel主要功能如下:
- 语法转换 Syntax
- 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如: core-js)
- 源码转换(codemods)
Babel 通过语法转换器来支持新版本的 JavaScript 语法。
Stage-X(实验性质的预设)提案共分为五个阶段:
TC39 将提案分为以下几个阶段:
- Stage-0- 设想(Strawman) : 只是一个想法,可能有 Babel插件。
- Stage-1 - 建议(Proposal). : 这是值得跟进的。
- Stage-2 - 草案(Draft) : 初始规范。
- Stage-3 - 候选(Candidate). : 完成规范并在浏览器上初步实现。
- Stage 4 - 完成(Finished) : 将添加到下一个年度版本发布中。
*** 注:该文以@Babel 7.24.2 作为研究对象 ***
Babel 基础组件有哪些功能
- @babel/core: 核心包负责整个编译过程的调度和控制
- @babel/cli: 命令行工具,通过终端控制台命令使用 Babel 进行代码转换
- @babel/parser: JavaScript/Typescript 源代码解析成抽象语法树(AST)
- @babel/traverse: 遍历和修改 AST 的工具
- @babel/types: 创建、检查和修改 AST操作工具库,包括判断、断言、创建3类
- @babel/template: 用来快速创建AST模板语法
- @babel/runtime: 包含Babel转换产生的工具方法(
_classCallCheck之类的),以及一份regenerator-runtime,配合@babel/plugin-transform-runtime插件使用- @babel/code-frame: 用于生成错误信息并且打印出错误原因与行数。(其实就是个console工具类) @babel/helpers 也是工具类,提供了一些内置的函数实现, 主要用于babel插件的开发。
- @babel/helper-*: 预定义的@babel/template模板方法,供Babel插件使用
- @babel/generator: 把AST转回JS代码
- @babel/polyfill: 包含一些语言特性补丁(完整的ES2015+环境支持),包括core-js和regenerator runtime(在7.4.0之后@deprecated)
Babel 是如何工作的?
一. 工作原理
Babel有三个阶段 1.解析(PARSE), 2.转换(TRANSFORM), 3.生成(GENERATE),宏观认识一下Babel的工作流程(如下图):
从代码A (Code Source) 转化到B (Target Source) 代码,它是先将A代码转化为AST语法树,再通过Babel插件Plugin修改AST语法树,最终将修改后的AST语法树生成为目标代码。
loadFullConfig通过buildRootChain完成对配置文件中的preset、plugin、options的初始化与合并操作- 初始化结束后,递归调用
recursePresetDescriptors函数来查找所有preset预设中的plugin,loadPresetDescriptor函数负责解析并加载Presets预设包中的plugins,并按指定顺序返回 - 最终,
config.passes作为入参传递给run函数,并由transformFile完成后续的转换工作。
二. Plugin与Preset加载顺序
Plugins内部遵循自上而下(从左往右),Presets内部遵循自下而上(从右往左)的执行方式。
// .babelrc 配置文件,根据个人喜好也可以使用babel.config.js\.babelrc.json
// Preset 是逆序排列的(从后往前)。
// Plugin 顺序从前往后排列,在 Presets 前运行。
{
presets: [E,D],
plugins: [A, B, C]
}
具体的实现细节感兴趣的码友可以去翻一下Babel源代码在packages/babel-core/config目录中代码,其中loadFullConfig函数实现了presets、plugins的加载顺序、装载Plugin等逻辑(recursePresetDescriptors, loadPluginDescriptor)
initialPluginsDescriptors为.babelrc、babel.config.js、.babelrc.json中的plugins节点下所有的插件loadPresetDescriptor会读取.babelrc、babel.config.js、.babelrc.json中的presets节点下所有的预设插件recursePresetDescriptors递归的方式将loadPresetDescriptor获取的plugins通过unshift的方式加载到队列的头部,以此类推形成了一个先进后出的形式,(这里提醒一下 ownpass)
loadPluginDescriptors优先将initialPluginsDescriptors配置文件中的plugins优先放在第一个数组的首位,确保转码是从plugins开始。
三. 完整流程堆栈信息演示
.babelrc 配置,当然也可以使用babel.config.js/.babelrc.json
(preset-env 有个debug参数设置为debug: true 控制台会输出加载成功的pluginName)
Babel在解析AST前会通过 loadFullConfig 完成配置文件解析加载
buildRootChain完成对配置文件中presets、plugins、options读取与加载,最后由loadPrivatePartialConfig 完成presets、plugins、options的merge操作
loadFullConfig 函数通过递归调用 recursePresetDescriptors 函数来查找所有preset预设中的plugin,loadPresetDescriptor 函数负责解析并加载Presets预设包中的plugins,最终,config.passes 被返回给 run 函数,并由 transformFile 执行最终的转换工作。
结合以上调试堆栈,可以看出Babel初始化顺序为优先plugin(插件),最后加载presets(预设)
四. 总结
presets为预设plugin插件,一般为官方plugin插件,presets放在最后加载。个人觉得为了保障最终生成js的完整性,对于presets节点配置建议是是preset-env要放在第一个(保证它在plugin执行顺序的末尾),框架层的plugin放在底部。
Preset 、Plugin功能介绍
Preset 官方的解释是预置Plugin集合、从npm包的角度来看,Preset库入口js export default函数 会返回一个{ pulgins : [] }这样的结构,里面包含了一组Plugin
Runtime 与 Polyfill区别
Babel它只负责转化Syntax语法转化工作,API兼容性需要引入core-js,7.4.0 版本之前 使用@babel/polyfill 导入core-js, 可以通过useBuiltIns 选项来按需加载需要的组件
- 当useBuiltIns 设置为usage,该配置打包出来是按需加载core-js下面对应的模块
- 当useBuiltIns 设置为entry 或 false,该模式代码不会进行任何操作,需要自行导入core-js( 这里需要根据实际babel版本来决定导入
@babel/polyfillorcore-js/stableregenerator-runtime/runtime)
这里需要注意的是需要在打包的入口js/ts(x)文件导入core-js相关库,否则无法在当前编译文件中生效
- 注:
Babel > 7.4.0
import 'core-js/stable'
import 'regenerator-runtime/runtime'
- 注:
Babel < 7.4.0
import '@babel/polyfill'
常见QA:
1. Typescript和Babel 并存的意义是什么?
在大多数情况下,如果你的项目是纯 TypeScript 项目,没有引入任何 JavaScript 的语法扩展或者特性,那么你通常不需要使用 Babel。TypeScript 可以直接编译成 JavaScript,并且已经包含了一些常见的 JavaScript 扩展,比如箭头函数、模板字符串等,所以在这种情况下,Babel 并不是必需的。
- 与 JavaScript 生态系统的互操作性:如果你的项目需要与其他 JavaScript 库或者工具进行交互,特别是那些使用了 Babel 的库,那么你可能需要在项目中使用 Babel,以确保代码之间的兼容性。
- JavaScript 的语法扩展:虽然 TypeScript 已经包含了许多常见的 JavaScript 扩展,但有些新特性可能还不支持,或者在 TypeScript 中的实现可能不完整。在这种情况下,你可能需要使用 Babel 来处理这些 JavaScript 的语法扩展。
- 优化和转换:Babel 提供了一些优化和转换的插件,可以帮助你优化代码、移除不必要的代码、提取重复的代码等。即使你的项目是纯 TypeScript 项目,你也可能会发现某些 Babel 插件对于项目的优化和性能提升是有帮助的。
- 跨端适配:随着大前端的发展,不少前端团队面临着多平台业务落地的需求。为了节省工作量会考虑通过Babel去实现在编译阶段完成API的兼容性适配,从而高效的完成业务发展。
总之,虽然在大多数情况下,纯 TypeScript 项目不需要使用 Babel,但在一些特定的情况下,你可能仍然需要使用 Babel 来确保项目的兼容性、处理 JavaScript 的语法扩展或者进行代码优化和转换。
2. 可以使用esBuild替换Babel吗?
理论上可以使用 esbuild 替代 Babel。esbuild 是一个快速的 JavaScript 构建工具,它具有出色的性能和速度。 虽然 Babel 和 esbuild 都可以用于 JavaScript 代码的转换和构建,但它们在设计理念和功能上有一些不同:
- 性能和速度: esbuild 被设计为尽可能快速地构建项目,它采用了高效的 Go 语言实现,并且在大多数情况下比 Babel 更快。
- 功能: Babel 是一个功能强大且灵活的工具,它可以处理 JavaScript 的语法扩展、转换和优化,支持插件系统和自定义配置。esbuild 的功能相对较少,主要专注于 JavaScript 的构建和打包,不支持像 Babel 那样的丰富的语法转换和优化功能。
- 生态系统: Babel 拥有庞大的生态系统,有大量的插件和预设可供选择,可以满足各种不同的需求。esbuild 虽然功能较少,但也有一些社区插件和整合方案。