淺析 Vite 插件機制

前言

對於 Vite 來說,它是基於esbuildrollup雙引擎設計的,在開發階段使用esbuild進行依賴預構建,然後基於瀏覽器原生支持的ESM完成開發預覽,而在生產環境打包時,直接使用的rollup構建。那麼在這種背景下,Vite的插件機制應該如何設計?

在源碼中,我們能夠經常看到PluginContainer的身影,Vite正是通過它來模擬rollup的行爲

pluginContainer

PluginContainer 的 實現 基於借鑑於 WMR 中的 rollup-plugin-container.js ,主要功能有兩個:

插件生命週期

在開發階段,vite 會模擬rollup的行爲,所以插件的執行機制也與rollup相同

  1. 調用 options 鉤子進行配置的轉換,得到處理後的配置對象。

  2. 調用buildStart鉤子,正式開始構建流程。

  3. 調用 resolveId 鉤子中解析文件路徑。(從 input 配置指定的入口文件開始)。

  4. 調用load鉤子加載模塊內容。

  5. 緊接着 Rollup 執行所有的 transform 鉤子來對模塊內容進行進行自定義的轉換(比如 babel 轉譯)

  6. Rollup 拿到最後的模塊內容,進行 AST 分析,得到所有的 import 內容,調用 moduleParsed 鉤子

  7. 直到所有的 import 都解析完畢,Rollup 執行buildEnd鉤子,Build 階段結束。

這裏需要注意的是:在 vite 中由於 AST 分析是通過 esbuild 進行的,所有沒有模擬 moduleParsed 鉤子

傳遞上下文對象

上下文對象通過 Context 實現 PluginContext 接口定義,PluginContext 實際上是 Rollup 內部定義的類型,可以在源碼中看到 vite 實現了 Rollup 上下文對象

class Context implements PluginContext {
  //... 具體實現
}

type PluginContext = Omit<
  RollupPluginContext, // Rollup 定義插件上下文接口
  // not documented
  | 'cache'
  // deprecated
  | 'moduleIds'
>

Context 上下文對象一共有 14 個核心方法,其中有 3 個方法是比較核心的方法

更多內容可以查看 rollup 文檔

rollup 插件

rollup 構建流程主要分爲兩大類:buildoutput,build 階段主要負責創建模塊依賴圖,初始化各個模塊的 AST 以及模塊之間的依賴關係。output 階段纔是真正的打包構建過程

插件 hook 類型(構建階段)

通過構建流程rollup的 hook 類型可以分爲:build hookoutput hook 兩大類

插件 hook 類型(執行方式)

除了上面這種分類,rollup插件還可以根據各自的執行方式來進行分類:

除了根據構建階段可以將 Rollup 插件進行分類,根據不同的 Hook 執行方式也會有不同的分類,主要包括AsyncSyncParallelSquentialFirst這五種。在後文中我們將接觸各種各樣的插件 Hook,但無論哪個 Hook 都離不開這五種執行方式。

1. Async & Sync

首先是AsyncSync鉤子函數,兩者其實是相對的,分別代表異步同步的鉤子函數,這個很好理解。

2. Parallel

這裏指並行的鉤子函數。如果有多個插件實現了這個鉤子的邏輯,一旦有鉤子函數是異步邏輯,則併發執行鉤子函數,不會等待當前鉤子完成 (底層使用 Promise.all)。

比如對於Build階段的buildStart鉤子,它的執行時機其實是在構建剛開始的時候,各個插件可以在這個鉤子當中做一些狀態的初始化操作,但其實插件之間的操作並不是相互依賴的,也就是可以併發執行,從而提升構建性能。反之,對於需要依賴其他插件處理結果的情況就不適合用 Parallel 鉤子了,比如 transform

3. Sequential

Sequential 指串行的鉤子函數。這種 Hook 往往適用於插件間處理結果相互依賴的情況,前一個插件 Hook 的返回值作爲後續插件的入參,這種情況就需要等待前一個插件執行完 Hook,獲得其執行結果,然後才能進行下一個插件相應 Hook 的調用,如transform

4. First

如果有多個插件實現了這個 Hook,那麼 Hook 將依次運行,直到返回一個非 null 或非 undefined 的值爲止。比較典型的 Hook 是 resolveId,一旦有插件的 resolveId 返回了一個路徑,將停止執行後續插件的 resolveId 邏輯。

通用 hook

以下鉤子在服務器啓動時被調用:

以下鉤子會在每個傳入模塊請求時被調用:

它們還有一個擴展的 options 參數,包含其他特定於 Vite 的屬性。

以下鉤子在服務器關閉時被調用:

