我们知道,ES2015+ 在低版本浏览器中存在严重的兼容问题,为此前人们尝试了各种方式:
Shim/Sham
在远古时代的代码中,我们可能看到过这样的处理:
1 | <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/0.34.2/es5-shim.min.js"></script> |
shim
仅依靠旧版浏览器中现有的 API,实现了一些新版本 ES 的特性,而 sham
则是仅仅为了保证新版本特性能在浏览器中不抛出错误(因为有些新特性在低版本浏览器中根本无法实现)。
Babel
为了彻底解决上面的问题,Babel
诞生了。Babel 这个名字指的是巴比伦文明里面的通天塔,在那里,人们只说同一种语言。而在前端工程中,我们可以依靠 Babel 将 ES6+ 的语法转换为旧浏览器可以识别的版本。
在浏览器端,Babel 提供了一个运行时的转换器 babel-standlone
,它内置了大量的插件,所以可以直接在浏览器中运行并编译特定标签(type=”text/babel” 的 script 标签)内的代码。
而在 Webpack 中,我们则可以使用 babel-loader
来完成语法的转换。
Polyfill
但是 Babel 并不是万能的,它默认只转换 JavaScript 语法,而不转换新的 API,比如 Promise、Set、Map 等全局对象, Array.from、Object.assign 等全局静态函数和 Array.prototype.includes 等 实例方法也不会被转码。为了解决这个问题,我们需要使用 polyfill。
polyfill 可以认为是 shim 的一种,和常规的 shim 不同的是,它致力于抹平不同浏览器之间的差异。
polyfill.io
1 | <script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script> |
我们可以在页面中引入这样一个 polyfill.io 提供的服务来实现最简单的 polyfill。服务器会根据用户浏览器的 UA 来判断对新特性的支持程度而返回不同的 polyfill 文件,如果用户的浏览器足够强大,那么这个服务将不返回任何内容,做到了一定意义上的按需加载。
但是它的缺点也很明显,这种方式并不能按照代码所用到的新特性按需进行 polyfill,而是会 polyfill 用户浏览器不支持的所有特性。
babel-plugins
babel 为我们提供了几乎所有新特性的插件,比如 @babel/transform-es2015-classes
、 @babel/transform-es2015-arrow-functions
等,使用方式如下:
1 | // .babelrc |
这样,babel 就可以帮我们转换指定的特性了,可以看一下下面的栗子:
1 | const foo = (a, b) => { |
可以看到,@babel/plugin-transform-object-assign 使用 ES5 自己实现了一份 Object.assign 并插入到了我们的代码之前。
但是这样做有两个问题,一是在不同的模块中如果都使用了 Object.assign,它可能在每个使用到的模块中都被实现一次并将这些 helpers 代码添加到模块顶部,会大幅增加构建包的体积;另一个问题则是我们需要手动为我们用到的特性添加所有的插件。
在这个背景下, @babel/plugin-transform-runtime
应运而生。在 Babel 的插件配置中,我们只需要引入一个 @babel/plugin-transform-runtime ,它会自动检测我们代码中需要转换的部分并重写,例如 Object.assign 会被重写为 _extends 并引入,而重复的模块也会被抽离成一份。配置如下:
1 | // .babelrc |
和单独引入各个插件一样,transform-runtime 同样存在模块级别的限制,因此它也依然无法转换 Array.prototype.includes 这样的方法。
babel-polyfill
对于这种全局方法,Babel 提供了 babel-polyfill,我们可以这样引入:1
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.2.5/polyfill.js"></script>
显然,这样会全量引入所有的补丁,不论你的项目是否需要。
也可以这样使用 npm 包的形式加载:
1 | import '@babel/polyfill'; |
和使用 script 标签引入类似,全量引入后,这个仅有2行的原始文件,打包后的大小变成了 246K:1
2
3
4
5
6
7
8Build completed in 1.286s
Hash: 3103b6ec6e9cb29c304b
Version: webpack 4.29.6
Time: 1464ms
Built at: 2019-03-01 16:05:13
Asset Size Chunks Chunk Names
app.js 246 KiB app [emitted] [big] app
为了实现按需引入,我们需要依靠 @babel/env
,而在 Babel7 中,提供了 useBuiltIns: usage
这样一个新配置。结合配置如下:
1 | "presets": [ |
useBuiltIns 配置为 entry 时, 会根据指定的 targets 中的 browserlist 将polyfills拆分引入,仅引入有浏览器不支持的polyfill。而配置为 usage ,babel 则会检测代码中新特性等的使用情况,仅仅加载代码中用到的 polyfills,同时,不需要再手动 import '@babel/polyfill'
。browserlist 的配置可以在这里查看: https://browserl.ist/
重新打包,app.js 恢复了正常大小:1
2
3
4
5
6
7
8Build completed in 0.769s
Hash: 21afcbb413dad4242589
Version: webpack 4.29.6
Time: 946ms
Built at: 2019-03-01 16:13:23
Asset Size Chunks Chunk Names
app.js 22.8 KiB app [emitted] app
总结
对于常规项目来说,我们可以手动指定要兼容到的 targets,进行如下配置来保证尽可能的安全并且按需加载:
1 | { |
而如果在开发的是一个库,为了避免全局的污染,则只能使用 transform-runtime 的方案,这种方案不能解决 Array.prototype.includes 等实例方法的转换,如果库中有应用,需要在文档中特意提醒开发者注意。
展望
在HTML5中,有一个新 API module
,我们可以使用这个特性来检测浏览器对 ES6的支持程度,例如,支持 <script type="module">
的浏览器也支持 async、await、Class、Promise 等新特性。
因此,我们可以打包出2份 JavaScript 文件同时引入:
1 | <script type="module" src="app.js"></script> |
大部分现代浏览器都会自动识别 script 标签中的 type="module"
和 nomodule
属性,加载 app.js 忽略 app-legacy.js。
我们只需在 app-legacy.js 中添加 polyfill 即可,这样,在兼容低版本浏览器的同时,我们也将在现代浏览器中体验到巨大的性能提升。
参考
https://www.babeljs.cn/docs/usage/polyfill/
https://github.com/sorrycc/blog/issues/80
https://segmentfault.com/a/1190000010106158