bg-pic

是时候好好认识下AST这个熟悉而又陌生的朋友了 这篇博客中,讲解了AST相关的知识,我在摸索AST的过程中顺腾摸瓜,顺便也把Babel好好研究了一番,于是趁热打铁输出了这篇博客,废话不多说,开搞

bqb

何为Babel

注意:因为目前Babel已经更新到了7.13,所以下文也都是基于 Babel7 进行讲解

打开 Babel 的官网,映入眼帘的是很醒目的一句话:Babel 是一个 JavaScript 编译器,由此可见官方对Babel的定位是一个js的编译器。其实准确来说,它应该是一个转换编译器(transpiler),借助它可以实现”源码到源码”的编译。Babel的工作流程大致分为以下三步

Babel本身是不进行任何编译转换的,所有具体的编译转换操作都被下发到了各个插件中(plugins),不同的插件负责不同目的的转换,比如 @babel/plugin-transform-arrow-functions负责转换es6下的箭头函数,而 @babel/plugin-transform-runtime 负责整合Babel提供的 helpers@babel/runtime 公共库中,从而减少代码体积

由此可见Babel更类似于一个插件的 装配工厂,负责插件的安装与调度,从而生产出我们所需要的内容

Babel基于这种插件架构理论上可以实现任何转换,但我认为Babel最重要的转换还是下面两种

在大致了解Babel后,接下来,开始深入探索Babel咯,准备好了吗?

bqb

Babel的配置与使用

要想探索Babel,总得先跑起来吧,怎么跑呢?其实只需要下载 @babel/cli@babel/core 后,就能以命令行的方式运行Babel,安装命令为 npm install @babel/cli @babel/core --save-dev

注意这里 @babel/cli 是安装在项目里的,而不是全局安装的,虽然也可以全局安装,但是不建议,原因有两点

当安装完Babel-cli后,就可以开始编译我们的源代码了

默认情况下,编译输出的内容会打印到控制台中,但这肯定不是我们想要的效果,通常情况下都是输出到指定的文件中,想要达到这个目的只需要传递参数给Babel-cli就可以了,常用的参数如下

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
/* 默认不使用任何参数,会将输出的内容打印到控制台中 */
babel source.js

/* 指定输出的内容到文件中,使用 -o */
babel source.js -o target.js

/* 文件被修改后 自动编译该文件,使用 -w */
babel source.js -w

/* 额外输出sourcemap文件,使用 -s,注意这里必须指定输出的文件,否则不会额外生成sourcemap文件,而是inline形式 */
babel source.js -o target.js -s

/* 内联输出sourcemap,使用 -s inline */
babel source.js -s inline

/* 编译整个src目录下的文件并输出到lib目录,使用 -d,这不会覆盖lib目录下的任何其他文件或目录 */
babel src -d lib

/* 忽略部分文件,使用 --ignore */
babel src -d lib --ignore "src/**/*.spec.js","src/**/*.test.js"

/* 使用插件,使用 --plugins */
babel source.js --plugins=@babel/proposal-class-properties,@babel/transform-modules-amd

/* 使用presets,使用 --presets */
babel source.js --presets=@babel/preset-env,@babel/flow

可以看到Babel-cli支持的参数是很多的,但是如果每次编译都需要手动输入这么多内容,那没人会喜欢这个Babel的,毕竟我们的时间都很宝贵,Babel当然也考虑到了这一点,所以提供了配置文件来定制化Babel的功能

配置文件提供了很多方式,列举如下

有了配置文件,接下来就该添加配置信息啦,下面列出常用配置项的说明,走你~

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/* plugins,这个不用说,Babel的核心功能,用于列出所要用到的插件集合,插件可以携带参数 */

/* 不带参数的形式 */
{
"plugins": [
"@babel/some-plugin"
]
}
/* 带参数的形式 */
{
"plugins": [
[
"@babel/some-plugin",
{
opt: 'opt'
}
]
]
}



