自从 ESModule
成为标准实现以来,开发者们不断讨论在生产环境落地的可能性。
ESModule 带来的变化
直至当下,大部分开发者们仍习惯以 webpack
之类的 bund 工具来搭建开发环境,在几年以前这并没有什么不好,但随着项目越来越大,webpack
的一些缺点受到越来越多的吐槽,其中最大的槽点莫过于在开发环境 webpack
会将所有的代码打包成一个非常大的 bund,当项目达到数十万行时,这将是是一个非常耗时的过程,hot reload
也将变的非常缓慢。而在生产环境时,webpack
构建后的代码会远大于编写的代码,这些额外的代码有一部分是对用户来说无用的依赖管理代码,一些是为了兼容用户量不足10%的低版本浏览器 polyfill
。
自高版本浏览器支持 ESModule
以来,一些 nobund
构建工具如 Snowpack
、Vite
便利用该特性,直接跑 ESModule
,而无需像 Webpack 一样打包成一个 bund。

Snowpack vs Webpack
export const str = 'hello world';
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<script type="module">
import { str } from './main.js';
console.log(str); // hello world
</script>
</body>
</html>
以上就是一个最简单的实例代码。
当落地到开发环境时,nobund
工具先会将所有的用到的 npm
包转换为 ESModule
(Snowpack 3 版本支持「[stream import](https://www.snowpack.dev/posts/2021-01-13-snowpack-3-0)
」不会预先构建 npm
包),并放入特定文件夹中。它们启动的 dev server
会对代码按需编译,通常情况下,首次启动的时间在 50ms 以内,hot reload 也能控制在极短的时间。
在生产环境时,一些开发者认为由于「嵌套导入」的影响,将会比较大的影响性能,倾向于打包成一个 bundle。但是该种方式却不利于缓存(修改一行代码,也将会导致缓存失效),另外一些开发者更倾向于部署 ESModule
,对于 npm 中使用的包,也只是做一些 tree shaking
和转化成 ESModule
之类的简单处理,这能带来尽可能多的包被缓存,但是这也意味着你的代码需要更长时间被加载完成。
面对的问题
缓存与加载效率
正如上文所提及的,如何在缓存与加载效率之间取的平衡是一个令人头疼的问题,把第三方插件经过 tree shaking
后打包成一个公共的包,所编写的代码由 import()
拆分,似乎是一个不错的注意(Vite
所使用的方式),但是对于一个处于迭代中的项目来说,增加插件或者是升级插件会是一件比较频繁的事情。一些开发者则倾向于更细粒度地打包,通过 rollup
提供的 manualChunks
功能,把相关模块的包打包到一个 chunk
中:
manualChunks(id) {
if (id.includes('node_modules')) {
// The directory name following the last `node_modules`.
// Usually this is the package, but it could also be the scope.
const directories = id.split(path.sep);
const name = directories[directories.lastIndexOf('node_modules') + 1];
// Group react dependencies into a common "react" chunk.
// NOTE: This isn't strictly necessary for this app, but it's included
// as an example to show how to manually group common dependencies.
if (name.match(/^react/) || ['prop-types', 'scheduler'].includes(name)) {
return 'react';
}
// Group `tslib` and `dynamic-import-polyfill` into the default bundle.
// NOTE: This isn't strictly necessary for this app, but it's included
// to show how to manually keep deps in the default chunk.
if (name === 'tslib' || name === 'dynamic-import-polyfill') {
return;
}
// Otherwise just return the name.
return name;
}
}
使用该方式拆分包后,大部分的包都能被很好的缓存,但是加载的时间会稍长。
除此之外,社区出现了 http-import
即不打包的形式。所使用的包,都从 CDN 上拉取,缓存、转 ESM、依赖地狱的优化等都由 CDN 来处理。
<script type="module">
import lodash from 'https://cdn.skypack.dev/lodash';
</script>
当配合 import-maps
使用时,
<script type="importmap">
// 可由构建工具打包出一份 maps
{
"imports": {
"lodash/": "https://cdn.skypack.dev/lodash/"
}
}
</script>
<script type="module">
// 构建后的代码
import array from 'lodash/array';
// do things
</script>
不过该 proposal 正式使用时不知道是猴年马月了。
Snowpack
3.0 中 Streaming Imports
实际上也是用了这种形式:
// you do this:
import * as React from 'react';
// but get behavior like this:
import * as React from 'https://cdn.skypack.dev/react@17.0.1';
本地无须装包,当识别到 import
第三方包时,可以直接从 CDN 上拉取。
npm 包的规范性
由于一些历史原因,打包发布的 npm 包通常只含有 CommonJS
模块,并会在 package.json
中指定该模块的入口:
{
"main": "./index.js"
}
但是 ESModule
却不能加载 CommonJS
,需要通过一些必要的插件处理,如 Rollup
@rollup/plugin-commonjs
插件,会将 CommonJS
模块转化为 ESModule
模块。
对于一些 ESModule
的 npm 包,可以在 package.json
中使用 module
字段来标明该包的输出格式是 ESModule
:
{
"module": "./index.ems.js",
}
当使用 rollup
webpack
这类构建工具时,如果识别到有该字段,则会优先加载指定的 ESModule
模块,这也有利于它们进行 tree-sharking
之类的优化。
当 npm 包同时有 CommonJS
模块、ESModule
模块时,那么推荐的做法是:
// recommended: web + node.js package.json
{
"main": "./index.cjs.js",
"module": "./index.esm.js"
}
此外,可以添加 package.json
exports
字段,用作 Conditional exports
:
{
"main": "./index.cjs.js",
"module": "./index.esm.js",
"exports": {
"require": "./index.cjs.js",
"import": "./index.esm.js"
}
}
其他
- 对于不兼容 ESModule 的浏览器而言,有一个 fallback 的方案
<script type="module" src="module.mjs"></script>
<script nomodule src="fallback.js"></script>
- 高效预加载
ESModule
<!-- 可由构建工具生成 -->
<link rel="modulepreload" href="/modules/main.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-one.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-two.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-three.XXXX.mjs">
<!-- ... -->
<script type="module" src="/modules/main.XXXX.mjs"></script>
随着 No bound 构建工具的出现,ESModule
在生产环境的落地越来越简单。适用于 ESM 下一代 CDN 的发展,也让 ESModule
的落地有了更多的想象空间。相信在不久的未来,ESModule
将会成为主流,在开发体验及网站速度上,都会有较大幅度的提升。