遇到的困境

在浏览器支持 ES 模块之前,开发者没有以模块化的方式开发 JavaScript 的原生机制。这也是 “打包” 这个概念出现的原因:使用工具抓取、处理和链接我们的源码模块到文件中,使其可以运行在浏览器中。

webpack, Rollup 等打包工具的出现极大的提升了前端开发体验。JS 文件数量的指数级增加,使得。

Let browser do part of things.

Bundler based vs ESM based

bundler based: 启动 dev server 之前需要对所有文件都进行构建
vite: vite 将应用中的包划分为两个类别 dependeciessource code。类比 webpack 中的 dll 文件。

  • dependencies: 例如组件库文件 ECharts, Element-UI。通常依赖还会以不同的模块格式发布(ESM 或者 CommonJS)。vite 使用 esbuild 对依赖文件进行预打包。
  • source code: 源码文件中通常包含需要进行转换的非纯 JS 文件(JSX,CSS,Vue components),并且会被经常修改。同样的,并非所有的源代码都需要在同一时刻被加载(route-based code-splitting)。vite 使用原生的 ESM 来传送代码。这实际上是让浏览器接手打包器的部分工作。vite 只需要在浏览器发起请求的时候将源代码进行按需的转换。

    why bundle

  • 减少发起的请求数量

  • 减小构建产物的体积

    bundler based 遇到的问题

  • 启动开发服务器耗时很长

  • 即使有 HMR 加持,修改文件之后页面更新速度

原因:冷启动开发服务器的时候,需要将整个应用都进行构建(模块转换,构建依赖图等)才可以进行使用。对于 bundler based 方案,每当对文件进行修改之后都需要对整个应用文件重新做一次构建过程。应用的体积越大,这个更新速度就会越慢。即使有 HMR 的加持,其更新速度也会随着应用体积增大而变低。

vite 解决上述问题的方案

  • 使用浏览器原生的 ESM 模块方案
  • 使用 ESbuild 对文件进行构建

实施方案:将应用中的模块分割为依赖(dependencies)和源码(source code)两个部分。使用 esbuild 对不同的依赖进行预打包,包括不同模块格式的转换。而对于需要经常变更的源码,vite 通过原生 ESM 来提供服务。vite 只需要在浏览器对文件发起请求的时候,将相关的源码按需进行转换即可。

相较于 bundler based 方案,ESM 方案将模块的相互依赖关系的梳理交给浏览器来进行处理。他有两个特点:

  1. 构建复杂度非常低,修改任何组件都只需要做但文件编译,时间复杂度永远是 O(1)
  2. 借助 ESM 的能力,模块化交给浏览器端,不存在资源重复加载的问题,如果不涉及到 jsx 或者 ts 语法,甚至可以不用编译直接运行

因为在 vite 中 HMR 是通过 ESM 来实现的。当某个文件被编辑之后,vite 只需要精确的使相关模块与其临近的 HMR 边界连接失效即可,这样一来,HMR 更新速度就不会因为应用体积的增加而变慢。

vite 还利用了浏览器的缓存策略来提升页面重载时的响应速度。设置响应头使得依赖模块(dependency module)进行强缓存,而源码文件通过设置 304 Not Modified 而变成可依据条件而进行更新。

HMR 在 vite 中的实现方式

对外暴露 import.meta.hot 接口来进行调用。

  1. interface ImportMeta {
  2. readonly hot?: {
  3. readonly data: any
  4. accept(): void
  5. accept(cb: (mod: any) => void): void
  6. accept(dep: string, cb: (mod: any) => void): void
  7. accept(deps: string[], cb: (mods: any[]) => void): void
  8. dispose(cb: (data: any) => void): void
  9. decline(): void
  10. invalidate(): void
  11. on(event: string, cb: (...args: any[]) => void): void
  12. }
  13. }

内部实现是使用 websocket 建立客户端和服务端的连接,当文件发生改变的时候调用 ws.send() 方法传递信息给服务端(实现),服务端对 message 事件进行监听(封装实现)。

  1. socket.addEventListener('message', async ({ data }) => {
  2. handleMessage(JSON.parse(data))
  3. })