下一代前端開發利器——Vite(原理源碼解析)
本文作者是 360 奇舞團前端開發工程師
前言
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] 可以使用。
啓動
{
"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<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
打包,與開發環境最終執行的代碼不一致。
與 webpack 對比
由於Vite
主打的是開發環境的極致體驗,生產環境集成Rollup
,這裏的對比主要是Webpack-dev-server
與Vite-dev-server
的對比:
-
到目前很長時間以來
Webpack
在前端工程領域佔統治地位,Vite
推出以來備受關注,社區活躍,GitHub star 數量激增,目前達到 37.4K -
Webpack
配置豐富使用極爲靈活但上手成本高,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]
參考資料
[1] 《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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/oroQSMSPxtSEfnjuxu2pew