下一代前端開發(fā)利器——Vite(原理源碼解析)
大廠技術(shù)??堅持周更??精選好文
前言
Hi,大家好!
前段時間用Vue3搭建項目時看到同時推出的Vite,只當(dāng)它是一個新打包工具或者vue-cli的升級版,仍然選擇了用Webpack構(gòu)建項目。最近看了尤雨溪在VueConf上的演講視頻:《Vue3 生態(tài)進展和計劃》[1],感覺它確實解決了現(xiàn)階段前端工程化的一些痛點,也能體會到尤雨溪對Vite的重視和大力推廣的決心,再加上Vue本身的龐大用戶基數(shù),Vite確實有可能成為下一代前端構(gòu)建工具的突破口。
本文將討論下Vite出現(xiàn)的背景,解決的痛點,核心功能的實現(xiàn),存在的意義和預(yù)期的未來。Vite本身并不復(fù)雜。中文官方文檔非常清晰簡潔,建議大家使用前仔細(xì)讀下文檔。
大綱
背景 什么是Vite? 基本用法 實現(xiàn)原理 源碼分析 優(yōu)勢與不足 與傳統(tǒng)構(gòu)建工具對比 兼容性 未來
背景
這里的背景介紹會從與Vite緊密相關(guān)的兩個概念的發(fā)展史說起,一個是JavaScript的模塊化標(biāo)準(zhǔn),另一個是前端構(gòu)建工具。
共存的模塊化標(biāo)準(zhǔn)
為什么JavaScript會有多種共存的模塊化標(biāo)準(zhǔn)?因為js在設(shè)計之初并沒有模塊化的概念,隨著前端業(yè)務(wù)復(fù)雜度不斷提高,模塊化越來越受到開發(fā)者的重視,社區(qū)開始涌現(xiàn)多種模塊化解決方案,它們相互借鑒,也爭議不斷,形成多個派系,從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ā)那點歷史》@玉伯[2],而Vite的核心正是依靠瀏覽器對ES Module規(guī)范的實現(xiàn)。
發(fā)展中的構(gòu)建工具
近些年前端工程化發(fā)展迅速,各種構(gòu)建工具層出不窮,目前Webpack仍然占據(jù)統(tǒng)治地位,npm 每周下載量達到兩千多萬次。下面是我按 npm 發(fā)版時間線列出的開發(fā)者比較熟知的一些構(gòu)建工具。

當(dāng)前工程化痛點
現(xiàn)在常用的構(gòu)建工具如Webpack,主要是通過抓取-編譯-構(gòu)建整個應(yīng)用的代碼(也就是常說的打包過程),生成一份編譯、優(yōu)化后能良好兼容各個瀏覽器的的生產(chǎn)環(huán)境代碼。在開發(fā)環(huán)境流程也基本相同,需要先將整個應(yīng)用構(gòu)建打包后,再把打包后的代碼交給dev server(開發(fā)服務(wù)器)。
Webpack等構(gòu)建工具的誕生給前端開發(fā)帶來了極大的便利,但隨著前端業(yè)務(wù)的復(fù)雜化,js代碼量呈指數(shù)增長,打包構(gòu)建時間越來越久,dev server(開發(fā)服務(wù)器)性能遇到瓶頸:
緩慢的服務(wù)啟動:大型項目中
dev server啟動時間達到幾十秒甚至幾分鐘。緩慢的HMR熱更新:即使采用了 HMR 模式,其熱更新速度也會隨著應(yīng)用規(guī)模的增長而顯著下降,已達到性能瓶頸,無多少優(yōu)化空間。
緩慢的開發(fā)環(huán)境,大大降低了開發(fā)者的幸福感,在以上背景下Vite應(yīng)運而生。
什么是Vite?
基于esbuild與Rollup,依靠瀏覽器自身ESM編譯功能, 實現(xiàn)極致開發(fā)體驗的新一代構(gòu)建工具!
概念
先介紹以下文中會經(jīng)常提到的一些基礎(chǔ)概念:
依賴:指開發(fā)不會變動的部分(npm包、UI組件庫),esbuild進行預(yù)構(gòu)建。 源碼:瀏覽器不能直接執(zhí)行的非js代碼(.jsx、.css、.vue等),vite只在瀏覽器請求相關(guān)源碼的時候進行轉(zhuǎn)換,以提供ESM源碼。
開發(fā)環(huán)境
利用瀏覽器原生的 ES Module編譯能力,省略費時的編譯環(huán)節(jié),直給瀏覽器開發(fā)環(huán)境源碼,dev server只提供輕量服務(wù)。瀏覽器執(zhí)行ESM的 import時,會向dev server發(fā)起該模塊的ajax請求,服務(wù)器對源碼做簡單處理后返回給瀏覽器。Vite中HMR是在原生 ESM 上執(zhí)行的。當(dāng)編輯一個文件時,Vite 只需要精確地使已編輯的模塊失活,使得無論應(yīng)用大小如何,HMR 始終能保持快速更新。使用 esbuild處理項目依賴,esbuild使用go編寫,比一般node.js編寫的編譯器快幾個數(shù)量級。
生產(chǎn)環(huán)境
集成 Rollup打包生產(chǎn)環(huán)境代碼,依賴其成熟穩(wěn)定的生態(tài)與更簡潔的插件機制。
處理流程對比
Webpack通過先將整個應(yīng)用打包,再將打包后代碼提供給dev server,開發(fā)者才能開始開發(fā)。

Vite直接將源碼交給瀏覽器,實現(xiàn)dev server秒開,瀏覽器顯示頁面需要相關(guān)模塊時,再向dev server發(fā)起請求,服務(wù)器簡單處理后,將該模塊返回給瀏覽器,實現(xiàn)真正意義的按需加載。
基本用法
創(chuàng)建vite項目
$?npm?create?vite@latest
選取模板
Vite內(nèi)置6種常用模板與對應(yīng)的TS版本,可滿足前端大部分開發(fā)場景,可以點擊下列表格中模板直接在?StackBlitz[3]?中在線試用,還有其他更多的社區(qū)維護模板[4]可以使用。
| JavaScript | TypeScript |
|---|---|
| vanilla | vanilla-ts |
| vue | vue-ts |
| react | react-ts |
| preact | preact-ts |
| lit | lit-ts |
| svelte | svelte-ts |
啟動
{
??"scripts":?{
????"dev":?"vite",?//?啟動開發(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)物
??}
}
實現(xiàn)原理
ESbuild 編譯
esbuild使用go編寫,cpu密集下更具性能優(yōu)勢,編譯速度更快,以下摘自官網(wǎng)的構(gòu)建速度對比:
瀏覽器:“開始了嗎?”
服務(wù)器:“已經(jīng)結(jié)束了。”
開發(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請求,會造成網(wǎng)絡(luò)擁塞。Vite使用esbuild,將有大量內(nèi)部模塊的ESM關(guān)系轉(zhuǎn)換成單個模塊,以減少import模塊請求次數(shù)。
按需加載
服務(wù)器只在接受到import請求的時候,才會編譯對應(yīng)的文件,將ESM源碼返回給瀏覽器,實現(xiàn)真正的按需加載。
緩存
HTTP緩存:充分利用 http緩存做優(yōu)化,依賴(不會變動的代碼)部分用max-age,immutable強緩存,源碼部分用304協(xié)商緩存,提升頁面打開速度。文件系統(tǒng)緩存: Vite在預(yù)構(gòu)建階段,將構(gòu)建后的依賴緩存到node_modules/.vite,相關(guān)配置更改時,或手動控制時才會重新構(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與客戶端建立連接,實現(xiàn)熱更新,源碼實現(xiàn)基本可分為兩部分,源碼位置在:
vite/packages/vite/src/clientclient(用于客戶端)vite/packages/vite/src/nodeserver(用于開發(fā)服務(wù)器)
client 代碼會在啟動服務(wù)時注入到客戶端,用于客戶端對于WebSocket消息的處理(如更新頁面某個模塊、刷新頁面);server 代碼是服務(wù)端邏輯,用于處理代碼的構(gòu)建與頁面模塊的請求。
簡單看了下源碼([email protected]),核心功能主要是以下幾個方法(以下為源碼截取,部分邏輯做了刪減):
命令行啟動服務(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í)行做了很多工作,如整合配置項、創(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)聽變動,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)聽到文件變化時觸發(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)備就緒,就能在瀏覽器控制臺看到熟悉的打印?[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ā)送信息
????????//?多個客戶端同時觸發(fā)
????????send(payload:?HMRPayload)?{
????????????const?stringified?=?JSON.stringify(payload)
????????????wss.clients.forEach((client)?=>?{
????????????????//?readyState?1?means?the?connection?is?open
????????????????client.send(stringified)
????????????})
????????}
????}
}
在服務(wù)啟動時會向瀏覽器注入代碼,用于處理客戶端接收到的 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,?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
優(yōu)勢
快!快!非常快!! 高度集成,開箱即用。 基于ESM急速熱更新,無需打包編譯。 基于 esbuild的依賴預(yù)處理,比Webpack等node編寫的編譯器快幾個數(shù)量級。兼容 Rollup龐大的插件機制,插件開發(fā)更簡潔。不與 Vue綁定,支持React等其他框架,獨立的構(gòu)建工具。內(nèi)置SSR支持。 天然支持TS。
不足
Vue仍為第一優(yōu)先支持,量身定做的編譯插件,對React的支持不如Vue強大。雖然已經(jīng)推出2.0正式版,已經(jīng)可以用于正式線上生產(chǎn),但目前市場上實踐少。 生產(chǎn)環(huán)境集成 Rollup打包,與開發(fā)環(huán)境最終執(zhí)行的代碼不一致。
與 webpack 對比
由于Vite主打的是開發(fā)環(huán)境的極致體驗,生產(chǎn)環(huán)境集成Rollup,這里的對比主要是Webpack-dev-server與Vite-dev-server的對比:
到目前很長時間以來 Webpack在前端工程領(lǐng)域占統(tǒng)治地位,Vite推出以來備受關(guān)注,社區(qū)活躍,GitHub star 數(shù)量激增,目前達到37.4K
Webpack配置豐富使用極為靈活但上手成本高,Vite開箱即用配置高度集成Webpack啟動服務(wù)需打包構(gòu)建,速度慢,Vite免編譯可秒開Webpack熱更新需打包構(gòu)建,速度慢,Vite毫秒響應(yīng)Webpack成熟穩(wěn)定、資源豐富、大量實踐案例,Vite實踐較少Vite使用esbuild編譯,構(gòu)建速度比webpack快幾個數(shù)量級
兼容性
默認(rèn)目標(biāo)瀏覽器是在 script標(biāo)簽上支持原生 ESM 和 原生 ESM 動態(tài)導(dǎo)入可使用官方插件 @vitejs/plugin-legacy,轉(zhuǎn)義成傳統(tǒng)版本和相對應(yīng)的polyfill
未來探索
傳統(tǒng)構(gòu)建工具性能已到瓶頸,主打開發(fā)體驗的 Vite,可能會受到歡迎。主流瀏覽器基本支持ESM,ESM將成為主流。 Vite在Vue3.0代替vue-cli,作為官方腳手架,會大大提高使用量。Vite2.0推出后,已可以在實際項目中使用Vite。如果覺得直接使用 Vite太冒險,又確實有dev server速度慢的問題需要解決,可以嘗試用Vite單獨搭建一套dev server
相關(guān)資源
官方插件
除了支持現(xiàn)有的Rollup插件系統(tǒng)外,官方提供了四個最關(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)進展和計劃-尤雨溪[9] Vite源碼解析[10] Develop with Vite | Vite快速入門 - Anthony Fu ? Vue北京聚會 Day 13[11]
參考資料
《Vue3 生態(tài)進展和計劃》:https://www.yuque.com/vueconf/mkwv0c/xqyxix
[2]《前端模塊化開發(fā)那點歷史》:https://github.com/seajs/seajs/issues/588
[3]StackBlitz:https://vite.new/
[4]社區(qū)維護模板: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)進展和計劃-尤雨溪: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