/* presets,指定插件集,包含一系列插件,可以让我们不用一个一个去导入插件 */

/* 不带参数的形式 */
{
"presets": ["@babel/preset-react"]
}
/* 带参数的形式 */
{
"presets": [
[
"@babel/preset-react",
{
"pragma": "dom", // default pragma is React.createElement (only in classic runtime)
"pragmaFrag": "DomFrag", // default is React.Fragment (only in classic runtime)
"throwIfNamespace": false, // defaults to true
"runtime": "classic" // defaults to classic
}
]
]
}


/* targets,指定我们项目所支持的目标环境,通过它可以实现按需编译 */

/* 可以以 browserslist-compatible query 指定目标环境*/
{
"targets": "> 0.25%, not dead"
}
/* 通过一个对象指定支持最低的环境版本号,可用环境值有chrome, opera, edge, firefox, safari, ie, ios, android, node, electron */
{
"targets": {
"chrome": "58",
"ie": "11"
}
}

/*
假如我们没有指定targets,Babel会默认认为我们项目的目标环境是最老的版本,那么@babel/preset-env插件就会将所有es6+的代码转换为es5,这样就会大大增加输出产物的体积,所以我们要记住去设置这个值
*/

需要注意的是,plugins与presets的执行是存在顺序的,规则如下:

除此之外,还有两个小技巧教给你

经过上文的讲解,我相信你对Babel的配置与使用已经了然于胸了,接下来再介绍三个Babel提供的工具函数,借助这些工具函数可以使我们在Babel之外的环境实现源代码的转换,所以一起来瞅瞅吧~

bqb

Babel提供的工具函数

它们分别是:

1
2
3
4
5
6
7
8
9
10
import traverse from "babel-traverse";
import * as t from "babel-types";

traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "n" })) {
path.node.name = "x"
}
}
})

看了这么多官方提供的插件工具,是不是也想自己捣鼓一个Babel插件出来呢?

bqb

创建与使用自定义Babel插件

其实Babel插件的创建非常简单,talk is cheap, show me the code,示例如下

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
/*
可以看到创建一个Babel插件只需要导出一个函数,函数返回一个对象,这个对象有 visitor 属性,将其作为插件的访问者

访问者是一个用于AST遍历的跨语言的模式,简单的说它就是一个对象,定义了用于在一个树状结构中获取具体节点的方法,
比如下面的例子中,每当在树中遇见一个 Identifier 节点的时候都会调用 Identifier() 方法,其实我们有两次机会来访问
同一个节点,分别是 enter 和 exit

访问者的钩子函数中有两个参数:path 和 state,下面依次解释

path:AST通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,或者也可以用Path(路径)来简化这件事情,它是表示两个节点之间连接的对象,包含了添加、更新、移动和删除节点有关的很多方法

state: 状态是抽象语法树AST转换的敌人,状态管理会不断牵扯你的精力,而且几乎所有你对状态的假设,总是会有一些未考虑到的语法最终证明你的假设是错误的,我们可以把一些自定义的变量存储在state中,以便所有节点都可以访问它,同时也可以通过state.opts访问到我们传递给插件的参数

最后需要注意的是插件返回的函数接受Babel对象作为参数,通过Babel对象可以获取到很多有用的信息,比如 babel.types 就可以拿到 @babel/types 工具函数,其他的可以自行研究
*/
module.exports = function(babel) {
return {
visitor: {
/* 这种方式是 进入 节点时触发*/
Identifier(path,state) {
const name = path.node.name
// reverse the name: JavaScript -> tpircSavaJ
path.node.name = name
.split("")
.reverse()
.join("")
},
/* 这种方式 进入 和 退出 节点时都会触发对应的钩子函数,相对于上一种方式其控制粒度更细*/
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!");
}
}
}
}
}

插件定义好了,那么我们应该怎么使用呢?其实使用Babel插件的方式也很简单,分为两种

