来到第五部分,主要记录一下 babel-polyfill 与 相关插件和包。

前情提要

“I am … the Flash”

Previously on the Flash

嘿嘿嘿,其实我还有看 DC 的美剧《闪电侠》的。大家应该都不想吐槽我看脑残剧了,但明明人家剧里还有讲平行世界和时间线这种我等凡人理解不能的知识呀~

好,回到正题。上一节《Babel 入门教程(五):babel-register 与 babel-core》,分别讲解了 babel-registerbabel-core 以及它们的区别联系和使用场景。但是细心可爱的读者应该注意到了,我在上上节《Babel 入门教程(四):babel-cli 与 babel-node 及工程实践》中埋了个坑,其中一节「工程实践」,照着做是会报错的。

其实是我疏忽了,但也正好借这个机会加强大家的印象,请大家再回看第 4 部分文章的「工程实践」,报错的原因是我们必须还得安装 babel-core 包,因为 gulp 工程里的 gulp-babel 和 webpack 工程里的 babel-loader 是依赖 babel-core 的。

不信的话,我们可以新初始化一个工程文件夹,然后先安装 gulp-babel 或者 babel-loader,然后注意终端界面给出的提示。

只安装 gulp-babel 时给出的警告:
图片

只安装 babel-loader 时给出的警告:
图片

结果给出的警告都是需要安装 babel-core,但是现在没有被安装。

所以现在我们重新来规范一下 ES6 工程实践的搭建基础。

babel 结合 gulp 构建工具

安装相关依赖:

1
2
3
4
5
$ npm i -D gulp
$ npm i -D gulp-babel
# gulp-babel 依赖于 node_modules/babel-core
$ npm i -D babel-core
$ npm i -D babel-preset-env

新建 .babelrc 文件:

1
2
3
4
5
6
{
"presets": [
"env"
],
"plugins": []
}

新建 gulpfile.js 文件:

1
2
3
4
5
6
7
8
var gulp = require("gulp");
var babel = require("gulp-babel");

gulp.task("default", function () {
return gulp.src("src/example.js")
.pipe(babel())
.pipe(gulp.dest("lib"));
});

babel 结合 webpack 模块化打包工具

安装相关依赖:

1
2
3
4
5
$ npm i -D webpack
$ npm i -D babel-loader
# babel-loader 依赖于 node_modules/babel-core
$ npm i -D babel-core
$ npm i -D babel-preset-env

新建 .babelrc 文件:

1
2
3
4
5
6
{
"presets": [
"env"
],
"plugins": []
}

新建 webpack.config.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
entry: './src/app.js',
output: {
path: __dirname,
filename: './bin/app.bundle.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}]
}
}

其他的

十分要注意的一点是,要清楚 babel-register 包的使用场景,它用于实时转译 ES6 代码,适合开发环境,但不适合与生产环境中使用。

另外,不能在 require('babel-register') 的当前文件中写 ES6 代码,这个逻辑应该清楚。

前言

好了,回顾和完善好之前的内容,就来到《Babel 入门教程》系列的最后一部分内容啦。其实每天发布完文章,我在第二天发布之前又会重读好几遍,然后看看读者在群或者是给我留言的消息,然后根据遗漏点再于新文章的前情提要里面去进行完善和补充。非常感谢各位读者的反馈!

今天的核心内容是介绍 babel-polyfillbabel-runtimebabel-plugin-transform-runtime

相信大家在学习网上教程或者是采用别人的脚手架快速搭建项目原型的时候会有看到,这里就来梳理清楚关系吧!

babel-polyfill

对于 babel-polyfill,官方原文介绍如下:

Babel includes a polyfill that includes a custom regenerator runtime and core-js.

This will emulate a full ES2015+ environment and is intended to be used in an application rather than a library/tool. This polyfill is automatically loaded when using babel-node.

This means you can use new built-ins like Promise or WeakMap, static methods like Array.from or Object.assign, instance methods like Array.prototype.includes, and generator functions (provided you use the regenerator plugin). The polyfill adds to the global scope as well as native prototypes like String in order to do this.

即Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

我想到之前的项目好像都并没有安装这个库,但是用了那些新的 API 在 Chrome 上面跑,也并没有出错呀!然后再终端上使用 babel-node 进入了 REPL 环境,进行了如下操作:
图片

我并没有引入和使用 babel-polyfill 包呀!感觉不对劲,然后单纯地使用 node 进入 REPL 环境,如上进行操作,居然也是可以的。然后使用 Chrome 浏览器的控制台重复代码运行,哎哟,也可以,查看浏览器版本为 版本 63.0.3239.132(正式版本)(64 位)

这时候我明白了,babel-polyfill 是一个「垫片」,它是为那些还不支持相关新 API 的环境去仿效着提供一个完整的 ES2015+ 环境,并意图运行于一个应用中而不是一个库/工具。也就是说可以为低版本的不支持新 API 的浏览器去提供一个能运行高版本 ECMAScript 脚本语言的环境。

现在我们来针对官方的第一句解释,来理清一些库之间的关系,以便更清晰地理解这个包到底是干什么用的,与其他包有什么关联和联系。

core-js 标准库

这是所有 Babel polyfill 方案都需要依赖的开源库zloirock/core-js,它提供了 ES5、ES6 的 polyfills,包括 promises 、symbols、collections、iterators、typed arrays、ECMAScript 7+ proposals、setImmediate 等等。

如果使用了 babel-runtime、babel-plugin-transform-runtime 或者 babel-polyfill,你就可以间接的引入了 core-js 标准库。比如 Array.from 就是来自于 core-js/array/from.js 。

regenerator 运行时库

