原文:《Using JavaScript modules on the web》 https://developers.google.com/web/fundamentals/primers/modules

译者序

JS modules,即ES6的模块化特性,通过

那些支持 type=module的浏览器会忽略掉 nomodule的脚本,而不兼容也会优雅降级,执行fallback.js。

译者注:亲测在IE7+到edge,oppo手机自带的浏览器都能够降级而执行fallback.js。不过加载fallback的同时,也会把index.mjs一并加载,而支持module的浏览器则不会加载fallback。

22feea3f2733bee926f22b440277af5a.png

IE系列均会执行fallback.js

57416b3c9886308d9cd0e5cb0315f907.png

加载fallback的同时,也会把index.mjs一并加载

814df64f4da046d4967fe056543e52b9.png

而支持module的浏览器则只会加载模块

有没想过另外一个好处:既然浏览器能够识别module,那它必然也能够支持ES67的其他特性,如箭头函数、async-await。你不需要为这些特性进行babel编译,现代浏览器跑着更小和最大部分未编译的模块化代码,而不兼容的则使用nomodule的降级代码。

浏览器加载方面的异同:模块脚本vs传统脚本

上面介绍了模块脚本和传统脚本在语言层面的异同,除此之外,在浏览器加载过程中也有所不同。

同样的模块脚本只会执行一次,而传统脚本会声明多次。

模块脚本跨域需要加跨域头

模块脚本及其依赖是通过CORS来获取的,也就是说模块脚本一旦跨域就需要加上适当的返回头,比如 Access-Control-Allow-Origin:*。而众所周知,传统脚本则不需要(译者注:还记得传说中的JSONP吗)。

async属性对内联脚本有效

加了async属性会使得脚本在下载过程中不阻塞DOM渲染,而下载完成后立即执行,两个async脚本之间的执行时序不确定,执行时机也不确定,有可能在domContentLoaded之前或者之后。但这一属性对传统的内联脚本是无效的,而对模块的内联脚本却是有效的。

关于 .mjs文件后缀

你可能会对前面的 .mjs后缀感到好奇,但是在互联网的世界里,文件后缀并不重要,只要服务器下发的MIME类型( Content-Type:text/javascript)正确就可以。浏览器是通过script标签上的type属性来识别模块脚本的,而不是后缀名。

所以无论使用 .js还是 .mjs都是可以的。但是我们还是建议使用 .mjs,原因有两个:

  1. 在开发的时候,可以不需要看代码,通过后缀名非常直观地看出哪些是模块脚本。
  2. nodejs中,ES6的模块化特性仍在实验性阶段,而该特性只支持 .mjs后缀的脚本。

模块资源标识符 – module specifier

在import一个模块时,后面的相对或绝对路径字符串称为module specifier或import specifier,也就是模块资源路径。

import {shout} from './lib.mjs';// ^^^^^^^^^^^

浏览器对于模块资源路径做了一些限制。不支持类似下面这种只有模块名或部分文件名的资源路径(称之为bare module specifiers)。这样的限制是为了以后浏览器在支持自定义模块加载器之后,加载器能够自行决定bare module specifiers的解析方式。

// Not supported (yet):import {shout} from 'jquery';import {shout} from 'lib.mjs';import {shout} from 'modules/lib.mjs';

目前,模块资源路径必须是完整的URL,或者以 /, ./, ../开头的相对URL

// Supported:import {shout} from './lib.mjs';import {shout} from '../lib.mjs';import {shout} from '/modules/lib.mjs';import {shout} from 'https://simple.example/modules/lib.mjs';

模块script默认是defer

传统脚本的加载和解析会阻塞html的解析,可以通过添加 defer属性解决(让脚本加载和html解析并行)

42f65a2612f1e00b522c63da2b29a4f6.png

但这里想告诉你的是,模块脚本默认具备defer的并行功能,因此无需画蛇添足加上defer属性。还有不仅仅只有主模块与html解析并行,其他子模块也一样。

JS模块化的其他特性

动态引入: import()

我们之前仅仅用到了静态的 import,它需要在首屏就把全部模块资源都下载下来。但有时候按需加载或异步加载会更为合理,这有助于提高首次加载时间,而 import()可以用来解决这个问题。

不像静态 import只能用在

这对于有复杂依赖关系模块的应用尤为重要。没有 rel=”modulepreload”,浏览器需要发出多个HTTP请求来计算出整个依赖关系。而如果你把所有依赖模块通过 rel=”modulepreload”提前告诉浏览器,那么浏览器则无需再渐进式地去计算。

采用HTTP/2协议

HTTP/2支持多路复用,多个请求及响应信息可以同时进行传输,这有助于提高模块树的加载效率。

Chrome团队还预研了服务器推送——另一个HTTP/2特性,是否能够作为部署高度模块化应用的一个可行方案。但结局令人失望,HTTP/2的服务器推送比想象中要难以应用,并且web服务器及浏览器的对其实现目前并没有针对高度模块化web应用进行优化。另一方面,服务器很难只推送未被缓存的资源。如果通过告知服务器完整的用户缓存状态来解决这个问题的话,又存在隐私泄露风险。

无论如何,采用HTTP/2协议吧!只要记住目前HTTP/2的服务器推送目前还不能作为一个好的解决方案。

目前的使用率

JS modules正在缓慢地被接纳使用。我们的使用统计显示只有0.08%(不包括动态 import()或者worklets)的页面目前使用了

这个模块脚本引入了 virtual-scrollerAPI,如果浏览器支持则会直接读取内置layered APIs集合(std:virtual-scroller),反之则网络加载对应的polyfill。

译者:对于Layered APIs更多的中文介绍 https://zhuanlan.zhihu.com/p/37008246