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 的工作流程基本分为三步:

  1. 解析(Parse):将源代码解析成抽象语法树(AST)
  2. 转换(Transform):对 AST 进行遍历和转换
  3. 生成(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.jsbabel.config.json(项目根目录)
  • package.json 中的 babel 字段

推荐使用 .babelrc.jsonbabel.config.js

// .babelrc
{
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

运行 npm run build

// compiled.js
let str = 'str'
const fun = function fun() {}

成功进行转换,但是只是转换了箭头函数,constlet并没有被转换。

添加更多插件

继续安装 @babel/plugin-transform-block-scoping 来转换 letconst

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 () {}

可以看到 constlet都成功转换了。

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(如 PromiseMapSet 等)并没有进行转换。

语法 vs API

  • 语法:如 letconst、箭头函数、class 等 → Babel 可以转换
  • API:如 PromiseArray.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 是一个包含大部分新 APIpolyfill 库。

安装时注意要添加到 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 有以下问题:

  1. 体积过大:全量引入,即使只用了一个 API,也会导入整个 polyfill 库(~90KB+)
  2. 污染全局:直接在全局对象(如 Array.prototype)上添加方法,可能导致冲突
  3. 已被废弃: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@2core-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
    }]
  ]
}

参考链接