在现代浏览器中使用ES-Module

目录#

简介#

本文翻译自作者 Mark Brown 的 Using ES Modules in the Browser Today

本文将向您展示如何在浏览器中使用ES Modules。

前言#

一段时间以前,Javascript 还没有模块的概念,这对于在一个 Javascript 文件里直接引入另一个 Javascript 文件的内容来说,这样的想法是无法实现的。而随着网页应用的复杂度和规模不断增长,为浏览器编写Javascript代码变得越来越棘手了。

此前的一个常见的解决办法是在网页用通过 script 标签来加载任意脚本,然而这个方法会带来其特有的麻烦。举例来说,每个脚本执行会阻塞 HTTP 请求,这就会导致使用大量Javascript脚本的网页会变得卡顿和反应迟钝;同时,依赖管理也变得复杂,因为脚本加载的顺序也很重要。

ES6 (ES2015) 通过引入一个单一的、标准化的本地模块解决了这个问题(你可以点此了解更多ES6 Modules的内容)。但由于早期浏览器对ES6模块的支持度不高,开发者们开始使用模块加载器来将依赖项捆绑到 ES5 跨浏览器兼容文件中,这个办法同样也有它的问题和复杂程度。

但是,好消息是在如今的浏览器环境中,支持度变得越来越好,让我们看看如何在当今的浏览器中使用ES6 Modules吧!

当前的 ES Modules 是怎么用的?#

Safari,Chrome,Firefox 和 Edge 均支持ES6 Modules导入语法,用起来会像是这样子:

1
2
3
4
5
6
<script type="module">
import { tag } from './html.js'

const h1 = tag('h1', ' Hello Modules!')
document.body.appendChild(h1)
</script>
1
2
3
4
5
6
7
// html.js
export function tag (tag, text) {
const el = document.createElement(tag)
el.textContent = text

return el
}

或者作为外部脚本来获取:

1
<script type="module" src="app.js"></script>
1
2
3
4
5
// app.js
import { tag } from './html.js'

const h1 = tag('h1', ' Hello Modules!')
document.body.appendChild(h1)

只需要简单的把 type="module" 这一句添加到 script 标签里,浏览器将其识别为 ES Modules,并遍历所有的 import 路径,并且只下载和执行一次;

ES modules: Network graph showing loading

旧的浏览器不会执行未知类型的脚本,但可以使用 nomodule 属性添加一个后备脚本;

1
2
<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>

Requirements#

你需要一台服务器才能导入ES Modules 中 import 的内容,但它不能在file://协议上使用。你可以使用 npx serve 在当前目录中启动服务器以进行本地测试。

如果希望在不同的域中加载ES Modules,则需要启用CORS;

如果你有足够的信心在现今的生产环境中尝试这样做,你仍然需要为旧的浏览器创建单独的包。browseres -es-module-loader 是一个polyfill,它符合规范。但是,仍然不推荐用于生产。

1
2
3
<script nomodule src="https://unpkg.com/browser-es-module-loader/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader"></script>
<script type="module" src="./app.js"></script>

性能#

不要放弃你的构建工具,比如 Babel 和 Webpack,因为尽管浏览器在不断寻求实现优化资源获取的方法,在将来的ES Modules中仍然会存在性能和收益的缺陷。

为什么我们要将各个资源放在一起去获取#

今天,我们将 JavaScript 捆绑在一起以减少 HTTP请求 的数量,因为网络通常是加载web页面中最慢的部分。这在今天仍然是一个非常现实的问题,但是未来是光明的:ES Modules 配合 HTTP2 能够通过 服务器推送浏览器预加载 来同时获取多个资源。

预加载#

link rel=”modulepreload” 会快就能使用,它不会让浏览器逐个地解析脚本,而是生成一个可控制地网络流;一下举例说明:

如果不添加这个属性:

1
<script type="module" src="./app.js"></script>

那么网络流是这样的:

1
2
3
4
5
6
7
8
---> GET index.html
<---
---> GET app.js
<---
---> GET html.js
<---
---> GET lib.js
<---

而当你通过这个属性明确地告诉浏览器,页面需要 html.jslib.js,就像这样:

1
2
3
<link rel="modulepreload" href="html.js">
<link rel="modulepreload" href="lib.js">
<script type="module" src="./app.js"></script>
1
2
3
4
5
6
7
8
---> GET /index.html
<---
---> GET app.js
---> GET html.js
---> GET lib.js
<---
<---
<---

HTTP2#

与只能传递一个资源的HTTP1.1相比,HTTP2能够在单个响应中推送多个资源。 这将有助于使网络请求数量保持最少。

在我们的示例中,可以在一个请求中获取 index.htmlapp.jshtml.js

1
2
3
4
5
---> GET /index.html
<--- index.html
<--- app.js
<--- html.js
<--- lib.js

缓存#

传递多个较小的ES Modules可能有利于发挥缓存地优势,因为浏览器仅需要获取更新过的模块。如果使用大量脚本绑定在一个Module内,如果更新,就需要一次更新整个Modules;

async / defer#

ES modules 默认不会阻塞渲染进程,类似于 <script defer>,如果你的 Modules 不需要按照HTML中定义的顺序执行,你可以添加 async 属性令其加载后尽快执行;

Libraries#

许多流行的库现在已开始以 ES Modules 的形式发布,但是它们仍以构建工具为目标,而不是本文所介绍的这种直接导入的方式。

这个不起眼的import会触发640个请求:

1
2
3
<script type="module">
import _ from 'https://unpkg.com/lodash-es'
</script>

如果我们只导入我们需要的那一个函数呢?我们现在只有119个请求:

1
2
3
<script type="module">
import cloneDeep from 'https://unpkg.com/lodash-es/cloneDeep'
</script>

这是个小例子,用来例证目前为止 lodash-es 并不是直接用来给浏览器 import 的,要做到能够直接import,你仍然需要创建自己的 ES 库。

浏览器的支持#

如下表所示,浏览器对 ES Modules 的支持很好(并且一直在改进)。

现在是时候开始在浏览器中尝试ES Modules了。很快,你就可以在没有编译器或构建工具的情况下,在所有现代浏览器中使用ES Modules。