Vite 的實現原理,確實很巧妙
vite 是新興的構建工具,它相比 webpack 最大的特點就是快。
那它是如何做到這麼快的呢?
因爲 vite 在開發環境並不做打包。
我們創建個 vite 項目:
npx create-vite
安裝依賴,然後把服務跑起來:
npm install
npm run dev
瀏覽器訪問下:
本地是 main.tsx 引入了 App.tsx,並且還有 react 和 react-dom/client 的依賴:
用 devtools 看下:
可以看到,main.tsx、App.tsx 還有 react 和 react-dom/client 的依賴都是直接引入的,做了編譯,但是並沒有打包。
這是基於瀏覽器的 type 爲 module 的 script 實現的:
我們加一個 index2.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="aaa.js"></script>
</body>
</html>
然後添加 aaa.js
import { add } from './bbb.js'
console.log(add(1, 2));
bbb.js
export function add(a, b) {
return a + b;
}
起個靜態服務訪問下:
npx http-server
瀏覽器訪問下 http://localhost:8080/index2.html
可以看到,aaa 和 bbb 模塊都被下載並執行了。
當然,我們沒有做編譯,如果有 ts 或者 jsx 的語法,需要做一次編譯。
那我們是不是可以起個服務器,請求的時候根據 url 找到對應的文件,編譯之後返回呢?
沒錯,如果你這樣想了,那你也可以寫一個 vite。
vite 在開發環境下就是起了一個做編譯的服務器,根據請求的 URL 找到對應的模塊做編譯之後返回。
當你執行 npm run dev 的時候:
vite 會跑一個開發服務:
這個開發服務是基於 connect 實現的,vite 給它加了很多中間件來處理請求:
當你請求 index.html 的時候,它會通過 ast 遍歷,找到其中所有的 script:
然後提前對這些文件做編譯:
編譯是通過不同插件完成的:
插件就是一個對象,它導出了 transform 方法的話,就會在 transform 的時候被調用。
比如圖中有 css 插件來編譯 css、esbuild 插件來編譯 ts/js 等。
每個插件都會判斷下,只處理對應的資源:
比如 vite:esbuild 插件,就是對 js/ts 做編譯,然後返回編譯後的 code 和 sourcemap:
還有個 import-anlysis 插件,在 esbuild 完成編譯之後,分析模塊依賴,繼續處理其它模塊的 transform:
這樣,瀏覽器只要訪問了 index.html,那麼你依賴的所有的 js 模塊,就都給你編譯了。
這就是 vite 爲什麼叫 no bundle 方案,它只是基於瀏覽器的 module import,在請求的時候對模塊做下編譯。
但不知道大家有沒有想過一個問題:
瀏覽器支持 es module 的 import,那如果 node_modules 下的依賴有用 commonjs 模塊規範的代碼呢?
是不是就不行了。
這種就需要提前做一些轉換,把 commonjs 轉成 import。
還有一個問題,如果每個模塊都是請求時編譯,那向 lodash-es 這種包,它可是有幾百個模塊的 import 呢:
這樣跑起來,一個 node_modules 下的包就有幾百個請求,依賴多了以後,很容易就幾千個請求。
這誰受的了?
所以我們要提前處理下,不但要把 node_modules 下代碼的 commonjs 提前轉成 es module,還有提前對這些包做一次打包,變成一個 es module 模塊。
所以,vite 加了一個預構建功能 pre bunle。
在啓動完開發服務器的時候,就馬上對 node_modules 下的代碼做打包,這個也叫 deps optimize,依賴優化。
如何優化呢?
首先,掃描出所有的依賴來:
這一步是用 esbuild 做的:
esbuild.context 和 esbuild.build 差不多的功能。
可以看到,用 esbuild 對入口 index.html 開始做打包,輸出格式爲 esm,但是 write 爲 false,不寫入磁盤。
有同學說,esbuild 支持 import html 麼?
這裏用到了一個 esbuild scan plugin:
vite 實現的,用來記錄依賴的:
它會在每種模塊路徑解析的時候做處理,其中支持了 html 的處理。
這樣用 esbuild 處理完一遍,是不是就知道預打包哪些包了?
我們在項目裏引入下 dayjs 和 lodash-es 再試下:
依然給你一個不少的給分析了出來:
接下來調用 esbuild 打包就行。
但打包之前呢,還會對路徑做扁平化,比如 react-dom/client 變成 react-dom_client
效果就是打包之後文件是平級的。
從每個依賴包作爲入口打包,輸出 esm 格式的模塊到 node_modules/.vite 下。
之後還會生成一個 _metadata.json 文件寫入 node_modules/.vite 下:
這樣的:
這個 metadata.json 是幹啥的呢?
看到這幾個 hash 了麼
vite 會在這些預打包的模塊後加一個 query 字符串帶上 hash,然後用 max-age 強緩存:
因爲這些依賴一般不會變,不用每次都請求,強緩存就行。
但是在 lock 文件變化或者 config 有一些變化的時候也需要重新 build:
重新預編譯,然後在資源請求時帶上新的 query,這樣就讓強緩存失效了。
這裏強緩存的用法很典型,面試官們可以記一下作爲考點。
這樣,vite 的開發服務的請求時編譯,再就是預構建就都完成了。
有的同學可能會問,爲啥預構建要用 esbuild 呢?
原因就是快:
vite 在 dev 時的核心原理我們理清了,但是在 build 的時候總要打包的吧。
那肯定,在 build 的時候 vite 會用 rollup 做打包。
那不會導致開發時的代碼和生產環境不一致麼?
不會。
能做到這一點也很巧妙。
看下 build 時的 rollup 插件:
是不是似曾相識?
對比下 dev 時跑的 vite 插件:
沒錯,vite 插件時兼容 rollup 插件的,這樣在開發的時候,在生產環境打包的時候,都可以用同樣的插件對代碼做 transform 等處理。
處理用的插件都一樣,又怎麼會開發和生產不一致呢?
這也是 vite 的巧妙之處。
在 dev 的時候,它實現了一個 PluginContainer,用和 rollup 插件同樣的參數來調用 vite 插件:
然後 build 的時候,可以把這些插件直接作爲 rollup 插件用。
對了,vite 在 dev 的時候還支持熱更新,也就是本地改了代碼能夠自動同步到瀏覽器。
這個就是基於 chokidar 監聽了本地文件變動:
然後在模塊變動的時候通過 websocket 通知瀏覽器端:
瀏覽器端接受之後做相應處理就好了:
我們改下 Aaa.tsx,可以看到瀏覽器端收到了 update 的 ws 消息:
收到消息之後,把模塊換成這個新的,加上 timestamp 重新請求就好了:
總結
今天我們分析了下 rollup 的實現原理。
它是基於瀏覽器的 type 爲 module 的 script 可以直接下載 es module 模塊實現的。
做了一個開發服務,根據請求的 url 來對模塊做編譯,調用 vite 插件來做不同模塊的 transform。
但是 node_modules 下的文件有的包是 commonjs 的,並且可能有很多個模塊,這時 vite 做了預構建也叫 deps optimize。
它用 esbuild 分析依賴,然後用 esbuild 打包成 esm 的包之後輸出到 node_modules/.vite 下,並生成了一個 metadata.json 來記錄 hash。
瀏覽器裏用 max-age 強緩存這些預打包的模塊,但是帶了 hash 的 query。這樣當重新 build 的時候,可以通過修改 query 來觸發更新。
在開發時通過 connect 起了一個服務器,調用 vite 插件來做 transform,並且對 node_modules 下的模塊做了預構建,用 esbuild 打包。
在生產環境用 rollup 來打包,因爲 vite 插件兼容了 rollup 插件,所以也是用同樣的插件來處理,這樣能保證開發和生產環境代碼一致。
此外,vite 還基於 chokidar 和 websocket 來實現了模塊熱更新。
這就是 vite 的實現原理。
回想下,不管是基於瀏覽器 es module import 實現的編譯服務,基於 esbuild 做的依賴預構建,基於 hash query 做的強緩存和緩存更新,還是兼容 rollup 的 vite 插件可以在開發服務和 rollup 裏同時跑,這些功能實現都挺巧妙的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ejkfARh6hlOAUnw5Eadb6Q