[译]在生产环境中使用ES2015+代码

译者:supotjavascript

原文:philipwalton.com/articles/de…html

我最近交流过的前端开发人员都喜欢使用 async/awiatclasses、箭头函数这些新特性去编写他们的JavaScript代码。尽管全部的现代浏览器均可以运行ES2015+的代码而且原生支持上面提到的新特性,可是绝大多数开发人员仍是会把他们ES2015+的代码编译成ES5的格式,而且提供一份polyfill文件,使得不多一部分使用旧浏览器的用户可以正常访问页面。前端

这很糟糕。在理想的状况中,咱们不会发送没有必须的代码。java

对于JavaScript和DOM新的API,咱们能够在运行时去检测这些API的支持度,而后按需的去引入相应的polyfill。可是使用一些新的语法,这会很是棘手。浏览器在遇到未知的语法时,会致使解析错误,后面的代码将没法执行。node

虽然咱们目前没有针对特性检测新语法的解决方案,可是如今咱们有一种方案去检测ES2015的语法支持。webpack

解决方案就是<script type="module">git

大部分开发人员认为<script type="module">是加载ES模块的方式(这固然是对的),可是<script type="module">还有一个更加直接和实用的使用场景--加载ES2015+的JavaScript文件而且知道浏览器可以正确处理这些具备新特性的JavaScript文件。es6

换句话说,每个支持<script type="module">的浏览器也将支持绝大数你熟悉并喜欢的ES2015+特性。例如:github

  • 每一个支持<script type="module">的浏览器都支持async/await
  • 每一个支持<script type="module">的浏览器都支持Classes
  • 每一个支持<script type="module">的浏览器都支持箭头函数
  • 每一个支持<script type="module">的浏览器都支持fetchPromisesMapSet,以及更多的ES2015+特性

接下来要作的惟一一件事就是为不支持<script type="module">的浏览器提供一个回退方案。幸运的是,若是你如今已经给你的代码提供了ES5的版本,那你已经完成了这项工做。你如今须要作的就是为你的代码提供一个ES2015+的版本。web

接下来的部分将介绍如何实现这个功能,而且讨论发布ES2015+代码将如何改变咱们编写模块的方式。

实现

若是你已经在使用webpack或者rollup来打包生成你的代码,那么你应该继续这么作。

接下来,除了当前生成的build文件,你还须要生成第二份build文件,他们惟一的区别就是第二份文件不会编译为ES5的格式,而且再也不包含用不到的polyfill(好比Map和Set的polyfill文件)。

若是你正在使用babel-preset-env(你应该这样),那么第二步也很是简单。你所须要作的就是把目标浏览器列表改为仅支持<script type="module">的浏览器,Babel将不会转义那些目标浏览器已经支持的语法,配合babel-preset-env的一些配置项,也能够去掉一些已经支持的polyfill文件。

换句话说,它将输出ES2015+的代码而不是ES5的代码。

举个栗子,若是你正在使用webpack,而且入口文件是./path/to/main.mjs,那么当前你的ES5版本的配置项可能像这样(注意,我将输出文件命名为main.es5.js,由于它是ES5版本的代码):

