【译】如何在生产环境中部署ES2015+

avatar
UX @京东
原文链接: jdc.jd.com

原文:philipwalton.com/articles/de…

大部分前端开发人员热衷于使用新的 JavaScript 语言特性来书写 JS 代码,例如 async 、 await 、 classes 、 arrow functions 等。然而,尽管目前所有的前沿浏览器都能运行 ES2015+ 代码(译注:ES2015及俗称的ES6),自然也能够支持我刚刚列举的新特性,但是为了兼容占有小比例的低版本浏览器用户,大部分的开发者仍然使用 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">
     的浏览器也支持 asyncawait 函数。
  • 支持
    <script type="module">
     的浏览器也支持 Class 类
  • 支持
    <script type="module">
     的浏览器也支持 arrow functions
  • 支持
    <script type="module">
     的浏览器也支持 fetchPromisesMapSet 等更多 ES2015+ 语法。

因此,唯一需要做的就是为不支持

<script type="module">
 的浏览器提供一个降级方案。幸运的是,如果你正在开发一个 ES5 版本的代码,你其实已经完成了该工作。现在你所需要做的是考虑如何生成 ES2015+ 版本的代码!

本文接下来将介绍如何实现这个方法,并讨论对 ES2015+ 代码的处理过程对我们未来如何编写模块有何影响。

实现方式

如果你已经使用了 webpack 或者 rollup 这类模块打包工具来生成 JS 文件,那么你应该继续保持。

接下来,除了当前的代码包,你还需要生成类似于第一份的另外一份代码包。(该代码包使用了 ES2015+ 语法),唯一的不同是你不需要将其编译成 ES5 语法的代码,并且不需要引入 polyfills 插件。

如果你已经开始使用 babel-preset-env (实际上你应该使用该插件),第二个步骤将非常简单,你所需要做的事情就是使用支持

<script type="module">
 的浏览器,这样 babel 就会忽略没必要转化的语法。

换言之,这样操作之后就会输出 ES2015+ 语法代码,而不是 ES5 代码。

例如,假设你使用了 webpack 并且 JS 的入口文件是 ./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',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

一旦运行,这两个配置文件就会输出两个 JS 文件:

  • 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>
 标签前,在 HTML 中使用内联JavaScript代码片段(注意:这个插件已经安装在 Safari11 版本中了)。

注意事项

在大多数情况下,这种方法“仅仅是能够实现”,在实现该方法之前需要注意一些关于如何加载模块的细节:

  1. 模块的加载方式类似于
    <script defer>
     ,这意味着它们直到文档被解析之后才被执行,如果您的一些代码需要在此之前运行,最好将该代码拆分并单独加载。
  2. 模块总是在严格的模式下运行代码,因此,如果出于任何原因,您的代码需要在非严格模式下运行,那么它必须单独加载。
  3. 模块处理全局var和函数声明不同于 script 文件,例如,在 scipt 文件中, var foo = ‘bar’ 和 function foo() {…} 等同于读取 foo,但是在模块中就不是这种情况,请确保在你的代码中不会依赖这种行为。

实例

我创建了 webpack-esnext-boilerplate 项目,开发者通过这个实例可以看到该技术的真实实现。

为了展示这个技术在实际场景中如何使用的,我特意在该实例中包含了几个高级的 webpack 特性,如下所示:

因为我不会推荐一些连我自己都不会使用的技术,所以我也使用该技术来更新这篇文章。如果你想了解更多,可以点击这里 查看此篇博文的源码。

如果你没有使用 webpack 打包,而是使用了其他的打包工具,这个过程是类似的。我之所以在本文中使用了 webpack 打包工具作为样例,是因为 webpack 是目前最为流行且最为复杂的打包工具。因此,我认为既然该技术能够在webpack 下应用,则同样能够适用于其他的场景。

这真的是多此一举吗?

我认为,完全不是多此一举!这样做的节省是相当可观的。例如,下面这个表格显示了,本文章在使用这两种方法后生成的总文件大小。

