昨天實際體驗到了為什麼 Vite 在啟 dev server 時需要做 pre-bundling 的原因,關於怎麼做這件事,目前從文件上看起來只知道最後是交由 esbuild 來幫忙做轉譯,但實際上裡面有什麼魔法是無從得知。
於是今天想站在菜市場阿龍的肩膀上,效法《Vite 原始碼解讀》系列的精神,試著追追看 Vite 原始碼了解一下原理,雖然不知道有什麼用,就當作一個練習的開端也不錯,有興趣也可以跟我一起學習或交流怎麼追更快。
容我再廢話幾句,有另一個原因是目前我把 Vite、Rsbuild、Rolldown、Rspack 四個 repo. 都載了下來稍微瀏覽過,前兩者幾乎都是 Node.js/TypeScript,後兩者則多為 Rust,接下來幾篇想試著先從熟悉的 TypeScript 開始追一部份原始碼,之後看能不能進階來學學 Rust。
而 Rsbuild 做為 dev server,稍微瀏覽過去原始碼看起來跟 Vite 蠻像的,甚至有點懷疑其實這兩個團隊可以互補來推進 Rolldown 的前進,而且好像還真有這麼一回事 (ref):
所以在想追 Vite 其實某種程度也是在理解 Rsbuild,說不定哪天他們的生態系會合而為一。那以下就開始進入正題吧!
踏入開源大門的第一步先把專案載下來用自己熟悉的 IDE 與 highlight syntax 更好追:
git clone https://github.com/vitejs/vite.git --depth=3
💡 冷知識:這裡的
—depth=3
是 git 中一個叫做 shallow clone 的小技巧,指的是只想抓最近的 3 筆 commit,好處是可以透過只抓少量的紀錄加快下載效率與節省硬碟空間
main
branch 的 packages/vite/package.json
中可以看到版本號是 6.0.0-beta.1
💡 現在是
6.0.0-beta.1
在猜可能最近正在趕下週四 10/3 的 ViteConf 2024 想要發佈 Vite 與 Rolldown 的整合進度,如果對最新進度有興趣可以參考這個 roadmap,從其中這個 PR 可以發現可能在 Vite v6 正式版有機會看到 Rolldown。
當第一次接觸到一個陌生的 codebase,我大多會從 package.json
做為進入點開始理解,這個檔案就像是一本書的目錄一樣,就算文件上沒寫,也可以從一些屬性得到一些資訊:
dependencies
、devDependencies
:從這裡可以大概知道它背後用上了哪些套件scripts
:可以了解它怎麼在 dev 做開發、怎麼打包與部屬、怎麼執行測試等等engines
:Node.js 版本要求main
:如果有的話,大概可以有一些主程式進入點在哪的線索但如果用 cmd + p
搜尋檔案時,會看到怎麼會有這麼多個,這裡其實我們要關心的 dev server 會是在 packages/vite/package.json
這個底下,但順帶一提會有這麼多個 package.json
的原因是因為在有規模的開源專案中,通常會用 monorepo 在根目錄來管理許多子專案。
可以看到根目錄中有個 pnpm-workspace.yaml
的檔案:
packages:
- 'packages/*'
- 'playground/**'
- 'packages/**/__tests__/**'
- docs
從這裡可以大概知道這整個 codebase 底下還分成以下幾個子專案:
packages/create-vite
:建專案時可以用的各種 templatepackages/plugin-legacy
:看起來是為了一些較舊版瀏覽器的 polyfills 跟模組轉譯等用途的專案,可不管packages/vite
:這一臉就是我們的大哥docs
:應該就是官方文件、部落格的靜態文件專案另外用圖片示意我們的主戰場 packages/vite
底下的結構:
bin
:Vite CLI 指令 binary 檔案原始碼src/client
:蠻單純的,會在啟動 dev server 時在瀏覽器去載入這個檔案,其中會用來接收 WebSocket 事件src/node
:Node.js server,dev server 的本體,建立 server 連線、pre-bundling、HMR 等邏輯都在裡面當我們在一個 Vite 專案中執行了 npm run dev
會發生什麼事呢?
這個邏輯的入口點會是在 vite/bin/vite.js 底下,這裡可以看到當你今天沒有用 --profile
作為 CLI 上的參數時,就會載入 cli.js
去執行:
// packages/vite/bin/vite.js
function start() {
return import('../dist/node/cli.js')
}
if (profileIndex > 0) {
// ... 略 ...
} else {
start()
}
而這個路徑是 build 出來的檔案,實際的原始檔會是在 vite/src/node/cli.ts 中:
// packages/vite/src/node/cli.ts
// ...
// dev
cli
.command('[root]', 'start dev server')
.alias('serve')
.alias('dev')
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
filterDuplicateOptions(options)
// 這裡可以看到 server 主程式的引入位置
const { createServer } = await import('./server')
try {
// 建立 server
const server = await createServer({...})
await server.listen()
// ...
}
// ...
})
從上面這段程式碼會看到:
npm run dev
或 npm run server
都是一樣效果,被設定成同一種 aliascreateServer
顧名思義看起來就是拿來建立 server 連線的方法於是我們再追進去 createServer 這個 function 的定義會看到許多主要連線與監聽的邏輯都在裡面,這裡有 500 多行,原檔案是 TypeScript,這邊把一些前置作業與型別去掉簡化一下比較好理解:
// packages/vite/src/node/server/index.ts
// ...
export async function _createServer(
inlineConfig,
options,
) {
// ... 處理與準備設定檔相關程式碼略 ...
// 用 connect 這個套件來建立 middleware
const middlewares = connect()
// 建立 http server
const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 建立 websocket server
const ws = createWebSocketServer(httpServer, config, httpsOptions)
// 用 Chokidar 來做為 watcher 監聽檔案變化
const watcher = chokidar.watch(
// config file dependencies and env file might be outside of root
[
root,
...config.configFileDependencies,
...getEnvFilesForMode(config.mode, config.envDir),
// Watch the public directory explicitly because it might be outside
// of the root directory.
...(publicDir && publicFiles ? [publicDir] : []),
],
resolvedWatchOptions,
)
// 用前面建立好的設定組合成一個 dev server 的物件供後面使用
let server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
ws,
// ... 暫略 ...
}
// ... 檔案異動相關邏輯,下集待續 ...
}
這裡我們會看到有用上 Chokidar
這個套件來監聽檔案變化,這部份會有點複雜,待下一篇繼續來研究研究。
💡 補充一個如果遇到需要追一個超級長的巢狀 function 的小技巧,在 VS Code 中可以用組合鍵來一次把檔案中的 scope 收起來:
- 收合:先按
cmd + k
再接cmd + 0
,這個數字對應到看你想收幾層,以下面這張圖來說我是用cmd + 3
,就是只去收三層以上的 scope- 展開:先按
cmd + k
再接cmd + j
- 如果是 Cursor 使用者的話,因為
cmd + k
是 inline AI chat 的快捷鍵,會稍微不太一樣,前置組合鍵會是cmd + r
,或你可以開啟你編輯器的 Keyboard Shortcuts 搜尋fold all
確認與調整
話說今天有個疑問是,在思考如果今天我想發 PR 給 Vite,不確定有什麼手動測試手法或該怎麼寫單元測試,似乎沒找到 local 開發的相關文件,目前也還沒追到,先留下一個疑問待我繼續研究研究。
📝 補充:後來有找到了如何貢獻的文件發現原來他們用了一套 StackBlitz Codeflow 的雲端編輯器工具來做整合,可以直接在上面測試並串接自己的 GitHub 帳號做 fork 與發 PR。
今天嘗試開始追 Vite 的原始碼,從 npm run dev
的入口點,一路追到了建立 server 連線的地方,最後有看到一個 Chokidar 這個寫著 watcher 的東西,看起來會是拿來監聽檔案新增、修改、刪除的調整時,對應去推送 WebSocket 事件的用途,明天再來繼續往下追,應該可以快看到 pre-bundling 的邏輯了。