請注意 moduleParsed 鉤子在開發中是 不會 被調用的,因爲 Vite 爲了性能會避免完整的 AST 解析。

output階段的 hook(除了 closeBundle) 在開發中是 不會 被調用的。你可以認爲 Vite 的開發服務器只調用了 rollup.rollup() 而沒有調用 bundle.generate()

Vite 獨有 hook

Vite 插件也可以提供鉤子來服務於特定的 Vite 目標。當然這些鉤子會被 Rollup 忽略。

config

在解析 Vite 配置前調用。鉤子接收原始用戶配置(命令行選項指定的會與配置文件合併)和一個描述配置環境的變量,包含正在使用的 modecommand。它可以返回一個將被深度合併到現有配置中的部分配置對象,或者直接改變配置(如果默認的合併不能達到預期的結果)。

// 返回部分配置(推薦)
const partialConfigPlugin = () =({
  name: 'nanjiu-plugin',
  config(config, { command }) {
    console.log('config', config, command)
  }
})

需要注意的是:用戶插件在運行這個鉤子之前會被解析,因此在 config 鉤子中注入其他插件不會有任何效果。

configResolved

在解析 Vite 配置後調用。使用這個鉤子讀取和存儲最終解析的配置。

const examplePlugin = () ={
  let config

  return {
    name: 'read-config',

    configResolved(resolvedConfig) {
      // 存儲最終解析的配置
      config = resolvedConfig
    },

    // 在其他鉤子中使用存儲的配置
    transform(code, id) {
      if (config.command === 'serve') {
        // dev: 由開發服務器調用的插件
      } else {
        // build: 由 Rollup 調用的插件
      }
    },
  }
}

configureServer

是用於配置開發服務器的鉤子。最常見的用例是在內部 connect 應用程序中添加自定義中間件

const myPlugin = () =({
  name: 'configure-server',
  configureServer(server) {
    server.middlewares.use((req, res, next) ={
      // 自定義請求處理...
    })
  },
})

configurePreviewServer

configureServer 相同,但用於預覽服務器。configurePreviewServer 這個鉤子與 configureServer 類似,也是在其他中間件安裝前被調用。如果你想要在其他中間件 之後 安裝一個插件,你可以從 configurePreviewServer 返回一個函數,它將會在內部中間件被安裝之後再調用

const myPlugin = () =({
  name: 'configure-preview-server',
  configurePreviewServer(server) {
    // 返回一個鉤子,會在其他中間件安裝完成後調用
    return () ={
      server.middlewares.use((req, res, next) ={
        // 自定義處理請求 ...
      })
    }
  },
})

transformIndexHtml

轉換 index.html 的專用鉤子。鉤子接收當前的 HTML 字符串和轉換上下文。上下文在開發期間暴露ViteDevServer實例,在構建期間暴露 Rollup 輸出的包。

這個鉤子可以是異步的,並且可以返回以下其中之一:

默認情況下 orderundefined,這個鉤子會在 HTML 被轉換後應用。爲了注入一個應該通過 Vite 插件管道的腳本, order: 'pre' 指將在處理 HTML 之前應用。order: 'post' 是在所有未定義的 order 的鉤子函數被應用後才應用。

const htmlPlugin = () ={
  return {
    name: 'nanjiu-plugin',
    transformIndexHtml(html) {
      return html.replace(/<title>(.*?)<\/title>/,
                          `<title> nanjiu plugin </title>`)
    },
  }
}

handleHotUpdate

執行自定義 HMR 更新處理。鉤子接收一個帶有以下簽名的上下文對象

interface HmrContext {
  file: string
  timestamp: number
  modules: Array<ModuleNode>
  read: () => string | Promise<string>
  server: ViteDevServer
}
const hotPlugin = () ={
  return {
    name: 'nanjiu-plugin',
    handleHotUpdate({ server, modules, timestamp}) {
      console.log('handleHotUpdate', modules)
    },
  }
}

當我修改App.vue文件時,modules可以獲取到如下信息:

插件順序

一個 Vite 插件可以額外指定一個 enforce 屬性(類似於 webpack 加載器)來調整它的應用順序。enforce 的值可以是prepost。解析後的插件將按照以下順序排列:

請注意,這與鉤子的排序是分開的,鉤子的順序仍然會受到它們的 order 屬性的影響,這一點 和 Rollup 鉤子的表現一樣

總結

vite 在 開發環境中,會使用 createPluginContainer 方法創建插件容器,插件容器有兩個核心功能:管理插件生命週期傳遞插件上下文

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1gVnUIHUI9X2VkS3ci0RQw