Skip to content
大纲

vite

定义

一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  • 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。

  • 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。

双引擎架构

Esbuild - 性能利器

基于 Golang 开发的一款打包工具,相比传统的打包工具,主打性能优势,在构建速度上可以比传统工具快 10~100 倍。那么,它是如何达到这样超高的构建性能的呢?主要原因可以概括为 4 点。

  1. 使用 Golang 开发,构建逻辑代码直接被编译为原生机器码,而不用像 JS 一样先代码解析为字节码,然后转换为机器码,大大节省了程序运行时间。
  2. 多核并行。内部打包算法充分利用多核 CPU 优势,所有的步骤尽可能并行,这也是得益于 Go 当中多线程共享内存的优势。
  3. 从零造轮子。 几乎没有使用任何第三方库,所有逻辑自己编写,大到 AST 解析,小到字符串的操作,保证极致的代码性能。
  4. 高效的内存利用。Esbuild 中从头到尾尽可能地复用一份 AST 节点数据,而不用像 JS 打包工具中频繁地解析和传递 AST 数据(如 string -> TS -> JS -> string),造成内存的大量浪费。

作为 Vite 的双引擎之一,Esbuild 在很多关键的构建阶段(如依赖预编译、TS 语法转译、代码压缩)让 Vite 获得了相当优异的性能。

  1. 依赖预构建——作为 boundle 工具

    vite 是提倡no-bundle的构建工具,单只是针对业务源码,第三方工具库还是需要打包的,但是是使用 Esbuild 来完成这一过程。为什么需要预构建呢?

    1. 将其他格式例如 commonjs 转化为 esm 格式,使其可以在浏览器上正常加载
    2. 将第三方库的分散文件合并到一起,减少引入时的 http 请求。

    依赖预构建是自动开启的,可以在 node_modules 的.vite 目录里看到,并且 vite 的 dev serve 会对依赖设置强缓存,过期时间 1 年。如果以下三个地方没有改变,则一直使用缓存文件。

    1. package.json 里的依赖
    2. lockfile
    3. optimizeDeps 里的内容
  2. 单文件编译——作为 ts 和 jsx 编译工具 作为 vite 插件,将 ts(x)和 js(x)单文件进行语法转译,这个插件功能在开发环境和生产环境都会执行,但其没有检查 ts 类型的功能,所以在 vite 构建脚本里有 tsc 命令,先做代码类型检查

  3. 压缩代码 共享 AST 和原生语言编写的压缩逻辑效率都比传统工具快极其多

Esbuild 打包工具缺点

  1. 不支持降级到 es5
  2. 不支持 const enum 等语法
  3. 没提供操作打包产物的接口、钩子
  4. 不支持拆包策略

Rollup - 构建基石

解决 esbuild 支持不了的功能,加上预加载、异步加载这些功能,并提供插件功能,自定义产出。vite 插件完全兼容 Rollup,反之不一定。

实现原理

关键点:

  1. 快速冷启动:No Buonld + Esbuild 预构建
  2. 热更新模块:基于 ESM 的 HMR 和浏览器的缓存策略
  3. 真正的按需加载:利用浏览器的 ESM 支持,实现真正的按需加载

实现方法

开发阶段

  1. 首先是基于 ESM 为核心,
    • 浏览器可以使用该模块,做 No Bounld 机制,
    • 自带按需加载的机制 import,只有使用到时才去加载
    • 同时会在本地会起一个 node 服务去拦截请求,并在响应头里添加强缓存、协商缓存的信息,在服务端使用自身插件和 Esbuild 对文件做 resolve load transform parse,然后以 ESM 模块返回给浏览器。做到了冷启动。
  2. 其次是 Esbuild 的预构建机制
    • 将其他格式的依赖转为 ESM 的
    • 然后将分散的 import,整合成一个,减少模块请求。
  3. 然后是实现热更新 HMR,
    • 基于 EMS HMR 规范实现了自己的一套 HMR API,用来做模块更新、旧模块失效、删除这些操作。
    • 同时在服务中会创建依赖某块图,使用 chokidar 监听文件变化,监听到变化还是根据依赖图生产 HMR 边界模块,后续的更新操作只在 HMR 边界内生效。
    • 页面访问后在客户端使用 websocket 建立连接,根据接收到的信息,由浏览器去加载变化的文件,做模块的操作。
