avatar
threadsinstagram

Vite 原始碼 (1) - 從 npm run dev 到 createServer

Table of Contents

前言

昨天實際體驗到了為什麼 Vite 在啟 dev server 時需要做 pre-bundling 的原因,關於怎麼做這件事,目前從文件上看起來只知道最後是交由 esbuild 來幫忙做轉譯,但實際上裡面有什麼魔法是無從得知。

於是今天想站在菜市場阿龍的肩膀上,效法《Vite 原始碼解讀》系列的精神,試著追追看 Vite 原始碼了解一下原理,雖然不知道有什麼用,就當作一個練習的開端也不錯,有興趣也可以跟我一起學習或交流怎麼追更快。

容我再廢話幾句,有另一個原因是目前我把 ViteRsbuildRolldownRspack 四個 repo. 都載了下來稍微瀏覽過,前兩者幾乎都是 Node.js/TypeScript,後兩者則多為 Rust,接下來幾篇想試著先從熟悉的 TypeScript 開始追一部份原始碼,之後看能不能進階來學學 Rust。

而 Rsbuild 做為 dev server,稍微瀏覽過去原始碼看起來跟 Vite 蠻像的,甚至有點懷疑其實這兩個團隊可以互補來推進 Rolldown 的前進,而且好像還真有這麼一回事 (ref):

viteconf 2023

所以在想追 Vite 其實某種程度也是在理解 Rsbuild,說不定哪天他們的生態系會合而為一。那以下就開始進入正題吧!

下載專案

踏入開源大門的第一步先把專案載下來用自己熟悉的 IDE 與 highlight syntax 更好追:

git clone https://github.com/vitejs/vite.git --depth=3

💡 冷知識:這裡的 —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 做為進入點開始理解,這個檔案就像是一本書的目錄一樣,就算文件上沒寫,也可以從一些屬性得到一些資訊:

  • dependenciesdevDependencies:從這裡可以大概知道它背後用上了哪些套件
  • scripts:可以了解它怎麼在 dev 做開發、怎麼打包與部屬、怎麼執行測試等等
  • engines:Node.js 版本要求
  • main:如果有的話,大概可以有一些主程式進入點在哪的線索
package.json file demo

但如果用 cmd + p 搜尋檔案時,會看到怎麼會有這麼多個,這裡其實我們要關心的 dev server 會是在 packages/vite/package.json 這個底下,但順帶一提會有這麼多個 package.json 的原因是因為在有規模的開源專案中,通常會用 monorepo 在根目錄來管理許多子專案。

可以看到根目錄中有個 pnpm-workspace.yaml 的檔案:

packages:
  - 'packages/*'
  - 'playground/**'
  - 'packages/**/__tests__/**'
  - docs

從這裡可以大概知道這整個 codebase 底下還分成以下幾個子專案:

  • packages/create-vite建專案時可以用的各種 template
  • packages/plugin-legacy:看起來是為了一些較舊版瀏覽器的 polyfills 跟模組轉譯等用途的專案,可不管
  • packages/vite:這一臉就是我們的大哥
  • docs:應該就是官方文件、部落格的靜態文件專案

另外用圖片示意我們的主戰場 packages/vite 底下的結構:

files structure
  • 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 去執行:

// 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 devnpm run server 都是一樣效果,被設定成同一種 alias
  • createServer 顧名思義看起來就是拿來建立 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 這個套件來監聽檔案變化,這部份會有點複雜,待下一篇繼續來研究研究。

💡 補充: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 確認與調整
fold scope demo

💡 補充:今天的一個小疑問

話說今天有個疑問是,在思考如果今天我想發 PR 給 Vite,不確定有什麼手動測試手法或該怎麼寫單元測試,似乎沒找到 local 開發的相關文件,目前也還沒追到,先留下一個疑問待我繼續研究研究。

(更新) 後來有找到了如何貢獻的文件發現原來他們用了一套 StackBlitz Codeflow 的雲端編輯器工具來做整合,可以直接在上面測試並串接自己的 GitHub 帳號做 fork 與發 PR。

小結

今天嘗試開始追 Vite 的原始碼,從 npm run dev 的入口點,一路追到了建立 server 連線的地方,最後有看到一個 Chokidar 這個寫著 watcher 的東西,看起來會是拿來監聽檔案新增、修改、刪除的調整時,對應去推送 WebSocket 事件的用途,明天再來繼續往下追,應該可以快看到 pre-bundling 的邏輯了。