Vite 构建原理
- 快速的冷启动:No Bundle + esbuild 预构建
- 即时的模块热更新: 基于 ESM 的 HMR,同时利用浏览器缓存策略提升速度
- 真正的按需加载: 利用浏览器的 ESM 支持,实现真正的按需加载
Vite 其核心原理是利用浏览器现在已经支持 ES6 这一特性,碰见 ES6 的 import 就会发送一个 HTTP 请求去加载文件,Vite 启动一个 Koa 服务器拦截这些请求,并在后端进行相应的处理 将项目中使用的文件通过简单的分解与整合,然后再 ESM 格式返回给浏览器。Vite 整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的 webpack 开发编译速度快出许多!
核心原理
基于 ESM 的 Dev server
在 Vite 出来之前,传统的 打包工具 如 webpack 是先解析依赖、打包构建再启动开发服务器,Dev server 必须等所有的模块构建完成,当我们修改了 bundle 模块中的一个子模块,整个 bundle 文件都会重新打包然后输出。项目应用越大,启动时间越长。
而 Vite 利用浏览器对 ESM 的支持,当 import 模块时,浏览器就会下载被导入的模块。先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。暂时没有用到的路由代码不会参与构建过程。项目应用增多,也不会影响其构建速度
基于 ESM 的 HMR 热更新
目前所有的打包工具实现热更新的思路都大同小异,主要是通过 websocket 创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作更新。
- Webpack: 重新编译,请求变更后模块的代码,客户端重新加载
- Vite: 请求变更的模块,再重新加载
Vite 通过 chokidar 来监听文件系统的变更,只用对发送生变更的模块重新加载,只需要精确的使相关模块与其临近的 HMR 边界连接失效即可,这样 HMR 更新速度就不会因为应用体积的增加而变慢。
而 Webpack 还要经历一次打包构建。所以 HMR 场景下,Vite 表现也比 Webpack 好
Vite 的整个热更新分为 4 步:
- 创建一个 websocket 服务端和 client 文件,启动服务
- 通过 chokidar 监听文件变更
- 当代码变更后,服务端进行判断并推送到客户端
- 客户端根据推送的信息执行不同操作的更新。
Vite本地启动时会创建一个WebSocket连接,同时去监听本地的文件变化
当用户修改了本地的文件时,WebSocket的服务端会拿到变化的文件的ID或者其他标识,并推送给客户端
客户端获取到变化的文件信息之后,便去请求最新的文件并刷新页面
客户端:websocket 通信和更新处理
当我们配置了热更新且不是 ssr 的时候,Vite 底层在处理 html的时候会把 HMR 相关的客户端代码写入到我们的代码中,当接收到服务端推送的消息,通过不同的消息类型做相应的处理,如(connected、update、custom…)在实际开发热更新中使用最频繁的是 update和full-reload 事件。
优化:浏览器的缓存策略提高响应速度:**Vite 还利用HTTP加速整个页面的重新加载。设置响应头使得依赖模块(dependency module)进行强缓存,而源码文件通过设置 304 Not Modified 而变成可依据条件而进行更新。
基于 esbuild 的依赖预编译优化
为什么需要预构建 ?
- 支持 common JS 依赖
- 上面提到 Vite 是基于浏览器原生支持 ESM 的能力实现的,但要求用户的代码模块必须是 ESM 模块,因此必须将 common JS 的文件提前处理,转成 ESM 模块并缓存如 node_modules/.vite
- 减少模块和请求数量
Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面性能加载。
为什么使用 esbuild
编译运行 VS 解释运行
大多数前端打包工具都是基于 JavaScript 实现的,大家都知道 JavaScript 是解释型语言,边运行边解释。而 ESBuild 采用 go 编写,该语言可以编译成原生代码,在编译的时候都将语言转为机器语言,在启动时直接执行,更具性能优势。多线程 VS 单线程
JavaScript 本质上是一门单线程语言,直到引入 webworker 之后才有可能在浏览器、Node 中实现多线程操作。
Go 天生具有多线程优势
- 对构建流程进行了优化 充分利用 CPU 资源
实现原理
Vite 预编译之后,将文件缓存在 node_modules/.vite 文件夹下。根据以下地方来决定是否需要重新执行预构建
- package.json 中 dependencies 发送变化
- 包管理器的 lockfile
如果想强制让 Vite 重新预构建依赖,可以使用 –force 启动开发服务器,或者直接删掉 node_modules/.vite 文件夹
基于 Rollup 的 Plugins
Vite 从 preat 的 WMR 中得到启发,将 Vite Plugins 继承 Rollup Plugins API,在其基础上进行一些扩展,同时 Vite 也基于 Rollup plugins 机制提供了强大的插件 API
vite 插件是什么
使用 vite 插件可以扩展 vite 能力,通过暴露一些构建打包过程的一些时机配合工具函数,让用户可以自定义写一些配置代码,执行在打包过程中。比如解析用户自定义的文件输入,在打包代码前转译代码
在实际的实现中,vite 只需要基于 rollup 设计的接口进行扩展。
Vite 钩子函数
- config: 可以在 Vite 被解析之前修改 Vite 的相关配置
- configResolved: 解析Vite 配置后调用,配置确认
- configureserverL: 主要用来转换 index.html,为 dev-server 添加自定义的中间件
- transformindexhmlt: 主要用来转换 index.html,钩子接收当前的 HTML 字符串和转换上下文
- handlehotupdate: 执行自定义 HMR 更新,可以通过 ws 往客户端发送自定义事件