下一代前端開發(fā)利器——Vite(原理源碼解析)
本文作者是360奇舞團(tuán)前端開發(fā)工程師
大廠技術(shù) 高級前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級Node交流群
前言
Hi,大家好!
前段時(shí)間用Vue3搭建項(xiàng)目時(shí)看到同時(shí)推出的Vite,只當(dāng)它是一個(gè)新打包工具或者vue-cli的升級版,仍然選擇了用Webpack構(gòu)建項(xiàng)目。最近看了尤雨溪在VueConf上的演講視頻:《Vue3 生態(tài)進(jìn)展和計(jì)劃》[1],感覺它確實(shí)解決了現(xiàn)階段前端工程化的一些痛點(diǎn),也能體會(huì)到尤雨溪對Vite的重視和大力推廣的決心,再加上Vue本身的龐大用戶基數(shù),Vite確實(shí)有可能成為下一代前端構(gòu)建工具的突破口。
本文將討論下Vite出現(xiàn)的背景,解決的痛點(diǎn),核心功能的實(shí)現(xiàn),存在的意義和預(yù)期的未來。Vite本身并不復(fù)雜。中文官方文檔非常清晰簡潔,建議大家使用前仔細(xì)讀下文檔。
大綱
-
背景 -
什么是Vite? -
基本用法 -
實(shí)現(xiàn)原理 -
源碼分析 -
優(yōu)勢與不足 -
與傳統(tǒng)構(gòu)建工具對比 -
兼容性 -
未來
背景
這里的背景介紹會(huì)從與Vite緊密相關(guān)的兩個(gè)概念的發(fā)展史說起,一個(gè)是JavaScript的模塊化標(biāo)準(zhǔn),另一個(gè)是前端構(gòu)建工具。
共存的模塊化標(biāo)準(zhǔn)
為什么JavaScript會(huì)有多種共存的模塊化標(biāo)準(zhǔn)?因?yàn)閖s在設(shè)計(jì)之初并沒有模塊化的概念,隨著前端業(yè)務(wù)復(fù)雜度不斷提高,模塊化越來越受到開發(fā)者的重視,社區(qū)開始涌現(xiàn)多種模塊化解決方案,它們相互借鑒,也爭議不斷,形成多個(gè)派系,從CommonJS開始,到ES6正式推出ES Modules規(guī)范結(jié)束,所有爭論,終成歷史,ES Modules也成為前端重要的基礎(chǔ)設(shè)施。
-
CommonJS:現(xiàn)主要用于Node.js([email protected]開始支持直接使用ES Module) -
AMD: require.js依賴前置,市場存量不建議使用 -
CMD: sea.js就近執(zhí)行,市場存量不建議使用 -
ES Module:ES語言規(guī)范,標(biāo)準(zhǔn),趨勢,未來
對模塊化發(fā)展史感興趣的可以看下《前端模塊化開發(fā)那點(diǎn)歷史》@玉伯[2],而Vite的核心正是依靠瀏覽器對ES Module規(guī)范的實(shí)現(xiàn)。
發(fā)展中的構(gòu)建工具
近些年前端工程化發(fā)展迅速,各種構(gòu)建工具層出不窮,目前Webpack仍然占據(jù)統(tǒng)治地位,npm 每周下載量達(dá)到兩千多萬次。下面是我按 npm 發(fā)版時(shí)間線列出的開發(fā)者比較熟知的一些構(gòu)建工具。
當(dāng)前工程化痛點(diǎn)
現(xiàn)在常用的構(gòu)建工具如Webpack,主要是通過抓取-編譯-構(gòu)建整個(gè)應(yīng)用的代碼(也就是常說的打包過程),生成一份編譯、優(yōu)化后能良好兼容各個(gè)瀏覽器的的生產(chǎn)環(huán)境代碼。在開發(fā)環(huán)境流程也基本相同,需要先將整個(gè)應(yīng)用構(gòu)建打包后,再把打包后的代碼交給dev server(開發(fā)服務(wù)器)。
Webpack等構(gòu)建工具的誕生給前端開發(fā)帶來了極大的便利,但隨著前端業(yè)務(wù)的復(fù)雜化,js代碼量呈指數(shù)增長,打包構(gòu)建時(shí)間越來越久,dev server(開發(fā)服務(wù)器)性能遇到瓶頸:
-
緩慢的服務(wù)啟動(dòng): 大型項(xiàng)目中
dev server啟動(dòng)時(shí)間達(dá)到幾十秒甚至幾分鐘。 -
緩慢的HMR熱更新: 即使采用了 HMR 模式,其熱更新速度也會(huì)隨著應(yīng)用規(guī)模的增長而顯著下降,已達(dá)到性能瓶頸,無多少優(yōu)化空間。
緩慢的開發(fā)環(huán)境,大大降低了開發(fā)者的幸福感,在以上背景下Vite應(yīng)運(yùn)而生。
什么是Vite?
基于esbuild與Rollup,依靠瀏覽器自身ESM編譯功能, 實(shí)現(xiàn)極致開發(fā)體驗(yàn)的新一代構(gòu)建工具!
概念
先介紹以下文中會(huì)經(jīng)常提到的一些基礎(chǔ)概念:
-
依賴: 指開發(fā)不會(huì)變動(dòng)的部分(npm包、UI組件庫),esbuild進(jìn)行預(yù)構(gòu)建。 -
源碼: 瀏覽器不能直接執(zhí)行的非js代碼(.jsx、.css、.vue等),vite只在瀏覽器請求相關(guān)源碼的時(shí)候進(jìn)行轉(zhuǎn)換,以提供ESM源碼。
開發(fā)環(huán)境
-
利用瀏覽器原生的 ES Module編譯能力,省略費(fèi)時(shí)的編譯環(huán)節(jié),直給瀏覽器開發(fā)環(huán)境源碼,dev server只提供輕量服務(wù)。 -
瀏覽器執(zhí)行ESM的 import時(shí),會(huì)向dev server發(fā)起該模塊的ajax請求,服務(wù)器對源碼做簡單處理后返回給瀏覽器。 -
Vite中HMR是在原生 ESM 上執(zhí)行的。當(dāng)編輯一個(gè)文件時(shí),Vite 只需要精確地使已編輯的模塊失活,使得無論應(yīng)用大小如何,HMR 始終能保持快速更新。 -
使用 esbuild處理項(xiàng)目依賴,esbuild使用go編寫,比一般node.js編寫的編譯器快幾個(gè)數(shù)量級。
生產(chǎn)環(huán)境
-
集成 Rollup打包生產(chǎn)環(huán)境代碼,依賴其成熟穩(wěn)定的生態(tài)與更簡潔的插件機(jī)制。
處理流程對比
Webpack通過先將整個(gè)應(yīng)用打包,再將打包后代碼提供給dev server,開發(fā)者才能開始開發(fā)。
Vite直接將源碼交給瀏覽器,實(shí)現(xiàn)dev server秒開,瀏覽器顯示頁面需要相關(guān)模塊時(shí),再向dev server發(fā)起請求,服務(wù)器簡單處理后,將該模塊返回給瀏覽器,實(shí)現(xiàn)真正意義的按需加載。
基本用法
創(chuàng)建vite項(xiàng)目
$ npm create vite@latest
選取模板
Vite 內(nèi)置6種常用模板與對應(yīng)的TS版本,可滿足前端大部分開發(fā)場景,可以點(diǎn)擊下列表格中模板直接在 StackBlitz[3] 中在線試用,還有其他更多的 社區(qū)維護(hù)模板[4]可以使用。
| JavaScript | TypeScript |
|---|---|
| vanilla | vanilla-ts |
| vue | vue-ts |
| react | react-ts |
| preact | preact-ts |
| lit | lit-ts |
| svelte | svelte-ts |
啟動(dòng)
{
"scripts": {
"dev": "vite", // 啟動(dòng)開發(fā)服務(wù)器,別名:`vite dev`,`vite serve`
"build": "vite build", // 為生產(chǎn)環(huán)境構(gòu)建產(chǎn)物
"preview": "vite preview" // 本地預(yù)覽生產(chǎn)構(gòu)建產(chǎn)物
}
}
實(shí)現(xiàn)原理
ESbuild 編譯
esbuild 使用go編寫,cpu密集下更具性能優(yōu)勢,編譯速度更快,以下摘自官網(wǎng)的構(gòu)建速度對比:
瀏覽器:“開始了嗎?”
服務(wù)器:“已經(jīng)結(jié)束了?!?/strong>
開發(fā)者:“好快,好喜歡!!”
依賴預(yù)構(gòu)建
-
模塊化兼容: 如開頭背景所寫,現(xiàn)仍共存多種模塊化標(biāo)準(zhǔn)代碼, Vite在預(yù)構(gòu)建階段將依賴中各種其他模塊化規(guī)范(CommonJS、UMD)轉(zhuǎn)換 成ESM,以提供給瀏覽器。 -
性能優(yōu)化: npm包中大量的ESM代碼,大量的 import請求,會(huì)造成網(wǎng)絡(luò)擁塞。Vite使用esbuild,將有大量內(nèi)部模塊的ESM關(guān)系轉(zhuǎn)換成單個(gè)模塊,以減少import模塊請求次數(shù)。
按需加載
-
服務(wù)器只在接受到import請求的時(shí)候,才會(huì)編譯對應(yīng)的文件,將ESM源碼返回給瀏覽器,實(shí)現(xiàn)真正的按需加載。
緩存
-
HTTP緩存: 充分利用 http緩存做優(yōu)化,依賴(不會(huì)變動(dòng)的代碼)部分用max-age,immutable 強(qiáng)緩存,源碼部分用304協(xié)商緩存,提升頁面打開速度。 -
文件系統(tǒng)緩存: Vite在預(yù)構(gòu)建階段,將構(gòu)建后的依賴緩存到node_modules/.vite,相關(guān)配置更改時(shí),或手動(dòng)控制時(shí)才會(huì)重新構(gòu)建,以提升預(yù)構(gòu)建速度。
重寫模塊路徑
瀏覽器import只能引入相對/絕對路徑,而開發(fā)代碼經(jīng)常使用npm包名直接引入node_module中的模塊,需要做路徑轉(zhuǎn)換后交給瀏覽器。
-
es-module-lexer掃描 import 語法 -
magic-string重寫模塊的引入路徑
// 開發(fā)代碼
import { createApp } from 'vue'
// 轉(zhuǎn)換后
import { createApp } from '/node_modules/vue/dist/vue.js'
源碼分析
與Webpack-dev-server類似Vite同樣使用WebSocket與客戶端建立連接,實(shí)現(xiàn)熱更新,源碼實(shí)現(xiàn)基本可分為兩部分,源碼位置在:
-
vite/packages/vite/src/clientclient(用于客戶端) -
vite/packages/vite/src/nodeserver(用于開發(fā)服務(wù)器)
client 代碼會(huì)在啟動(dòng)服務(wù)時(shí)注入到客戶端,用于客戶端對于WebSocket消息的處理(如更新頁面某個(gè)模塊、刷新頁面);server 代碼是服務(wù)端邏輯,用于處理代碼的構(gòu)建與頁面模塊的請求。
簡單看了下源碼([email protected]),核心功能主要是以下幾個(gè)方法(以下為源碼截取,部分邏輯做了刪減):
-
命令行啟動(dòng)服務(wù) npm run dev后,源碼執(zhí)行cli.ts,調(diào)用createServer方法,創(chuàng)建http服務(wù),監(jiān)聽開發(fā)服務(wù)器端口。
// 源碼位置 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方法的執(zhí)行做了很多工作,如整合配置項(xiàng)、創(chuàng)建http服務(wù)(早期通過koa創(chuàng)建)、創(chuàng)建WebSocket服務(wù)、創(chuàng)建源碼的文件監(jiān)聽、插件執(zhí)行、optimize優(yōu)化等。下面注釋中標(biāo)出。
// 源碼位置 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
// 創(chuàng)建http服務(wù)
const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 創(chuàng)建ws服務(wù)
const ws = createWebSocketServer(httpServer, config, httpsOptions)
// 創(chuàng)建watcher,設(shè)置代碼文件監(jiān)聽
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
...watchOptions
}) as FSWatcher
// 創(chuàng)建server對象
const server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
ws,
moduleGraph,
listen,
...
}
// 文件監(jiān)聽變動(dòng),websocket向前端通信
watcher.on('change', async (file) => {
...
handleHMRUpdate()
})
// 非常多的 middleware
middlewares.use(...)
// optimize
const runOptimize = async () => {...}
return server
}
-
使用chokidar[5]監(jiān)聽文件變化,綁定監(jiān)聽事件。
// 源碼位置 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] 來創(chuàng)建 WebSocket服務(wù),用于監(jiān)聽到文件變化時(shí)觸發(fā)熱更新,向客戶端發(fā)送消息。
// 源碼位置 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) => {
// 服務(wù)就緒
if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
wss.handleUpgrade(req, socket as Socket, head, (ws) => {
wss.emit('connection', ws, req)
})
}
})
} else {
...
}
// 服務(wù)準(zhǔn)備就緒,就能在瀏覽器控制臺(tái)看到熟悉的打印 [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),
// 向客戶端發(fā)送信息
// 多個(gè)客戶端同時(shí)觸發(fā)
send(payload: HMRPayload) {
const stringified = JSON.stringify(payload)
wss.clients.forEach((client) => {
// readyState 1 means the connection is open
client.send(stringified)
})
}
}
}
-
在服務(wù)啟動(dòng)時(shí)會(huì)向?yàn)g覽器注入代碼,用于處理客戶端接收到的 WebSocket消息,如重新發(fā)起模塊請求、刷新頁面。
//源碼位置 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
}
}
}
優(yōu)勢
-
快!快!非??欤?! -
高度集成,開箱即用。 -
基于ESM急速熱更新,無需打包編譯。 -
基于 esbuild的依賴預(yù)處理,比Webpack等node編寫的編譯器快幾個(gè)數(shù)量級。 -
兼容 Rollup龐大的插件機(jī)制,插件開發(fā)更簡潔。 -
不與 Vue綁定,支持React等其他框架,獨(dú)立的構(gòu)建工具。 -
內(nèi)置SSR支持。 -
天然支持TS。
不足
-
Vue仍為第一優(yōu)先支持,量身定做的編譯插件,對React的支持不如Vue強(qiáng)大。 -
雖然已經(jīng)推出2.0正式版,已經(jīng)可以用于正式線上生產(chǎn),但目前市場上實(shí)踐少。 -
生產(chǎn)環(huán)境集成 Rollup打包,與開發(fā)環(huán)境最終執(zhí)行的代碼不一致。
與 webpack 對比
由于Vite主打的是開發(fā)環(huán)境的極致體驗(yàn),生產(chǎn)環(huán)境集成Rollup,這里的對比主要是Webpack-dev-server與Vite-dev-server的對比:
-
到目前很長時(shí)間以來 Webpack在前端工程領(lǐng)域占統(tǒng)治地位,Vite推出以來備受關(guān)注,社區(qū)活躍,GitHub star 數(shù)量激增,目前達(dá)到37.4K
-
Webpack配置豐富使用極為靈活但上手成本高,Vite開箱即用配置高度集成 -
Webpack啟動(dòng)服務(wù)需打包構(gòu)建,速度慢,Vite免編譯可秒開 -
Webpack熱更新需打包構(gòu)建,速度慢,Vite毫秒響應(yīng) -
Webpack成熟穩(wěn)定、資源豐富、大量實(shí)踐案例,Vite實(shí)踐較少 -
Vite使用esbuild編譯,構(gòu)建速度比webpack快幾個(gè)數(shù)量級
兼容性
-
默認(rèn)目標(biāo)瀏覽器是在 script標(biāo)簽上支持原生 ESM 和 原生 ESM 動(dòng)態(tài)導(dǎo)入 -
可使用官方插件 @vitejs/plugin-legacy,轉(zhuǎn)義成傳統(tǒng)版本和相對應(yīng)的polyfill
未來探索
-
傳統(tǒng)構(gòu)建工具性能已到瓶頸,主打開發(fā)體驗(yàn)的 Vite,可能會(huì)受到歡迎。 -
主流瀏覽器基本支持ESM,ESM將成為主流。 -
Vite在Vue3.0代替vue-cli,作為官方腳手架,會(huì)大大提高使用量。 -
Vite2.0推出后,已可以在實(shí)際項(xiàng)目中使用Vite。 -
如果覺得直接使用 Vite太冒險(xiǎn),又確實(shí)有dev server速度慢的問題需要解決,可以嘗試用Vite單獨(dú)搭建一套dev server
相關(guān)資源
官方插件
除了支持現(xiàn)有的Rollup插件系統(tǒng)外,官方提供了四個(gè)最關(guān)鍵的插件
-
@vitejs/plugin-vue提供 Vue3 單文件組件支持 -
@vitejs/plugin-vue-jsx提供 Vue3 JSX 支持(專用的 Babel 轉(zhuǎn)換插件) -
@vitejs/plugin-react提供完整的 React 支持 -
@vitejs/plugin-legacy為打包后的文件提供傳統(tǒng)瀏覽器兼容性支持
UI組件庫
-
Element UI[7]:支持 vite 引入
相關(guān)鏈接
-
Vite官網(wǎng)[8] -
Vue3 生態(tài)進(jìn)展和計(jì)劃-尤雨溪[9] -
Vite源碼解析[10] -
Develop with Vite | Vite快速入門 - Anthony Fu ? Vue北京聚會(huì) Day 13[11]
Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個(gè)小忙:
1. 點(diǎn)個(gè)「在看」,讓更多人也能看到這篇文章
2. 訂閱官方博客 www.inode.club 讓我們一起成長
點(diǎn)贊和在看就是最大的支持??
參考資料
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個(gè)小忙:
點(diǎn)贊和在看就是最大的支持??
《Vue3 生態(tài)進(jìn)展和計(jì)劃》: https://www.yuque.com/vueconf/mkwv0c/xqyxix
[2]《前端模塊化開發(fā)那點(diǎn)歷史》: https://github.com/seajs/seajs/issues/588
[3]StackBlitz: https://vite.new/
[4]社區(qū)維護(hù)模板: 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官網(wǎng): https://cn.vitejs.dev/
[9]Vue3 生態(tài)進(jìn)展和計(jì)劃-尤雨溪: https://www.yuque.com/vueconf/mkwv0c/xqyxix
[10]Vite源碼解析: http://vite.ssr-fc.com/
[11]Develop with Vite | Vite快速入門 - Anthony Fu ? Vue北京聚會(huì) Day 13: https://www.youtube.com/watch?v=xx8gEHet6n8
