
Halo 内容哈希化 ESM UI Provider 启动资源统一模块身份与缓存失效的设计实践【免费下载链接】haloHalo 是一款强大易用的开源建站工具从个人博客、知识库到企业官网、在线商城Halo 都能助您轻松实现一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/haloHalo 的 UI Provider插件 / 主题的前端模块通过 Vite 或 Rsbuild 打包后由运行时加载 ESM 入口并执行PluginModule。本文围绕 openspec 变更设计文档讲解 Halo 如何将默认 ESM 启动资源改为内容哈希命名、让清单manifest记录真实产物、并在描述符 URL 中去掉查询缓存键从而消除“同一份模块被浏览器加载两次”的隐患。读完你既能理解这套缓存与模块身份机制的来龙去脉也能在构建真实插件/主题时正确配置与验证产物。背景问题稳定的入口名 查询缓存键破坏了 ESM 模块身份在引入本设计之前Halo 运行时通过描述符descriptor以如下形式加载 ESM Provider 入口main.js?vprovider-cache-key这里的?v是 Halo 用于突破浏览器缓存的历史手段。问题在于Vite/Rsbuild 产物中的异步 chunk 会静态 import 入口模块的导出而 chunk 内部生成的引用是相对路径../main.js并不带查询参数。浏览器模块系统规定URL query 参与模块身份判定。因此运行时最初 import 的是main.js?vprovider-cache-keychunk 反向 import 的是../main.js无 query。二者被浏览器视为两个完全不同的模块入口模块代码可能被二次请求、二次求值。更糟的是Halo 对生产静态资源默认设置一年的长缓存chunk 中无 query 的引用可能命中并复用旧的、长期缓存的稳定入口与新的异步 chunk 形成版本错位。这正是本变更要解决的核心矛盾在“内容可寻址content-addressed”成为浏览器缓存事实标准的环境里稳定文件名必须由内容哈希代替URL 必须只有一个“规范形态canonical form”。目标与非目标依据设计文档design.md本次变更的目标是让默认的 Vite 与 Rsbuild ESM 启动 JavaScript 拥有内容哈希文件名让所有指向 ESM 入口的引用都解析到同一个、不带 query 的规范 URL在ui-plugin.json中记录 Vite 与 Rsbuild 实际产出的入口文件名与启动样式文件名保持 IIFE旧版兼容输出与调用方 API 完全不变。对应的非目标明确不做包括不强制校验调用方覆盖后的哈希命名、不改变 Provider 清单 schema 与遗留聚合端点、不在构建后对 Provider 模块做热替换。决策一默认 ESM 启动资源使用内容哈希命名两种打包器各自沿用其原生的缓存失效模型构建器 / 产物默认文件名规则语义Vite ESM 入口main.[hash].jsVite 内置[hash]基于产物内容推导Vite ESM chunkchunks/[name].[hash].js异步 chunk 独立寻址Vite ESM 资源assets/[name].[hash][extname]未被内联的图片等资源Rsbuild ESM 入口main.[contenthash:8].jsRspack 内容哈希截取 8 位Rsbuild ESM 启动样式style.[contenthash:8].css与 Vite 端对齐Rsbuild 其余 JS/CSS[name].[contenthash:8].js/[name].[contenthash:8].css异步 chunk / 样式同样带哈希IIFE 产物两种构建器main.js/style.css保持稳定兼容旧版Vite 侧源码证据在 ui/packages/ui-plugin-bundler-kit/src/vite.ts 中当格式被选定为 ESM 时预设输出配置为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的注释——说明稳定 IIFE 名称是面向旧版 Halo 与聚合加载的兼容性资产本次不触碰。Rsbuild 侧源码证据在 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 返回稳定的main.js异步产物与样式则一律携带[contenthash:8]。ESM 模式的output还额外开启module: true / chunkFormat: module / chunkLoading: import并对外部化共享包使用externalsType: module保证产物按原生 ESM 语义运行。决策二清单由真实产物推导而不是写死 main.js关键转变是ui-plugin.json不再假设入口一定叫main.js而是把打包器实际产出的入口路径写进清单。清单 schemaui-plugin.json常量ESM_PROVIDER_MANIFEST见 provider-manifest.ts的 schema 为{ format: esm, entry: ./main.ab12cd34.js, style: ./style.ef56gh78.css }format固定为esm用于与 IIFE 产物区分entry实际产出的 ESM 入口相对路径必填style最多一个启动样式相对路径可选。入口与样式路径需满足“Provider 根相对”约束不能以/、协议头开头不能带?/#也不允许../逃逸出 Provider 资源根目录provider-manifest.ts。validateEsmProviderManifest负责在构建期强校验写入前做./规范化。Vite从 chunk 元数据读取Vite 端在generateBundle后处理阶段vite-esm.ts执行过滤出产物 bundle 中的全部 chunk对 chunk 源码执行共享依赖校验SharedDependencyValidator取isEntry的 chunk断言恰好一个入口且入口导出包含 default否则报错ESM UI provider output must contain one entry with a default PluginModule export通过 Vite 输出 chunk 元数据viteMetadata.importedCss拿到入口关联的 CSS 集合断言至多一个入口样式拼出 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. ); }该路径不重新检查磁盘上的最终资源、也不二次产出文件因为文件名本身已由预设的[hash]规则保证派生自内容。Rsbuild从 main 编译入口点推导Rsbuild 端在processAssets的summarize阶段rsbuild-esm.ts从 Rspack 编译产物取证据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 真实存在于产物assets[entryFile]缺失即报错读取入口源码文本用export default或export { ... default }正则验证入口确实暴露默认PluginModule导出从mainentrypoint 的.css文件中取启动样式同样约束至多一个用compilation.emitAsset把ui-plugin.json作为真实产物写盘。这与“不用 manifest 插件、保持ui-plugin.json为唯一 Provider 契约”的决策一致对应任务 tasks.md 中 2.1–2.3。与 schema 兼容的旧 UI 资产新语义从 UI 包既有结构看本次设计没有新增“能力capability”而是修改了两个既有能力的行为ui-plugin-bundler-provider默认 ESM 启动资源哈希化、清单记录真实文件名与ui-plugin-esm-runtimeESM 启动资源以规范的内容寻址 URL 提供、不再携带查询缓存键。IIFE 产物、Provider 源码 API、manifest schema 与共享依赖机制均保持不变详见 proposal.md。决策三ESM 启动 URL 去掉查询缓存键回归单一定义设计文档明确了第三条决策后端生成 ESM manifest 资源路径时不附加任何 query 参数。这样chunk 反引入口时用的无 query URL与 Halo 首次 import 入口用的 URL 完全一致 → 浏览器只保留一份模块不会二次 fetch / 二次求值内容哈希本身就是生产环境的失效手段入口内容一变main.[hash].js立即变成新 URL天然绕开一年期的静态资源长缓存开发环境静态资源本就以no-cache提供且 watch 重建会因入口内容变化而改变哈希文件名从而触发 manifest 与 URL 同步刷新见 design.md 决策三。而以下资源保留原有 query 缓存键遗留legacyJavaScript 与遗留样式聚合 bundleaggregate bundleURL以其当前目录版本号catalog version为缓存键IIFE Provider 的稳定main.js引用与既有全局变量。具体到 specs/ui-plugin-esm-runtime/spec.md 中的验收场景默认预设产出、未覆盖命名规则时入口与启动样式文件名必须含内容哈希描述符 URL 使用清单选中的 Provider 相对路径且不得追加 query异步 chunk 与资源使用 Provider 相对的内容哈希 URL遗留聚合 URL 则必须带当前目录版本缓存键。统一 URL 规则与回退目录在ui-plugin-esm-runtime的既有要求中ESM 产物的模块预加载、动态 import、异步 CSS 与发散的资产都必须解析到“加载入口/样式所在目录”Provider-root 相对而不能硬编码首选资源目录ui。这意味着即使某插件目标偏好ui资源目录、Halo 却通过 legacyconsole回退目录发现完整 ESM 产物运行时也能基于相对关系正确取回文件Provider 既有构建脚本无需修改输出拷贝目录。异步 chunk 与 CSS 一律使用 Provider 根相对 URL同时适用于插件与主题两种宿主。异步 CSS 的边界另一个容易被忽略的细节当 CSS 只属于某个异步import 的 JS chunk 时该 CSS 不应出现在 Provider manifest 中manifest 只描述“启动即需”的样式而应交给产出的 JavaScript 运行时按需从 Provider 根安全 URL 加载。Vite 侧对viteMetadata.importedCss的过滤、Rsbuild 侧对mainentrypoint.css文件的过滤共同实现了这一语义详见 specs/ui-plugin-bundler-provider/spec.md。缓存边界整页刷新是模块替换的唯一契约ui-plugin-esm-runtime明确把“Console / UC 完整页面加载”定义为受支持的模块替换边界当插件或主题的 UI Provider 在页面模块图已经启动之后发生安装、升级、启用、禁用、激活等变化时Halo 会要求或提示整页刷新绝不热卸载或热替换已运行的 Provider 模块。这是配合内容哈希命名的前提——既然缓存失效粒度就是“新 URL”旧模块实例只能在下次页面加载时整体退役。开发态的行为同样写入验收场景当开发态 Provider 被反复描述、且其直接加载产物未变化时清单选中的入口与样式 URL 保持不变当 manifest、入口或启动样式变化时内容哈希文件名与目录版本随之变化而其它未变化的 Provider 的直链资源 URL 不受牵连见 specs/ui-plugin-esm-runtime/spec.md。task 4.2 也要求实际构建 Vite 与 Rsbuild 的插件/主题工程验证 manifest、反向入口 import 与开发态 watch 重建行为tasks.md。调用方覆盖保留逃生舱但责任随之上移设计文档反复强调“equivalence SHALL NOT be claimed after caller overrides”——Vite 与 Rsbuild 的等价性只在默认预设下成立。当调用方通过原生配置或构建钩子改动依赖解析、格式、入口、public path、资源命名、优化或输出时helper 只会把用户配置合并到预设之后不会试图证明或恢复默认 ESM 契约specs/ui-plugin-bundler-provider/spec.md 的 “Caller overrides ESM preset output” 场景。此时manifest 一致性、浏览器解析、运行时模块身份、资源搬迁与缓存失效全部由调用方负责Halo 不会去改写覆盖后的资源也不会给 ESM 入口/样式 URL 追加 query若调用方把入口改回稳定的main.js一年期的生产长缓存可能导致旧模块被复用——缓存正确性成为 Provider 开发者自己的责任。此外默认 ESM 预设还必须满足一个细节Vite 应将 Provider 视为最终浏览器入口而非保留空白语义的 library 分发产物来构建但需保留入口模块的导出签名与预设的相对资源/内容哈希默认值ESM 生产默认开启 JS 与 CSS 压缩minification且默认预设产出的异步 JS/CSS 与未被内联资源都必须携带内容哈希文件名specs/ui-plugin-bundler-provider/spec.md。风险与权衡设计文档对三个主要风险给出了明确回应调用方覆盖后恢复稳定 ESM 名→ 保留此前文档化的逃生舱生产缓存正确性归 Provider 开发者负责。Provider 包内含过期哈希文件→ 描述符只引用 manifest 选中的入口且默认构建会清理输出目录ViteemptyOutDir: true、RsbuildcleanDistPath: true脏文件不会被引用。哈希文件名改变既有产物断言→ 测试与消费者应改为读取ui-plugin.json而不是假设 ESM 一定叫main.js。这一点在 bundler-kit 的测试与真实的插件/主题构建断言中均有覆盖例如 ui/packages/ui-plugin-bundler-kit/src/tests/provider.spec.ts 中同时断言了 Vite 的main.[hash].js与 Rsbuild 的main.[contenthash:8].js、style.[contenthash:8].css产物形态。从任务清单看落地的完整闭环该变更在归档任务清单 tasks.md 中表现为四个已完成阶段锁定启动资源行为为 Vite/Rsbuild 增加真实构建断言默认 ESM manifest 引用内容哈希入口/样式增加 Vite 回归断言哈希 chunk 反引入口必须指向同一内容哈希入口而非稳定main.js别名增加后端描述符断言ESM 入口/样式 URL 无 query、legacy URL 保留 query。产出内容寻址的 ESM 启动资源Vite 与 Rsbuild 默认 ESM 文件名内容哈希化IIFE 输出稳定Rsbuild 从实际main编译入口点推导 manifest 的 entry 与可选启动样式。规范化运行时 URLmanifest 选中的 ESM 入口/样式路径不带 querylegacy 资源与聚合保持 cache key更新缓存边界文档。验证执行 bundler-kit 聚焦测试、类型检查、包构建、后端服务测试、格式检查与 OpenSpec 严格校验并真实构建 Vite/Rsbuild 插件与主题工程核验。关联资料变更文档design.md、proposal.md、tasks.md验收规格specs/ui-plugin-bundler-provider/spec.md、specs/ui-plugin-esm-runtime/spec.md实现源码Vite 预设 vite.ts、Rsbuild 预设 rsbuild.ts、Vite ESM 插件 vite-esm.ts、Rsbuild ESM 插件 rsbuild-esm.ts、清单 schema 与路径校验 provider-manifest.ts【免费下载链接】haloHalo 是一款强大易用的开源建站工具从个人博客、知识库到企业官网、在线商城Halo 都能助您轻松实现一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/halo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考