认识babel7

新写的项目用的 vue-cli4.0, 用到的 babel 版本是 7.0, 对其中的配置方面一知半解. 刚巧看到一篇讲解不错的博客, 赶紧学习学习.

Babel 是什么

Babel 是一个 JavaScript 编译器

Babel 是一个工具链, 主要用于将 ECMAScript 2015+版本的代码转换为向后兼容的 JavaScript 语法, 以便能够运行在当前和旧版本的浏览器或其他环境中.

Babel 能为我们做些什么?

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性(通过@babel/polyfill 模块)
  • 源码转换(codemods)

通俗点说

  • ES2015+的语法转化(入箭头函数转成普通函数)
  • ES2015+新增的方法转化(如数组新增的includes方法转化兼容低版本浏览器)
1
2
3
4
5
6
7
// Babel 输入: ES2015 剪头函数
[1, 2, 3].map((n) => n + 1);

// Babel 输出: ES5
[1, 2, 3].map(function (n) {
return n + 1;
});

本篇文章我们主要目的是为了搞清楚 Babel 的使用和配置, 搞清楚@babel/runtime, @babel/polyfill, @babel/plugin-transform-runtime 这些作用是什么, 插件和预设都是用来干什么的, 为什么需要配置它们.

新建一个项目, 取名 babelTest, 使用npm init -y进行初始化, 创建src/index.js,文件内容如下

1
2
3
const fn = () => {
console.log("a");
};

继续之前, 先了解一下接下来我们用到的理论知识:

核心库@babel/core

Babel 的核心功能包含在@babel/core模块中, 该模块是 babel 的核心. 不安装@babel/core, 无法使用 babel 进行编译.

CLI 命令行工具@babel/cli

babel 提供的命令行工具, 主要是提供 babel 这个命令, 适合安装在项目里.

@babel/node提供了babel-node命令, 但是@babel/node更适合全局安装, 不适合安装在项目里.

1
npm install --save-dev @babel/core @babel/cli

使用 babel 编译项目(如果不安装@babel/core, 会报错)

可以将命令配置在package.json文件的scripts字段中

1
2
3
"scripts": {
"compiler": "babel src --out-dir lib --watch"
}

使用npm run compiler来执行编译, 我们会发现新建了个lib文件夹里面有个index.js文件. 发现编译前后的代码是一样的.

因为Babel虽然是开箱即用, 但是什么动作也不做, 如果想要Babel做一些实际的工作, 就需要为其添加插件(plugin).

@babel/cli与babel-cli区别

在网上找资料的时候,经常看到有的babel配置的是 babel-cli 有的是配置 @babel/cli 。为什么会不一样呢? 因为babel升级到了babel7。原先babel6的时候用的包都是如bable-cli这类的。升级babel7以后,用的包都是以 @ 开头的如@babel/cli,@babel/core 这样的包。所以有@开头的是babel7 没有@开头的是babel6。所以安装包的时候别安装错了。

插件

Babel构建在插件之上, 使用现有的或者自己编写的插件可以组成一个转换通道, Babel的插件分为两种: 语法插件和转换插件.

语法插件

这些插件只允许*Babel*解析特定类型的语法(不是转换), 可以在AST转换时使用, 以支持解析新语法, 例如:

1
2
3
4
5
import * as babel from "@babel/core";
const code = bebel.transformFromAstSycn(ast, {
plugins: ["@babel/plugin-proposal-optional-chaining"],
babelrc: false,
}).code;

转换插件

如果插件发布在npm上, 可以直接填写插件的名称, Babel会自动检查它是否已经被安装在node_modules目录下(自己试了下发现没起作用, 至于什么原因以后发现了补上. 现在老老实实用 npm 装), 在项目目录下新建.babelrc 文件, 配置如下

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

也可以指定插件的相对/绝对路径

1
2
3
{
"plugins": ["./node_modules/@babel/plugin-transform-arrow-functions"]
}

执行npm run compiler, 可以看到箭头函数已经被编译 OK, lib/index.js内容如下

1
2
3
const fn = function () {
console.log("a");
};

现在, 我们的项目仅支持转换箭头函数.如果想将其他的新的 JS 特性转换成低版本, 需要使用其他对应的 plugin. 如果我们一个个配就会非常繁琐. 为了简化这个过程, 强大的预设应运而生

预设

通过使用或创建一个present即可轻松使用一组插件

官方 Preset:

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

注: 从 Babel v7 开始, 所有针对标准提案阶段的功能所编写的预设(stage preset)都已被弃用, 官方已经移除了@babel/preset-stage-x.

@babel/preset-env

