vite
定义
一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
双引擎架构
Esbuild - 性能利器
基于 Golang 开发的一款打包工具,相比传统的打包工具,主打性能优势,在构建速度上可以比传统工具快 10~100 倍。那么,它是如何达到这样超高的构建性能的呢?主要原因可以概括为 4 点。
- 使用 Golang 开发,构建逻辑代码直接被编译为原生机器码,而不用像 JS 一样先代码解析为字节码,然后转换为机器码,大大节省了程序运行时间。
- 多核并行。内部打包算法充分利用多核 CPU 优势,所有的步骤尽可能并行,这也是得益于 Go 当中多线程共享内存的优势。
- 从零造轮子。 几乎没有使用任何第三方库,所有逻辑自己编写,大到 AST 解析,小到字符串的操作,保证极致的代码性能。
- 高效的内存利用。Esbuild 中从头到尾尽可能地复用一份 AST 节点数据,而不用像 JS 打包工具中频繁地解析和传递 AST 数据(如 string -> TS -> JS -> string),造成内存的大量浪费。
作为 Vite 的双引擎之一,Esbuild 在很多关键的构建阶段(如依赖预编译、TS 语法转译、代码压缩)让 Vite 获得了相当优异的性能。
依赖预构建——作为 boundle 工具
vite 是提倡
no-bundle的构建工具,单只是针对业务源码,第三方工具库还是需要打包的,但是是使用 Esbuild 来完成这一过程。为什么需要预构建呢?- 将其他格式例如 commonjs 转化为 esm 格式,使其可以在浏览器上正常加载
- 将第三方库的分散文件合并到一起,减少引入时的 http 请求。
依赖预构建是自动开启的,可以在 node_modules 的.vite 目录里看到,并且 vite 的 dev serve 会对依赖设置强缓存,过期时间 1 年。如果以下三个地方没有改变,则一直使用缓存文件。
- package.json 里的依赖
- lockfile
- optimizeDeps 里的内容
单文件编译——作为 ts 和 jsx 编译工具 作为 vite 插件,将 ts(x)和 js(x)单文件进行语法转译,这个插件功能在开发环境和生产环境都会执行,但其没有检查 ts 类型的功能,所以在 vite 构建脚本里有 tsc 命令,先做代码类型检查
压缩代码 共享 AST 和原生语言编写的压缩逻辑效率都比传统工具快极其多
Esbuild 打包工具缺点
- 不支持降级到 es5
- 不支持 const enum 等语法
- 没提供操作打包产物的接口、钩子
- 不支持拆包策略
Rollup - 构建基石
解决 esbuild 支持不了的功能,加上预加载、异步加载这些功能,并提供插件功能,自定义产出。vite 插件完全兼容 Rollup,反之不一定。
实现原理
关键点:
- 快速冷启动:No Buonld + Esbuild 预构建
- 热更新模块:基于 ESM 的 HMR 和浏览器的缓存策略
- 真正的按需加载:利用浏览器的 ESM 支持,实现真正的按需加载
实现方法
开发阶段
- 首先是基于 ESM 为核心,
- 浏览器可以使用该模块,做 No Bounld 机制,
- 自带按需加载的机制 import,只有使用到时才去加载
- 同时会在本地会起一个 node 服务去拦截请求,并在响应头里添加强缓存、协商缓存的信息,在服务端使用自身插件和 Esbuild 对文件做 resolve load transform parse,然后以 ESM 模块返回给浏览器。做到了冷启动。
- 其次是 Esbuild 的预构建机制
- 将其他格式的依赖转为 ESM 的
- 然后将分散的 import,整合成一个,减少模块请求。
- 然后是实现热更新 HMR,
- 基于 EMS HMR 规范实现了自己的一套 HMR API,用来做模块更新、旧模块失效、删除这些操作。
- 同时在服务中会创建依赖某块图,使用 chokidar 监听文件变化,监听到变化还是根据依赖图生产 HMR 边界模块,后续的更新操作只在 HMR 边界内生效。
- 页面访问后在客户端使用 websocket 建立连接,根据接收到的信息,由浏览器去加载变化的文件,做模块的操作。
该阶段与 webpack 对比
优势:
- 更快的启动,利用 ESM 做 No_bounld,业务代码不编译,只做简单的分析转换。webpack 需要先构建再打包成 boundle
- 更快的热更新,还是利用了 ESM,只去更新边界内的模块,业务代码不编译,只做简单的分析转换。webpack 需要将相关依赖全部编译
劣势:
- 首屏很慢
- 预构建
- 需要转化为 ESM 模块,并且附带着大量的请求
- 懒加载慢
- 需要转化为 ESM 模块,并且附带着大量的请求
- 生态不如 webpack
生产阶段
- 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 是双引擎架构,开发阶段和生产阶段涉及引擎使用不统一,所以要分开做对比
开发阶段
- 构建原理不一样
- webpack 需要将所有依赖编译构建好标准 js 执行文件,再启动本地服务;
- vite 业务代码构建,将第三方依赖预构建,页面访问时再去转换成 ESM 返回给浏览器执行,并带上浏览器缓存。
- HMR 实现不一样
- webpack 监听到文件修改,构建成新模块,生成新的编译标识,然后旧就模块对比,确定更新部分,再通知浏览器根据标识获取到更新文件,浏览器执行文件,对模块进行替换更新删除;
- vite 基于 ESM HDR 规范,对依赖分析出边界,生成 ESM 新模块,交给浏览器更新替换删。
生产阶段
使用 Rollup 打包
- treeshaking 的实现逻辑不一样
- webpack,先收集所以模块的导出值,形成导出列表,放到依赖图对象里去,然后再递归遍历模块标记哪些导出值使用到了哪些没使用到,按不同方式构建出代码后,使用工具将未使用的代码删除。
- rollup,在将模块生成 AST 后,分析当前模块时,会定义出哪些是使用,哪些是未使用的变量。生成代码写时也只会,去将使用的变量加入字符串生成当中。实现 treeshaking
JStar