在生产环境配置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. 模块以不同的方式处理全局的varfunction声明。例如:
在脚本中var foo = 'bar'function foo() {…}等同于读取window.foo。但是在模块中就不是这种情况,请确保在你的代码中不会依赖这种行为。

示例

我创建了webpack-esnext-boilerplate,开发者也能来切身体验。

在这个模板中我有意的添加了 webpack 的最新特性,为了展示这个技术在实际场景中如何使用。下面包含了打包的最佳实践:

我从不推荐一些我没用的技术,更新的这篇博客也是用的这项技术。如果你想了解更多,可以检查我的源代码

如果你用了除 webpack 之外的其它打包工具,这个过程或多或少都是一样。我选择 webpack 作为示例,因为它是当前最受欢迎的打包工具,它也是最复杂的。我想既然该技术可以与 webpack 一起使用,那么它也可以用于其它场景。

这样做真的值得吗?

在我看来,确实值得!这样做节省是很可观的。例如,以下是博客中实际生成的代码的两个版本的总文件大小的比较:

版本大小(压缩)大小(压缩+ gzipped)
ES2015 +(main.js)80K21k
ES5(main-legacy.js175K43K

传统的 ES5 版本是 ES2015+版本的两倍多大小。

我们知道文件越大下载时间越长,而且解析和评估的时间也会更长。从我站点的两个版本比较,旧版本的代码解析和执行的时间花费了将近一倍(这些测试是使用wepagetest.org在 MoTo G4 上运行的):

版本解析/评估 时间 (单独运行)解析/评估 时间 (平均值)
ES2015+ (main.js)184ms, 164ms, 166ms172ms
ES5 (main-legacy.js)389ms, 351ms, 360ms367ms

虽然这些绝对文件的大小在解析、评估的时间不是很长,但要知道这只是一个博客,我不会在上面加载很多脚本。但是这个案例放在其他网站,有更多的脚本加载,你将看到使用 ES2015+带来的巨大收益。

如果你还有质疑,并认为文件大小和执行时间不同主要是由于需要更多的 polyfills 来支持传统环境,这样的的想法不完全没有道理。但是,无论好坏,这是当今的网站上普遍方式。

对 HTTPArchive 数据集的快速查询显示,Alexa 排名网站中有 85181 个在其生产中包含babel-polyfillcore-jsregenerator-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 代码对用户来说是一个胜利。

延伸阅读:

关于Fundebug

Fundebug专注于JavaScript、微信小程序、支付宝小程序线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了80亿+错误事件。欢迎大家免费试用

版权声明

转载时请注明作者 Fundebug以及本文地址:
https://blog.fundebug.com/2018/07/31/deploying_es2015+/

您的用户遇到BUG了吗?

体验Demo 免费使用