这是 Facebook 提供的 facebook/regenerator 库,用来实现 ES6/ES7 中 generators、yield、async 及 await 等相关的 polyfills。

在下面即将提到的 babel-runtime 中被引用。有些初学者遇到的“regeneratorRuntime is not defined”就是因为只在 presets 中配置了 stage-0 却忘记加上 babel-polyfill。如果使用了 babel-runtime、babel-plugin-transform-runtime 或者 babel-polyfill,你就可以间接的引入了 regenerator-runtime 运行时库(非必选)。

babel-runtime 库

babel-runtime 是由 Babel 提供的 polyfill 库,它本身就是由 core-js 与 regenerator-runtime 库组成,除了做简单的合并与映射外,并没有做任何额外的加工。

所以在使用时,你需要自己去 require,举一个例子,如果你想使用 Promise,你必须在每一处需要用到 Promise 的 module 里,手工引入 promise 模块:

1
const Promise = require('babel-runtime/core-js/promise');

由于这种方式十分繁琐,事实上严谨的使用还要配合 interopRequireDefault() 方法使用,所以 Babel 提供了一个插件,即 babel-plugin-transform-runtime。

babel-plugin-transform-runtime 插件

这个插件让 Babel 发现代码中使用到 Symbol、Promise、Map 等新类型时,自动且按需 进行 polyfill,因为是“自动”所以非常受大家的欢迎。

在官网中,Babel 提醒大家如果正在开发一个 library 的话,建议使用这种方案,因为没有全局变量和 prototype 污染。

全局变量污染,是指 babel-plugin-transform-runtime 插件会帮你实现一个沙盒(sandbox),虽然你的 ES6 源代码显式的使用了看似全局的 Promise、Symbol,但是在沙盒模式下,Babel 会将它们转译。

安装 babel-plugin-transform-runtime

1
$ npm i -D babel-plugin-transform-runtime

ES6 代码

1
2
3
4
5
const sym = Symbol();

const promise = new Promise();

console.log(arr[Symbol.iterator]());

转译后的代码

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

var _getIterator2 = require("babel-runtime/core-js/get-iterator");
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _promise = require("babel-runtime/core-js/promise");
var _promise2 = _interopRequireDefault(_promise);
var _symbol = require("babel-runtime/core-js/symbol");
var _symbol2 = _interopRequireDefault(_symbol);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var sym = (0, _symbol2.default)();

var promise = new _promise2.default();

console.log((0, _getIterator3.default)(arr));

你会发现,这个插件至始至终没有在 Global 对象下挂在全局的 Symbol 和 Promise 变量。这样一来,如果你引入的其他类库使用了 bluebird 之类的第三方 polyfill 也不会受此影响。

那么什么是 prototype 污染呢,这就要说到 ES6 的 Array、String 等内建类型扩展了很多新方法,如 Array 原型上的 includes()filter() 等新方法,babel-plugin-transform-runtime 插件是不会进行扩展修改的,很多人往往忽略了这一点。要区分的是,Array.from 等静态方法(也有人称类方法)还是会被插件 polyfill 的。

因此,babel-plugin-transform-runtime 这个插件更适合于开发类库(library)时去使用,而不适合直接用在独立的前端工程中。另外,它可以按需polyfill,所以从一定程度上控制了polyfill 文件的大小。

再谈 babel-polyfill

初衷是模拟(emulate)一整套 ES2015+ 运行时环境,所以它的确会以全局变量的形式去 polyfill Map、Set、Promise 之类的类型,也的确会以类似 Array.prototype.includes() 的方式去注入污染原型,这也是官网中提到最适合应用级开发的 polyfill,再次提醒如果你在开发 library 的话,不推荐使用(或者说绝对不要使用)。

不同于插件,你所要做的事情很简单,就是将 babel-polyfill 一次性的引入到你的工程中,通常是和其他的第三方类库(如 jQuery、React 等)一同打包在 vendor.js 中即可。

在你写程序的时候,你完全不会感知 babel-polyfill 的存在,如果你的浏览器已经支持 Promise,它会优先使用 native 的 Promise,如果没有的话,则会采用 polyfill 的版本(这个行为与 babel-plugin-transform-runtime 一致),在使用 babel-polyfill 后,你不需要引入 babel-plugin-transform-runtime 插件和其他依赖的类库。

另外,它的缺点也显而易见,那就是占文件空间并且无法按需定制。

安装 babel-polyfill:

1
$ npm i -S babel-polyfill

引入:

1
2
3
4
// 在我们的应用入口顶部中引入,且确保它在任何其他代码/依赖声明之前被调用
require("babel-polyfill");
// 或者
import "babel-polyfill";

如果是 Webpack 中使用,则要在 webpack.config.js 中,将 babel-polyfill 加到你的 entry 数组中:

1
2
3
module.exports = {
entry: ["babel-polyfill", "./app/js"]
};

结语

Ahhhh… 终于结束连续一周的连载了,其实我说花了几周的周末去写的这个教程,然后每天进行发布的时候再进行深度的审核校验以保证质量。因为工作日实在太忙了,前端技术日新月异,必须跟上浪潮,但还是以核心技术为主。

希望这个系列的《Babel 入门教程》是真的帮助到大家入了 babel 的大门,深造的话就靠大家的努力了,也还会依赖于具体工程的需求。万一后面还有相关的 babel 知识要补充,我也是会及时更新上来的话,如果你觉得本教程不错的话,可以持续关注本叔或者分享给朋友,谢谢你们的阅读和支持。

这周上映《移动迷宫3:死亡解药》,你期待吗?反正本宝要去观影啦,激动激动!



微信公众号