Vite 构建项目及实现原理
一、Vite 的构建及使用
1、优势 & 劣势
优势:
- 快速的冷启动;
- 即时的模块热更新;
- 真正的按需编译;
- 内置 SSR 支持。
劣势:
- 只能针对现代浏览器(ES6+)
- 与 CommonJS 模块不完全兼容;
- 对 React 的支持有限。
2、与 webpack 的区别
- webpack 启动服务需打包构建,速度没有 vite 块;
- webpack 热更新也需要打包构建,速度没有 vite 块;
- webpack 较为成熟,而且有大量实践案例,而 vite 的实践较少;
- vite 使用 ESbuild 编译,构建速度比 webpack 块。
3、项目搭建
# NPM
npm create vite@latest
# Yarn
yarn create vite
# PNPM
pnpm create vite
4、插件安装
举个例子,安装 @vitejs/plugin-legacy 为传统浏览器提供支持:
yarn add @vitejs/plugin-legacy -D
import { defineConfig } from 'vite'
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
legacy({
targets: ['defaults', 'not IE 11']
})
]
})
官方插件:
- @vitejs/plugin-vue:提供 Vue 3 单文件组件支持;
- @vitejs/plugin-vue-jsx:提供 Vue-JSX 支持;
- @vitejs/plugin-react 提供完整的 React 支持;
- @vitejs/plugin-legacy 为打包后的文件提供传统浏览器兼容性支持。
二、Vite 的实现原理
1、源码分析
与 webpack-dev-server 类似,Vite 同样使用 WebSocket
与客户端建立连接,实现热更新,源码实现基本可分为两部分:
- vite/packages/vite/src/client:在启动服务时注入到客户端,用于客户端对
WebSocket
消息的处理(如更新页面某个模块、刷新页面) - vite/packages/vite/src/node:服务端逻辑,用于处理代码的构建与页面模块的请求。
命令行启动服务 npm run dev
后,源码执行 cli.ts,调用 createServer
方法,创建 http 服务,监听开发服务器端口。
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/cli.ts
const { createServer } = await import('./server')
try {
const server = await createServer({
root,
base: options.base,
// ...
})
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
await server.listen()
createServer
方法的执行做了很多工作,如整合配置项、创建 http 服务、创建 WebSocket
服务、创建源码的文件监听、插件执行、optimize 优化等。下面注释中标出。
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/index.ts#L293:23
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// Vite 配置整合
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server
// 创建 http 服务
const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 创建 ws 服务
const ws = createWebSocketServer(httpServer, config, httpsOptions)
// 创建 watcher,设置代码文件监听
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
...watchOptions
}) as FSWatcher
// 创建 server 对象
const server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
ws,
moduleGraph,
listen,
...
}
// 文件监听变动,websocket 向前端通信
watcher.on('change', async (file) => {
...
handleHMRUpdate()
})
// 非常多的 middleware
middlewares.use(...)
// optimize
const runOptimize = async () => {...}
return server
}
其中,创建 watcher,设置代码文件监听用到 chokidar 来监听文件变化,绑定监听事件。
通过 createWebSocketServer 方法来创建 WebSocket
服务,用于监听到文件变化时触发热更新,向客户端发送消息。
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/ws.ts
export function createWebSocketServer(...) {
let wss: WebSocket
const hmr = isObject(config.server.hmr) && config.server.hmr
const wsServer = (hmr && hmr.server) || server
if (wsServer) {
wss = new WebSocket({ noServer: true })
wsServer.on('upgrade', (req, socket, head) => {
// 服务就绪
if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
wss.handleUpgrade(req, socket as Socket, head, (ws) => {
wss.emit('connection', ws, req)
})
}
})
} else {
// ...
}
// 服务准备就绪,就能在浏览器控制台看到熟悉的打印 [vite] connected.
wss.on('connection', (socket) => {
socket.send(JSON.stringify({ type: 'connected' }))
// ...
})
// 失败
wss.on('error', (e: Error & { code: string }) => {
// ...
})
// 返回 ws 对象
return {
on: wss.on.bind(wss),
off: wss.off.bind(wss),
// 向客户端发送信息
// 多个客户端同时触发
send(payload: HMRPayload) {
const stringified = JSON.stringify(payload)
wss.clients.forEach((client) => {
// readyState 1 means the connection is open
client.send(stringified)
})
}
}
}
在服务启动时会向浏览器注入代码,用于处理客户端接收到的 WebSocket
消息,如重新发起模块请求、刷新页面。
// https://github.com/vitejs/vite/blob/main/packages/vite/src/client/client.ts
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`)
break
case 'update':
notifyListeners('vite:beforeUpdate', payload)
// ...
break
case 'custom': {
notifyListeners(payload.event as CustomEventName<any>, payload.data)
// ...
break
}
case 'full-reload':
notifyListeners('vite:beforeFullReload', payload)
// ...
break
case 'prune':
notifyListeners('vite:beforePrune', payload)
// ...
break
case 'error': {
notifyListeners('vite:error', payload)
// ...
break
}
default: {
const check: never = payload
return check
}
}
}
2、Vite 核心原理
目前的打包工具实现热更新基本都是通过 WebSocket
创建浏览器和服务器的通信监听文件的改变。
Vite 的核心原理是利用浏览器对 ESM 的支持,当 import 模块时,发送 HTTP 请求去加载文件,并启动一个开发服务器拦截这些请求,在后端将项目中的文件进行分解与整合,然后以 ESM 的格式返回给浏览器,整个过程中没有对文件进行打包编译。同时 Vite 通过 chokidar
来监听文件系统的变更,只用对发生变更的模块重新加载,这样热更新速度不会因项目体积的增加而变慢。
而 webpack 则是先解析依赖、打包构建再启动开发服务器,Dev Server 必须等待所有模块构建完成,当修改了 bundle 模块中的一个子模块,整个 bundle 文件都会重新打包然后输出,这就导致了项目应用越大,启动时间越长。
3、Vite 热更新流程
Vite 整个热更新过程可以分成四步:
- 创建一个
WebSocket
服务端和 client 文件,启动服务; - 通过
chokidar
来监听文件变更; - 当代码变更后,服务端进行判断并推送到客户端;
- 客户端根据推送的信息执行不同操作的更新。
4、Vite 预编译原理
Vite 预编译之后,将文件缓存在 node_modules/.vite/
文件夹下,根据 package.json 的 dependencies 以及包管理器的 lockfile 文件的变更来决定是否需要重新执行预构建。
如果想强制让 Vite 重新预构建依赖,可以使用 --force 启动开发服务器,或者删掉 node_modules/.vite/
文件夹。