该阶段与 webpack 对比

优势:

  1. 更快的启动,利用 ESM 做 No_bounld,业务代码不编译,只做简单的分析转换。webpack 需要先构建再打包成 boundle
  2. 更快的热更新,还是利用了 ESM,只去更新边界内的模块,业务代码不编译,只做简单的分析转换。webpack 需要将相关依赖全部编译

劣势:

  1. 首屏很慢
    • 预构建
    • 需要转化为 ESM 模块,并且附带着大量的请求
  2. 懒加载慢
    • 需要转化为 ESM 模块,并且附带着大量的请求
  3. 生态不如 webpack

生产阶段

  1. Rollup 打包

插件机制

在开发阶段 vite 有部分 Hook 是和 Rollup 的 Hook 是通用的,再加上自己的独有 Hook;生产阶段则是和 Rollup 一样。

通用 Hook:

  • 服务器启动阶段:options 和 buildStart
  • 请求响应阶段:resolved, load, transform
  • 服务器关闭阶段:buildEnd, closeBundle

独有 Hook

  • config: 用来进一步修改配置。
  • configResolved: 用来记录最终的配置信息。
  • configureServer: 用来获取 Vite Dev Server 实例,添加中间件。
  • transformIndexHtml: 用来转换 HTML 的内容。
  • handleHotUpdate: 用来进行热更新模块的过滤,或者进行自定义的热更新处理。

Hook 执行顺序

  • 服务启动阶段: config、configResolved、options、configureServer、buildStart
  • 请求响应阶段: 如果是 html 文件,仅执行 transformIndexHtml 钩子;对于非 HTML 文件,则依次执行 resolveId、load 和 transform 钩子。相信大家学过 Rollup 的插件机制,已经对这三个钩子比较熟悉了。
  • 热更新阶段: 执行 handleHotUpdate 钩子。
  • 服务关闭阶段: 依次执行 buildEnd 和 closeBundle 钩子。

插件执行顺序

  • Alias (路径别名)相关的插件。
  • 带有 enforce: 'pre' 的用户插件。
  • Vite 核心插件。
  • 没有 enforce 值的用户插件,也叫普通插件。
  • Vite 生产环境构建用的插件。
  • 带有 enforce: 'post' 的用户插件。
  • Vite 后置构建插件(如压缩插件)。

与 webpack 的区别

vite 是双引擎架构,开发阶段和生产阶段涉及引擎使用不统一,所以要分开做对比

开发阶段

  1. 构建原理不一样
    • webpack 需要将所有依赖编译构建好标准 js 执行文件,再启动本地服务;
    • vite 业务代码构建,将第三方依赖预构建,页面访问时再去转换成 ESM 返回给浏览器执行,并带上浏览器缓存。
  2. HMR 实现不一样
    • webpack 监听到文件修改,构建成新模块,生成新的编译标识,然后旧就模块对比,确定更新部分,再通知浏览器根据标识获取到更新文件,浏览器执行文件,对模块进行替换更新删除;
    • vite 基于 ESM HDR 规范,对依赖分析出边界,生成 ESM 新模块,交给浏览器更新替换删。

生产阶段

使用 Rollup 打包

  1. treeshaking 的实现逻辑不一样
    • webpack,先收集所以模块的导出值,形成导出列表,放到依赖图对象里去,然后再递归遍历模块标记哪些导出值使用到了哪些没使用到,按不同方式构建出代码后,使用工具将未使用的代码删除。
    • rollup,在将模块生成 AST 后,分析当前模块时,会定义出哪些是使用,哪些是未使用的变量。生成代码写时也只会,去将使用的变量加入字符串生成当中。实现 treeshaking

Released under the MIT License.