Version Size (minified) Size (minified + gzipped)
ES2015+ (main.js) 80K 21K
ES5 (main-legacy.js) 175K 43K

从上表可以看出编译完的 ES5 的版本是 ES2015+ 版本大小的两倍多(这里是 gzipped 压缩后的文件)。我们知道文件越大,下载文件所花费的时间就越多,因此也需要花费更长的时间用来解析和评估代码。对比我网站上的这两个版本,老版本的代码解析和执行的时间同样花费了将近一倍(这些测试都是使用了webpagetest.org 在 Moto G4上运行的)。

Version Parse/eval time (individual runs) Parse/eval time (avg)
ES2015+ (main.js) 184ms, 164ms, 166ms 172ms
ES5 (main-legacy.js) 389ms, 351ms, 360ms 367ms

虽然这些文件大小和解析、执行代码时间不是特别长,但要意识到这仅仅是一篇博客,我没有加载很多 script 脚本。但对于大多数网站来说,情况就不是如此乐观了。加载的 script 脚本越多,你使用 ES2015+ 进行转换所获得的优化就愈加明显。

如果你仍然怀疑并且认为文件的大小和执行时间的差异主要是由于为了支持老版本浏览器而加载了大量的 polyfills ,这种想法并非完全没有道理。但不管怎样,加载 polyfills 是当今网络上非常普遍的做法。

对 HTTPArchive 数据集的快速查询显示 Alexa 排名靠前的网站,有 85,181个网站在其代码包引入了babel-polyfillcore-js, 或者regenerator-runtime。然而6个月前这个数量仅仅是34,588个。

实际上,转换和引入 polyfills 很快即将成为新的规范。令人遗憾的是,这意味着数以亿计的用户把数以兆计的不必要的字节传送到浏览器上,而这些浏览器本来就完全有能力运行未经转换的代码。

是时候开始发布 ES2015+ 编译的模块了!

目前这个技术的主要问题是大多数模块作者不发布 ES2015+ 版本的源码,他们发布的是被转换后的ES5版本。现在是时候改变这个现象了,因为现在部署 ES2015+ 代码是可行的。

我完全明白这在近期内会带来很多挑战。目前大多数的构建工具都会发布文档,而这些文档推荐配置所有的模块都是 ES5 。这意味着,如果模块作者开始发布 ES2015+ 源码到 npm ,这一行为可能会破坏一些用户的构建,并会引起混乱。

问题是,大多数使用 Babel 的开发人员都将其配置为在 node_modules 中,不进行任何转换,但是如果使用 ES2015+ 源码发布模块的话,这会是一个问题。幸运的是,解决方法很简单,您只需要从构建配置中删除 node_modules。

rules: [
  {
    test: /\.js$/,
    exclude: /node_modules/, // Remove this line
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['env']
      }
    }
  }
]

这样做的缺点是,如果像Babel这样的工具不得不在 node_modules 中开始对依赖项进行转换,除了本地依赖,构建将会更慢。幸运的是,这是一个可以在工具级别上使用持久的本地缓存解决的问题。

在 ES2015+ 成为新的模块发布标准的道路上,无论面对多大的阻碍,我们都值得为此而奋斗!因为作为模块作者的我们,如果仅仅是把 ES5 版本的代码发布到npm上,这将强迫用户不得不接受臃肿且执行缓慢的代码。我们给开发者提供了一个发布 ES2015+ 版本代码的选择,这将最终惠及到每个人。

结论

尽管

<script type="module">
 的设计初衷是在浏览器中加载 ES 模块(还有他们的依赖文件),但是其作用不应该仅仅局限于此。
<script type="module">
 很容易地加载一个 JavaScript 文件,这为开发人员提供了一种非常必要的方法——可以在浏览器中加载其支持的新语法代码文件。这与 nomodule 属性一起,提供了一种在生产中使用 ES2015+ 代码的方法,这样,我们终于可以不用再向浏览器发送冗余的代码了。

编写 ES2015+ 代码是开发人员的胜利,部署 ES2015+ 代码则是用户的胜利!

延伸阅读: