译者序
本文将介绍JS模块化;怎样在不经过打包的情况下直接在浏览器中使用模块化;以及Chrome团队在JS模块化的优化和普及上正在做的一些事情。
JS模块化
你可能用过命名空间、CommonJS或者AMD规范进行JS模块化,但所有的这些模块解决方案万变不离其宗:引入(import)其他模块,作为一个模块输出(export)。如果说命名空间、CommonJS、AMD都是野路子,那ES6的JS modules则是正规军,将模块化语法统一起来(一统江湖,千秋万代)。
exportconstfunction
importrepeatshout
default
default
模块脚本与常规脚本有所区别:
varfoo=42;foowindow.fooimportexport
正因为这些差异,模块脚本和传统脚本显然需要各自不同的解析方式。因此JS解析器需要标识出哪些脚本属于是模块类型的。
浏览器如何识别模块脚本
type=modulenomodule
译者注:亲测在IE7+到edge,oppo手机自带的浏览器都能够降级而执行fallback.js。不过加载fallback的同时,也会把index.mjs一并加载,而支持module的浏览器则不会加载fallback。
IE系列均会执行fallback.js
加载fallback的同时,也会把index.mjs一并加载
而支持module的浏览器则只会加载模块
有没想过另外一个好处:既然浏览器能够识别module,那它必然也能够支持ES67的其他特性,如箭头函数、async-await。你不需要为这些特性进行babel编译,现代浏览器跑着更小和最大部分未编译的模块化代码,而不兼容的则使用nomodule的降级代码。
浏览器加载方面的异同:模块脚本vs传统脚本
上面介绍了模块脚本和传统脚本在语言层面的异同,除此之外,在浏览器加载过程中也有所不同。
同样的模块脚本只会执行一次,而传统脚本会声明多次。
模块脚本跨域需要加跨域头
Access-Control-Allow-Origin:*
async属性对内联脚本有效
加了async属性会使得脚本在下载过程中不阻塞DOM渲染,而下载完成后立即执行,两个async脚本之间的执行时序不确定,执行时机也不确定,有可能在domContentLoaded之前或者之后。但这一属性对传统的内联脚本是无效的,而对模块的内联脚本却是有效的。
.mjs
.mjsContent-Type:text/javascript
.js.mjs.mjs
.mjs
模块资源标识符 - module specifier
在import一个模块时,后面的相对或绝对路径字符串称为module specifier或import specifier,也就是模块资源路径。
浏览器对于模块资源路径做了一些限制。不支持类似下面这种只有模块名或部分文件名的资源路径(称之为bare module specifiers)。这样的限制是为了以后浏览器在支持自定义模块加载器之后,加载器能够自行决定bare module specifiers的解析方式。
/./../
模块script默认是defer
defer
但这里想告诉你的是,模块脚本默认具备defer的并行功能,因此无需画蛇添足加上defer属性。还有不仅仅只有主模块与html解析并行,其他子模块也一样。
JS模块化的其他特性
import()
importimport()
import"import()
import()
import.meta
import.meta
import.meta.url
性能优化建议
继续使用打包工具
通过模块脚本,开发时我们可以无需再用webpack、Rollup、Parcel等打包工具就可以享受原生的模块化福利,在以下场景建议可以直接使用原生的模块脚本:
- 开发环境下
- 不超过100个模块且相对较浅的依赖层级关系(小于5)的小型web应用
然而,我们在性能瓶颈分析中发现,加载一个模块化库(大约300个模块),经过打包的性能数据要比未经过打包直接使用原生模块脚本的好。
importexportimportexport
我们的总体建议是继续使用打包工具进行上线前的模块打包处理。毕竟从某种程度上,打包可以帮助你尽可能减少代码体积,用户不必要加载无用的脚本,更有利于页面性能。
开发者工具的代码覆盖率检查能帮助你检测源码中是否存在无用代码。我们同时也建议通过代码分割对模块进行合理拆分,以及延迟加载非首屏关键路径的脚本。
打包与使用模块脚本的权衡取舍
通常在web开发领域,所有方案都有利弊,需要权衡取舍。与加载一个未经过代码拆分的打包脚本相比,使用模块脚本也许会降低首次加载性能(cold cache),但是可以提升用户再次加载(warm cache)的速度。比如对于总大小200KB的代码,在修改一个细颗粒化的模块之后,那么用户只需要更新有变更的代码,这总比重新加载所有代码(打包脚本)要强。
如果相对于首次访问体验来说,你更关注用户再次访问体验,并且你的应用不超过数百个细颗粒化模块的话,你不妨尝试下使用模块脚本,通过性能数据对比之后再做出最后的选择。
浏览器工程师们正努力提升模块脚本的性能,我们希望模块脚本以后能够适用于更多的应用场景。
使用细颗粒化的模块
尽可能让你的代码以细颗粒化的模块进行组织。当在开发时,每个模块最好不要输出过多的内容。
./util.mjsdroppluckzip
pluck
./util.js
pluckdropzip./pluck.mjs
import
此外,使用细颗粒化的模块也有助于对接未来的浏览器原生打包功能。
预加载模块
rel="modulepreload"rel="modulepreload"
采用HTTP/2协议
HTTP/2支持多路复用,多个请求及响应信息可以同时进行传输,这有助于提高模块树的加载效率。
Chrome团队还预研了服务器推送——另一个HTTP/2特性,是否能够作为部署高度模块化应用的一个可行方案。但结局令人失望,HTTP/2的服务器推送比想象中要难以应用,并且web服务器及浏览器的对其实现目前并没有针对高度模块化web应用进行优化。另一方面,服务器很难只推送未被缓存的资源。如果通过告知服务器完整的用户缓存状态来解决这个问题的话,又存在隐私泄露风险。
无论如何,采用HTTP/2协议吧!只要记住目前HTTP/2的服务器推送目前还不能作为一个好的解决方案。
目前的使用率
import()
JS Modules未来的发展
Chrome团队正在通过不同的方式,致力于提高基于JS modules的开发体验。下面列举其中的几种。
更高效、确定性更高的模块解析算法
我们提交了一版对于目前模块解析算法的优化。新算法目前已经被同时列入了HTML规范和ECMASciprt规范,并且已在Chrome 63版本中实现。希望这项优化能够在更多的浏览器中落地。
新算法更快更高效,旧算法在计算依赖图谱(dependency graph)大小的时间复杂度为O(n²),在Chrome中的实现也是一样。而新算法则提升至O(n)。
此外,新算法在报解析错误时更加准确。如果一个依赖图谱中有多个错误,那么基于旧算法,每次执行都会报不同的解析错误。这给开发调试带来不必要的困难。新算法则保证每次执行都会报相同的解析错误。
Worklets 和 web workers
Chrome实现了worklets,允许web开发者自定义那些在浏览器底层的硬编码逻辑。目前开发者可以将一个JS模块引入到渲染管道(rendering pipeline)或者音频处理管道。
PaintWorklet
AudioWorkletAnimationWorklet
LayoutWorklet
chrome://flags/#enable-experimental-web-platform-features
在shared workers和service workers传入模块脚本也即将支持。
包名映射表 - Package name maps
在nodejs/npm中,我们经常会通过它们的包名引入模块,比如:
根据现行的HTML规范,类似上述的包名写法(bare import specifiers)会抛出异常。我们提交的“包名映射表”提案将会支持上述写法(包括在生产环境)。该映射表(JSON格式)将帮助浏览器将包名转换为完整资源路径(full URLs)。
包名映射表目前仍处于提案阶段(proposal stage)。
Web packaging:浏览器原生打包
Chrome loading团队正在探索一种原生的web打包格式(下称为web packaging),作为一种新模式来分发web应用。web packaging的主要特性如下:
- Signed HTTP Exchanges:可以让浏览器信任某个HTTP请求对(request/response)确实是来自于所声明的源服务器。
- Bundled HTTP Exchanges:是多个请求对的集合,不要求当中的每个请求都进行签名(signed),只要携带某些元数据(metadata)用于描述如何将请求束作为一个整体来解析。
两者结合起来,这种web打包格式就能够将多个同源资源安全地整合到一个HTTP GET相应中。
市面上的打包工具如webpack、Rollup、Parcel,都会将多个模块最终打包成一个或少数几个bundle,这会导致源码中进行的模块拆分在上线后就丧失了它的意义。那么通过原生打包,浏览器可以将bundle反解成原样。
简单来说,你可以把一个HTTP请求对包(Bundled HTTP Exchange)理解为一个资源文件包,它可以通过目录表(manifest)随意访问,并且里面的资源能够被高效地缓存以及根据相对优先级的高低来标记。有了这个机制,原生模块能够提升开发调试的体验。当你在Chrome开发者工具查看资源时,浏览器会精准定位到原生的模块代码中,而不需要复杂的source-map。
Chrome已经实现了一部分提案(SignedExchanges),但是打包格式(bundling format)以及在高度模块化app中的应用仍在探索阶段。
Layered APIs
移植新的功能和API到浏览器中无可避免会带来持续性的维护成本以及运行成本。每一个新特性都会污染浏览器的命名空间,增加启动开销,并且也增大引入bug的可能性。Layered APIs的目的是以一种更具扩展性的方式通过浏览器来实现或移植一些高级API。而模块脚本是实现Layered APIs的一项关键技术。
- 由于模块是显式引入的,所以通过模块来引入layered APIs可实现按需使用(不会默认内置)。
- 模块的加载源可自定义,因此layered APIs实现了一套自动加载polyfill(当不支持时)的机制。
模块脚本和layered APIs如何协同运作,具体细节仍在制定中,但目前的协议如下:
virtual-scroller
译者:对于Layered APIs更多的中文介绍 https://zhuanlan.zhihu.com/p/37008246
此文已由腾讯云+社区在各渠道发布