Vite 原始碼解讀系列(圖文結合) —— 本地開發伺服器篇

曬兜斯發表於2022-02-24

哈嘍,很高興你能點開這篇部落格,本部落格是針對 Vite 原始碼的解讀系列文章,認真看完後相信你能對 Vite 的工作流程及原理有一個簡單的瞭解。

Vite 是一種新型的前端構建工具,能夠顯著提升前端開發體驗。

我將會使用圖文結合的方式,儘量讓本篇文章顯得不那麼枯燥(顯然對於原始碼解讀類文章來說,這不是個簡單的事情)。

如果你還沒有使用過 Vite,那麼你可以看看我的前兩篇文章,我也是剛體驗沒兩天呢。(如下)

本篇文章解讀的主要是 vite 原始碼本體,vite 通過 connect 庫提供開發伺服器,通過中介軟體機制實現多項開發伺服器配置。而 vite 在本地開發時沒有藉助 webpack 或是 rollup 這樣的打包工具,而是通過排程內部 plugin 實現了檔案的轉譯,從而達到小而快的效果。

好了,話不多說,我們開始吧!

vite dev

專案目錄

本文閱讀的 Vite 原始碼版本是 2.8.0-beta.3,如果你想要和我一起閱讀的話,你可以在這個地址下載 Vite 原始碼

我們先來看看 Vite 這個包的專案目錄吧。(如下圖)

image

這是一個整合管理的專案,其核心就是在 packages 裡面的幾個包,我們來分別看看這幾個包是做什麼的吧。(如下)

包名作用
viteVite 主庫,負責 Vite 專案的本地開發(外掛排程)和生產產物構建(Rollup 排程)
create-vite用於建立新的 Vite 專案,內部存放了多個框架(如 react、vue)的初始化模板
plugin-vueVite 官方外掛,用於提供 Vue 3 單檔案元件支援
plugin-vue-jsxVite 官方外掛,用於提供 Vue 3 JSX 支援(通過 專用的 Babel 轉換外掛)。
plugin-reactVite 官方外掛,用於提供完整的 React 支援
plugin-legacyVite 官方外掛,用於為打包後的檔案提供傳統瀏覽器相容性支援
playgroundVite 內建的一些測試用例及 Demo

這幾個原始碼倉庫其實有閱讀的價值,但是我們這次還是先專注一下我們本期的主線 —— Vite,從 Vite 開始吧。

接下來我們重點解讀 vite 本地開發服務命令 —— vite / vite dev / vite serve

vite dev

我們來了解一下 vite dev 命令,也就是本地開發服務的內部工作流程。

vite dev 呼叫了內部的 createServer 方法建立了一個服務,這個服務利用中介軟體(第三方)支援了多種能力(如 跨域靜態檔案伺服器等),並且內部建立了 watcher 持續監聽著檔案的變更,進行實時編譯和熱過載。

createServer 做的事情就是我們需要關注的核心邏輯。

createServer 方法中,首先進行了對配置的收集工作 —— resolveConfig

vite 支援的配置

我們正好可以通過原始碼看看 vite 專案支援的配置,你也可以直接參照 Vite 官方文件。(如下)

配置名稱配置說明
configFile配置檔案,預設讀取根目錄下的 vite.config.js 配置檔案
envFile環境變數配置檔案,預設讀取根目錄下的 .env 環境變數配置檔案
root專案的根目錄,預設值是執行命令的目錄 —— process.cwd()
base類似於 webpack 中的 publicPath,也就是資源的公共基礎路徑
server本地執行時的服務設定,比如設定 host(主機地址)、port(執行埠)...詳細配置可以參考 vite 文件
build構建生產產物時的選項,可以參考 vite 文件
preview預覽選項,在使用了 build 命令後,可以執行 vite preview 對產物進行預覽,具體配置可以參考 vite 文件
publicDir靜態資源目錄,用於放置不需要編譯的靜態資源,預設值是 public 目錄
cacheDir快取資料夾,用於放置 vite 預編譯好的一些快取依賴,加速 vite 編譯速度
mode編譯模式,本地執行時預設值是 development,構建生產產物時預設是 production
define定義全域性變數,其中開發環境每一項會被定義在全域性,而生產環境將會被靜態替換
plugins配置 vite 專案的外掛
resolveresolve 支援的配置較多,可以參考 vite 文件
css關於 css 檔案的編譯選項,可以參考 vite 文件
json關於 json 檔案的編譯選項,可以參考 vite 文件
esbuild看官方文件是用於轉換檔案的,但是不太清楚具體的工作是做什麼的,有了解的麻煩在評論區留言解惑一下
assetsInclude設定需要被 picomatch 模式(一種檔案匹配模式)獨立處理的檔案型
optimizeDeps依賴優化選項,具體可以參考 vite 文件
ssrssr 的相關選項,具體可以參考 vite 文件
logLevel調整控制檯輸出的級別,預設為 info
customLogger自定義 logger,該選項沒有暴露,是一個內部選項
clearScreen預設為 true,配置為 false 後,每次重新編譯不會清空之前的內容
envDir用於載入環境變數配置檔案 .env 的目錄,預設為當前根目錄
envPrefix環境變數的字首,帶字首的環境變數將會被注入到專案中
worker配置 bundle 輸出型別、plugins 以及 Rollup 配置項

