今天就在浏览器中使用 ES Modules
今天就在浏览器中使用 ES Modules
本文将会告诉你如何在浏览器中使用 ES modules。
直到不久之前, JavaScript 还没有模块的概念。曾经,无法在一个 JavaScript 文件中引用另一个 JavaScript 文件。随着应用的量级和复杂度的上升,使得在浏览器中写 JavaScript 代码变得很 trick 。
一种常见的解决方案是在页面中使用<script>
标签来加载任意的 JavaScript 文件。然而,这种方法也有自己的问题。例如,每个标签都会产生一个阻塞渲染的 HTTP 请求,这使得包含较多 JavaScript 代码的页面变得很缓慢。由于加载顺序和依赖关系相关,依赖管理也会变得很复杂。
ES6 (ES2015) 引入了一种单模块、原生 module 标准来解决这个问题。(你可以在这里阅读更多关于 ES6 modules 的内容。)然而,浏览器对 ES6 modules 的支持很差,人们开始使用模块加载器来将单个 ES5 跨浏览器兼容的文件打包。这种方法也带来了很多问题,增加了复杂度。
但是好消息来了,浏览器的支持开始变得更好了,我们来看看如何在现代浏览器中使用 ES6 模块。
当前的 ES Modules 概览
Safari, Chrome, Firefox and Edge 都支持 ES6 Modules import
语法,像这样:
<script type="module">
import { tag } from './html.js'
const h1 = tag('h1', '👋 Hello Modules!')
document.body.appendChild(h1)
</script>
// html.js
export function tag (tag, text) {
const el = document.createElement(tag)
el.textContent = text
return el
}
或者引入一个外部脚本:
<script type="module" src="app.js"></script>
// app.js
import { tag } from './html.js'
const h1 = tag('h1', '👋 Hello Modules!')
document.body.appendChild(h1)
简单地在你的 script 标签上添加 type="module"
,浏览器就会读取你的 ES Modules 。浏览器会根据引入的路径,下载并执行每个模块一次。

比较老的浏览器不会执行有未知的 type
属性的 script
标签脚本,但你可以使用 nomodule
属性定义一个回退的脚本:
<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>
需求
你需要一个服务器来处理 import
的文件请求,因为它不能使用 file://
标签。你可以使用 npx serve
在当前目录启动服务器来在本地调试。
如果你想从不同的域名来加载模块,你需要开启 CORS。
如果你很有勇气,在生产环境用 ES module ,你仍然需要为旧的浏览器创建一个打包好的文件。有一个遵循规范的 polyfill browser-es-module-loader。然而,并不建议在生产环境使用。
<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 的请求数量,因为网络经常是页面加载最慢的部分。这仍然是我们现在需要关注的,但未来很光明:使用 HTTP2 的 server push (服务端推送) , ES Modules 可以加载多个静态资源,浏览器也在实现预加载的能力。
预加载
link rel=”modulepreload” 快来到浏览器了。不用再在浏览器里面一个一个 import
模块,像这样瀑布式地处理请求...
<script type="module" src="./app.js"></script>
---> GET index.html
<---
---> GET app.js
<---
---> GET html.js
<---
---> GET lib.js
<---
你可以提前告诉浏览器这个页面需要html.js
和lib.js
,让瀑布在你的控制之下:
<link rel="modulepreload" href="html.js">
<link rel="modulepreload" href="lib.js">
<script type="module" src="./app.js"></script>
---> GET /index.html
<---
---> GET app.js
---> GET html.js
---> GET lib.js
<---
<---
<---
HTTP2 的 Server Push (服务端推送)
相比于 HTTP1.1 一次只能获取一个资源, HTTP2 可以在单个请求的相应中推送多个资源。这可以使得来来回回的请求数量保持到最少。
在我们的例子中,可以在单个请求中发送 index.html
、 app.js
和 html.js
三个文件:
---> GET /index.html
<--- index.html
<--- app.js
<--- html.js
<--- lib.js
缓存
提供多个比较小的 ES 模块可以通过缓存获取一些好处,因为浏览器只需要获取那些改变的模块。一个大的打包的 JS 文件的问题是,即使只改变了一行代码,整个打包的代码的缓存就失效了。
async / defer
ES 模块默认不阻塞页面的渲染,就像使用了 <script defer>
。如果你的模块的执行顺序不需要与在 HTML 文档中定义的顺序相同,你也可以添加 async 来让模块下载完成之后立即执行。
库
很多库开始以 ES modules 的形式发布,然而,它们仍然是为了打包而不是直接 import
。
这个微不足道的小 import
导致了640个瀑布式的请求:
<script type="module">
import _ from 'https://unpkg.com/lodash-es'
</script>
如果我们改进一下,只导入我们需要的函数呢?就只需要119个请求了:
<script type="module">
import cloneDeep from 'https://unpkg.com/lodash-es/cloneDeep'
</script>
这个例子只是说明 lodash-es 这个库并目前不适用于在浏览器中直接使用 import 。为了实现在浏览器中使用 import ,你仍然需要创建你自己的 ES modules 。
浏览器支持
下表显示了浏览器对原生import的支持还是很不错的(而且在变得越来越好)。

是时候在浏览器中使用 ES modules 了。要不了多久,你就可以不使用打包、转译工具,在所有现代浏览器中使用它了,如果你想的话。