【源碼】Vite源碼分析,是時候弄清楚Vite的原理了

注:本篇是組內PPT分享內容,說的部分會比較多。弄成文章后還未整理,后續整理一下。

0. 源碼目錄結構

https://github.com/vitejs/vite

image.png

image.jpeg

1. 源碼入口

1.1 運行npm run dev后發生了什么?

先準備一個vite腳手架生成的項目vite-vue3,當運行npm run dev后發生了什么?在package.json中看到運行的是vite命令。

vite命令是在哪里注冊的呢,在node_modules/vite/package.json中查看bin字段。

"bin" 字段的作用是能讓我們在命令窗口全局輸入命令執行??梢钥吹?vite 命令:

  "bin": {
    "vite": "bin/vite.js"
  }

打開vite.js看到,其中主要運行的是

function start() {
  require('../dist/node/cli')
}

這里的/dist/node/cli.js是打包后的文件,可能有點長,可以配合vite打包前的源碼一起閱讀。

在/dist/node/cli.js中,首先使用 cac 命令工具處理用戶的輸入。

cli

然后命令執行的是createServer,這個createServer./chunks/dep-55830a1a.js引入,這里也是打包后的代碼,比較長。

1.2 createServer

我們找到了前面大概執行的順序后,這里回到源碼,在 packages/vite/src/node/server/index.ts 里面找到createServer

export async function createServer(
    inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
    // Vite 配置整合
    const config = await resolveConfig(inlineConfig, 'serve', 'development')
    const root = config.root
    const serverConfig = config.server

    // 創建http服務
    const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)

    // 創建ws服務
    const ws = createWebSocketServer(httpServer, config, httpsOptions)

    // 創建watcher,設置代碼文件監聽
    const watcher = chokidar.watch(path.resolve(root), {
        ignored: [
            '**/node_modules/**',
            '**/.git/**',
            ...(Array.isArray(ignored) ? ignored : [ignored])
        ],
        ...watchOptions
    }) as FSWatcher

    // 創建server對象
    const server: ViteDevServer = {
        config,
        middlewares,
        httpServer,
        watcher,
        ws,
        moduleGraph,
        listen,
        ...
    }

    // 文件監聽變動,websocket向前端通信
    watcher.on('change', async (file) => {
        ...
        handleHMRUpdate()
    })

    // 服務 middleware
    middlewares.use(...)
    
    // optimize: 預構建
    await initDepsOptimizer(config, server)
    
    // 監聽端口,啟動服務
    httpServer.listen = (async (port, ...args) => { ... })
    
    return server
}

可以看到 createServer 做了很多事情,上面列舉主要的幾個:
resolveConfig:整合配置(resolvePlugins)
注冊各種中間件(indexHtml、transformMiddleware、static)
HMR:使用chokidar監聽文件的修改
optimizeDeps:預構建
創建httpServer,啟動服務
...

image

2. 預構建

2.1 no-bundle vs bundle

怎么理解no-bundle?不打包?


bundle
no-bundle
2.2 為什么要預構建?

1、Vite是基于瀏覽器原生支持ESM的能力實現的,要求用戶的代碼模塊必須是ESM模塊,因此必須將commonJs的文件提前處理,轉化成 ESM 模塊并緩存入 node_modules/.vite

2、減少模塊和請求數量。例如,lodash-es 有超過 600 個內置模塊。當我們執行 import { debounce } from 'lodash-es' 時,瀏覽器同時發出 600 多個 HTTP 請求!盡管服務器在處理這些請求時沒有問題,但大量的請求會在瀏覽器端造成網絡擁塞,導致頁面的加載速度相當慢。
通過預構建 lodash-es 成為一個模塊,我們就只需要一個 HTTP 請求了!

2.3 預構建的核心流程

預構建的核心流程,包括緩存判斷、依賴掃描、依賴打包和元信息保存這四個主要的步驟。

image (5).png

關于預構建的實現代碼都在optimizeDeps函數當中,在倉庫源碼的 packages/vite/src/node/optimizer/index.ts 查看 optimizeDeps:

image

optimizeDeps首先調用loadCachedDepOptimizationMetadata獲取node_modules/.vite/_metadata.json中的元信息。

image (7).png

然后調用getDepHash,這個函數是讀取目錄下的package-lock.json的內容,然后將文件內容進行hash得到一個hash值。

然后兩個hash進行對比是否相等。
也就是說,預編譯就是看node_modules的包有沒有變化,如果不相等。會調用scanImports去掃,在scanImports中,得到入口文件后,對入口文件進行了解析,當然,具體的解析過程在依賴掃描階段的 Esbuild 插件(esbuildScanPlugin)中得以實現。


