Vite 原始碼 (1) - 從 npm run dev 到 createServer
前言
昨天實際體驗到了為什麼 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 更好追:
💡 冷知識:這裡的
—depth=3
是 git 中一個叫做 shallow clone 的小技巧,指的是只想抓最近的 3 筆 commit,好處是可以透過只抓少量的紀錄加快下載效率與節省硬碟空間
環境
- VS Code 或 Cursor
- Node.js 版本:v20.17.0
- Vite 版本:目前今天的最新版在
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
的檔案:
從這裡可以大概知道這整個 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 等邏輯都在裡面
從 npm run dev 開始
當我們在一個 Vite 專案中執行了 npm run dev
會發生什麼事呢?
這個邏輯的入口點會是在 vite/bin/vite.js 底下,這裡可以看到當你今天沒有用 --profile
作為 CLI 上的參數時,就會載入 cli.js
去執行:
而這個路徑是 build 出來的檔案,實際的原始檔會是在 vite/src/node/cli.ts 中:
從上面這段程式碼會看到:
npm run dev
或npm run server
都是一樣效果,被設定成同一種 aliascreateServer
顧名思義看起來就是拿來建立 server 連線的方法
於是我們再追進去 createServer 這個 function 的定義會看到許多主要連線與監聽的邏輯都在裡面,這裡有 500 多行,原檔案是 TypeScript,這邊把一些前置作業與型別去掉簡化一下比較好理解:
這裡我們會看到有用上 Chokidar
這個套件來監聽檔案變化,這部份會有點複雜,待下一篇繼續來研究研究。
💡 補充:VS Code 收合 scope 的小技巧
補充一個如果遇到需要追一個超級長的巢狀 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 的邏輯了。