Babel
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel 主要做的两件事情:
-
Babel 通过语法转换器来支持新版本的 JavaScript 语法(转换 ES6 为 ES5 代码)
-
通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)
@babel/cli
@babel/cli
是一个能够从终端(命令行)使用的工具。在 Webpack 中使用 babel-loader
进行代替。
@babel/core
Babel 的核心功能包含在 @babel/core 模块中,是运行 Babel 的基础库。
@babel/preset-env
Babel 的代码转换功能是以插件的形式出现的,用于指导 Babel 如何对代码进行转换。
我们也可以编写自己的插件来实现自己想要的代码转换功能。
例如我们可以使用 @babel/plugin-transform-arrow-functions
插件将 ES6 的箭头函数转换为 ES5 兼容的函数表达式。
可以使用 @babel/plugin-transform-modules-commonjs
插件将 ES6 的 export 语句转换为 commonJS。
可以使用 @babel/plugin-transform-for-of
插件将 ES6 的 for..of
语句转换为 ES5 所兼容的代码。
实际项目开发过程中,我们的代码肯定不止少数几个 ES6+ 的特性,我们不可能一个接一个的去添加所有需要的插件。
这个时候我们就可以使用一组预先配置好的插件集合,我们称之为“preset”。
而 preset-env
则包含了支持所有最新的 JavaScript (ES2015+)特性的所有插件的集合。
另外我们还可以使用 babel.config.js
配置文件,为 preset 添加参数配置,使得代码转换结果达到项目所需的最优状态。
@babel/preset-react
此预设配置,用于支持转换 JSX 语法。主要包含以下插件:
@babel/plugin-syntax-jsx
@babel/plugin-transform-react-jsx
@babel/plugin-transform-react-display-name
@babel/plugin-transform-react-pure-annotations
@babel/preset-flow
此预设配置,用于支持 Flow 类型注释。主要包含以下插件:
@babel/plugin-transform-flow-strip-types
@babel/preset-typescript
此预设配置,用于支持 TypeScript 类型注释。主要包含以下插件:
@babel/plugin-transform-typescript
@babel/polyfill
默认情况下,Babel
只进行 JavaScript
代码的语法转换,比如箭头函数
、let/const块级作用域
、let..of遍历器
、async-await异步请求
等。
而对于 ES6 新增的诸如 Promise
、Set
等内置对象,以及 Array.from
或 Object.assign
之类的静态方法,还有 String.prototype.includes
之类的实例方法,均不会进行额外处理。
如果需要在较低版本中支持并使用它们,则需要加入对应的 polyfill
垫片,使得浏览器支持这些新的特性。
@babel/polyfill
模块包含 core-js
和一个自定义的 regenerator runtime
来模拟完整的 ES2015+
环境。
我们可以安装 @babel/polyfill
并在 preset-env
中通过 useBuiltIns
参数进行搭配使用,可以按需引入相关的 polyfill
,而不是全部导入。
babel.config.js 配置文件
babel.config.js
配置文件用于指导 Babel 如何工作,可以根据项目需求自定义配置。
module.exports = {
"presets": [
[
"@babel/env",
{
// 默认是 false 开启后控制台会看到 哪些语法做了转换,Babel的日志信息,开发的时候强烈建议开启
"debug": false,
// 用来指定转换需要支持哪些浏览器
"targets": [ "> 1%", "last 2 versions", "not ie <= 8" ],
// 指定引入 polyfill 的方式
// false - 啥也不干,不会引入任何 polyfill
// 'entry' - 会把所有的 polyfill 全部引入
// 'usage' - 只会引入代码中用到的 polyfill
"useBuiltIns": 'usage',
// 指定core-js版本
"corejs": "2.6.12",
// import默认会被编译成了require,如果想要编译出来的模块引入规范还是import,则可以在preset-env的配置项中添加"modules": false即可。
// modules的选项有:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为"auto"
"modules": false,
}
]
]
}
Babel >= 7.4.0 弃用 @babel/polyfill 直接使用 core-js
core-js
是 JavaScript
的模块化标准库,包括了 ECMAScript 2015+ api 的向后兼容实现。它和 babel
高度集成,是 babel
解决新特性在浏览器中兼容问题的核心依赖。
目前 core-js
的版本是 3.x
,与 core-js@2
相比不仅在本身的架构上有重大调整,还对 babel
中的一些插件有重大影响。
@babel/preset-env
除了语法转换,另一个重要的功能是对 api
的处理,也就是在代码中引入 polyfill
。但是,@babel/preset-env
默认是不开启处理 api
的功能,只有设置了 useBuiltIns
选项(不为false)才会开启。
@babel/preset-env
主要还是依赖 core-js
来处理 api
的兼容性,在升级到 7.4.0
以上的版本以后,既支持 core-js@2
,也支持 core-js@3
,所以增加了 corejs
的配置来控制所需的版本。如果设置了useBuiltIns
选项(不为false)就得设置 corejs
版本,否则 babel
将会发出警告。
@babel/polyfill
是一个运行时包,主要是通过核心依赖 core-js@2
来完成对应浏览器不支持的新的全局和实例 api
的添加。在升级到 core-js@3
后,如果还要保留 @babel/polyfill
的使用,就要在@babel/polyfill
中添加 core-js@2
和 core-js@3
切换的选项,这样 @babel/polyfill
中将包含core-js@2
和 core-js@3
两个包,出于这个原因官方决定弃用 @babel/polyfill
。
从 Babel
7.4.0 版本开始,@babel/polyfill
已被弃用!
目前而言,配合 useBuiltIns
选项,有以下几种不同的组合使用方式:
useBuiltIns: false
只做了语法转换,不会导入任何 polyfill
进来,并且 corejs
配置将无效。
编译前:
const result = [1, 2, 3, 4, 5].copyWithin(0, 3)
const instance = new Promise((resolve, reject) => {
resolve(123)
})
const shen = result?.a
编译后:
"use strict";
var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new Promise(function (resolve, reject) {
resolve(123);
});
var shen = result === null || result === void 0 ? void 0 : result.a;
useBuiltIns: 'entry'
需要在打包入口先行引入 core-js/stable
和 regenerator-runtime/runtime
,会将 browserslist
环境不支持的所有 polyfill
都导入。
编译前:
import "core-js/stable"; // yarn add core-js
import "regenerator-runtime/runtime"; // regenerator-runtime 会在安装 @babel/preset-env 的时候自动安装
const result = [1, 2, 3, 4, 5].copyWithin(0, 3)
const instance = new Promise((resolve, reject) => {
resolve(123)
})
编译后:
"use strict";
require("core-js/modules/es.symbol.js");
// ... 此处省略400+行代码
require("regenerator-runtime/runtime");
var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new Promise(function (resolve, reject) {
resolve(123);
});
useBuiltIns: 'usage'
代码中不用主动 import
,babel
会自动将代码里已使用到的且 browserslist
环境不支持的 polyfill
导入。
编译前:
const result = [1, 2, 3, 4, 5].copyWithin(0, 3)
const instance = new Promise((resolve, reject) => {
resolve(123)
})
编译后:
"use strict";
require("core-js/modules/es.array.copy-within.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new Promise(function (resolve, reject) {
resolve(123);
});
@babel/preset-env 全局污染、重复的辅助函数、第三方库无法检测的问题
全局污染
@babel/preset-env
使用 polyfill
实现支持 Promise
、WeaMap
之类的内置对象,Array.from
、Object.assign
之类的静态方法,还有 Array.prototype.includes
之类的实例方法,还有生成器函数(generator functions
)等。
为了添加这些功能,polyfill
将添加到全局范围(global scope
)和类似 String
这样的对象原型(native prototypes
)中去,从而产生全局污染。
对于软件库/工具的作者来说,如果不需要类似 Array.prototype.includes
的实例方法,可以使用 transform runtime
插件而不是对全局范围(global scope
)造成污染的 polyfill
。
重复的辅助函数
网上有很多人认为 @babel/polyfill
除了有全局污染的缺点外,还会让不同的文件中包含重复的代码,增加编译后的体积。举个例子:
编译前:
const key = 'babel'
const obj = {
[key]: 'polyfill',
}
编译后:
"use strict";
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var key = 'babel';
var obj = _defineProperty({}, key, 'polyfill');
编译后的代码中插入了 _defineProperty
函数。
的确,如果多个文件中使用了对象的属性名表达式,则会插入多个 _defineProperty
函数。
但是,这件事情并不是 @babel/polyfill
这种 polyfill
方案实现的,而是 @babel/preset-env
本身在语法转换的时候,会使用一些辅助函数来实现一些语法的模拟。而事实的确,这只是一种语法转换。
第三方库无法检测
Babel
默认不处理 node_modules
中的第三方库,这也意味着如果其中一个依赖需要特殊的 polyfill
,默认情况下 Babel
无法将其检测出来。
@babel/runtime 通过运行时解决全局污染的问题
在使用 @babel/preset-env
提供的语法转换和 api
添加的功能时,难免会造成文件的体积增加以及 api
的全局污染。为了解决这类问题,引入了 runtime
的概念,runtime
的核心思想是以引入替换的方式来解决兼容性问题。
runtime 包有三个:
@babel/runtime
@babel/runtime-corejs2
@babel/runtime-corejs3
三个包都依赖 helpers
、regenerator-runtime
模块来实现语法的替换,helpers
中提供了一些语法模拟的函数,regenerator-runtime
中实现了 async/await
语法的转换。
只有在 @babel/preset-env
的帮助下,runtime
包的语法模拟替换功能才会发挥作用。
三个包不同的区别是:
-
@babel/runtime
只能处理语法替换。 -
@babel/runtime-corejs2
相比较@babel/runtime
增加了core-js@2
来支持全局构造函数和静态方法兼容。 -
@babel/runtime-corejs3
相比较@babel/runtime-corejs2
支持了实例方法的兼容,同时还支持对ECMAScript
提案的api
进行模拟。
@babel/runtime-corejs2
会从 core-js
中的 library
模块去加载对应的 runtime
代码:
// runtime-corejs2/core-js/array/from.js
module.exports = require("core-js/library/fn/array/from")
@babel/runtime-corejs3
会从 core-js-pure
这个包中去加载对应的 runtime
代码:
// runtime-corejs3/core-js/array/from.js
module.exports = require("core-js-pure/features/array/from")
对于数组的 includes
方法,@babel/runtime-corejs3
提供了模拟 api
,而 @babel/runtime-corejs2
没有:
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"))
(0, _includes.default)(_context = [1, 2, [3, 4]]).call(_context)
如果我们想在一个不支持 Promise
的环境下使用 Promise
,可以这样:
// @babel/runtime-corejs2
// var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
// var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
// @babel/runtime-corejs3
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var instance = new _promise["default"](function (resolve, reject) {
resolve(123);
});
显然这样一个个手动导入很麻烦,这个时候我们就需要借助自动导入插件来帮助我们完成这项工作。
@babel/plugin-transform-runtime 自动导入项目中所需的运行时
@babel/plugin-transform-runtime
就是为了方便 @babel/runtime
的使用。
通过 ast
的分析,自动识别并替换代码中的新 api
,解决手动 require
的烦恼。
// babel.config.js
module.exports = {
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
corejs
选项来配置使用的是 @babel/runtime-corejs2
还是 @babel/runtime-corejs3
。
编译前:
const result = [1, 2, 3, 4, 5].copyWithin(0, 3)
const instance = new Promise((resolve, reject) => {
resolve(123)
})
const key = 'babel'
const obj = {
[key]: 'polyfill',
}
使用 @babel/runtime-corejs2
编译后:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new _promise["default"](function (resolve, reject) {
resolve(123);
});
var key = 'babel';
var obj = (0, _defineProperty2["default"])({}, key, 'polyfill');
使用 @babel/runtime-corejs3
编译后:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
var _copyWithin = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/copy-within"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _context;
var result = (0, _copyWithin["default"])(_context = [1, 2, 3, 4, 5]).call(_context, 0, 3);
var instance = new _promise["default"](function (resolve, reject) {
resolve(123);
});
var key = 'babel';
var obj = (0, _defineProperty2["default"])({}, key, 'polyfill');
可以看到,使用 @babel/runtime-corejs3
可以模拟数组上的 copyWithin
方法,而 @babel/runtime-corejs2
则不能。
Babel 处理兼容性问题总结方案
目前,babel 处理兼容性问题有两种方案:
1、@babel/preset-env + corejs@3
实现简单语法转换 + 复杂语法注入api替换 + 在全局和者构造函数静态属性、实例属性上添加api,支持全量加载和按需加载,我们简称 polyfill 方案
;
2、@babel/preset-env + @babel/runtime-corejs3 + @babel/plugin-transform-runtime
实现简单语法转换 + 引入替换复杂语法和api,只支持按需加载,我们简称 runtime 方案
。
两种方案一个依赖核心包 core-js
,一个依赖核心包 core-js-pure
,两种方案各有优缺点:
1、polyfill
方案很明显的缺点就是会造成全局污染,而且会注入冗余的工具代码;优点是可以根据浏览器对新特性的支持度来选择性的进行兼容性处理;
2、runtime
方案虽然解决了 polyfill
方案的那些缺点,但是不能根据浏览器对新特性的支持度来选择性的进行兼容性处理,也就是说只要在代码中识别到的 api
,并且该 api
也存在 core-js-pure
包中,就会自动替换,这样一来就会造成一些不必要的转换,从而增加代码体积。
所以,polyfill
方案比较适合单独运行的业务项目,如果你是想开发一些供别人使用的第三方工具库,则建议你使用 runtime
方案来处理兼容性方案,以免影响使用者的运行环境。