module.exports = {
  entry: './path/to/main.mjs',
  output: {
    filename: 'main.es5.js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.m?js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  '> 1%',
                  'last 2 versions',
                  'Firefox ESR',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};
复制代码

要生成一份ES2015+的代码,你须要完成第二份配置单,将目标环境设置为支持<script type="module">的浏览器。它看起来多是下面的配置项(注意,这里使用.mjs做为扩展名,由于它是一个ES6的模块):

module.exports = {
  entry: './path/to/main.mjs',
  output: {
    filename: 'main.mjs',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.m?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',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};
复制代码

构建运行后,这两个配置将生成两个用于生产环境的build文件

  • main.mjs (ES2015+语法)
  • main.es5.js (ES5语法)

下一步是更新你的HTML模板文件,使得在支持ES6 模块语法的浏览器中能加载ES2015+的文件。你可使用<script type="module"><script nomodule>的组合:

<!-- 支持ES module 的浏览器将加载这个文件. -->
<script type="module" src="main.mjs"></script>

<!-- 不支持ES module的浏览器将加载这个文件(支持ES module的浏览器会忽略这个文件)-->
<script nomodule src="main.es5.js"></script>
复制代码

Note: 我已经更新了文章中的示例,将全部的模块的拓展名都改为了.mjs。由于这种作法比较新,因此,若是我没有指出使用它的时候会遇到的问题,这将会是个人失职:

  • 你的web服务器须要使用content-type: text/javascript来提供对.mjs文件的支持。若是你的现代浏览器没法加载.mjs文件,可能就是这个缘由形成的。

  • 若是你使用webpack和babel来构建你的项目,你须要对配置项作出一些修改,将正则中的/.js$/改为 /.m?js$/

  • webpack比较老的版本不会为.mjs文件生成sourcemap文件,可是已经在4.19.1的版本中修复了,请使用4.19.1以上的版本

重要的考虑因素

在绝大多数的状况下,这种方案"只是起做用",在实现落地这个方案以前,咱们须要了解一些如何去加载模块(.mjs文件)的细节信息:

  1. 模块文件的加载和<script defer>同样,这意味着这些模块只有在文档解析完以后才会执行。普通的js脚本在加载的时候会阻塞html的解析,可是加了defer之后,脚本的下载会和html解析并行执行。若是你的代码须要在此以前运行,最好将该代码拆分并单独加载。
  2. 模块老是以严格模式运行的,因此若是你须要在非严格模式下运行代码,须要把代码拆出来单独加载。
  3. module处理顶级的var和函数声明不一样于常规的js脚本。在常规的js脚本中,定义变量var foo = 'bar';,能够经过window.foo来访问这个变量,可是在一个模块中没法这么使用。请确保你的代码中没有依赖这种行为。

警告! 在Safari 10中不支持nomodule属性,可是你能够在全部<script nomodule>以前经过内联注入这段代码来解决这个问题。(Safari 11中已经修复这个问题)

一个能够运行的例子

我在github建立了一个仓库webpack-esnext-boilerplate,开发者能够经过这个例子来了解这个方案的具体实现。

在这个项目中,我有意包含了几个webpack的高级功能,由于我想证实这个方案是可用于生产环境的。这些高级功能包含了以下的的一些最佳实践:

由于我永远不会推荐我本身没有使用的东西,因此我已经用这个方案对这个博客网站已经了重构。若是你想了解更多信息,能够查看源码

若是你使用webpack以外的打包构建工具,这个改造的过程和上面介绍的不会有很大的出入。在这个示例中,我之因此选择使用webpack,是由于webpack是如今最流行的构建工具,并且它足够复杂。我想若是这个方案可以和webpack一块儿使用,那么它能够适用于其余任何的构建工具。

额外的投入真的值得吗?

在我看来,它绝对值得!它带来了巨大的提高。例如,下面是这个博客网站生成的两个版本文件大小的比较:

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

ES5的文件大小是ES2015+版本的两倍多(甚至是gzip压缩后)。

更大的文件,须要更长的时间去下载,而且须要更长的时间去解析和执行。两个版本的文件在个人博客网站中的实际效果,ES5的解析和执行时间也是ES2015+版本的两倍:

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

虽然这些文件的大小不是很大,解析/执行的时间也不是特别长,可是这只是一个博客网站,我不须要加载大量的脚本。对于大部分的网站来讲,状况并不是如此。你用的脚本越多,你使用ES2015+所得到的收益就越大。

若是你任然持怀疑态度,而且认为文件大小和执行时间的差别主要是由于ES5须要更多的polyfill文件而形成的,那么你并无彻底错误。可是不管好坏,引入大量的polyfill文件已是今天不少网站很是广泛的作法了。

HTTPArchive收集到的数据显示,Alexa排名最高的网站中,有85181个网站中包含babel-polyfill、core-js或者regenerator-runtime,而在六个月以前,这个数字是34588!

现实正在转变,包含polyfill正在迅速成为新的常态。不幸的是,这意味着数十亿的用户经过网络去下载数万亿个字节的代码,而这些浏览器原本是能够直接运行没有转义的ES2015+的代码的。

是时候发布ES2015的模块了

目前这个方案的主要问题是不少npm包的做者并不发布ES2015+的代码,而是发布了转义后的ES5的代码。

既然已经能够部署ES2015+的代码,那么是时候去改变它了。

我彻底明白这对眼前的将来提出了不少挑战。如今绝大多数的构建工具都会发布文档,而且建议全部的模块都是ES5的。这意味着,若是一个包的做者想npm发布一个ES2015+的代码,他们可能会破坏用户的构建任务,而且一般会致使一些混淆。

问题是绝大多数的开发者在使用babel时,会经过配置忽略node_modules里面的代码,不对node_modules里面的代码进行转义。可是若是使用ES2015+的代码进行发布,这就会产生问题。幸运的是,这个问题很容易修复。你只须要删除你配置项中的node_modules:

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

修改之后带来的问题是,babel将会转义因此node_modules里面的文件,构建速度会变慢。幸运的是,这个问题能够经过构建工具的本地缓存解决。

不管在ES2015+做为新的包发布标准的路上遇到何种困难,我认为全部的努力都是值得的。若是咱们做为包的做者,只将ES5的版本发布到npm上,那么咱们会强制包的使用者去使用体积更大、执行效率更低的代码。

经过发布ES2015的代码,咱们为开发者提供了一个选项,而且最终是全部人都会从中受益。

总结

虽然<script type="module">是为了在浏览器中加载使用模块,可是它能作的不只仅只有这些。

<script type="module">能够在浏览器中加载JavaScript文件,这为开发者提供了一个急需的方法,能够在支持模块的浏览器中使用一些新的特性。

经过和nomodule属性的配合,为咱们在生产环境中使用ES2015+代码提供了一个方案,咱们终于能够中止向那些不须要代码转义的浏览器发送转换后的代码了。

编写ES2015+的代码对于开发者来讲是一个胜利,部署ES2015+的代码对于用户来讲是一个胜利。

Further reading

相关文章
相关标签/搜索