@babel/preset-env 主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载 polyfill , 在不进行任何配置的情况下, @babel/preset-env 所包含的插件将支持所有最新的 JS 特性(ES2015, ES2016 等, 不包含 stage 阶段), 将其转换为 ES5 代码. 例如, 如果你的代码中使用了可选链(目前, 仍在 stage 阶段), 那么只配置 @babel/preset-env , 转换时会抛出错误, 需要另外安装相应的插件

1
2
3
4
// .babelrc
{
"presets": ["@babel/preset-env"]
}

需要说明的是 @babel/preset-env 会根据你配置的目标环境, 生成插件列表来编译. 对于基于浏览器或 Electron 的项目, 官方推荐使用 .browserslistrc 文件来指定目标环境. 默认情况下, 如果你没有在 Babel 配置文件中(如.babelrc)设置 targets 或 ignoreBrowserslistConfig , @babel/preset-env 会使用 browserslist 配置源.

如果你不是要兼容所有的浏览器和环境, 推荐你指定目标环境, 这样你的编译代码能够保持最小.

例如, 仅包括浏览器市场份额超过 0.25%的用户所需的 polyfill 和代码转换(忽略没有安全更新的浏览器, 如 IE10 和 BlackBerry):

1
2
3
// .browserslistrc
> 0.25%
not dead

查看browserslist 的更多配置

例如, 你将 .browserlistrc 的内容配置为:

1
last 2 Chrome versions

然后再执行 npm run compiler , 你会发现箭头函数不会被编译成 ES5, 因为 Chrome 的最新 2 个版本都可能支持箭头函数. 现在, 我们将 .browserslistrc 仍然换成之前的配置.

但是!!! 语法的转换只是将高版本的语法转换成低版本的, 但是新的内置函数,实例方法无法转换.

举个栗子

修改src/index.js

1
2
3
4
5
const isHas = [1, 2, 3].includes(2);

const p = new Promise((resolve, reject) => {
resolve(100);
});

编译出来的结果为:

1
2
3
4
var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});

我们看到 includes 和 Promise 语法都没有被转换, 这显然是不行的, 因为低版本浏览器中根本没办法识别 includes 函数和 Promise 对象. 这时候就轮到 polyfill 上场了.

Polyfill

@babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块, 可以模拟完整的 ES2015+环境.

这意味着可以使用诸如 Promise 和 WeakMap 之类的新的内置组件, Array.from 或 Object.assing 之类的静态方法, Array.prototype.includes 之类的实例方法以及生成器函数(前提是使用了 @babel/plugin-transform-regenerator 插件). 为了添加这些功能, polyfill 将添加到全局范围和类似 String 这样的内置原型中(会对全局环境造成污染).

首先, 安装 @babel/polyfill 依赖:

1
npm install --save @babel/polyfill

注意: 不使用—save-dev, 因为这是一个需要在源码之前运行的垫片.

我们需要将完整的 polyfill 在代码之前加载, 修改我们的 src/index.js

1
2
3
4
5
6
7
8
"use strict";

require("@babel/polyfill");

var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});

@babel/polyfill 需要在其他代码之前引入, 我们也可以在 webpack 中进行配置.

例如:

1
2
3
4
entry: [
require.resolve('./polyfills')
path.resolve('./index')
]

polyfill.js 文件内容如下

1
import "@babel/polyfill";

现在我们的代码不管在低版本还是高版本浏览器(node 环境)中都能正常运行了. 不过, 很多时候, 我们未必需要完整的 @babel/polyfill , 这会导致我们最终构建的包的体积增大, @babel/polyfill 的包大小为 89k(当前 @babel/polyfill 版本为 7.7.0).

我们更期望的是, 如果我使用了某个新特性, 再引入对应的 polyfill, 避免引入无用的代码.

useBuiltIns

@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.

安装依赖:

1
npm install --save core-js@3

core-js: JavaScript 的模块化标准库, 包含 Promise, Symbol, Iterator 和许多其他的特性, 它可以让你仅加载必需的功能

修改 Babel 的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
// .babelrc
{
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3,
},
],
];
}

Babel 会检查所有代码, 以便查找在目标环境中缺失的功能, 然后仅仅把需要的 polyfill 包含进来.

比如: src/index.js 代码不变:

1
2
3
4
5
6
7
const fn = () => {
console.log("a");
};
const isHas = [1, 2, 3].includes(2);
const p = new Promise((resolve, reject) => {
resolve(100);
});

查看编译结果lib/index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict";

require("core-js/modules/es.array.includes");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

var fn = function fn() {
console.log("a");
};

var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});

同样的代码, 我们用 webpack 构建一下, 能看到最终的代码大小仅为:20KB. 而如果我们引入整个 @babel/polyfill 的话, 构建出的包大小为: 89KB

