前段时间用 Vue3 搭建项目时看到同时推出的 Vite ,只当它是一个新打包工具或者 vue-cli 的升级版,仍然选择了用 Webpack 构建项目。最近看了尤雨溪在 VueConf 上的演讲视频:Vue3 生态进展和计划[1],感觉它确实解决了现阶段前端工程化的一些痛点,也能体会到尤雨溪对 Vite 的重视和大力推广的决心,再加上 Vue 本身的庞大用户基数, Vite 确实有可能成为下一代前端构建工具的突破口。
本文将讨论下 Vite 出现的背景,解决的痛点,核心功能的实现,存在的意义和预期的未来。 Vite 本身并不复杂。中文官方文档非常清晰简洁,建议大家使用前仔细读下文档。
这里的背景介绍会从与 Vite 紧密相关的两个概念的发展史说起,一个是 JavaScript 的模块化标准,另一个是前端构建工具。
为什么 JavaScript 会有多种共存的模块化标准?因为 js 在设计之初并没有模块化的概念,随着前端业务复杂度不断提高,模块化越来越受到开发者的重视,社区开始涌现多种模块化解决方案,它们相互借鉴,也争议不断,形成多个派系,从 CommonJS 开始,到 ES6 正式推出 ES Modules 规范结束,所有争论,终成历史, ES Modules 也成为前端重要的基础设施。
对模块化发展史感兴趣的可以看下《前端模块化开发那点历史》@玉伯[2],而 Vite 的核心正是依靠浏览器对 ES Module 规范的实现。
近些年前端工程化发展迅速,各种构建工具层出不穷,目前 Webpack 仍然占据统治地位,npm 每周下载量达到两千多万次。下面是我按 npm 发版时间线列出的开发者比较熟知的一些构建工具。

现在常用的构建工具如 Webpack ,主要是通过抓取-编译-构建整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给 dev server (开发服务器)。
Webpack 等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js 代码量呈指数增长,打包构建时间越来越久, dev server (开发服务器)性能遇到瓶颈:
缓慢的开发环境,大大降低了开发者的幸福感,在以上背景下 Vite 应运而生。
基于 esbuild 与 Rollup,依靠浏览器自身 ESM 编译功能, 实现极致开发体验的新一代构建工具!
先介绍以下文中会经常提到的一些基础概念:
Webpack 通过先将整个应用打包,再将打包后代码提供给 dev server ,开发者才能开始开发。

Vite 直接将源码交给浏览器,实现 dev server 秒开,浏览器显示页面需要相关模块时,再向 dev server 发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。

$ npm create vite@latest
Vite 内置 6 种常用模板与对应的 TS 版本,可满足前端大部分开发场景,可以点击下列表格中模板直接在 StackBlitz[3] 中在线试用,还有其他更多的 社区维护模板[4]可以使用。
| JavaScript | TypeScript |
|---|---|
| vanilla | vanilla-ts |
| vue | vue-ts |
| react | react-ts |
| preact | preact-ts |
| lit | lit-ts |
| svelte | svelte-ts |
{
"scripts": {
"dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`
"build": "vite build", // 为生产环境构建产物
"preview": "vite preview" // 本地预览生产构建产物
}
}
esbuild 使用 go 编写,cpu 密集下更具性能优势,编译速度更快,以下摘自官网的构建速度对比: 浏览器:“开始了吗?” 服务器:“已经结束了。” 开发者:“好快,好喜欢!!”

浏览器 import 只能引入相对/绝对路径,而开发代码经常使用 npm 包名直接引入 node_module 中的模块,需要做路径转换后交给浏览器。
// 开发代码
import { createApp } from 'vue'
// 转换后
import { createApp } from '/node_modules/vue/dist/vue.js'
与 Webpack-dev-server 类似 Vite 同样使用 WebSocket 与客户端建立连接,实现热更新,源码实现基本可分为两部分,源码位置在:
client 代码会在启动服务时注入到客户端,用于客户端对于 WebSocket 消息的处理(如更新页面某个模块、刷新页面);server 代码是服务端逻辑,用于处理代码的构建与页面模块的请求。
简单看了下源码(vite@2.7.2),核心功能主要是以下几个方法(以下为源码截取,部分逻辑做了删减):
// 源码位置 vite/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()
}
// 源码位置 vite/packages/vite/src/node/server/index.ts
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
}
// 源码位置 vite/packages/vite/src/node/server/index.ts
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
...watchOptions
}) as FSWatcher
// 源码位置 vite/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)
})
}
}
}
//源码位置 vite/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
}
}
}
由于 Vite 主打的是开发环境的极致体验,生产环境集成 Rollup ,这里的对比主要是 Webpack-dev-server 与 Vite-dev-server 的对比:

除了支持现有的 Rollup 插件系统外,官方提供了四个最关键的插件