這里就會使用esbuild.build去編譯文件,其中esbuildDepPlugin就是打包的插件:

生成出來保存到.vite下。最后,執行writeFile,再將相關信息保存到_metadata.json

3. 核心編譯流程

webpack中plugin和loader的區別?

3.1 Rollup插件機制

Rollup 的打包過程中,會定義一套完整的構建生命周期,從開始打包到產物輸出,中途會經歷一些標志性的階段,并且在不同階段會自動執行對應的插件鉤子函數(Hook)。
Vite 的插件機制是基于 Rollup 來設計的。Vite 模擬了 Rollup 的插件機制,設計了一個 PluginContainer 對象來調度各個插件。
PluginContainer 的 實現 基于借鑒于 WMR 中的rollup-plugin-container.js,主要分為 2 個部分:

1、實現 Rollup 插件鉤子的調度
2、實現插件鉤子內部的 Context 上下文對象

PluginContainer的定義了一系列執行plugin的方法。如buildStart、resolveId、load、transform。

3.2 vite插件的接口定義

packages/vite/src/node/plugin.ts:

export interface Plugin extends RollupPlugin {
  enforce?: 'pre' | 'post'
  apply?: 'serve' | 'build' | ((config: UserConfig, env: ConfigEnv) => boolean)
  config?: (
    config: UserConfig,
    env: ConfigEnv
  ) => UserConfig | null | void | Promise<UserConfig | null | void>
  configResolved?: (config: ResolvedConfig) => void | Promise<void>
  configureServer?: ServerHook
  configurePreviewServer?: PreviewServerHook
  transformIndexHtml?: IndexHtmlTransform
  handleHotUpdate?(
    ctx: HmrContext
  ): Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
  resolveId?(
    this: PluginContext,
    source: string,
    importer: string | undefined,
    options: {
      custom?: CustomPluginOptions
      ssr?: boolean
      /**
       * @internal
       */
      scan?: boolean
    }
  ): Promise<ResolveIdResult> | ResolveIdResult
  load?(
    this: PluginContext,
    id: string,
    options?: { ssr?: boolean }
  ): Promise<LoadResult> | LoadResult
  transform?(
    this: TransformPluginContext,
    code: string,
    id: string,
    options?: { ssr?: boolean }
  ): Promise<TransformResult> | TransformResult
}

3.3 當瀏覽器一個JS請求到vite服務時,發生了什么?

例如:
<script type="module" src="/src/main.js"></script> 或者 import { get } from './utils';

// main transform middleware
  middlewares.use(transformMiddleware(server))

可以看到pluginContainer會執行插件中的鉤子。對于不同的資源會有不同的插件去處理。
vite的內置插件:


路徑解析插件(packages/vite/src/node/plugins/resolve.ts)
路徑解析插件(即vite:resolve)是 Vite 中比較核心的插件,幾乎所有重要的 Vite 特性都離不開這個插件的實現,諸如依賴預構建、HMR、SSR 等等。

CSS 編譯插件(packages/vite/src/node/plugins/css.ts)

import分析插件(packages/vite/src/node/plugins/importAnalysis.ts)
重寫import語句,如import Vue from 'vue';導入路徑會重寫為預構建文件夾的路徑;
注入HMR客戶端腳本。

Esbuild 轉譯插件(packages/vite/src/node/plugins/esbuild.ts)
用來進行 .js、.ts、.jsx和tsx,代替了傳統的 Babel 或者 TSC 的功能,這也是 Vite 開發階段性能強悍的一個原因。

4. HMR流程

打包工具實現熱更新的思路都大同小異:主要是通過WebSocket創建瀏覽器和服務器的通信監聽文件的改變,當文件被修改時,服務端發送消息通知客戶端修改相應的代碼,客戶端對應不同的文件進行不同的操作的更新。

瀏覽器文件是幾時被注入的?在importAnalysis插件中:

      if (hasHMR && !ssr) {
        debugHmr(
          `${
            isSelfAccepting
              ? `[self-accepts]`
              : acceptedUrls.size
              ? `[accepts-deps]`
              : `[detected api usage]`
          } ${prettyImporter}`
        )
        // inject hot context
        str().prepend(
          `import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
            `import.meta.hot = __vite__createHotContext(${JSON.stringify(
              importerModule.url
            )});`
        )
      }

5. 源碼運行演示

6. 參考

Vite中文網

深入理解Vite核心原理 - 掘金

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容