This website requires JavaScript.

生产环境落地 ESModule

2021.05.09 20:05 字数 5494 喜欢 15 评论 2

自从 ESModule 成为标准实现以来,开发者们不断讨论在生产环境落地的可能性。

ESModule 带来的变化

直至当下,大部分开发者们仍习惯以 webpack 之类的 bund 工具来搭建开发环境,在几年以前这并没有什么不好,但随着项目越来越大,webpack 的一些缺点受到越来越多的吐槽,其中最大的槽点莫过于在开发环境 webpack 会将所有的代码打包成一个非常大的 bund,当项目达到数十万行时,这将是是一个非常耗时的过程,hot reload 也将变的非常缓慢。而在生产环境时,webpack 构建后的代码会远大于编写的代码,这些额外的代码有一部分是对用户来说无用的依赖管理代码,一些是为了兼容用户量不足10%的低版本浏览器 polyfill

自高版本浏览器支持 ESModule 以来,一些 nobund 构建工具如 SnowpackVite 便利用该特性,直接跑 ESModule,而无需像 Webpack 一样打包成一个 bund。

https://www.snowpack.dev/img/snowpack-unbundled-example-3.png

Snowpack vs Webpack

export const str = 'hello world';
<!DOCTYPE html>
<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 将会成为主流,在开发体验及网站速度上,都会有较大幅度的提升。

参考

Contact

微信公众号

微信

相关推荐

暂无推荐文章