容哈?;?ESM UI Provider 啟動資源:統(tǒng)一模塊身份與緩存失效的設(shè)計(jì)實(shí)踐)
Halo 內(nèi)容哈希化 ESM UI Provider 啟動資源統(tǒng)一模塊身份與緩存失效的設(shè)計(jì)實(shí)踐【免費(fèi)下載鏈接】haloHalo 是一款強(qiáng)大易用的開源建站工具從個(gè)人博客、知識庫到企業(yè)官網(wǎng)、在線商城Halo 都能助您輕松實(shí)現(xiàn)一站式滿足您的多樣化建站需求。項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ha/haloHalo 的 UI Provider插件 / 主題的前端模塊通過 Vite 或 Rsbuild 打包后由運(yùn)行時(shí)加載 ESM 入口并執(zhí)行PluginModule。本文圍繞 openspec 變更設(shè)計(jì)文檔講解 Halo 如何將默認(rèn) ESM 啟動資源改為內(nèi)容哈希命名、讓清單manifest記錄真實(shí)產(chǎn)物、并在描述符 URL 中去掉查詢緩存鍵從而消除“同一份模塊被瀏覽器加載兩次”的隱患。讀完你既能理解這套緩存與模塊身份機(jī)制的來龍去脈也能在構(gòu)建真實(shí)插件/主題時(shí)正確配置與驗(yàn)證產(chǎn)物。背景問題穩(wěn)定的入口名 查詢緩存鍵破壞了 ESM 模塊身份在引入本設(shè)計(jì)之前Halo 運(yùn)行時(shí)通過描述符descriptor以如下形式加載 ESM Provider 入口main.js?vprovider-cache-key這里的?v是 Halo 用于突破瀏覽器緩存的歷史手段。問題在于Vite/Rsbuild 產(chǎn)物中的異步 chunk 會靜態(tài) import 入口模塊的導(dǎo)出而 chunk 內(nèi)部生成的引用是相對路徑../main.js并不帶查詢參數(shù)。瀏覽器模塊系統(tǒng)規(guī)定URL query 參與模塊身份判定。因此運(yùn)行時(shí)最初 import 的是main.js?vprovider-cache-keychunk 反向 import 的是../main.js無 query。二者被瀏覽器視為兩個(gè)完全不同的模塊入口模塊代碼可能被二次請求、二次求值。更糟的是Halo 對生產(chǎn)靜態(tài)資源默認(rèn)設(shè)置一年的長緩存chunk 中無 query 的引用可能命中并復(fù)用舊的、長期緩存的穩(wěn)定入口與新的異步 chunk 形成版本錯(cuò)位。這正是本變更要解決的核心矛盾在“內(nèi)容可尋址content-addressed”成為瀏覽器緩存事實(shí)標(biāo)準(zhǔn)的環(huán)境里穩(wěn)定文件名必須由內(nèi)容哈希代替URL 必須只有一個(gè)“規(guī)范形態(tài)canonical form”。目標(biāo)與非目標(biāo)依據(jù)設(shè)計(jì)文檔design.md本次變更的目標(biāo)是讓默認(rèn)的 Vite 與 Rsbuild ESM 啟動 JavaScript 擁有內(nèi)容哈希文件名讓所有指向 ESM 入口的引用都解析到同一個(gè)、不帶 query 的規(guī)范 URL在ui-plugin.json中記錄 Vite 與 Rsbuild 實(shí)際產(chǎn)出的入口文件名與啟動樣式文件名保持 IIFE舊版兼容輸出與調(diào)用方 API 完全不變。對應(yīng)的非目標(biāo)明確不做包括不強(qiáng)制校驗(yàn)調(diào)用方覆蓋后的哈希命名、不改變 Provider 清單 schema 與遺留聚合端點(diǎn)、不在構(gòu)建后對 Provider 模塊做熱替換。決策一默認(rèn) ESM 啟動資源使用內(nèi)容哈希命名兩種打包器各自沿用其原生的緩存失效模型構(gòu)建器 / 產(chǎn)物默認(rèn)文件名規(guī)則語義Vite ESM 入口main.[hash].jsVite 內(nèi)置[hash]基于產(chǎn)物內(nèi)容推導(dǎo)Vite ESM chunkchunks/[name].[hash].js異步 chunk 獨(dú)立尋址Vite ESM 資源assets/[name].[hash][extname]未被內(nèi)聯(lián)的圖片等資源Rsbuild ESM 入口main.[contenthash:8].jsRspack 內(nèi)容哈希截取 8 位Rsbuild ESM 啟動樣式style.[contenthash:8].css與 Vite 端對齊Rsbuild 其余 JS/CSS[name].[contenthash:8].js/[name].[contenthash:8].css異步 chunk / 樣式同樣帶哈希IIFE 產(chǎn)物兩種構(gòu)建器main.js/style.css保持穩(wěn)定兼容舊版Vite 側(cè)源碼證據(jù)在 ui/packages/ui-plugin-bundler-kit/src/vite.ts 中當(dāng)格式被選定為 ESM 時(shí)預(yù)設(shè)輸出配置為cssCodeSplit: true, rollupOptions: { external: [...SHARED_PACKAGE_ROOTS], input: src/index.ts, preserveEntrySignatures: allow-extension, output: { format: es, entryFileNames: main.[hash].js, chunkFileNames: chunks/[name].[hash].js, assetFileNames: assets/[name].[hash][extname], }, },同文件中 IIFE 分支仍使用fileName: () main.js與cssFileName: stylevite.ts并帶有TODO(Halo 3): Remove after legacy IIFE UI provider support ends的注釋——說明穩(wěn)定 IIFE 名稱是面向舊版 Halo 與聚合加載的兼容性資產(chǎn)本次不觸碰。Rsbuild 側(cè)源碼證據(jù)在 ui/packages/ui-plugin-bundler-kit/src/rsbuild.ts 中文件名按 chunk 名分支生成css: (pathData) pathData.chunk?.name main ? format esm ? style.[contenthash:8].css : style.css : [name].[contenthash:8].css, js: (pathData) pathData.chunk?.name main ? format esm ? main.[contenthash:8].js : main.js : [name].[contenthash:8].js,注意這里的主 chunk 恰好叫main它對 ESM 返回main.[contenthash:8].js、對 IIFE 返回穩(wěn)定的main.js異步產(chǎn)物與樣式則一律攜帶[contenthash:8]。ESM 模式的output還額外開啟module: true / chunkFormat: module / chunkLoading: import并對外部化共享包使用externalsType: module保證產(chǎn)物按原生 ESM 語義運(yùn)行。決策二清單由真實(shí)產(chǎn)物推導(dǎo)而不是寫死 main.js關(guān)鍵轉(zhuǎn)變是ui-plugin.json不再假設(shè)入口一定叫main.js而是把打包器實(shí)際產(chǎn)出的入口路徑寫進(jìn)清單。清單 schemaui-plugin.json常量ESM_PROVIDER_MANIFEST見 provider-manifest.ts的 schema 為{ format: esm, entry: ./main.ab12cd34.js, style: ./style.ef56gh78.css }format固定為esm用于與 IIFE 產(chǎn)物區(qū)分entry實(shí)際產(chǎn)出的 ESM 入口相對路徑必填style最多一個(gè)啟動樣式相對路徑可選。入口與樣式路徑需滿足“Provider 根相對”約束不能以/、協(xié)議頭開頭不能帶?/#也不允許../逃逸出 Provider 資源根目錄provider-manifest.ts。validateEsmProviderManifest負(fù)責(zé)在構(gòu)建期強(qiáng)校驗(yàn)寫入前做./規(guī)范化。Vite從 chunk 元數(shù)據(jù)讀取Vite 端在generateBundle后處理階段vite-esm.ts執(zhí)行過濾出產(chǎn)物 bundle 中的全部 chunk對 chunk 源碼執(zhí)行共享依賴校驗(yàn)SharedDependencyValidator取isEntry的 chunk斷言恰好一個(gè)入口且入口導(dǎo)出包含 default否則報(bào)錯(cuò)ESM UI provider output must contain one entry with a default PluginModule export通過 Vite 輸出 chunk 元數(shù)據(jù)viteMetadata.importedCss拿到入口關(guān)聯(lián)的 CSS 集合斷言至多一個(gè)入口樣式拼出 manifest 后以 asset 形式emitFile生成ui-plugin.json。const entryStyles [ ...((entries[0] as ViteOutputChunkMetadata).viteMetadata ?.importedCss || []), ].sort(); if (entryStyles.length 1) { throw new Error( ESM UI provider output must contain at most one entry stylesheet. ); }該路徑不重新檢查磁盤上的最終資源、也不二次產(chǎn)出文件因?yàn)槲募旧硪延深A(yù)設(shè)的[hash]規(guī)則保證派生自內(nèi)容。Rsbuild從 main 編譯入口點(diǎn)推導(dǎo)Rsbuild 端在processAssets的summarize階段rsbuild-esm.ts從 Rspack 編譯產(chǎn)物取證據(jù)const entryFiles compilation.entrypoints.get(main)?.getFiles() || []; const entryScripts entryFiles.filter((f) f.endsWith(.js)); if (entryScripts.length ! 1) { throw new Error( ESM UI provider output must contain exactly one entry JavaScript file. ); }隨后它還會斷言入口 asset 真實(shí)存在于產(chǎn)物assets[entryFile]缺失即報(bào)錯(cuò)讀取入口源碼文本用export default或export { ... default }正則驗(yàn)證入口確實(shí)暴露默認(rèn)PluginModule導(dǎo)出從mainentrypoint 的.css文件中取啟動樣式同樣約束至多一個(gè)用compilation.emitAsset把ui-plugin.json作為真實(shí)產(chǎn)物寫盤。這與“不用 manifest 插件、保持ui-plugin.json為唯一 Provider 契約”的決策一致對應(yīng)任務(wù) tasks.md 中 2.1–2.3。與 schema 兼容的舊 UI 資產(chǎn)新語義從 UI 包既有結(jié)構(gòu)看本次設(shè)計(jì)沒有新增“能力capability”而是修改了兩個(gè)既有能力的行為ui-plugin-bundler-provider默認(rèn) ESM 啟動資源哈希化、清單記錄真實(shí)文件名與ui-plugin-esm-runtimeESM 啟動資源以規(guī)范的內(nèi)容尋址 URL 提供、不再攜帶查詢緩存鍵。IIFE 產(chǎn)物、Provider 源碼 API、manifest schema 與共享依賴機(jī)制均保持不變詳見 proposal.md。決策三ESM 啟動 URL 去掉查詢緩存鍵回歸單一定義設(shè)計(jì)文檔明確了第三條決策后端生成 ESM manifest 資源路徑時(shí)不附加任何 query 參數(shù)。這樣chunk 反引入口時(shí)用的無 query URL與 Halo 首次 import 入口用的 URL 完全一致 → 瀏覽器只保留一份模塊不會二次 fetch / 二次求值內(nèi)容哈希本身就是生產(chǎn)環(huán)境的失效手段入口內(nèi)容一變main.[hash].js立即變成新 URL天然繞開一年期的靜態(tài)資源長緩存開發(fā)環(huán)境靜態(tài)資源本就以no-cache提供且 watch 重建會因入口內(nèi)容變化而改變哈希文件名從而觸發(fā) manifest 與 URL 同步刷新見 design.md 決策三。而以下資源保留原有 query 緩存鍵遺留legacyJavaScript 與遺留樣式聚合 bundleaggregate bundleURL以其當(dāng)前目錄版本號catalog version為緩存鍵IIFE Provider 的穩(wěn)定main.js引用與既有全局變量。具體到 specs/ui-plugin-esm-runtime/spec.md 中的驗(yàn)收場景默認(rèn)預(yù)設(shè)產(chǎn)出、未覆蓋命名規(guī)則時(shí)入口與啟動樣式文件名必須含內(nèi)容哈希描述符 URL 使用清單選中的 Provider 相對路徑且不得追加 query異步 chunk 與資源使用 Provider 相對的內(nèi)容哈希 URL遺留聚合 URL 則必須帶當(dāng)前目錄版本緩存鍵。統(tǒng)一 URL 規(guī)則與回退目錄在ui-plugin-esm-runtime的既有要求中ESM 產(chǎn)物的模塊預(yù)加載、動態(tài) import、異步 CSS 與發(fā)散的資產(chǎn)都必須解析到“加載入口/樣式所在目錄”Provider-root 相對而不能硬編碼首選資源目錄ui。這意味著即使某插件目標(biāo)偏好ui資源目錄、Halo 卻通過 legacyconsole回退目錄發(fā)現(xiàn)完整 ESM 產(chǎn)物運(yùn)行時(shí)也能基于相對關(guān)系正確取回文件Provider 既有構(gòu)建腳本無需修改輸出拷貝目錄。異步 chunk 與 CSS 一律使用 Provider 根相對 URL同時(shí)適用于插件與主題兩種宿主。異步 CSS 的邊界另一個(gè)容易被忽略的細(xì)節(jié)當(dāng) CSS 只屬于某個(gè)異步import 的 JS chunk 時(shí)該 CSS 不應(yīng)出現(xiàn)在 Provider manifest 中manifest 只描述“啟動即需”的樣式而應(yīng)交給產(chǎn)出的 JavaScript 運(yùn)行時(shí)按需從 Provider 根安全 URL 加載。Vite 側(cè)對viteMetadata.importedCss的過濾、Rsbuild 側(cè)對mainentrypoint.css文件的過濾共同實(shí)現(xiàn)了這一語義詳見 specs/ui-plugin-bundler-provider/spec.md。緩存邊界整頁刷新是模塊替換的唯一契約ui-plugin-esm-runtime明確把“Console / UC 完整頁面加載”定義為受支持的模塊替換邊界當(dāng)插件或主題的 UI Provider 在頁面模塊圖已經(jīng)啟動之后發(fā)生安裝、升級、啟用、禁用、激活等變化時(shí)Halo 會要求或提示整頁刷新絕不熱卸載或熱替換已運(yùn)行的 Provider 模塊。這是配合內(nèi)容哈希命名的前提——既然緩存失效粒度就是“新 URL”舊模塊實(shí)例只能在下次頁面加載時(shí)整體退役。開發(fā)態(tài)的行為同樣寫入驗(yàn)收場景當(dāng)開發(fā)態(tài) Provider 被反復(fù)描述、且其直接加載產(chǎn)物未變化時(shí)清單選中的入口與樣式 URL 保持不變當(dāng) manifest、入口或啟動樣式變化時(shí)內(nèi)容哈希文件名與目錄版本隨之變化而其它未變化的 Provider 的直鏈資源 URL 不受牽連見 specs/ui-plugin-esm-runtime/spec.md。task 4.2 也要求實(shí)際構(gòu)建 Vite 與 Rsbuild 的插件/主題工程驗(yàn)證 manifest、反向入口 import 與開發(fā)態(tài) watch 重建行為tasks.md。調(diào)用方覆蓋保留逃生艙但責(zé)任隨之上移設(shè)計(jì)文檔反復(fù)強(qiáng)調(diào)“equivalence SHALL NOT be claimed after caller overrides”——Vite 與 Rsbuild 的等價(jià)性只在默認(rèn)預(yù)設(shè)下成立。當(dāng)調(diào)用方通過原生配置或構(gòu)建鉤子改動依賴解析、格式、入口、public path、資源命名、優(yōu)化或輸出時(shí)helper 只會把用戶配置合并到預(yù)設(shè)之后不會試圖證明或恢復(fù)默認(rèn) ESM 契約specs/ui-plugin-bundler-provider/spec.md 的 “Caller overrides ESM preset output” 場景。此時(shí)manifest 一致性、瀏覽器解析、運(yùn)行時(shí)模塊身份、資源搬遷與緩存失效全部由調(diào)用方負(fù)責(zé)Halo 不會去改寫覆蓋后的資源也不會給 ESM 入口/樣式 URL 追加 query若調(diào)用方把入口改回穩(wěn)定的main.js一年期的生產(chǎn)長緩存可能導(dǎo)致舊模塊被復(fù)用——緩存正確性成為 Provider 開發(fā)者自己的責(zé)任。此外默認(rèn) ESM 預(yù)設(shè)還必須滿足一個(gè)細(xì)節(jié)Vite 應(yīng)將 Provider 視為最終瀏覽器入口而非保留空白語義的 library 分發(fā)產(chǎn)物來構(gòu)建但需保留入口模塊的導(dǎo)出簽名與預(yù)設(shè)的相對資源/內(nèi)容哈希默認(rèn)值ESM 生產(chǎn)默認(rèn)開啟 JS 與 CSS 壓縮minification且默認(rèn)預(yù)設(shè)產(chǎn)出的異步 JS/CSS 與未被內(nèi)聯(lián)資源都必須攜帶內(nèi)容哈希文件名specs/ui-plugin-bundler-provider/spec.md。風(fēng)險(xiǎn)與權(quán)衡設(shè)計(jì)文檔對三個(gè)主要風(fēng)險(xiǎn)給出了明確回應(yīng)調(diào)用方覆蓋后恢復(fù)穩(wěn)定 ESM 名→ 保留此前文檔化的逃生艙生產(chǎn)緩存正確性歸 Provider 開發(fā)者負(fù)責(zé)。Provider 包內(nèi)含過期哈希文件→ 描述符只引用 manifest 選中的入口且默認(rèn)構(gòu)建會清理輸出目錄ViteemptyOutDir: true、RsbuildcleanDistPath: true臟文件不會被引用。哈希文件名改變既有產(chǎn)物斷言→ 測試與消費(fèi)者應(yīng)改為讀取ui-plugin.json而不是假設(shè) ESM 一定叫main.js。這一點(diǎn)在 bundler-kit 的測試與真實(shí)的插件/主題構(gòu)建斷言中均有覆蓋例如 ui/packages/ui-plugin-bundler-kit/src/tests/provider.spec.ts 中同時(shí)斷言了 Vite 的main.[hash].js與 Rsbuild 的main.[contenthash:8].js、style.[contenthash:8].css產(chǎn)物形態(tài)。從任務(wù)清單看落地的完整閉環(huán)該變更在歸檔任務(wù)清單 tasks.md 中表現(xiàn)為四個(gè)已完成階段鎖定啟動資源行為為 Vite/Rsbuild 增加真實(shí)構(gòu)建斷言默認(rèn) ESM manifest 引用內(nèi)容哈希入口/樣式增加 Vite 回歸斷言哈希 chunk 反引入口必須指向同一內(nèi)容哈希入口而非穩(wěn)定main.js別名增加后端描述符斷言ESM 入口/樣式 URL 無 query、legacy URL 保留 query。產(chǎn)出內(nèi)容尋址的 ESM 啟動資源Vite 與 Rsbuild 默認(rèn) ESM 文件名內(nèi)容哈希化IIFE 輸出穩(wěn)定Rsbuild 從實(shí)際main編譯入口點(diǎn)推導(dǎo) manifest 的 entry 與可選啟動樣式。規(guī)范化運(yùn)行時(shí) URLmanifest 選中的 ESM 入口/樣式路徑不帶 querylegacy 資源與聚合保持 cache key更新緩存邊界文檔。驗(yàn)證執(zhí)行 bundler-kit 聚焦測試、類型檢查、包構(gòu)建、后端服務(wù)測試、格式檢查與 OpenSpec 嚴(yán)格校驗(yàn)并真實(shí)構(gòu)建 Vite/Rsbuild 插件與主題工程核驗(yàn)。關(guān)聯(lián)資料變更文檔design.md、proposal.md、tasks.md驗(yàn)收規(guī)格specs/ui-plugin-bundler-provider/spec.md、specs/ui-plugin-esm-runtime/spec.md實(shí)現(xiàn)源碼Vite 預(yù)設(shè) vite.ts、Rsbuild 預(yù)設(shè) rsbuild.ts、Vite ESM 插件 vite-esm.ts、Rsbuild ESM 插件 rsbuild-esm.ts、清單 schema 與路徑校驗(yàn) provider-manifest.ts【免費(fèi)下載鏈接】haloHalo 是一款強(qiáng)大易用的開源建站工具從個(gè)人博客、知識庫到企業(yè)官網(wǎng)、在線商城Halo 都能助您輕松實(shí)現(xiàn)一站式滿足您的多樣化建站需求。項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ha/halo創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考