1
2
3
4
5
6
/* 也可以传递参数给自定义插件,方式与非自定义插件一致 */
module.exports = {
"plugins": [
"./myBabelPlugin.js"
]
}

现在我们应该已经能熟练地创建并使用自定义插件了,很开心吧🤪?接下来准备聊聊关于polyfill的内容,从而可以让我们对Babel有个更全面的认知,那么开始吧

bqb

使用polyfill的三种姿势

我们知道Babel可以将高版本的js语法转为低版本的,是的,这很酷😍!但是,Babel不会转译高版本中新增的类或方法(Promise、Array.isArray、’a’.repeat(5)),很明显,这是存在问题的,因为这些新的api在低版本浏览器下根本不存在。这个时候大名鼎鼎的 @babel/polyfill 就登场了,它是一个垫片库,用于抹平不同环境下存在的差异,让所有环境都处于同一水平线上,这样我们就可以畅通无阻地使用各种最新的api了😍

@babel/polyfill 本身由自定义的 regenerator runtimecore-js 组成, core-js 提供绝大部分js新特性的polyfill,regenerator runtime 主要提供(generator,yield)与(async,await)两组的polyfill

其实 @babel/runtime 也可以做到抹平浏览器差异的事情,并且做的更好,下面会讲解关于 @babel/runtime@babel/polyfill 的三种使用姿势,让你能对此有个更全面的认知,所有方式都是以下面的代码为示例进行说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Array.isArray(params)

[1].includes(1)

''.charAt(1)

''.repeat(2)

const exampleFunc = ()=>{
console.log(a(1,2))
}

class exampleClass {
constructor(a,b){
this.a = a
this.b = b
}
}

const asyncFunc = async()=>{
await 1
}

【姿势一】👉 全量导入@babel/polyfill

这种方式分为以下两步

我们先看下这种方式构建出来的代码长啥样

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
44
45
46
"use strict";

require("@babel/polyfill");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

Array.isArray(params)[1].includes(1);
''.charAt(1);
''.repeat(2);

var exampleFunc = function exampleFunc() {
console.log(a(1, 2));
};

var exampleClass = function exampleClass(a, b) {
_classCallCheck(this, exampleClass);

this.a = a;
this.b = b;
};

var asyncFunc = /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 1;

case 2:
case "end":
return _context.stop();
}
}
}, _callee);
}));

return function asyncFunc() {
return _ref.apply(this, arguments);
};
}();

这种全量导入带来的后果就是构建产物的体积非常大,原因显而易见:这种使用方式是全量导入 @babel/polyfill,因此导致一股脑把所有新特性的polyfill都导入了,而不管我们有没有使用到

这种方式显然是不合理的,我们想要的是类似 按需引入 的效果,伟大的Babel也考虑到了这一点,请继续往下看

bqb

【姿势二】👉 按需导入@babel/polyfill

想要使用这种方式,改动的地方并不多,主要是以下两处

这里必须要指定corejs的版本,否则在编译时会报错。当前 @babel/polyfill (v7.12.1)默认会安装corejs2,因为corejs2已经停止维护,所以推荐使用corejs3,这种方式需要我们手动安装corejs3 npm install --save core-js@3

我们看下经过这种方式构建出来的代码长什么样的

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
"use strict";

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

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

require("regenerator-runtime/runtime.js");

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

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

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

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

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

Array.isArray(params)[1].includes(1);
''.charAt(1);
''.repeat(2);

var exampleFunc = function exampleFunc() {
console.log(a(1, 2));
};

var exampleClass = function exampleClass(a, b) {
_classCallCheck(this, exampleClass);

this.a = a;
this.b = b;
};

var asyncFunc = /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 1;

case 2:
case "end":
return _context.stop();
}
}
}, _callee);
}));

return function asyncFunc() {
return _ref.apply(this, arguments);
};
}();

