基于 ES Module 构建

2,049 阅读4分钟

前言

随着项目日常的迭代,项目的体积不断变大,对开发人员影响较大的就是开发阶段了,业界主流使用的构建工具还是 webpack,日常需要启动本地服务开始调试和修改代码,导致项目反复编译,耗时巨大,严重影响了开发效率与体验。各种资源构建的优化手段数不胜数,都是开发同学的沉淀。

减少处理的资源数量基本上是最快提升构建效率的途径了,例如针对我们 MPA 的项目,直接提供配置入口,显示的指定构建页面来提升开发阶段效率。

随着 Vite、Snowpack 等基于 ES Module 的构建工具出现,在社区掀起了一波 ES Module 热潮,在 Snowpack 发布的时候,其团队更发布了一篇《A Future Without Webpack》的文章,文中表示大家可以尝试采用全新的打包方案。其官网介绍了两种打包方案的差异。
image.png
(来源 Snowpack 官网)

使用 ES Module

<body>
  <script type="module">
    import React from 'react';
    console.log(React);
  </script>
</body>

使用 ESM 的方式很简单,在 script 标签上声明 type="module" 即表示改标签内的脚本使用的是 ESM 模式。
在 caniuse 中也可以看到当前主流浏览器的支持情况。
image.png

构建原理

基于上图可以看到,在开发过程中, Snowpack 为项目使用 Unbundled 处理。每个文件只需要构建一次,然后永久缓存。当文件更改时,Snowpack 会重建该单个文件。无需因单个文件修改而整体打包导致浪费时间,通过热更新,在浏览器中进行即时刷新。

这就是为什么基于 ESM 的构建工具会比 webpack 打包构建快很多的原因了。当然,我们日常开发中可不仅仅只写 js,还会在源码中使用 jsx、tsx、less、图片等等,浏览器是无法直接使用这些资源的。

依赖处理

NPM 包主要使用模块语法(Common.js 或 CJS)发布,如果没有一些构建处理就无法在 Web 上运行。即使使用浏览器原生 ESM import 和 export 直接在浏览器中运行。这里以 Snowpack 举例。

  • Snowpack 会扫描你的项目以查找所有使用过的 npm 包。
  • Snowpack 从 node_modules 目录中读取这些已安装的依赖项。
  • Snowpack 将所有依赖项单独打包到单个 JavaScript 文件中。例如:react 与 react-dom 分别转换为react.js 和 react-dom.js。
  • 每个生成的文件都可以直接在浏览器中运行,并通过 ESM import 语句导入。
  • 由于依赖项很少更改,因此 Snowpack 很少需要构建它们。

Snowpack 将三方依赖的语句均转化为 /web_modules/*.js。

import React from 'react';

// =>

import React from '/web_modules/react.js';

图片引入

处理图片资源时,实际上是需要获得静态资源的 URI,Snowpack 内部首先将此类资源的路径进行改写

import logo from './logo.png';

//  =>

import logo from './logo.png.proxy.js';

而在 logo.png.proxy.js 文件中则可以默认导出对应的文件地址:

// logo.png.proxy.js

export default '/src/assets/logo.png';

样式引入

CSS 资源通常是经过处理之后编程 JS 模块,通过直接创建 style 标签插入页面就可以生效了。样式导入的改写规则同图片类似,区别上仅仅是生成 *.css.proxy.js 。

// code 便是 css 文件中读取的内容
const code = ${JSON.stringify(code)};
const styleEl = document.createElement("style");
const codeEl = document.createTextNode(code);
styleEl.type = 'text/css';
styleEl.appendChild(codeEl);
document.head.appendChild(styleEl);


热更新

业界的一些规范: Snowpack 联合 Preact、 Vue 提出了 ESM-HMR Spec。ESM-HMR 是用于基于 ESM 的开发环境的标准 HMR API。ESM-HMR 是为浏览器的原生模块系统构建的,因此它可以在任何基于 ESM 的开发环境中使用。
使用示例:

export let foo = 1;

if (import.meta.hot) {
  // 从开发服务器接收任何更新,并相应地更新。
  import.meta.hot.accept(({ module }) => {
    try {
      foo = module.foo;
    } catch (err) {
      // 如果在接受更新时遇到问题,请将其标记为无效(重新加载页面)。
      import.meta.hot.invalidate();
    }
  });
  // 或者,在加载新副本之前清除模块中的任何副作用。
  import.meta.hot.dispose(() => {
    /* ... */
  });
}

小结

基于以上构建原理的理解,基于 ESM 的构建模式基本问题基本分析完毕,目前这种构建方式的确是一种渐进式的方式,提升开发体验。生产环境阶段还需要通过 webpack 打包出 JS Bundle ,其中基于 webpack 的钩子、插件及 loader 极大的满足了开发者在生产阶段的各类定制需求。

参考