生产环境中选择性的部署ES6+

来自Google工程师的一种更好的配置ES6+方法

TL;NR

通过检测浏览器是否支持<script type="module">让用户加载对应的JS文件,对现代浏览器用户提供ES6+代码的文件,对老版本用户提供编译成ES5并结合polyfill的文件。

起因

虽然现在大多数开发者逐渐在使用ES6以及ES7和ES8均提供的新特性,比如async/await, class和箭头函数等,不过为了满足那一部分没有升级的使用较老版本浏览器的用户,还是要将代码编译成ES5并使用一些polyfill文件,尽管很多现代浏览器都可以运行ES6+的代码并且本地也支持这些特性。

之前这位老哥想到了一种在运行时根据特性检测来选择性加载polyfill文件的方法

方法

当没有一个好的办法去特性检测新的语句时,可以通过检测是否支持ES6语句来进行特性检测。

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

说明

大多数开发者认为<script type="module">只是一个加载模块的用法而已(没有错),

换一个角度想,所有支持<script type="module">的浏览器也会支持大部分ES6+的特性,比如:

  • 所有支持<script type="module">的浏览器都支持async/await
  • 所有支持<script type="module">的浏览器都支持Class
  • 所有支持<script type="module">的浏览器都支持箭头函数
  • 所有支持<script type="module">的浏览器都支持fetch,Promise,Map,Set等等

知道了这个后,剩下要做的就是对那些不支持<script type="module">的浏览器加载回滚的方案,如果已有ES5版本的代码,只需要再打包一个ES6+版本的即可。

实现

若已经熟练使用webpack或rollup生成JS代码,那么请跟着往下做。

需要额外打包的文件与ES5版本的没什么大的不同,总之就是不需要编译成ES5并且不用加载polyfill了。

如果已经开始用babel-preset-env的话就很简单了,只需在列表中添加支持 <script type="module">的浏览器即可,Babel会自动识别并不对这些进行到ES5的转换,也就是说直接输出ES6+代码。

  1. 举个栗子,假如你的webpack主脚本的入口是./path/to/main.js,那么ES5版本的打包配置(注意,将这个打包文件命名为main-legacy)如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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',
    ],
    },
    }],
    ],
    },
    },
    }],
    },
    };

为了得到一个ES6+的版本需要另一份配置,将目标环境设置为支持<script type="module">的浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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(ES6+的语句)
  • main-legacy.js(ES5的语句)

那么下一步就是在html文件中根据浏览器对module的支持来选择性的加载ES6+的文件了,可以通过使用<script type="module"><script nomodule>的组合来实现:

1
2
3
4
5
6
<!-- 支持ES module的浏览器会加载这个文件 -->
<script type="module" src="main.js"></script>
<!-- 老式浏览器会加载这个文件 -->
<!-- 支持module的浏览器知道*不要*加载这个文件 -->
<script nomodule src="main-legacy.js"></script>

细节

这个方法虽然可以达到目的,不过在实现之前需要考虑一下模块加载的一些细节:

  1. 模块加载的行为类似<script defer>,它会在DOM树解析成功后加载(DOMContentLoaded事件),
  2. 模块所运行的代码是在严格模式下的,若有部分代码要在严格模式之外运行则需要分别加载。
  3. 模块中处理varfunction的行为与script有所不同。比如在script中var foo = 'bar'function foo() {…}可以通过window.foo访问,但在模块中并非如此,确保代码中没有这样的操作。

Demo

这位工程师专门写了一个项目来展示这种方法-webpack-esnext-boilerplate

效果

对作者博客源码编译后,两种脚本文件的尺寸如下:

版本 大小 (minified) 大小 (minified + gzipped)
ES2015+ (main.js) 80K 21K
ES5 (main-legacy.js) 175K 43K

可以看到,ES5的代码大小几乎是ES6+的两倍。较大的文件不仅需要更长的时间去下载,解析与执行也更费时。如下表,比较两个版本的解析与执行时间会发现老版本的会多花费一倍的时间(使用webpagetest.org在Moto G4上的测试)。

版本 解析/执行时间 (单独运行) 解析/执行时间 (平均)
ES2015+ (main.js) 184ms, 164ms, 166ms 172ms
ES5 (main-legacy.js) 389ms, 351ms, 360ms 367ms

这只是对一个博客源码进行的测试,加载的脚本并不是很多,当代码量增多时效果应会更加明显。

最后

Writing ES2015 code is a win for developers, and deploying ES2015 code is a win for users.

参考

Deploying ES2015+ Code in Production Today

文章目录
  1. 1. TL;NR
  2. 2. 起因
  3. 3. 方法
  4. 4. 说明
  5. 5. 实现
  6. 6. 细节
  7. 7. Demo
  8. 8. 效果
  9. 9. 最后
  10. 10. 参考
|