从构建出的代码可以看到所有用到的新特性polyfill都是单独进行引入的,这样就可以实现 按需引入 的目的啦😝

你是不是觉得这种方式已经是最优解了?答案当然不是了🤪,不然就不会有第三种方式了,其实细心观察输出的代码,我们会发现两个问题

下面介绍的方式就是为解决上述问题而出现的,一起来瞅瞅吧~

bqb

【姿势三】👉 @babel/plugin-transform-runtime与@babel/runtime双剑合璧

这种方式需要用到 @babel/plugin-transform-runtime@babel/runtime 这两个包,废话不多说,先安装上!

1
2
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime //因为@babel/runtime要跑在运行时,所以得安装成依赖项,而不是开发依赖项

接下来就是如何配置它们了,上代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
"presets": [
// 注意这里我们移除了@babel/preset-env的配置项
[
"@babel/preset-env"
]
],
/*
建议使用corejs的v3,因为v2不支持转译新增的实例方法,如'a'.repeat(5)、[].includes(5),所以需要再单独安装对应的polyfill

安装v3命令 npm install @babel/runtime-corejs3 --save
*/
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}

同样的,我们看下通过这种方式输出的代码长啥样

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
44
45
46
47
48
49
50
51
52
53
"use strict";

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

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

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

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

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

var _isArray = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/is-array"));

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

var _context, _context2;

(0, _includes["default"])(_context = (0, _isArray["default"])(params)[1]).call(_context, 1);
''.charAt(1);
(0, _repeat["default"])(_context2 = '').call(_context2, 2);

var exampleFunc = function exampleFunc() {
console.log(a(1, 2));
};

var exampleClass = function exampleClass(a, b) {
(0, _classCallCheck2["default"])(this, exampleClass);
this.a = a;
this.b = b;
};

var asyncFunc = /*#__PURE__*/function () {
var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
return _regenerator["default"].wrap(function _callee$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
_context3.next = 2;
return 1;

case 2:
case "end":
return _context3.stop();
}
}
}, _callee);
}));

return function asyncFunc() {
return _ref.apply(this, arguments);
};
}();

从输出的代码可以很明显地看到上述的两个问题在这里都得到了解决

三种方式的比较🧐

通过上文的讲解,可以知道第三种方式应该是最优解,既能减小构建产物的体积,也能不污染全局环境

构建产物的体积是一个很重要的衡量指标,为了比对这三种方式构建产物的体积,我通过结合webpack来对上述示例中的代码进行构建,下面贴出结果

方式 构建产物体积
全量导入@babel/polyfill 88kb
按需导入@babel/polyfill 28kb
@babel/plugin-transform-runtime与@babel/runtime双剑合璧 35kb

可以看到全量导入的方式远远大于其他方式,这也是我们意料之中的,但意料之外的是第二种方式构建的产物体积最小。虽然第二种方式构建产物的体积最小,但是与第三种相差并不大,并且还存在污染全局环境的隐患,所以最优解还是第三种🧐

至此,关于polyfill的讲解就结束了。由于上文都是以Babel7进行讲解的,所以接下来想聊聊Babel7与Babel6的差异,进而拓展我们对Babel的认知维度🤪

Babel7与Babel6的区别

其核心机制方面没有差异,插件、preset、解析转译生成这些都没有变化。主要变化有以下几点

至此,所有关于Babel的讲解就结束了,相信此时的你已经收获满满了吧🤪~

bqb

结语

Babel无疑是伟大的,它彻底释放了我们的生产力,从而告别了曾经刀耕火种的时代。现在前端领域的蓬勃发展,Babel所贡献的力量是不容忽视的,因此对于Babel的学习是每一个前端工程师绕不开的领域,只有我们磨好了这把利器🪓,才能在未来的开发中披荆斩棘,最终顺利达到我们想要去的彼岸,所以,一起加油吧,骚年😂!