首先,我问了自己一个问题:如果说我对 Babel 的配置项的作用不那么了解,是否会影响日常开发呢?老实说,大多情况下没有特别大的影响,毕竟真实的开发环境中都是直接协作整需求、写逻辑,而且大多数的项目都是ts环境,所以大都不感知babel。
不过呢,谁让我这个人有“技术洁癖”呢,于是认真阅读了 Babel 的文档,外加不断编译验证,输出了本篇文章,最终算是从实际配置出发推进了每个知识点的解析(每一个配置的引入都是有原因的),希望能够帮助你对 Babel 的各种配置有一个更清晰的认识。
定位一下,什么是Babel?
Babel 是一个 JS 编译器。Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel大概分为三大部分:
-
Parse(解析)阶段:将代码转换成 AST (比如babel-parser它的作用就是这一步)- 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
- 语法分析:分析token流(上面生成的数组)并生成 AST
-
Transform(转化)阶段:访问 AST 的节点进行变换操作生产新的 AST(babel/traverse就是在这一步进行遍历这棵树)-
通过预设转换高版本的语法
-
通过
Polyfill方式在目标环境中添加缺失的特性(@babel/polyfill模块) -
Taro就是利用 babel 完成的小程序语法转换
-
-
Generator(生成)阶段:以新的 AST 为基础生成代码(@babel/generator)
Babel核心
核心库 @babel/core
babel在编译代码过程中核心的库就是@babel/core这个库,babel-core是babel最核心的一个编译库,他可以将我们的代码进行词法分析--语法分析--语义分析过程从而生成AST抽象语法树,从而对于“这棵树”的操作之后再通过编译称为新的代码。
babel-core其实相当于@babel/parse和@babel/generator这两个包的合体,接触过js编译的同学可能有了解esprima(词法、语法分析)和escodegen(转换)这两个库,你可以将babel-core的作用理解称为这两个库的合体。
babel-core通过transform方法将我们的代码进行编译。
关于babel-core中的编译方法其实有很多种,比如直接接受字符串形式的transform方法或者接受js文件路径的transformFile方法进行文件整体编译。
插件,转换通道
Babel 构建在插件之上,使用现有的或者自己编写的插件可以组成一个转换通道
在项目目录下 .babelrc 文件指定babel配置,配置样例如下:
//.babelrc
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
-
语法插件 只允许
Babel解析(parse) 特定类型的语法(不是转换),可以在AST转换时使用,以支持解析新语法。 -
转换插件 转换插件会启用相应的语法插件(
因此不需要同时指定这两种插件),这点很容易理解,如果不启用相应的语法插件,意味着无法解析,连解析都不能解析,又何谈转换呢?
预设
新问题来了(原谅我,问题宝宝),那我们需要针对语法处理一个个插件进行配置吗?
答案是否定的,babel提供了预设,preset,贼强大。通过使用或创建一个preset即可轻松使用一组插件。
常用的官方预设:
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
Tips: 从 Babel@7 开始,所有针对标准提案阶段的功能所编写的预设(stage preset)都已被弃用,官方已经移除了
@babel/preset-stage-x。所以就不用在代码babelrc里配置一堆stage插件了。
插件的排列顺序很重要!!!
如果两个转换插件都将处理“程序(Program)”的某个代码片段,则将根据转换插件或 preset 的排列顺序依次执行。
- 插件在 Presets 前运行。
- 插件顺序从前往后排列。
- Preset 顺序是颠倒的(从后往前)。
{
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"]
}
先执行 @babel/plugin-proposal-class-properties,后执行 @babel/plugin-syntax-dynamic-import
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
preset 的执行顺序是颠倒的,先执行 @babel/preset-react, 后执行 @babel/preset-env。
插件参数
插件和 preset 都可以接受参数,参数由插件名和参数对象组成一个数组。preset 设置参数也是这种格式。
如:
{
"plugins": [
[
"@babel/plugin-proposal-class-properties",
{ "loose": true }
]
]
}
插件的短名称
如果插件名称为 @babel/plugin-XXX,可以使用短名称@babel/XXX :
{
"plugins": [
"@babel/transform-arrow-functions" //同 "@babel/plugin-transform-arrow-functions"
]
}
如果插件名称为 babel-plugin-XXX,可以使用短名称 XXX,该规则同样适用于带有 scope 的插件:
{
"plugins": [
"newPlugin", //同 "babel-plugin-newPlugin"
"@scp/myPlugin" //同 "@scp/babel-plugin-myPlugin"
]
}
创建 Preset
可以简单的返回一个插件数组
module.exports = function() {
return {
plugins: [
"A",
"B",
"C"
]
}
}
preset中也可以包含其他的preset,以及带有参数的插件。
module.exports = function() {
return {
presets: [
require("@babel/preset-env")
],
plugins: [
[require("@babel/plugin-proposal-class-properties"), { loose: true }],
require("@babel/plugin-proposal-object-rest-spread")
]
}
}
@babel/preset-react
通常我们在使用React中的jsx时,相信大家都明白实质上jsx最终会被编译称为React.createElement()方法。
babel-preset-react这个预设起到的就是将jsx进行转译的作用。
@babel/preset-typescript
对于TypeScript代码,我们有两种方式去编译TypeScript代码成为JavaScript代码。
- 使用
tsc命令,结合cli命令行参数方式或者tsconfig配置文件进行编译ts代码。 - 使用
babel,通过babel-preset-typescript代码进行编译ts代码。
@babel/preset-env
主要作用是对我们
所使用的并且目标浏览器中缺失的功能进行代码转换和加载 polyfill。
在不进行任何配置的情况下,@babel/preset-env 所包含的插件将支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段),将其转换成ES5代码。例如,如果你的代码中使用了可选链(目前,仍在 stage 阶段),那么只配置 @babel/preset-env,转换时会抛出错误,需要另外安装相应的插件。
preset-env内部集成了绝大多数plugin(State > 3)的转译插件,它会根据对应的参数进行代码转译。不会包含任何低于 Stage 3 的 JavaScript 语法提案。如果需要兼容低于Stage 3阶段的语法则需要额外引入对应的Plugin进行兼容。
//.babelrc
{
"presets": ["@babel/preset-env"]
}
- flow:包含 flow type 所需插件
- es2015:包含 es2015 语法标准所有相关插件
- es2016:包含 es2016 语法标准所有相关插件
- es2017:包含 es2017 语法标准所有相关插件
- latest:包含从 2015 开始历年语法标准所有相关插件
- env:在 latest 基础上提供环境配置能力,比如可以配置只支持某一个浏览器的某几个版本,会自动按需启用、禁用插件
- stage-0:包含处于标准提案 stage 0 阶段的语法所有相关插件
- stage-1:包含处于标准提案 stage 1 阶段的语法所有相关插件
- stage-2:包含处于标准提案 stage 2 阶段的语法所有相关插件
- stage-3:包含处于标准提案 stage 3 阶段的语法所有相关插件
又一个问题,语法兼容了,那需要处理浏览器适配吗?
看业务需求,但是如果不需要适配所有的浏览器的话,尽可能还是指定下目标环境,这样编译代码能保持最小。
@babel/preset-env是一系列预设的插件,所以会根据你配置的目标环境,生成插件列表来编译。 浏览器或者桌面相关的,官方推荐配置.browserslistrc指定目标环境。
目标环境配置优先级:
.babelrc设置 targets 或 ignoreBrowserslistConfig > browserslist 配置源
例如,你将 .browserslistrc 的内容配置为:
last 2 Chrome versions
然后再执行 npm run build,你会发现箭头函数不会被编译成ES5,因为 chrome 的最新2个版本都能够支持箭头函数。
babel-preset-env仅仅针对语法阶段的转译,语法转换只是将高版本语法阶段转换成低版本的,但是新的内置函数、实例方法无法转换。
首先我们来理清楚这三个概念:
- 最新
ES语法,比如:箭头函数,let/const。 - 最新
ES Api,比如Promise - 最新
ES实例/静态方法,比如String.prototype.include
babel-prest-env仅仅只会转化最新的es语法,并不会转化对应的Api和实例方法,比如说ES 6中的Array.from静态方法。babel是不会转译这个方法的,如果想在低版本浏览器中识别并且运行Array.from方法达到我们的预期就需要额外引入polyfill进行在Array上添加实现这个方法。
举个例子,
const isHas = [1,2,3].includes(2);
const p = new Promise((resolve, reject) => {
resolve(100);
});
编译出来的结果为:
"use strict";
var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});
这个编译结果再低版本浏览器端执行是存在问题的,低版本不存在includes和promise。
这时,就需要 polyfill 上场了。
Polyfill
polyfill的翻译成中文是垫片,所谓垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。
@babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块,可以模拟完整的 ES2015+ 环境(不包含第4阶段前的提议)。
这意味着可以使用诸如 Promise 和 WeakMap 之类的新的内置组件、 Array.from 或 Object.assign 之类的静态方法、Array.prototype.includes 之类的实例方法。
为了添加这些功能,polyfill 将添加到全局范围和类似 String 这样的内置原型中(会对全局环境造成污染,后面我们会介绍不污染全局环境的方法)。
V7.4.0 版本开始,
@babel/polyfill已经被废弃,需单独安装core-js和regenerator-runtime模块。
我们需要将完整的 polyfill 在代码之前加载。@babel/polyfill 需要在其它代码之前引入,我们也可以在 webpack 中进行配置。
例如:
entry: [
require.resolve('./polyfills'),
path.resolve('./index')
]
polyfills.js 文件内容如下:
//当然,还可能有一些其它的 polyfill,例如 stage 4之前的一些 polyfill
import '@babel/polyfill'; // 完整引入polyfill
现在,我们的代码不管在低版本还是高版本浏览器(或node环境)中都能正常运行了。不过,很多时候,我们未必需要完整的 @babel/polyfill,这会导致我们最终构建出的包的体积增大,@babel/polyfill的包大小为89K (当前 @babel/polyfill 版本为 7.7.0)。
我们更期望的是,如果我使用了某个新特性,按需引入。Babel 已经考虑到了这一点。
useBuiltIns--"usage"|"entry"|false
false
当我们使用preset-env传入useBuiltIns参数时候,默认为false。它表示仅仅会转化最新的ES语法,并不会转化任何Api和方法。
entry
当传入entry时,需要我们在项目入口文件中手动引入一次core-js,它会根据我们配置的浏览器兼容性列表(browserList)然后全量引入不兼容的polyfill。
usage
@babel/preset-env 提供了一个 useBuiltIns 参数,设置值为 usage 时,就只会包含代码需要的 polyfill 。
有一点需要注意:配置此参数的值为 usage ,必须要同时设置 corejs (如果不设置,会给出警告,默认使用的是"corejs": 2) ,注意: 这里仍然需要安装 @babel/polyfill(当前 @babel/polyfill 版本默认会安装 "corejs": 2)
说一下使用
core-js@3的原因,core-js@2分支中已经不会再添加新特性,新特性都会添加到core-js@3。例如你使用了Array.prototype.flat(),如果你使用的是core-js@2,那么其不包含此新特性。为了可以使用更多的新特性,建议大家使用core-js@3。core-js (点击了解更多) : JavaScript 的模块化标准库,包含
Promise、Symbol、Iterator和许多其他的特性,它可以让你仅加载必需的功能。
注意:诸如
Array.prototype.flat()等实例方法将不起作用,因为这需要修改现有的内置函数(可以使用@babel/polyfill来解决这个问题) ——> 对此需要说明的是如果你配置的是corejs3,core-js@3现在已经支持原型方法,同时不污染原型。
现在,修改 Babel 的配置文件如下:
//.babelrc
const presets = [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
这样,Babel 会检查所有代码,以便查找在目标环境中缺失的功能,然后仅仅把需要的 polyfill 包含进来。
例如,还是原来那个例子,编译结果如下:
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});
前面曾提到,在 useBuiltIns 参数值为 usage 时,仍然需要安装 @babel/polyfill,虽然我们上面的代码转换中看起来并没有使用到,但是,如果我们源码中使用到了 async/await,那么编译出来的代码需要 require("regenerator-runtime/runtime"),在 @babel/polyfill 的依赖中,当然啦,你也可以只安装 regenerator-runtime/runtime 取代安装 @babel/polyfill。
然后,我就搞了一个class语法文件,和核对编译结果。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
};
getX() {
return this.x;
}
}
let cp = new ColorPoint(25, 8);
编译出来的 lib/index.js,如下所示:
"use strict";
require("core-js/modules/es.object.define-property");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
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); return Constructor; }
var Point =
/*#__PURE__*/
function () {
function Point(x, y) {
_classCallCheck(this, Point);
this.x = x;
this.y = y;
}
_createClass(Point, [{
key: "getX",
value: function getX() {
return this.x;
}
}]);
return Point;
}();
var cp = new ColorPoint(25, 8);
咦,发现了问题,编译后多了几个方法,重试了几个文件,都会有。
这是什么呢?
答:Babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被添加(inject)到需要它的每个文件中。
这不行呀,岂不是会被inject好多好多次么,这很多余呀。
这个时候,就是 @babel/plugin-transform-runtime 插件大显身手的时候了,使用 @babel/plugin-transform-runtime 插件,所有帮助程序都将引用模块 @babel/runtime,这样就可以避免编译后的代码中出现重复的帮助程序,有效减少包体积。
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。
@babel/plugin-transform-runtime 需要和 @babel/runtime 配合使用。@babel/plugin-transform-runtime 通常仅在开发时使用,但是运行时最终代码需要依赖 @babel/runtime,所以 @babel/runtime 必须要作为生产依赖被安装。
@babel/plugin-transform-runtime 可以减少编译后代码的体积外,我们使用它还有一个好处,它可以为代码创建一个沙盒环境,如果使用 @babel/polyfill 及其提供的内置程序(例如 Promise ,Set 和 Map ),则它们将污染全局范围。虽然这对于应用程序或命令行工具可能是可以的,但是如果你的代码是要发布供他人使用的库,或者无法完全控制代码运行的环境,则将成为一个问题。
@babel/plugin-transform-runtime 会将这些内置别名作为 core-js 的别名,因此您可以无缝使用它们,而无需 polyfill。
修改 .babelrc 的配置,如下:
//.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime"
]
]
}
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var Point =
/*#__PURE__*/
function () {
function Point(x, y) {
(0, _classCallCheck2.default)(this, Point);
this.x = x;
this.y = y;
}
(0, _createClass2.default)(Point, [{
key: "getX",
value: function getX() {
return this.x;
}
}]);
return Point;
}();
var cp = new ColorPoint(25, 8);
可以看出,帮助函数现在不是直接被 inject 到代码中,而是从 @babel/runtime 中引入。前文说了使用 @babel/plugin-transform-runtime 可以避免全局污染,我们来看看是如何避免污染的。
继续编译原来的样例:编译结果如下
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var isHas = [1, 2, 3].includes(2);
new Promise(function (resolve, reject) {
resolve(100);
});
第一反应,没有啥不同吗?Array.prototype 上新增了 includes 方法,并且新增了全局的 Promise 方法,污染了全局环境,这跟不使用 @babel/plugin-transform-runtime 没有区别。
如果我们希望 @babel/plugin-transform-runtime 不仅仅处理帮助函数,同时也能加载 polyfill 的话,我们需要给 @babel/plugin-transform-runtime 增加配置信息。
首先新增依赖 @babel/runtime-corejs3:
npm install @babel/runtime-corejs3 --save
如果我们希望
@babel/plugin-transform-runtime不仅仅处理帮助函数,同时也能加载polyfill的话,我们需要给@babel/plugin-transform-runtime增加配置信息。
首先新增依赖 @babel/runtime-corejs3:
npm install @babel/runtime-corejs3 --save
然后重新编译,看一下,编译出来的结果(lib/index.js):
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _context;
var isHas = (0, _includes.default)(_context = [1, 2, 3]).call(_context, 2);
new _promise.default(function (resolve, reject) {
resolve(100);
});
可以看出,没有直接去修改 Array.prototype,或者是新增 Promise 方法,避免了全局污染。如果上面 @babel/plugin-transform-runtime 配置的 core-js 是 "2",其中不包含实例的 polyfill 需要单独引入。
划重点:如果我们配置的
corejs是3版本,那么不管是实例方法还是全局方法,都不会再污染全局环境。
看到这里,不知道大家有没有这样一个疑问?给 @babel/plugin-transform-runtime 配置 corejs 是如此的完美,既可以将帮助函数变成引用的形式,又可以动态引入 polyfill,并且不会污染全局环境。何必要给 @babel/preset-env 提供 useBuiltIns 功能呢,看起来似乎不需要呀。
带着这样的疑问,我新建了几个文件(内容简单且基本一致,使用了些新特性),然后使用 webpack 构建,以下是我对比的数据:
| 序号 | .babelrc 配置 | webpack mode production |
|---|---|---|
| 0 | 不使用 @babel/plugin-transform-runtime | 36KB |
| 1 | 使用@babel/plugin-transform-runtime,并配置参数 corejs: 3。不会污染全局环境 | 37KB |
| 2 | 使用@babel/plugin-transform-runtime,不配置 corejs | 22KB |
我猜测是 @babel/runtime-corejs3/XXX 的包本身比 core-js/modules/XXX 要大一些~
Babel应用
CLI命令行工具 @babel/cli
babel 提供的命令行工具,主要是提供 babel 这个命令,适合安装在项目里。
@babel/node 提供了 babel-node 命令,但是 @babel/node 更适合全局安装,不适合安装在项目里。
npm install --save-dev @babel/core @babel/cli
现在你就可以在项目中使用 babel 进行编译啦(如果不安装 @babel/core,会报错噢)
将命令配置在 package.json 文件的 scripts 字段中:
//...
"scripts": {
"build": "babel src --out-dir lib --watch"
}
使用 npm run build 来执行编译,因为 Babel 虽然开箱即用,但是什么动作也不做,如果想要 Babel 做一些实际的工作,就需要为其添加插件(plugin)。
配置文件
Babel 支持多种格式的配置文件。
.babelrc
在项目根目录下创建一个名为 .babelrc 的文件:
{
"presets": [],
"plugins": []
}
具体的配置可以参考 .babelrc 文档
package.json
可以将 .babelrc 中的配置信息作为 babel 键(key) 添加到 package.json 文件中:
{
"name": "my-package",
"babel": {
"presets": [],
"plugins": []
}
}
.babelrc.js
与 .babelrc 配置相同,但是可以使用JS编写。
//可以在其中调用 Node.js 的API
const presets = [];
const plugins = [];
module.exports = { presets, plugins };
webpack中
babel-loader
{
test: /\.js$/,
exclude: /node_modules/,//不需要对第三方模块进行转换,耗费性能
loader: "babel-loader" ,
options:{
// npm i babel-loader @babel/core @babel/preset-env -D
// 生产依赖,兼容低版本浏览器
// npm install --save @babel/polyfill
// 在 index.js 中:
// 在业务代码运行之前最顶部导入 import "@babel/polyfill";
// 全局引入
// 复制代码注意:在开发类库,第三方模块或组件库时不能用 @babel/polyfill
// 这种方案,因为会把声明的变量变成全局变量,会污染全局环境。
"presets": [ ["@babel/preset-env",{
targets: {
//这个项目运行在大于什么版本的浏览器上,
// 已经支持es6的语法的高版本浏览器就不需要转义成es5了
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns:'usage'
//按需添加polyfill,把用到的代码都转成低版本浏览器兼容的
}],
"@babel/preset-react"
],
//npm install --save-dev @babel/plugin-transform-runtime
//npm install --save @babel/runtime
//npm install --save @babel/runtime-corejs2
"plugins": [["@babel/plugin-transform-runtime",{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}]]
}
}
常用插件
-
babel-loader
-
@babel/core
-
@babel/preset-env
-
@babel/preset-react
-
@babel/polyfill
-
regenerator-runtime/runtime
-
core-js@3
-
@babel/plugin-transform-runtime
-
@babel/runtime
-
@babel/runtime-corejs2
-
@babel/runtime-corejs3
-
@babel/plugin-transform-regenerator
-
@babel/plugin-transform-arrow-functions
-
babel-plugin-transform-remove-strict-mode
-
@babeL/plugin-syntax-dynamic-import
参考文献:
-
[不容错过的Babel7知识]