Babel 完全指南:从基础到实战
⚠️ 更新说明:本文写于 2022 年,部分配置和最佳实践可能已有更新。
@babel/polyfill已在 Babel 7.4.0+ 被废弃,推荐使用core-js@3+@babel/preset-env或@babel/runtime-corejs3+@babel/plugin-transform-runtime。
Babel 是什么?
babel 是一个 JavaScript 编译器,因为部分 JavaScript 语法和 API 并没有得到所有环境的支持 ,所以需要使用 babel 将未支持的语法和 API 编译成支持的语法和 API。babel 能做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换
Babel 的工作流程
Babel 的工作流程基本分为三步:
- 解析(Parse):将源代码解析成抽象语法树(AST)
- 转换(Transform):对 AST 进行遍历和转换
- 生成(Generate):将转换后的 AST 生成目标代码
源代码 → [解析] → AST → [转换] → 新AST → [生成] → 目标代码
💡 提示:Babel 本身不进行转换,转换工作由各种插件完成。这就是为什么安装了
@babel/core后,代码不会自动转换的原因。
语法转换
下面我们通过实际例子来学习如何使用 Babel 进行语法转换。
环境准备
先创建一个测试项目
mkdir babel-demo && cd babel-demo # 创建项目
npm init -y # 初始化 npm
mkdir src && touch src/index.js # 添加测试文件
添加测试代码
// src/index.js
let str = 'str'
const fun = () => {}
核心包:@babel/cli & @babel/core
@babel/cli是一个命令行工具,我们需要用它来执行命令,编译文件。@babel/core是 babel 的核心代码包,它提供了解析和转换的 api 供插件使用 。
npm install --save-dev @babel/core @babel/cli
安装完成后就可以使用 babel 的命令编译文件了。
Babel 命令行常用参数:
- —out-file 或 -o:指定编译结果的输出文件
- —out-dir 或 -d:指定编译结果的输出目录
- —plugins:使用的插件
- —presets:使用的预设
- —help:查看帮助信息
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel ./src --out-file ./compiled.js"
}
}
此命令会将 src 目录下的所有代码都编译到 compiled.js下。
// compiled.js
let str = 'str'
const fun = () => {}
编译完成后发现代码并没有被转换,那是因为@babel/core 只提供了解析和转换的 api,实际的解析和转换需要使用插件来实现。
Plugins(插件)
插件是 Babel 转换代码的核心。每个插件负责一种特定的语法转换。
示例:转换箭头函数
安装 @babel/plugin-transform-arrow-functions 插件:
npm install --save-dev @babel/plugin-transform-arrow-functions
Babel 配置文件
Babel 的配置文件有多种形式:
.babelrc.json或.babelrc(项目根目录)babel.config.js或babel.config.json(项目根目录)package.json中的babel字段
推荐使用 .babelrc.json 或 babel.config.js。
// .babelrc
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
运行 npm run build
// compiled.js
let str = 'str'
const fun = function fun() {}
成功进行转换,但是只是转换了箭头函数,const 和 let并没有被转换。
添加更多插件
继续安装 @babel/plugin-transform-block-scoping 来转换 let 和 const:
npm install --save-dev @babel/plugin-transform-block-scoping
修改 .babelrc 文件
// babelrc
{
"plugins": [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping"
]
}
运行 npm run build
// compiled.js
var str = 'str'
var fun = function () {}
可以看到 const和 let都成功转换了。
Presets(预设)
什么是 Presets?
有没有发现每一种语法都需要单独的插件进行转换?如果每个都需要手动添加插件,消耗的时间和精力会很大。
Presets 就是为了解决这个问题而生的,它是插件的集合包,将众多插件打包在一起,应对常见的转换场景。
常见的 Presets:
@babel/preset-env:智能预设,根据目标环境自动确定需要的插件@babel/preset-react:转换 JSX 语法@babel/preset-typescript:转换 TypeScript
使用 @babel/preset-env
npm install --save-dev @babel/preset-env
更改 babel 配置
{
"presets": ["@babel/preset-env"]
}
运行 npm run build
// compiled.js
var str = 'str'
var fun = function () {}
效果是一样的,不用再一个一个加插件了 😌
API 转换
为什么需要 Polyfill?
我们通过 Presets 将 ES6+ 的语法转换为 ES5 的语法,但是除了语法之外,较新的 API(如 Promise、Map、Set 等)并没有进行转换。
语法 vs API:
- 语法:如
let、const、箭头函数、class等 → Babel 可以转换 - API:如
Promise、Array.includes()、Object.assign()等 → 需要 Polyfill
让我们看个例子:
// src/index.js
let str = 'str'
const fun = function fun() {}
const promise = new Promise()
运行 npm run build
// compiled.js
var str = 'str'
var fun = function fun() {}
var promise = new Promise()
可以看到 Promise 并没有被转换,这是因为 Babel 默认只转换语法,不转换 API。
要转换 API,我们需要使用 Polyfill。
什么是 Polyfill?
Polyfill(垫片/补丁)是一些较新的 JavaScript API 的旧版本实现代码,用于在不支持这些 API 的环境中模拟实现它们,从而抹平不同环境的 API 支持度差异。
下面介绍三种常见的 Polyfill 方案:
方案一:@babel/polyfill(已废弃)
⚠️ 废弃警告:
@babel/polyfill从 Babel 7.4.0 开始已被废弃,仅作为学习理解使用。
@babel/polyfill 是一个包含大部分新 API 的 polyfill 库。
安装时注意要添加到 dependencies 中去,因为转换过后也需要依赖 polyfill
npm install --save @babel/polyfill
在所有代码之前引入 polyfill
import '@babel/polyfill'
let str = 'str'
const fun = function fun() {}
const promise = new Promise()
运行 npm run build
// compiled.js
import '@babel/polyfill'
var str = 'str'
var fun = function fun() {}
var promise = new Promise()
可以看到并没有变化,实际上引入的 polyfill 里用 ES5 实现了例如 Promise 这类 API,并在全局对象上添加,所以达到了兼容效果。
存在的问题
@babel/polyfill 有以下问题:
- 体积过大:全量引入,即使只用了一个 API,也会导入整个 polyfill 库(~90KB+)
- 污染全局:直接在全局对象(如
Array.prototype)上添加方法,可能导致冲突 - 已被废弃:Babel 7.4.0+ 已不再推荐使用
方案二:core-js + @babel/preset-env
什么是 core-js?
core-js 是一个现代化的 JavaScript 标准库,包含了几乎所有 ECMAScript polyfill。它是目前最流行的 polyfill 库。
按需引入配置
为了解决 @babel/polyfill 全量引入的问题,我们可以使用 @babel/preset-env 配合 core-js 实现按需引入。
安装 core-js,记得添加到 dependencies 中去,因为打包后需要引入
npm install --save core-js
@babel/preset-env预设中自带了按需引入 polyfill 的功能,只需要在配置中使用 "useBuiltIns": "usage" 参数即可实现按需引入对应 core-js下的 polyfill
更改 babel 配置
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
运行 npm run build
// compiled.js
'use strict'
require('core-js/modules/es.object.to-string.js')
require('core-js/modules/es.promise.js')
var str = 'str'
var fun = function fun() {}
var promise = new Promise()
已经实现了按需引入 😃
优缺点
优点:
- ✅ 按需引入,体积优化
- ✅ 自动根据代码使用情况注入 polyfill
缺点:
- ❌ 仍会污染全局(修改原型链)
- ❌ 会重复定义辅助函数(见下一节)
core-js@2 和 core-js@3 的区别
| 特性 | core-js@2 | core-js@3 |
|---|---|---|
| 维护状态 | ❌ 已停止更新 | ✅ 持续维护 |
| 新特性支持 | ❌ 不支持新特性 | ✅ 支持最新 ECMAScript |
| 体积 | 较大 | 更小、更模块化 |
| 推荐度 | 不推荐 | ⭐ 推荐 |
💡 建议:始终使用
core-js@3
辅助函数问题
什么是辅助函数?
辅助函数(Helper Functions)是 Babel 在转译代码时自动生成的工具函数,用于实现某些语法转换。
比如转换 class 时,Babel 会生成 _classCallCheck、_createClass 等辅助函数。
问题演示
// src/index.js
class Person {}
运行 npm run build
'use strict'
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
if (staticProps) _defineProperties(Constructor, staticProps)
Object.defineProperty(Constructor, 'prototype', { writable: false })
return Constructor
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Person = /*#__PURE__*/ _createClass(function Person() {
_classCallCheck(this, Person)
})
可以看到转译后的代码中,Babel 定义了 _classCallCheck、_createClass、_defineProperties 这三个辅助函数。
问题来了:如果多个文件都使用 class,是不是每个文件都会重复定义这些辅助函数?
让我们测试一下:
// src/index.js
class Person {}
// src/test.js
class Person2 {}
运行 npm run build
'use strict'
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
if (staticProps) _defineProperties(Constructor, staticProps)
Object.defineProperty(Constructor, 'prototype', { writable: false })
return Constructor
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Person = /*#__PURE__*/ _createClass(function Person() {
_classCallCheck(this, Person)
})
;('use strict')
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
if (staticProps) _defineProperties(Constructor, staticProps)
Object.defineProperty(Constructor, 'prototype', { writable: false })
return Constructor
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Person2 = /*#__PURE__*/ _createClass(function Person2() {
_classCallCheck(this, Person2)
})
💥 问题确认:每个文件都重复定义了相同的辅助函数!这会导致:
- 代码体积增大
- 代码冗余
幸运的是,我们可以通过 @babel/plugin-transform-runtime 来解决这个问题。
方案三:@babel/runtime-corejs3 + transform-runtime(推荐)⭐
方案对比
前面的方案存在以下问题:
- 方案一:体积大、污染全局、已废弃
- 方案二:污染全局、重复定义辅助函数
方案三使用 @babel/plugin-transform-runtime 搭配 @babel/runtime-corejs3,可以完美解决上述所有问题。
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime是一个按需引入 polyfill的插件,它会找出需要使用polyfill和辅助函数的地方,按需引入@babel/runtime-corejs3下面的 polyfill和辅助函数,也不会修改全局方法
安装,这个插件本身安装到 devDependencies 中
npm install --save-dev @babel/plugin-transform-runtime
@babel/runtime-corejs3
@babel/runtime-corejs3 是一个运行时库,包含了 core-js@3 的所有 polyfill 和 Babel 辅助函数,可以配合 @babel/plugin-transform-runtime 来完成按需引入。
安装到 dependencies(生产环境需要)
npm install --save @babel/runtime-corejs3
修改配置
{
"presets": [["@babel/preset-env"]],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
运行 npm run build
'use strict'
var _interopRequireDefault = require('@babel/runtime-corejs3/helpers/interopRequireDefault')
var _createClass2 = _interopRequireDefault(
require('@babel/runtime-corejs3/helpers/createClass')
)
var _classCallCheck2 = _interopRequireDefault(
require('@babel/runtime-corejs3/helpers/classCallCheck')
)
var Person = /*#__PURE__*/ (0, _createClass2['default'])(function Person() {
;(0, _classCallCheck2['default'])(this, Person)
})
;('use strict')
var _interopRequireDefault = require('@babel/runtime-corejs3/helpers/interopRequireDefault')
var _createClass2 = _interopRequireDefault(
require('@babel/runtime-corejs3/helpers/createClass')
)
var _classCallCheck2 = _interopRequireDefault(
require('@babel/runtime-corejs3/helpers/classCallCheck')
)
var Person2 = /*#__PURE__*/ (0, _createClass2['default'])(function Person2() {
;(0, _classCallCheck2['default'])(this, Person2)
})
✅ 完美解决!可以看到:
- 辅助函数不再重复定义,而是通过
require()从@babel/runtime-corejs3中引入 - 代码体积优化
- 不污染全局环境
总结
语法转换
语法转换可以使用常用的 presets(如 @babel/preset-env)来实现转译。
API 转换(Polyfill)
API 可以使用以下几种 polyfill 方式来转译:
1. @babel/polyfill ❌ 已废弃
- ❌ 没有按需引入、体积大
- ❌ 修改全局方法
- ❌ 重复定义辅助函数
- ⚠️ Babel 7.4.0+ 已废弃
- 不推荐使用
2. core-js@3 + @babel/preset-env(useBuiltIns: ‘usage’)⚠️
- ✅ 有按需引入
- ❌ 会重复定义辅助函数
- ❌ 修改全局方法(污染全局)
- 适用场景:应用级项目(Application)
- 不适用:库/组件开发
3. @babel/runtime-corejs3 + @babel/plugin-transform-runtime ✅ 推荐
- ✅ 有按需引入
- ✅ 不会重复定义辅助函数
- ✅ 不修改全局方法(无污染)
- 推荐场景:库/组件开发、追求代码质量的应用项目
- 最佳实践 ⭐
选择建议
| 项目类型 | 推荐方案 | 原因 |
|---|---|---|
| 库/组件 | 方案 3 | 不污染全局,避免冲突 |
| 业务应用 | 方案 2 或 3 | 方案 2 配置更简单,方案 3 代码更优 |
| 遗留项目 | 保持现有配置 | 避免不必要的风险 |
关键配置示例
应用级项目(简单配置):
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
库/组件项目(推荐配置):
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}