在上面這些配置中,有一部分可以在啟動時,通過命令列引數新增,比如通過 vite --base / --mode development 的形式進行設定。

如果你希望該配置可以通過配置讀取,也可以全部通過 vite.config.js 來進行配置。

配置斷點除錯

在粗略看過一遍 vite 支援的配置後,我們回到 createServer 函式,準備開始閱讀。

在此之前,如果我們能夠直接執行 vite dev 命令並打上斷點,能夠更好地幫助我們更好的閱讀原始碼,所以我們先來配置一下。

我們需要先進入 vite/packages/vite,安裝依賴,然後在 scripts 中執行 npm run build,將 vite 構建到 dist 目錄中。

然後,我們使用 vscode 的除錯功能,建立一個 launch.json(如下),執行我們的一個 vite 專案。

// launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "pwa-node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "program": "packages/vite/bin/vite.js",
      "args": ["/Users/Macxdouble/Desktop/ttt/vite-try"]
    }
  ]
}

image

除錯配置完成後,我們可以在 resolveConfig 函式中打一個斷點,檢視效果(檔案位置在 dist 目錄中,大家需要根據自己的引用找到對應檔案)。

image

載入配置檔案

resolveConfig 的第一步就是載入專案目錄的配置檔案,如果沒有指定配置檔案位置,會自動在根目錄下尋找 vite.config.jsvite.config.mjsvite.config.tsvite.config.cjs

如果沒有找到配置檔案,則直接會中止程式。

vite 專案初始化時,會在專案根目錄下自動生成 vite.config.js 配置檔案。

image

在讀取配置檔案後,會將配置檔案和初始化配置(優先順序更高,有部分配置來自於命令列引數)進行合併,然後得到一份配置。(如下圖)

image

配置收集 - resolveConfig

createServer 的開頭,呼叫了 resolveConfig 函式,進行配置收集。

我們先來看看 resolveConfig 都做了哪些事情吧。

處理外掛執行順序

首先,resolveConfig 內部處理了外掛排序規則,對應下面的排序規則。

image

在後續處理的過程中,外掛將按照對應的排序規則先後執行,這樣能夠讓外掛更好地工作在各個生命週期節點。

合併外掛配置

在外掛排序完成後,vite外掛 暴露了一個配置 config 欄位,可以通過設定該屬性,使外掛能夠新增或改寫 vite 的一些配置。(如下圖)

image

處理 alias

然後,resolveConfig 內部處理了 alias 的邏輯,將指定的 alias 替換成對應的路徑。

image

讀取環境變數配置

接下來,resolveConfig 內部找到 env 的配置目錄(預設為根目錄),然後在目錄中讀取對應的 env 環境變數配置檔案。我們可以看看內部的讀取規則優先順序(如下圖)

image

可以看出,讀取的優先順序分別是 .env.[mode].local.env.[mode]。如果不存在對應 mode 的配置檔案,則會嘗試去尋找 .env.local.env 配置檔案,讀取到配置檔案後,使用 doteenv 將環境變數寫入到專案中;如果這些環境變數配置檔案都不存在的話,則會返回一個空物件。

該環境變數配置檔案並不影響專案執行,所以不配置也沒有什麼影響。

匯出配置

接下來,vite 初始化了構建配置,也就是文件中的 build 屬性,詳情可以參照 構建選項文件

image

最後,resolveConfig 處理了一些 publicDircacheDir 目錄後,匯出了下面這份配置。

