下一代前端开发利器——Vite(原理源码解析)
大厂技术 坚持周更 精选好文
前言
Hi,大家好!
前段时间用Vue3
搭建项目时看到同时推出的Vite
,只当它是一个新打包工具或者vue-cli
的升级版,仍然选择了用Webpack
构建项目。最近看了尤雨溪在VueConf上的演讲视频:《Vue3 生态进展和计划》[1],感觉它确实解决了现阶段前端工程化的一些痛点,也能体会到尤雨溪对Vite
的重视和大力推广的决心,再加上Vue
本身的庞大用户基数,Vite
确实有可能成为下一代前端构建工具的突破口。
本文将讨论下Vite
出现的背景,解决的痛点,核心功能的实现,存在的意义和预期的未来。Vite
本身并不复杂。中文官方文档非常清晰简洁,建议大家使用前仔细读下文档。
大纲
背景 什么是Vite? 基本用法 实现原理 源码分析 优势与不足 与传统构建工具对比 兼容性 未来
背景
这里的背景介绍会从与Vite
紧密相关的两个概念的发展史说起,一个是JavaScript
的模块化标准,另一个是前端构建工具。
共存的模块化标准
为什么JavaScript
会有多种共存的模块化标准?因为js在设计之初并没有模块化的概念,随着前端业务复杂度不断提高,模块化越来越受到开发者的重视,社区开始涌现多种模块化解决方案,它们相互借鉴,也争议不断,形成多个派系,从CommonJS
开始,到ES6
正式推出ES Modules
规范结束,所有争论,终成历史,ES Modules
也成为前端重要的基础设施。
CommonJS:现主要用于Node.js(Node@13.2.0开始支持直接使用ES Module) AMD: require.js
依赖前置,市场存量不建议使用CMD: sea.js
就近执行,市场存量不建议使用ES Module:ES语言规范,标准,趋势,未来
对模块化发展史感兴趣的可以看下《前端模块化开发那点历史》@玉伯[2],而Vite
的核心正是依靠浏览器对ES Module规范的实现。
发展中的构建工具
近些年前端工程化发展迅速,各种构建工具层出不穷,目前Webpack
仍然占据统治地位,npm 每周下载量达到两千多万次。下面是我按 npm 发版时间线列出的开发者比较熟知的一些构建工具。
当前工程化痛点
现在常用的构建工具如Webpack
,主要是通过抓取-编译-构建整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给dev server
(开发服务器)。
Webpack
等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server
(开发服务器)性能遇到瓶颈:
缓慢的服务启动:大型项目中
dev server
启动时间达到几十秒甚至几分钟。缓慢的HMR热更新:即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。
缓慢的开发环境,大大降低了开发者的幸福感,在以上背景下Vite
应运而生。
什么是Vite?
基于esbuild与Rollup,依靠浏览器自身ESM编译功能, 实现极致开发体验的新一代构建工具!
概念
先介绍以下文中会经常提到的一些基础概念:
依赖:指开发不会变动的部分(npm包、UI组件库),esbuild进行预构建。 源码:浏览器不能直接执行的非js代码(.jsx、.css、.vue等),vite只在浏览器请求相关源码的时候进行转换,以提供ESM源码。
开发环境
利用浏览器原生的 ES Module
编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server
只提供轻量服务。浏览器执行ESM的 import
时,会向dev server
发起该模块的ajax
请求,服务器对源码做简单处理后返回给浏览器。Vite
中HMR是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块失活,使得无论应用大小如何,HMR 始终能保持快速更新。使用 esbuild
处理项目依赖,esbuild
使用go编写,比一般node.js
编写的编译器快几个数量级。
生产环境
集成 Rollup
打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制。
处理流程对比
Webpack
通过先将整个应用打包,再将打包后代码提供给dev server
,开发者才能开始开发。
Vite
直接将源码交给浏览器,实现dev server
秒开,浏览器显示页面需要相关模块时,再向dev server
发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。
基本用法
创建vite项目
$ 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 编译
esbuild
使用go编写,cpu密集下更具性能优势,编译速度更快,以下摘自官网的构建速度对比:
浏览器:“开始了吗?”
服务器:“已经结束了。”
开发者:“好快,好喜欢!!”
依赖预构建
模块化兼容:如开头背景所写,现仍共存多种模块化标准代码, Vite
在预构建阶段将依赖中各种其他模块化规范(CommonJS、UMD)转换 成ESM,以提供给浏览器。性能优化:npm包中大量的ESM代码,大量的 import
请求,会造成网络拥塞。Vite
使用esbuild
,将有大量内部模块的ESM关系转换成单个模块,以减少import
模块请求次数。
按需加载
服务器只在接受到import请求的时候,才会编译对应的文件,将ESM源码返回给浏览器,实现真正的按需加载。
缓存
HTTP缓存:充分利用 http
缓存做优化,依赖(不会变动的代码)部分用max-age,immutable强缓存,源码部分用304协商缓存,提升页面打开速度。文件系统缓存: Vite
在预构建阶段,将构建后的依赖缓存到node_modules/.vite
,相关配置更改时,或手动控制时才会重新构建,以提升预构建速度。
重写模块路径
浏览器import
只能引入相对/绝对路径,而开发代码经常使用npm
包名直接引入node_module
中的模块,需要做路径转换后交给浏览器。
es-module-lexer
扫描 import 语法magic-string
重写模块的引入路径
// 开发代码
import { createApp } from 'vue'
// 转换后
import { createApp } from '/node_modules/vue/dist/vue.js'
源码分析
与Webpack-dev-server
类似Vite
同样使用WebSocket
与客户端建立连接,实现热更新,源码实现基本可分为两部分,源码位置在:
vite/packages/vite/src/client
client(用于客户端)vite/packages/vite/src/node
server(用于开发服务器)
client 代码会在启动服务时注入到客户端,用于客户端对于WebSocket
消息的处理(如更新页面某个模块、刷新页面);server 代码是服务端逻辑,用于处理代码的构建与页面模块的请求。
简单看了下源码(vite@2.7.2),核心功能主要是以下几个方法(以下为源码截取,部分逻辑做了删减):
命令行启动服务 npm run dev
后,源码执行cli.ts
,调用createServer
方法,创建http服务,监听开发服务器端口。
// 源码位置 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()
}
createServer
方法的执行做了很多工作,如整合配置项、创建http服务(早期通过koa创建)、创建WebSocket
服务、创建源码的文件监听、插件执行、optimize优化等。下面注释中标出。
// 源码位置 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
}
使用chokidar[5]监听文件变化,绑定监听事件。
// 源码位置 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
通过ws[6]来创建 WebSocket
服务,用于监听到文件变化时触发热更新,向客户端发送消息。
// 源码位置 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)
})
}
}
}
在服务启动时会向浏览器注入代码,用于处理客户端接收到的 WebSocket
消息,如重新发起模块请求、刷新页面。
//源码位置 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, 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
}
}
}
H5-Dooring
优势
快!快!非常快!! 高度集成,开箱即用。 基于ESM急速热更新,无需打包编译。 基于 esbuild
的依赖预处理,比Webpack
等node编写的编译器快几个数量级。兼容 Rollup
庞大的插件机制,插件开发更简洁。不与 Vue
绑定,支持React
等其他框架,独立的构建工具。内置SSR支持。 天然支持TS。
不足
Vue
仍为第一优先支持,量身定做的编译插件,对React
的支持不如Vue
强大。虽然已经推出2.0正式版,已经可以用于正式线上生产,但目前市场上实践少。 生产环境集成 Rollup
打包,与开发环境最终执行的代码不一致。
与 webpack 对比
由于Vite
主打的是开发环境的极致体验,生产环境集成Rollup
,这里的对比主要是Webpack-dev-server
与Vite-dev-server
的对比:
到目前很长时间以来 Webpack
在前端工程领域占统治地位,Vite
推出以来备受关注,社区活跃,GitHub star 数量激增,目前达到37.4KWebpack
配置丰富使用极为灵活但上手成本高,Vite
开箱即用配置高度集成Webpack
启动服务需打包构建,速度慢,Vite
免编译可秒开Webpack
热更新需打包构建,速度慢,Vite
毫秒响应Webpack
成熟稳定、资源丰富、大量实践案例,Vite
实践较少Vite
使用esbuild
编译,构建速度比webpack
快几个数量级
兼容性
默认目标浏览器是在 script
标签上支持原生 ESM 和 原生 ESM 动态导入可使用官方插件 @vitejs/plugin-legacy
,转义成传统版本和相对应的polyfill
未来探索
传统构建工具性能已到瓶颈,主打开发体验的 Vite
,可能会受到欢迎。主流浏览器基本支持ESM,ESM将成为主流。 Vite
在Vue3.0
代替vue-cli
,作为官方脚手架,会大大提高使用量。Vite2.0
推出后,已可以在实际项目中使用Vite
。如果觉得直接使用 Vite
太冒险,又确实有dev server
速度慢的问题需要解决,可以尝试用Vite
单独搭建一套dev server
相关资源
官方插件
除了支持现有的Rollup
插件系统外,官方提供了四个最关键的插件
@vitejs/plugin-vue
提供 Vue3 单文件组件支持@vitejs/plugin-vue-jsx
提供 Vue3 JSX 支持(专用的 Babel 转换插件)@vitejs/plugin-react
提供完整的 React 支持@vitejs/plugin-legacy
为打包后的文件提供传统浏览器兼容性支持
UI组件库
Element UI[7]:支持 vite 引入
相关链接
Vite官网[8] Vue3 生态进展和计划-尤雨溪[9] Vite源码解析[10] Develop with Vite | Vite快速入门 - Anthony Fu • Vue北京聚会 Day 13[11]
参考资料
《Vue3 生态进展和计划》:https://www.yuque.com/vueconf/mkwv0c/xqyxix
[2]《前端模块化开发那点历史》:https://github.com/seajs/seajs/issues/588
[3]StackBlitz:https://vite.new/
[4]社区维护模板:https://github.com/vitejs/awesome-vite#templates
[5]chokidar:https://www.npmjs.com/package/chokidar
[6]ws:https://www.npmjs.com/package/ws
[7]Element UI:https://element-plus.gitee.io/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5
[8]Vite官网:https://cn.vitejs.dev/
[9]Vue3 生态进展和计划-尤雨溪:https://www.yuque.com/vueconf/mkwv0c/xqyxix
[10]Vite源码解析:http://vite.ssr-fc.com/
[11]Develop with Vite | Vite快速入门 - Anthony Fu • Vue北京聚会 Day 13:https://www.youtube.com/watch?v=xx8gEHet6n8