前面曾提到, 在 useBuiltIns 参数为 usage 时, 仍然需要安装@babel/polyfill, 虽然我们上面的代码转换中看起来并没有使用到, 但是, 如果我们源码中使用到了 async/await , 那么编译出来的代码需要 require("regenerator-runtime/runtime") , 在@babel/polyfill 的依赖中, 当然也可以只安装 regenerator-runtime/runtime 取代安装 @babel/polyfill.

@babel/plugin-transform-runtime

引子: Babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法. 默认情况下, 它将被添加(inject)到需要它的每个文件中.

例子: 在 src/index.js 中添加内容

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
getX() {
return this.x;
}
}

let cp = new Point(25, 8);
let x = cp.getX();

编译出来的lib/index.js, 新增以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 Point(25, 8);
var x = cp.getX();

看起来代码没有问题, 但是如果有 10 个文件中都使用了这个 class, 那么 _classCallCheck, _defineProperties,_createClass 等方法就要被 inject10 次. 这显然会导致包体积增大, 最关键的是, 我们并不需要它 inject 多次.

这个时候就需要用到 @babel/plugin-transform-runtime , 使用@babel/plugin-transform-runtime 插件, 所有帮助程序都将引用模块 @babel/runtime 这样就可以避免编译后的代码中出现重复的帮助程序, 有效减少包体积.

@babel/plugin-transform-runtime : 是一个可以重复使用 Babel 注入的帮助程序, 以节省代码大小的插件.

注意: 诸如 Array.prototype.flat()等实例方法将不起作用, 因为这需要修改现有的内置函数(可以使用@babel/polyfill 来解决这个问题)

对此需要说明的是如果你配置的是 corejs3, core-js@3 现有已经支持原型方法, 同时不污染原型

另外, @babel/plugin-transform-runtime 需要和 @babel/runtime 配合使用.

首先安装依赖, @babel/plugin-transform-runtime 通常仅在开发时使用, 但是运行时最终代码需要依赖@babel/runtime, 所以@babel/runtime 必须要作为生产依赖被安装

1
2
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

除了前文说的, @babel/plugin-transform-runtime 可以减少编译后代码体积外, 我们使用它还有个好处.它可以为代码创建一个沙盒环境, 如果使用@babel/polyfill 及其提供的内置程序(例如 Promise, Set 和 Map), 则它们将污染全局范围.虽然这对于应用程序或命令行工具可能是可以的, 但是如果你的代码是要发布供他人使用的库, 或者无法完全控制代码运行的环境, 这将是个问题.

@babel/plugin-transform-runtime 会将这些内置别名为 core-js, 因此你可以无缝使用他们, 而无需 polyfill.

修改.babelrc 的配置, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3
}
]
],
plugins: [
[
"@babel/plugin-transform-runtime"
]
]
}

运行npm run compiler重新编译后的内容(lib/index.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 Point(25, 8);
var x = cp.getX();

@babel/plugin-transform-runtime还可以避免全局污染.

修改src/index.js内容如下:

1
2
3
4
5
let isHas = [1, 2, 3].includes(2)

new Promise((resolve, reject) => {
resolve(100)
})

重新编译npm run compiler, 查看lib/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"use strict";

require("core-js/modules/es.array.includes.js");

require("core-js/modules/es.promise.js");

require("core-js/modules/es.object.to-string.js");

require("./style.scss");

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如何避免全局污染?

新增依赖@babel/runtime-corejs3

1
npm i -D @babel/runtime-corejs3

修改配置项.babelrc:

1
2
3
4
5
6
7
8
9
10
{
"presets": [
["@babel/preset-env"]
],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}

再次重新编译npm run compiler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

require("./style.scss");

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 方法,避免了全局污染。

补充知识

插件执行顺序

如果两个转换插件都将处理“程序(Program)”的某个代码片段,则将根据转换插件或 preset 的排列顺序依次执行。如:

1
2
3
{
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"]
}

先执行 @babel/plugin-proposal-class-properties,后执行 @babel/plugin-syntax-dynamic-import

preset 的执行顺序是颠倒的

1
2
3
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

先执行 @babel/preset-react, 后执行 @babel/preset-env。

插件参数

插件和 preset 都可以接受参数,参数由插件名和参数对象组成一个数组。preset 设置参数也是这种格式。如:

1
2
3
4
5
6
7
8
{
"plugins": [
[
"@babel/plugin-proposal-class-properties",
{ "loose": true }
]
]
}

插件的短名称

如果插件名称为 @babel/plugin-XXX,可以使用短名称@babel/XXX :

1
2
3
4
5
{
"plugins": [
"@babel/transform-arrow-functions" //同 "@babel/plugin-transform-arrow-functions"
]
}