const resolved: ResolvedConfig = {
    ...config,
    configFile: configFile ? normalizePath(configFile) : undefined,
    configFileDependencies,
    inlineConfig,
    root: resolvedRoot,
    base: BASE_URL,
    resolve: resolveOptions,
    publicDir: resolvedPublicDir,
    cacheDir,
    command,
    mode,
    isProduction,
    plugins: userPlugins,
    server,
    build: resolvedBuildOptions,
    preview: resolvePreviewOptions(config.preview, server),
    env: {
      ...userEnv,
      BASE_URL,
      MODE: mode,
      DEV: !isProduction,
      PROD: isProduction
    },
    assetsInclude(file: string) {
      return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
    },
    logger,
    packageCache: new Map(),
    createResolver,
    optimizeDeps: {
      ...config.optimizeDeps,
      esbuildOptions: {
        keepNames: config.optimizeDeps?.keepNames,
        preserveSymlinks: config.resolve?.preserveSymlinks,
        ...config.optimizeDeps?.esbuildOptions
      }
    },
    worker: resolvedWorkerOptions
  }

image

resolveConfig 內部還有一些額外的工作處理,主要是收集內部外掛集合(如下圖),還有配置一些廢棄選項警告資訊。

image

本地開發服務 - createServer

回到 createServer 方法,該方法通過 resolveConfig 拿到配置後,第一時間處理了 ssr(服務端渲染)的邏輯。

如果使用了服務端渲染,則會通過別的方式進行本地開發除錯。

如果不是服務端渲染,則會建立一個 http server 用於本地開發除錯,同時建立一個 websocket 服務用於熱過載。(如下圖)

image

檔案監聽 + 熱過載

然後,vite 建立了一個 FSWatcher 物件,用於監聽本地專案檔案的變動。(這裡使用的是 chokidar 庫)

  const watcher = chokidar.watch(path.resolve(root), {
    ignored: [
      // 忽略 node_modules 目錄的檔案變更
      '**/node_modules/**',
      // 忽略 .git 目錄的檔案變更
      '**/.git/**',
      // 忽略使用者傳入的 `ignore` 目錄檔案的變更
      ...(Array.isArray(ignored) ? ignored : [ignored])
    ],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    disableGlobbing: true,
    ...watchOptions
  }) as FSWatcher

然後,vite 將多個屬性和方法組織成了一個 server 物件,該物件負責啟動本地開發服務,也負責服務後續的開發熱過載。

接下來,我們看看 watcher 是如何做頁面熱過載的吧,原理就是監聽到檔案變更後,重新觸發外掛編譯,然後將更新訊息傳送給客戶端。(如下圖)

image

外掛容器

接下來,vite 建立了外掛容器(pluginContainer),用於在構建的各個階段呼叫外掛的鉤子。(如下圖)

image

實際上外掛容器是在熱過載之前建立的,為了方便閱讀,文章將熱過載的內容都放在了一起。

中介軟體機制

接下來是一些內部中介軟體的處理,當配置開發伺服器選項時,vite 內部通過 connect 框架的中介軟體能力來提供支援。(如下圖)

image

其中,對 public 目錄、公共路徑等多項配置都是通過 connect + 中介軟體實現的,充分地利用了第三方庫的能力,而沒有重複造輪子。

預構建依賴

接下來,vite 內部對專案中使用到的依賴進行的預構建,一來是為了相容不同的 ES 模組規範,二來是為了提升載入效能。(如下圖)

image

準備工作就緒後,vite 內部呼叫 startServer 啟動本地開發伺服器。(如下)

// ...
httpServer.listen(port, host, () => {
  httpServer.removeListener('error', onError)
  resolve(port)
})

小結

至此,vite 本身的原始碼部分就解析完了。

可以看出,在本地開發時,vite 主要依賴 外掛 + 中介軟體體系 來提供能力支援。因為本地開發時只涉及到少量編譯工作,所以非常的快。只有在構建生產產物時,vite 才用到了 rollup 進行構建。

我們用一張流程圖來最後梳理一遍 vite 本地開發服務 內部的工作流程吧。

image

那麼本期文章就到此結束,在下一篇文章,我會挑選 1 - 2 個比較典型的外掛或是 build 篇(生產產物構建)來進行原始碼解析。

最後一件事

如果您已經看到這裡了,希望您還是點個贊再走吧~

您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!

如果覺得本文對您有幫助,請幫忙在 github 上點亮 star 鼓勵一下吧!

相關文章