简介
Babel 是一个广泛使用的 JavaScript 编译器,Babel 是一个强大的 JavaScript 编译器,主要用于将现代 JavaScript 代码转换为向后兼容的版本,并支持其他语言特性如 TypeScript
和 Flow
是一个强大的工具链
我们日常接触到的用法一般也就分为三大类
转译代码以实现兼容性
将 ESNext
(即最新的 ECMAScript 版本)、TypeScript
、Flow
等语法转换为目标环境支持的 JavaScript 语法
特定用途的代码转换
利用 Babel 提供的 API 进行自定义代码转换,比如
- 函数插桩:自动在函数中插入额外代码,例如用于性能监控或埋点
- 小程序开发:工具如
Taro
使用Babel API
实现跨平台的小程序开发 - 国际化:自动处理代码中的国际化字符串。
- 代码优化:进行各种编译时优化,如删除死代码、变量名混淆等。
代码静态分析
通过解析代码生成抽象语法树(AST
),并基于 AST
进行代码分析,比如
- 代码规范检查:
linter
工具(如 ESLint)使用AST
检查代码风格和潜在问题 - API 文档生成:提取源码中的注释并生成文档。
- 类型检查:根据
AST
中的类型信息进行静态类型检查,减少运行时错误。 - 压缩和混淆:通过
AST
分析进行代码优化,生成体积更小、性能更优的代码。 - 解释执行:直接解释执行
AST
,无需生成中间代码
本文主要研究第一种日常用法及优化
- 语法降级问题 转译 esnext:
- 将新的 JavaScript 语法(如箭头函数、解构赋值、类等)转换为旧版浏览器和环境可以理解的语法。
- 示例:ES6 箭头函数
() => {}
转换为 ES5 函数表达式function() {}
。
- 新特性支持(
提供 Polyfill
):- 提供对尚未被所有浏览器或环境完全支持的新 一些 JS API 的实现代码。
- 例如,异步函数 (
async/await
)、装饰器、类属性等。
安装
核心依赖
1. @babel/cli
命令行工具,用于从命令行运行 Babel
2. @babel/core
babel 核心编译库
3. @babel/preset-env
根据目标环境自动选择并应用所需的 Babel
插件,确保只对确实需要的特性进行转换,将新的 JavaScript
语法(如箭头函数、类、解构赋值等)转换为旧版本的 JavaScript
代码
在 @babel/preset-env
出现之前,如果你想要支持特定的新特性(例如箭头函数),你需要显式地安装相应的 Babel
插件,并在 .babelrc
或其他配置文件中声明它们。例如:
{ "plugins": ["transform-arrow-functions"] }
有了 @babel/preset-env 会根据这个配置自动决定哪些特性需要转换,并加载必要的插件
4. core-js
提供新 API 的 polyfills,如 Array.prototype.includes、Promise 等,这个包不需要单独安装,由 @babel/preset-env 自动引入
5. regenerator-runtime
是一个用于支持异步函数 (async/await) 和生成器函数 (function*) 的 polyfill,这个包也不需要单独安装,core-js
里面有,编译的时候会自己引入进来,这样引入同时这也是个代码冗余
问题后面我们会研究怎么优化
依赖安装
pnpm i @babel/cli @babel/core @babel/preset-env -D
使用
- 新建
src
文件夹,里面新建demo.js
文件,输入下内容
const fn = async ()=>{
console.log("fn");
}
Promise.resolve().then(() => {
console.log("Promise.resolve().then");
});
- 新建
.babelrc.json
配置文件
{
"presets": [
[
"@babel/preset-env",
{
// 指定兼容的浏览器版本
"targets": {
"chrome": "58",
"ie": "11"
},
// 基础库 core-js 的版本,一般指定为最新的大版本
"corejs": 3,
// Polyfill 注入策略,后文详细介绍
"useBuiltIns": "entry",
// 不将 ES 模块语法转换为其他模块语法
"modules": false
}
]
]
}
比较重要的是 useBuiltIns
,它决定了添加 Polyfill 策略,默认是 false
,即不添加任何的 Polyfill
"false"
:完全禁用 polyfills 的引入"entry"
:适合不需要细粒度控制的情况,但会增加打包体积"usage"
:推荐用于大多数项目,因为它能按需引入 polyfills,减少打包体积并优化性能
我们先看下 entry 属性下的打包结果,entry 需要在 入口文件 引入下 core-js 这个依赖
// demo.js 开头加上
import 'core-js';
- 执行编译命令
npx babel src --out-dir dist
可以看到所有的 polyfills 都会被引入到这个文件中,而不论它们是否在其他地方使用,没办法做到按需导入,接下来我们试试useBuiltIns: usage
,demo.js 里面的 import 'core-js' 去掉,再编译下代码
编译完代码少了许多,对于 core-js
里面的 Polyfill 代码和工具函数实现了按需导入
到现在我们就解决了解决了开头提出的两个主要问题,语法降级问题
(通过@babel/preset-env) 和 新特性支持(通过core-js + regenerator-runtime 两个核心的运行时库)
,但是这些方案也会有一些局限性:
- 全局污染
- 使用
core-js
和regenerator-runtime
时,默认情况下它们会向全局环境添加 polyfill,这在开发应用时通常没有问题,但在开发第三方工具库时可能会导致全局命名空间污染
- 代码冗余
- Babel 默认会在每个使用到辅助函数(如
_defineProperty
)的文件中重复生成这些函数的实现代码,像前面提到的_regeneratorRuntime
函数,可以看一下都是全量引入实现的,如果是多个文件会每个文件都引入实现一遍,这些都会导致打包体积过大
在 src 下面新建 demo1.js 把 demo2.js 的内容复制过去,执行编译命令
可以看到非常冗余的代码,那么有没有更优雅的 Polyfill 注入方案呢
使用 @babel/plugin-transform-runtime
@babel/plugin-transform-runtime
是一个专门设计来解决上述问题的 Babel 插件。它通过引入 @babel/runtime
库来提供运行时支持,而不是直接将辅助函数和 polyfill 内联到编译后的代码中。这样不仅可以避免全局污染,还可以减少代码冗余
主要功能
- 非全局版本的 Polyfill:
@babel/plugin-transform-runtime
不会向全局环境添加 polyfill,而是通过模块化的方式引入所需的运行时支持。 - 减少代码冗余:将常见的辅助函数提取到
@babel/runtime
中,避免每次编译时都重新生成相同的代码。 - 支持异步编程:引入
regenerator-runtime
来支持async
/await
和生成器函数。
新增依赖
pnpm i @babel/plugin-transform-runtime -D
pnpm i @babel/runtime-corejs3 -S
@babel/plugin-transform-runtime 是一个编译时的工具
@babel/runtime-corejs3 是运行时的,是一个特定版本的 @babel/runtime
core-js
的三种产物
core-js
:
- 全局 Polyfill:就是我们一开始使用的版本,它会将 polyfill 注入到全局环境中(如
window
或global
)。这种方式简单直接,适合开发应用时使用
core-js-pure
:
- 按需引入,不污染全局环境:这种形式不会将 polyfill 注入到全局环境中,而是以模块化的方式提供 polyfill。你可以按需引入特定的 polyfill,从而避免全局命名空间污染
- 适用场景:当你开发第三方库或工具时,这种方式可以确保你的库不会影响全局环境,并且可以更好地控制打包体积。
@babel/runtime-corejs3
使用的就是这种产物,它允许你通过模块化方式引入 polyfill,而不会污染全局命名空间
core-js-bundle
:
- 打包好的版本:这是一个包含了所有 polyfill 的预打包版本,适合那些不需要按需加载 polyfill 的场景。由于它包含了所有的 polyfill,因此体积较大,不太常用。
- 适用场景:当你需要一个包含所有 polyfill 的单一文件时,例如某些特定的构建流程或受限环境中
修改配置文件
{
"plugins": [
// 添加 transform-runtime 插件
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
],
"presets": [
[
"@babel/preset-env",
{
"targets": {
"ie": "11"
},
"corejs": 3,
// 关闭 @babel/preset-env 默认的 Polyfill 注入
"useBuiltIns": false,
"modules": false
}
]
]
}
因为我们使用了 transform-runtime 插件,所以要 把useBuiltIns
属性设为 false
,并且依赖的基础库也发生了变化,不再直接依赖,core-js
和regenerator-runtime
,而是引入@babel/runtime-corejs3
现在我们再次执行编译命令查看
左边是之前的 useBuiltIns
方案,右边是 @babel/plugin-transform-runtime
的方案,对比发现
transform-runtime 方案编译后,Babel 会将 async
/await/Promise
转换为对 @babel/runtime
中 _asyncToGenerator/_regeneratorRuntime/_Promise
的引用,而不是将完整的 regenerator-runtime
实现嵌入到每个文件中
transform-runtime,提供了一种高效且模块化的方式来处理 polyfills 和工具函数,避免了全局污染并显著减小了打包后的文件体积。