Vite的原理
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
背景
这里的背景介绍会从与Vite紧密相关的两个概念的发展史说起一个是JavaScript的模块化标准另一个是前端构建工具。
共存的模块化标准
为什么JavaScript会有多种共存的模块化标准因为js在设计之初并没有模块化的概念随着前端业务复杂度不断提高模块化越来越受到开发者的重视社区开始涌现多种模块化解决方案它们相互借鉴也争议不断形成多个派系从CommonJS开始到ES6正式推出ES Modules规范结束所有争论终成历史ES Modules也成为前端重要的基础设施。
CommonJS现主要用于Node.jsNode@13.2.0开始支持直接使用ES Module
AMDrequire.js 依赖前置市场存量不建议使用
CMDsea.js 就近执行市场存量不建议使用
ES ModuleES语言规范标准趋势未来
对模块化发展史感兴趣的可以看下《前端模块化开发那点历史》@玉伯而Vite的核心正是依靠浏览器对ES Module规范的实现。
当前工程化痛点
现在常用的构建工具如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 中在线试用还有其他更多的 社区维护模板可以使用。 |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监听文件变化绑定监听事件。
// 源码位置 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 来创建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<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
}
}
}
优势
快快非常快
高度集成开箱即用。
基于ESM急速热更新无需打包编译。
基于esbuild的依赖预处理比Webpack等node编写的编译器快几个数量级。
兼容Rollup庞大的插件机制插件开发更简洁。
不与Vue绑定支持React等其他框架独立的构建工具。
内置SSR支持。
天然支持TS。
不足
Vue仍为第一优先支持量身定做的编译插件对React的支持不如Vue强大。
虽然已经推出2.0正式版已经可以用于正式线上生产但目前市场上实践少。
生产环境集成Rollup打包与开发环境最终执行的代码不一致。