在生产环境配置ES2015+代码
摘要 :支持浏览器兼容ES2015+,提高用户体验
大多数web开发者使用JavaScript时喜欢使用最新的语言特性,比如:async、await、classes、箭头函数等。可是,尽管实际上现在所有最新版浏览器都能运行ES2015+代码并且支持我提到的这些特性,但开发者仍然使用polyfills将代码编译成ES5语法并打包,以便那些还在使用低版本浏览器的用户能够正常使用。
这点是很糟糕的。在理想情况下,我们无需传送不必要的代码。
用最新的JavaScript和DOM APIs,我们可以根据需求加载ployfills,因为我们可以在运行时使用特性检测它们的支持是否支持这些语法。但是随着一些新的JavaScript语法出现,,因为任何未知的语法都会导致解析错误,然后导致代码停止执行,所以单凭特性检测语法支持程度很棘手。
虽然我们目前针对新语法检测没有一个好的的解决方案,但现在有一种方式可以检测基本的ES2015语法支持。
解决方案是使用<script type="module">
。
大多数开发者认为<script type="module">
也是加载ES模型的一种方式(当然这是正确的),但是<script type="module">
也有更直接的功能,加载浏览器可处理的、使用ES2015+语法的JavaScript文件。
换句话说,每个支持<script type="module">
的浏览器也支持ES2015+特性。例如:
- 支持
<script type="module">
的浏览器也支持async、await - 支持
<script type="module">
的浏览器也支持Class类 - 支持
<script type="module">
的浏览器也支持箭头函数 - 支持
<script type="module">
的浏览器也支持fetch、Promise、Map、Set等
现在唯一需要做的是对于不支持<script type="module">
的浏览器做一个降级方案。如果你当前是ES5版本的代码,那么很幸运,你已经完成了这个工作。现在需要做的是生成ES2015+版本的代码。
接下来阐释如何实现这项技术,并讨论我们应该如何编写ES2015+代码。
实现方式
如果你已经在使用像webpack、rollup等打包JavaScript,你应该继续保持。
接下来,除了你当前的包,你将生成第二个包,和第一个方式一样。唯一不同的是你不需要转译为ES5并且你也不需要引入polyfills插件。
如果你已经在用babel-preset-env(你应该使用该插件),第二步是非常简单的。您所要做的就是使用支持<script type="module">
的浏览器,这样Babel将忽略不必要的转化的语法。
换句话说,它将输出ES2015+代码而不是ES5。
例如,如果你用webpack并且你的主脚本入口点是./path/to/main.js
,根据当前的配置,ES5版本应该是这样(注意:因为使用的是ES5语法,所有我命名为main-legacy
)
module.exports = { entry: { 'main-legacy': './path/to/main.js', }, output: { filename: '[name].js', path: path.resolve(__dirname, 'public'), }, module: { rules: [{ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['env', { modules: false, useBuiltIns: true, targets: { browsers: [ '> 1%', 'last 2 versions', 'Firefox ESR', ], }, }], ], }, }, }], },};
如果使用ES2015+版本,你需要按照第二种配置,该配置的使用环境是支持<script type="module">
的浏览器。配置如下:
module.exports = { entry: { 'main': './path/to/main.js', }, output: { filename: '[name].js', path: path.resolve(__dirname, 'public'), }, module: { rules: [{ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['env', { modules: false, useBuiltIns: true, targets: { browsers: [ 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15', ], }, }], ], }, }, }], },};
运行后,这两种配置会有两个为生产环境准备好的JavaScript文件:
- main.js(支持ES2015+语法)
- main-legacy.js(支持ES5语法)
下一步是修改你的HTML,使浏览器有条件的支持ES2015+模块。你能用<script type="module">
和<script nomodule>
的混合方式:
<!-- Browsers with ES module support load this file. --><script type="module" src="main.js"></script><!-- Older browsers load this file (and module-supporting --><!-- browsers know *not* to load this file). --><script nomodule src="main-legacy.js"></script>
警告:Safari 10 不支持 nomodule属性, 但是为了解决这一问题,你可以在使用<script nomodule>
前,使用内联JavaScript代码片段(注意:这个插件已经安装在Safari11版本中了)。
注意
大部分情况下,这个方法“仅仅是能够实现”,在实现方法之前需要注意一些关于如何加载模块的细节:
1. 像模块加载一样<script defer>
,这意味着在文档解析之后才能执行。如果有些代码需要在它之前运行,最好的方式是把代码分开并独立加载。
2. 模块运行总是用strict mode,如果出于某些原因你的代码不需要使用strict mode,最好分开加载。
3. 模块以不同的方式处理全局的var
和function
声明。例如:
在脚本中var foo = 'bar'
和function foo() {…}
等同于读取window.foo
。但是在模块中就不是这种情况,请确保在你的代码中不会依赖这种行为。
示例
我创建了webpack-esnext-boilerplate,开发者也能来切身体验。
在这个模板中我有意的添加了webpack的最新特性,为了展示这个技术在实际场景中如何使用。下面包含了打包的最佳实践:
我从不推荐一些我没用的技术,更新的这篇博客也是用的这项技术。如果你想了解更多,可以检查我的源代码。
如果你用了除webpack之外的其它打包工具,这个过程或多或少都是一样。我选择webpack作为示例,因为它是当前最受欢迎的打包工具,它也是最复杂的。我想既然该技术可以与webpack一起使用,那么它也可以用于其它场景。
这样做真的值得吗?
在我看来,确实值得!这样做节省是很可观的。例如,以下是博客中实际生成的代码的两个版本的总文件大小的比较:
版本 | 大小(压缩) | 大小(压缩+ gzipped) | ||
---|---|---|---|---|
ES2015 +(main.js) | 80K | 21k | ||
ES5(main-legacy.js | 175K | 43K |
传统的ES5版本是ES2015+版本的两倍多大小。
我们知道文件越大下载时间越长,而且解析和评估的时间也会更长。从我站点的两个版本比较,旧版本的代码解析和执行的时间花费了将近一倍(这些测试是使用wepagetest.org在MoTo G4上运行的):
版本 | 解析/评估 时间 (单独运行) | 解析/评估 时间 (平均值) | ||
---|---|---|---|---|
ES2015+ (main.js) | 184ms, 164ms, 166ms | 172ms | ||
ES5 (main-legacy.js) | 389ms, 351ms, 360ms | 367ms |
虽然这些绝对文件的大小在解析、评估的时间不是很长,但要知道这只是一个博客,我不会在上面加载很多脚本。但是这个案例放在其他网站,有更多的脚本加载,你将看到使用ES2015+带来的巨大收益。
如果你还有质疑,并认为文件大小和执行时间不同主要是由于需要更多的polyfills来支持传统环境,这样的的想法不完全没有道理。但是,无论好坏,这是当今的网站上普遍方式。
对HTTPArchive数据集的快速查询显示,Alexa排名网站中有85181个在其生产中包含babel-polyfill,core-js或regenerator-runtime。六个月前,这个数字是34588!
实际上,转换和引用polyfills正在迅速成为新的常态。不幸的是,这意味着数十亿用户通过网络将数万亿不必要的字节发送到本来可以不需要代码传输的浏览器上。
现在我们开始发布我们的ES2015
目前这种技术的主要问题是大多数模块作者不发布ES2015+版本的源码,他们发布了转换后的ES5版本。
既然可以部署ES2015+版本,我们就应该改变。
我完全明白,这对眼前来说提出了很多挑战。大多数生成工具发布文档,建议配置所有的模块都是ES5。这意味着如果模块作者开始向npm发布ES2015+源代码,他们可能会破坏一些用户的构建,并且通常会引起混淆。
问题是大多数开发人员使用Babel将其配置在node_modules中,不进行任何转换,但如果使用ES2015+源代码发布模块,则这是一个问题。幸运的是,修复很容易。只需要在构建配置中删除node_modules。
rules: [ { test: /\.js$/, exclude: /node_modules/, // Remove this line use: { loader: 'babel-loader', options: { presets: ['env'] } } }]
缺点是,如果node_modules除了本地依赖项之外,像Babel这样的工具必须开始转换依赖关系,那么构建将会变慢。幸运的是,这是一个可以在工具级别上使用持久的本地缓存解决的问题。
无论在ES2015+作为新模块发布标准的道路上我们可能面临什么困境,我们值得为此而奋斗。如果我们作为模块者,只将代码的ES5版本发布到npm,那么我们会强制用户使用臃肿且缓慢的代码。
通过发布ES2015,我们为开发人员提供了一个选择,并最终使每个人受益。
结论
虽然<script type="module">
的初衷是在浏览器中加载ES模块(及其依赖项)的机制,但它的目的不应该局限于此。
<script type="module">
将很容易加载一个JavaScript文件,这为开发人员提供了一种必要的方法,可以在支持它的浏览器中有条件的加载新功能。
这与nomodule
属性一起,为我们提供了一种在生产中使用ES2015+代码的方法,我们终于可以停止向不需要它的浏览器发送这么多的冗余代码。
编写ES2015代码对开发人员来说是一个胜利,部署ES2015代码对用户来说是一个胜利。
延伸阅读: