再谈Babel-polyfill

Babel 包含编译和polyfill两部分,这次我们专门来说一下polyfill部分(感觉很容易搞混)。

“babel 7”的图片搜索ç"“æžœ

Polyfill

直接说结论,Polyfill 的准确意思为:用于实现低版本浏览器还不支持的原生API的代码。

例如,querySelectorAll是很多现代浏览器都支持的原生Web API,但是有些古老的浏览器并不支持,那么假设有人写了库,只要用了这个库,你就可以在古老的浏览器里面使用document.querySelectorAll,使用方法跟现代浏览器原生API无异。那么这个库就可以称为Polyfill或者Polyfiller

polyfill的来历

Polyfill或者Polyfiller,是英国Web开发者 Remy Sharp 在咖啡店蹲坑的时候拍脑袋造出来的。当时他想用一个词来形容:用 JavaScript(或者Flash之类的什么鬼)来实现一些浏览器不支持的原生API。Shim这个已经有的词汇第一时间出现在他的脑海里。但是他回头想了一下Shim一般有自己的API,而不是单纯实现原生不支持的API。苦思冥想一直想不到合适的单词,于是他一怒之下造了一个单词Polyfill。除了他自己用这个词以外,他还给其他开发者用。随着他在各种Web会议演讲和他写的书《Introducing HTML5》中频繁提到这个词,大家用了都觉得很好,就一起来用。

tip:术语polyfill来自于一个家装产品Polyfilla,Polyfilla是一个英国产品,(在美国称之为Spackling Paste,在中国称为腻子,刮墙的)。把旧的浏览器想象成为一面有了裂缝的墙。这些 [polyfills] 会帮助我们把这面墙的裂缝抹平,还我们一个更好的光滑的墙壁(浏览器)。

shim和polyfill的区别

一个shim是一个库,它将一个新的API引入到一个旧的环境中,而且仅靠旧环境中已有的手段实现。两者对比网上说法有很多,这里从中选了一种我认为比较靠谱的:shim和polyfill有什么区别? - 李靖威的回答 - 知乎

polyfill 是 shim 的一种。

shim 是将不同 api 封装成一种,比如 jQuery 的 $.ajax 封装了 XMLHttpRequest 和 IE 用 ActiveXObject 方式创建 xhr 对象;

polyfill 特指 shim 成的 api 是遵循标准的,其典型做法是在IE浏览器中增加 window.XMLHttpRequest ,内部实现使用 ActiveXObject。

在实际中为了方便做对比,会特指 shim 的 api,不是遵循标准的,而是自己设计的。

tip:还有一种叫shiv的东西,它的作用是使得不支持HTML5标签的浏览器诸如ie6-8, 支持html5标签。

babel-polyfill

其实之前我们提到的”Babel 只编译语法不编译 API“说法并不完全正确(语法才需要编译,API不存在只能填充),Babel 是处于构建时(也就是传统Java等语言的编译时),转译出来的结果在默认情况下并不包括 ES6 对运行时的扩展,例如,builtins(内建对象,包括 Promise、Set、Map 等)、内建类型上的扩展(如 ES6 对 Array、Object、String 等内建类型上的扩展)以及 Regenerator(用于generators / yield)等都不包括在内。

tips:

  1. built-ins(内建),又可以叫”natives”(本地人) ,可以理解为内建对象,比如 String 和 Number。
  2. Regenerator,是Facebook开发的,一个用来转换es6的yield语法到es5的工具。

core-js 标准库

这是所有 Babel polyfill 方案都需要依赖的开源库zloirock/core-js,它提供了 ES5、ES6 的 polyfills,包括 promisessymbolscollections、iterators、typed arraysECMAScript 7+ proposalssetImmediate 等等。

如果使用了 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就是因为只在 preset 中配置了 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(作用是判断引入的是否是es6模块,是则直接返回obj,如果是commonjs模块,则将引入的内容放在一个对象的default属性上返回) 方法使用,所以 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 会将它们转译成:

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
17
18
19
20
21
"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

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

不同于插件(babel-plugin-transform-runtime 就是一个插件,需要配置在.babelrc中),你所要做的事情很简单,就是将babel-polyfill一次性的引入到你的工程中,通常是和其他的第三方类库(如 jQuery、React 等)一同打包在 vendor.js(vendor中文翻译为厂商,即第三方, webpack 中通常用vendor来命名我们项目中使用的第三方库,俗称三方库)中即可。在你写程序的时候,你完全不会感知 babel-polyfill 的存在,如果你的浏览器已经支持 Promise,它会优先使用 native 的 Promise,如果没有的话,则会采用 polyfill 的版本(这个行为与 babel-plugin-transform-runtime 一致),在使用 babel-polyfill 后,你不需要引入 babel-plugin-transform-runtime 插件和其他依赖的类库。它的缺点也显而易见,那就是占文件空间并且无法按需定制。

image-20181115130650463

tip:babel-polyfill 并不是一定会污染全局环境,在引入这个 js,并运行的时候,它会先判断当前有没有这个方法,在看要不要重写,如上图,是对Math.sign()`方法的替换。

对比babel-polyfill和babel-plugin-transform-runtime

babel-polyfill babel-plugin-transform-runtime
根本区别 不是一个plugin(插件),要所有代码之前引入,并且如果当前项目已经引入一次 polyfill 了,那你只能保留其一,改造目标环境,让目标环境拥有本来不支持的特性。 是一个plugin(插件),写的顺序应该没所谓,会改造你的代码,让你的代码能在所有目标环境上运行,但不改造目标环境
是否依赖bable-runtime 依赖 依赖
builtins(内建,包括 Promise、Set、Map)支持 支持(优先使用 native 的 Promise,如果没有的话,则会采用 polyfill 的版本) 支持(优先使用 native 的 Promise,如果没有的话,则会采用 polyfill 的版本)
是否支持内建类型上的扩展 支持类方法和原型方法(污染prototype) 支持类方法,不支持原型方法(所以不污染prototype)
Regenerator 支持 支持
使用时间 一开始就全量转换 babel发现使用到时自动且按需

如果你还是困惑,我推荐一个非常简单的区分方法 - 打开浏览器开发者工具,在 console 里执行代码:

  1. 引入 babel-polyfill 后的 IE 11,你可以在 console 下执行 Object.assign({}, {})
  2. 而引入 babel-plugin-transform-runtime 后的 IE 11,仍然提示你:Object doesn't support property or method 'assign'

总结

babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 内,babel-plugin-syntax开头的插件是语法插件,负责解析新的语法,babel-plugin-transform开头的插件负责转译,也就是解析语法+转换代码,也就是我们最终的目的,所以如果使用了转译插件,就不用再使用语法插件了。preset可以整合众多plugin,如早期的babel-preset-es2015整合了es6的语法和转译插件,现在推荐使用env。转译/编译是构建阶段的操作,除了转译/编译,babel还包括polyfill部分,处理运行时扩展,主要使用babel-runtime(包括core-js、regenerator和helpers)完成,使用又包括 babel-polyfill 和 transform-runtime,区别见上面讨论。

参考链接

Babel 编译出来还是 ES 6?难道只能上 polyfill? - Henry的回答 - 知乎