前两天一个同事跟我说了这么一个面试题,面试官上来就问他:“项目中用了babel还需要polyfill吗?” 开始他的内心是懵比的,怎么还有如此不按套路出牌的问题,按照面试的基本原则,答案一定是需要的,不然还怎么往下问啊。于是他说“要的”。当面试官深挖下去的时候他终于顶不住了。
其实平时开发的过程中用的大部分都是现成的脚手架,很少会仔细看babel的配置,更不会深挖babel的用法。那么今天我就来给大家好好回答一下这个问题。
首先说明:我们后面说的babel都是基于7.10.0这个版本。
啥是Babel
甩出中文官方文档的定义
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
下面列出的是 Babel 能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
- 源码转换 (codemods)
- 更多! (查看这些 视频 获得启发)
看完官方定义之后大家是不是觉得babel一个人就能把向后兼容的事情都做完了(不得不说确实有点误导),其实根本不是这样的。
真实的情况是babel只是提供了一个“平台”,让更多有能力的plugins入驻我的平台,是这些plugins提供了将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法的能力。
那么他是咋做到的呢?这就不得不提大名鼎鼎的AST了。
Babel 工作原理
babel的工作过程分为三个阶段:parsing(解析)、transforming(转化)、printing(生成)
- parsing阶段babel内部的 babylon 负责将es6代码进行语法分析和词法分析后转换成抽象语法树
- transforming阶段内部的 babel-traverse 负责对抽象语法树进行变换操作
- printing阶段内部的 babel-generator 负责生成对应的代码
其中第二步的转化是重中之重,babel的插件机制也是在这一步发挥作用的,plugins在这里进行操作,转化成新的AST,再交给第三步的babel-generator。
所以像我们上面说的如果没有这些plugins进驻平台,那么babel这个“平台”是不具备任何能力的。就好像这样:
const babel = code => code;
因此我们可以有信心的说出那个答案“需要”。不仅需要polyfill,还需要大量的plugins。
下面我们通过例子来说明,以及还需要哪些plugins才能将 ECMAScript 2015+ 版本的代码完美的转换为向后兼容的 JavaScript 语法。
preset-env, polyfill, plugin-transform-runtime 区别
现在我们通过 npm init -y 来创建一个例子,然后安装 @babel/cli 和 @babel/core。
代码结构参考这个截图,通过命令 babel index.js --out-file compiled.js
把 index 文件用 babel 编译成compiled.js
// index.js
const fn = () => {
console.log("wens");
};
const p = new Promise((resolve, reject) => {
resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);
不加任何plugins
为了印证上面的说法,我们首先测试不加任何plugins的情况,结果如下
//compiled.js
const fn = () => {
console.log("wens");
};
const p = new Promise((resolve, reject) => {
resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);
编译好的文件没有任何变化,印证了我们上面的说法。接下来我们加入 plugins。
在加入plugins测试之前我们需要知道一些前置知识,babel将ECMAScript 2015+ 版本的代码分为了两种情况处理:
- 语法层: let、const、class、箭头函数等,这些需要在构建时进行转译,是指在语法层面上的转译
- api方法层:Promise、includes、map等,这些是在全局或者Object、Array等的原型上新增的方法,它们可以由相应es5的方式重新定义
babel对这两种情况的转译是不一样的,我们需要给出相应的配置。
加入preset-env
上面的例子种const,箭头函数属于语法层面的,而promise和map属于api方法层面的,现在我们加入 preset-env 看看效果
// babel.config.js
module.exports = {
presets: ["@babel/env"],
plugins: []
};
babel 官方定义的 presets 配置项表示的是一堆plugins的集合,省的我们一个个的写plugins,他直接定义好了类似处理react,typescript等的preset
//compiled.js
"use strict";
var fn = function fn() {
console.log("wens");
};
var p = new Promise(function (resolve, reject) {
resolve("wens");
});
var list = [1, 2, 3, 4].map(function (item) {
return item * 2;
});
果然从语法层面都降级了。那么api层面要如何处理呢?
下面我们加入 @bable/polyfill
加入polyfill
对于 polyfill 的定义,相信经常逛 mdn 的同学一定不会陌生, 他就是把当前浏览器不支持的方法通过用支持的方法重写来获得支持。
在项目中安装 @bable/polyfill ,然后 index.js 文件中引入
// index.js
import "@bable/polyfill";
const fn = () => {
console.log("wens");
};
const p = new Promise((resolve, reject) => {
resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);
再次编译我们看看结果
// compiled.js
"use strict";
require("@bable/polyfill");
var fn = function fn() {
console.log("wens");
};
var p = new Promise(function (resolve, reject) {
resolve("wens");
});
var list = [1, 2, 3, 4].map(function (item) {
return item * 2;
});
没有别的变化,就多了一行require("@bable/polyfill"),其实这里就把这个库中的所有的polyfill都引入进来了,就好比我们项目中一股脑引入了全部的lodash方法。这样就支持了Promise和map方法。
细心的同学一定会发现这样有点“蠢”啊,lodash都提供了按需加载,你这个一下都引入进来了,可我只需要Promise和map啊。别慌,我们接着往下看。
配置 useBuiltIns
上面我们通过 import "@bable/polyfill"
的方式来实现针对api层面的“抹平”。然而从 babel v7.4.0开始官方就不建议采取这样的方式了。
因为引入 @bable/polyfill 就相当于在代码中引入下面两个库
import "core-js/stable";
import "regenerator-runtime/runtime";
这意味着不仅不能按需加载还有全局空间被污染的问题。因为他是通过向全局对象和内置对象的prototype上添加方法来实现的。
因此 babel 决定把这两个人的工作一并交给上面我们提到的@babel/env,不仅不会全局污染还支持按需加载,岂不是妙哉。现在我们再来看看改造后的配置
// index.js
// 去掉了polyfill
const fn = () => {
console.log("wens");
};
const p = new Promise((resolve, reject) => {
resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);
// webpack.config.js
module.exports = {
presets: [
[
"@babel/env",
{
useBuiltIns: "usage", // 实现按需加载
corejs: {
version: 3,
proposals: true
}
}
]
],
plugins: []
};
通过给 @babel/env 配置 useBuiltIns 和 corejs 属性,我们实现了polyfill方法的按需加载。关于全部配置项,参加官方文档
// compiled.js
"use strict";
require("core-js/modules/es.array.map");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var fn = function fn() {
console.log("wens");
};
var p = new Promise(function (resolve, reject) {
resolve("wens");
});
var list = [1, 2, 3, 4].map(function (item) {
return item * 2;
});
编译后的js文件只require了需要的方法,完美。那么我们还有可以优化的空间吗?有的有的,我们接着往下看。
加入 @babel/plugin-transform-runtime
改造上面的例子
// index.js
class Person {
constructor(name) {
this.name = name;
}
say() {
console.log(this.name);
}
}
只转换一个 Person 类,我们看看转换后的文件长啥样
// compiled.js
"use strict";
require("core-js/modules/es.function.name");
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 Person = /*#__PURE__*/function () {
function Person(name) {
_classCallCheck(this, Person);
this.name = name;
}
_createClass(Person, [{
key: "say",
value: function say() {
console.log(this.name);
}
}]);
return Person;
}();
除了require的部分,还多了好多自定义的函数。同学们想一想,现在只有一个index文件需要转换,然而实际项目开发中会有大量的需要转换的文件,如果每一个转换后的文件中都存在相同的函数,那岂不是浪费了,怎么才能把重复的函数去掉呢?
plugin-transform-runtime 闪亮登场。
上面出现的_classCallCheck,_defineProperties,_createClass三个函数叫做辅助函数,是在编译阶段辅助 Babel 的函数。
当使用了plugin-transform-runtime插件后,就可以将babel转译时添加到文件中的内联辅助函数统一隔离到babel-runtime提供的helper模块中,编译时,直接从helper模块加载,不在每个文件中重复的定义辅助函数,从而减少包的尺寸,下面我们看下效果:
// webpack.config.js
module.exports = {
presets: [
[
"@babel/env",
{
useBuiltIns: "usage",
corejs: { version: 3, proposals: true }
}
]
],
plugins: ["@babel/plugin-transform-runtime"]
};
// compiled.js
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
require("core-js/modules/es.function.name");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var Person = /*#__PURE__*/function () {
function Person(name) {
(0, _classCallCheck2["default"])(this, Person);
this.name = name;
}
(0, _createClass2["default"])(Person, [{
key: "say",
value: function say() {
console.log(this.name);
}
}]);
return Person;
}();
完美的解决了代码冗余的问题。
你们以为这就结束了吗,还没有。仔细看到这里的同学应该注意到了虽然上面使用 useBuiltIns
配置项实现了poilyfill的按需引用,可是他还存在全局变量污染的情况,就好比这句代码,重写了array的prototype方法,造成了全局污染。
require("core-js/modules/es.array.map");
最后再改造一次babel的配置文件
// webpack.config.js
module.exports = {
presets: ["@babel/env"],
plugins: [
[
"@babel/plugin-transform-runtime",
{
corejs: { version: 3 }
}
]
]
};
我们看到去掉了 @babel/env 的相关参数,而给 plugin-transform-runtime 添加了corejs参数,最终转换后的文件不会再出现polyfill的require的方法了。
// compiled.js
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var Person = /*#__PURE__*/function () {
function Person(name) {
(0, _classCallCheck2["default"])(this, Person);
this.name = name;
}
(0, _createClass2["default"])(Person, [{
key: "say",
value: function say() {
console.log(this.name);
}
}]);
return Person;
}();
综上所述,plugin-transform-runtime 插件借助babel-runtime实现了下面两个重要的功能
- 对辅助函数的复用,解决转译语法层时出现的代码冗余
- 解决转译api层出现的全局变量污染
结束
终于写完了,从翻阅文档到产出文章足足耗费了两天的时间。希望看到文章的同学也能和我一样有收获~~