为什么 ES Module 的浏览器支持没有意义

为什么 ES Module 的浏览器支持没有意义

早在 2015 年 7 月,ES 2015 就已正式发布,一个崭新的 Module System 也随之而来。

随后,ES Module 凭借其简洁的语法和优良的特性(以及作为规范本身的优越性),迅速成为了 JavaScript 工程中的主流模块化方案。

然而时至今日,ES Module 仍未进入到当前版本的主流浏览器中(Safari 的 TP 版本已经实现)。从 ES 2015 发布至今,很多人都希望 ES Module 能够被浏览器原生实现,但是 ES Module 的浏览器支持真的有那么美好吗?并不见得。

ES Module 在浏览器中是否可实现?

是,也不是。

之所以不可实现,是因为 ECMAScript 规范 中并没有任何关于 Module Resolution 的设定(ECMAScript® 2016 Language Specification),或者说的好听些叫 Implementation Defined/Undefined Behavior。所以根据一个 specifier,例如 import './something' 中的 ./something,如何对应到一个 HTTP(或其它 Scheme)的 URI 是不确定的,而在 Web 中,这种不确定意味着完全不可用。并不是不能实现,而是因为没有错误的实现,所以也没有 “正确” 的实现。

那为什么又说可以实现呢?因为虽然 ECMAScript 没有定义,但是(WHATWG的)HTML 规范 把实现补上了。也就是说,虽然 ECMAScript 中的 ES Module 没有办法实现,但是 通过 HTML 中的 script[type=module] 引入的 ES Module 是有办法实现的,即便不具备跨端的通用性,但对于纯 Web 来说已经具备实现的可能性了。

PS:就概念来说,这仍然是 HTML 的实现,而非 ECMAScript 的实现,所以如果有浏览器支持情况的 Benchmark,这个部分应该算入 HTML 的支持情况,而非 ECMAScript 的支持情况。

ES Module 的浏览器支持是否能够避免构建?

是,也不是。

先不考虑 Minify 的需求,假设我们的目的只是将 ES Module 直接跑在浏览器里。

之所以说能够避免构建,是因为 HTML 规范中所定义的部分确实是可实现的;而之所以说不能避免构建,是因为它不仅和现有的 Existing Code 不兼容,甚至和现有的 Dependency Management 体系都不兼容。

HTML Standard 中我们可以知道,一个 Module(ECMAScript 中的称呼,以下统一使用该称呼)/Module Script(HTML 中的称呼)的 specifier 只可能以 ./something../something 而不可能是 something(其余不常用的场景例如完整 URI 暂不讨论),并且不具备后缀名的自动匹配,所以我们不能写 import './another',只能写 import './another.js';不能写 import 'lib' 只能写 import '../node_module/dist/lib.js'

通过对静态文件服务器的配置可不可以解决呢?只是一个 .js 后缀或许可以,但要是能够自动解析 package.json 提取 main / esnext:main / module 字段再根据相对路径查找文件,这显然超出静态文件服务器的职责了。

于是,虽然都是用的 ES Module,但现在已有的 ES Module 还是没法跑,得靠构建成另外一个样子的 ES Module 才能跑。

另外,真的按照这样写就能避免构建了么?当然也还是不行,至少非开发环境不行。难道要作死把 node_modules 直接以静态文件目录的形式暴露出去?显然不可能(当然也很难说不会有真的这样作死的人),所以仍然需要用工具把源码 resolve 一遍从而确定所有用到的文件部分。

当然,所以说只要既愿意使用完整的相对路径又不需要引用第三方依赖的话避免构建还是可行的。


ES Module 的浏览器支持是否会降低性能?

是,也没有不是的可能。

很多人误以为只要有了 HTTP/2,那直接请求多个小文件不会对性能产生太大影响。这句话基本是对的,但对 ES Module 完全不适用,ES Module 的机制就决定了不论是多大的带宽多高的并发都救不回来。

很多用过 ES Module 的人都接触过 Tree-Shaking 这个概念。当然,本文和这个技术没有关系,只是 Tree-Shaking 为什么要叫 Tree-Shaking 呢?因为基于 ES Module 的代码结构就是一个 Dependency Tree。

于是乎,我们如何知道我们需要用到哪些 Module 呢?很简单,先解析第一个 Module(可能是 inline 的),解析这个 Module,得到依赖的其它 Module(的 specifier),然后再请求这些 Module 并解析,得到它们的依赖,以此类推。简单的说就是在一定程度上,对 Module 的请求只可能顺序执行,不可能并发执行。所以即便拥有无限带宽,仅仅靠 延迟 * 深度 也能把性能拖垮。并且最为反人类的地方是,一个项目的 Code Splitting 做的越好,复用程度越高,性能也就越差。

又有人会说,这个可不可以用 HTTP/2 的 Server Push 来解决呢?每次请求一个 Module 就把它所依赖的 Module 都一起加到响应中?不过,这里又要超越静态文件服务器了哟,不仅要能够递归识别 Module 的依赖,还要考虑被请求多个 Module 时依赖树的去重哦,于是乎,要让这套环境工作,除了原有的 Static File Server、API Server、Render Server 外,又来了一个 JS File Server 了。

然而即便真的有这个假想的 JS File Server 也还是无法解决,Server Push 由于完全基于单向需求猜想,并不会真正基于客户端的实际需求进行处理。在请求某个文件后,如果这个文件有 100 个依赖(以及递归依赖),那么便会收到这 100 个文件的响应。但是可能其中 50 个文件是公共依赖,完全无需重复请求,但我们没有任何办法跳过这其中的 50 个已有文件。以至于在一个智能的推送服务器上,结果反而得到了更长的加载时间和无意义的重复内容。

归根到底,没有任何办法可以解决(目前浏览器原生支持的)ES Module 的性能问题,除非把依赖自行扁平化,但那样也就完全失去了使用 ES Module 的意义。

写在最后

不是所有先进的技术(ES Module)都适合所有场景(浏览器),也不是所有先进的技术(HTTP/2)都能解决所有现有的问题。

编辑于 2018-06-05

文章被以下专栏收录