此為 Vite dependency pre-bundling 的下集,如果還沒看過上集可以點這裡
延續昨天的專案,今天我們來試著安裝一個輸出為 CommonJS 的套件:
$ pnpm add lodash
💡 你可能會問 lodash 與 lodash-es 有什麼不同,前者模組輸出的方式為 CommonJS,而後者是 ESM。兩種都放在同一個 repo 裡用 branch 去區分不同的模組化標準輸出到 NPM 上,甚至其他 branch 中還有 UMD 與 AMD 版本。
如果今天考慮想去減小專案的 bundle size 時,通常都會談到需要使用 ESM 版本,這是因為 ESM 可以方便 bundler 在進行 tree-shaking 時做靜態分析去決定哪些用不到的部份可以被移掉,有興趣可以參考我之前寫過的這個系列文章。
將原本的 main.js
引入的地方改成這樣:
// main.js
import has from './node_modules/lodash/has.js';
這時如果你重新啟動 Live Server 去看 console 後應該會看到這樣的錯誤:
Uncaught SyntaxError: The requested module './node_modules/lodash/has.js' does not provide an export named 'default'
儘管今天相對路徑寫對了,從錯誤訊息你可以看到它說在這個模組中並沒有去輸出 has
這個方法,如果你到 node_modules/lodash/has.js
這個路徑下找到這支檔案,會看到它是 CommonJS 的輸出方式:
...
function has(object, path) {
return object != null && hasPath(object, path, baseHas);
}
module.exports = has;
因此我們可以得到另一個問題是,當你想讓瀏覽器利用原生 ESM 去載入檔案時,這時如果有第三方套件是不同的模組化標準(CommonJS、UMD、AMD 等),就會需要做一些處理才能使用。
看完了以上 3 個實驗中的問題,接下來我們實際來試試用 Vite 的效果如何。
先建立一個 vanilla 的 Vite 專案,可以直接使用這個 GitHub 初始模板,或在前面章節中有建立過的話是同一個步驟:
$ pnpm create vite hello-vite --template vanilla
$ cd hello-vite
$ pnpm install
接著我們來安裝剛剛的兩種不同模組的 lodash 的套件後,就可以來確認以下的幾個部份:
$ pnpm add lodash lodash-es
前面我們提到純手工的做法時,會需要用相對路徑的方式來載入套件瀏覽器才能正確解析:
// main.js
import { has } from './node_modules/lodash-es/lodash.js';
但這會造成開發上的不方便,今天我們嘗試在 Vite 專案中這樣改寫:
// main.js
import { has } from 'lodash-es';
console.log(has({ a: 1 }, 'a'));
用 pnpm run dev
將 server 啟起來,打開 dev tool 觀察前面實驗的問題,會發現 console 上能正確印出 true
,就代表 lodash/has
有正確被載入進來了。這樣我們認為很基本的開發需求,其實也是需要由 Vite 在背後做處理。
至於實際載進來的內容是什麼呢,這會跟下一個實驗有關。
昨天實驗二中發現當我們去載入 lodash/has
時,會因為背後有許多依賴的小模組造成 request 過多的問題。這邊我們實際打開 dev tool 的 network 清掉 log 後,重整來確認狀況:
可以發現 request 從昨天的 60 多個減少到剩下 4 個,這是怎麼做到的呢?點開 main.js
來確認一下載進來的是什麼:
會發現這邊實際載入的內容是這樣的一個路徑:
import {has} from "/node_modules/.vite/deps/lodash-es.js?v=671bbcf6";
你會發現這並非預期中的 node_modules/lodash-es/lodash.js
,那這個 .vite
底下的內容是什麼呢?
這正是我們的主角 —— dependency pre-bundle。但在詳細說明前,我們先看完最後一個實驗結果。
將 Vite 專案中原本的 lodash-es
改成 lodash
:
// main.js
import { has } from 'lodash';
console.log(has({ a: 1 }, 'a'));
此時能正確在 console 中印出 true
,代表 CommonJS 版本的第三方套件也能正常使用。再回到 dev tool 上觀察時,會看到原本的 CommonJS 版本的 lodash 被 pre-bundle 成這樣:
這裡會看到一個有趣的東西,其中的這個 chunk-BUSYA2B4.js
,簡單理解的話就是個拿來在 ESM 環境中模擬 CommonJS 中 require
機制的模組:
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
export {
__commonJS
};
從這裡我們可以得到另一個結論,就是在做 pre-bundle 過程中也會一併處理模組標準化為 ESM 的部分。
參考文件,Vite 為了解決這些問題:
會在啟用 dev server 前,將同一個套件的內容 pre-bundle 成單一個 ESM 模組,讓你在載入時只需要一個 request。而這個過程會交由打包速度更快的 esbuild 來處理,所以幾乎感覺不到冷啟動時的延遲。
如果你回到 VS Code 確認 node_modules/.vite/deps/lodash-es.js
這支檔案,可以看到原本的 lodash-es
中的所有模組會被包在這裡面,而 has
也在其中:
再多觀察一些,在載入 lodash-es.js
這個 pre-bundle 好的模組時,後面有帶上一個 v=671bbcf6
這樣的 hash,看起來是為了瀏覽器快取做的。我們可以試著將 dev tool 上「Disable cache」的選項勾掉後重整,此時再觀察一次會發現瀏覽器能自動抓取 cache 中的內容:
這是 Vite 做的一個快取的優化 (ref)。因為一般來說在開發時不會頻繁地更動套件的版本,所以帶上這樣的 hash 能更方便瀏覽器去決定是否觸發快取失效。
再深入了解一些,文件中提到 Vite 對於這些 pre-bundle 的 response header 是用 max-age=31536000,immutable
這樣的快取策略,這是什麼意思呢?這裡也筆記一下:
max-age=31536000
:指的是這個 cache 保存期限為一年immutable
:當資源被標記為 immutable (不可變) 時,代表保證在保存期限內都不會更動,以此來減少瀏覽器對 dev server 不必要的 request而文件中也提到,如果今天想要強制觸發 pre-bundle,有兩種方式:
node_modules/.vite
"dev": "vite --force"
這兩天從三個實驗搭配實際使用 Vite 的效果,我們能由淺入深理解為什麼 Vite 會需要做 dependency pre-bundling,整理一下重點:
而除了解決以上問題之外,Vite 還幫忙做了像是快取優化、使用更快的打包工具 esbuild 來協助 pre-bundle,來加速冷啟動的效率。
第一次學習這部份如果有什麼看不懂或是講錯的部份也歡迎留言討論,感謝您的閱讀!