Nuxt 3 生产环境如何发现新版本:app:manifest:update 源码解析

📅 2026/8/26 18:23:51
Nuxt 3 生产环境如何发现新版本:app:manifest:update 源码解析
SPA 应用在生产环境中经常遇到一个问题用户长时间停留在旧页面其间服务端部署了新版本原有 JS Chunk 被替换或删除。用户再次切换路由时浏览器仍会尝试加载旧 Chunk最终可能因为文件不存在而报错甚至白屏Nuxt 为此提供了一套过期构建检测与页面刷新机制其中app:manifest:update是连接“发现新版本”和“执行更新策略”的关键钩子整套机制主要由三个客户端插件协作完成插件职责check-outdated-build.client.js轮询检测是否出现新构建chunk-reload.client.js检测到更新后在下次导航时刷新默认策略chunk-reload-immediate.client.js检测到更新后立即刷新1. Manifest 文件结构Nuxt 构建时会生成两类 Manifest 文件.output/public/_nuxt/builds/ ├── latest.json # 指向最新构建的 ID └── meta/ └── {buildId}.json # 每次构建的详细元数据latest.json只包含当前最新构建的 ID{ id: a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx }meta/{buildId}.json保存对应构建的详细信息包括路由匹配规则等内容客户端通过getAppManifest()获取并缓存每次构建都会生成新的buildId而latest.json始终指向最新构建。客户端只需比较本地 Manifest 与远端latest.json的 ID就能判断当前页面是否已经过期2. 检测新构建check-outdated-build.client.jscheck-outdated-build.client.js负责定时请求latest.json并比较构建 ID// nuxt/dist/app/plugins/check-outdated-build.client.js import { getAppManifest } from ../composables/manifest.js; import { onNuxtReady } from ../composables/ready.js; import { buildAssetsURL } from #internal/nuxt/paths; import { outdatedBuildInterval } from #build/nuxt.config.mjs; export default defineNuxtPlugin((nuxtApp) { if (import.meta.test) return; let timeout; async function getLatestManifest() { // 获取当前客户端缓存的 manifest let currentManifest; try { currentManifest await getAppManifest(); } catch (e) { if (!(status in e (e.status 404 || e.status 403))) { throw e; } } if (timeout) clearTimeout(timeout); // 设置下一次轮询 timeout setTimeout(getLatestManifest, outdatedBuildInterval); try { // 请求最新的 latest.json并通过时间戳绕过缓存 const meta await $fetch( buildAssetsURL(builds/latest.json) ?${Date.now()} ); // ID 不一致说明出现了新构建 if (meta.id ! currentManifest?.id) { nuxtApp.hooks.callHook(app:manifest:update, meta); // 检测到更新后停止轮询 if (timeout) clearTimeout(timeout); } } catch {} } // 应用就绪后开始首次轮询 onNuxtReady(() { timeout setTimeout(getLatestManifest, outdatedBuildInterval); }); });这段代码有几个值得注意的细节通过时间戳绕过缓存请求地址后拼接?${Date.now()}用于绕过浏览器或 CDN 缓存避免客户端反复拿到旧版latest.json应用就绪后再轮询检测任务通过onNuxtReady启动不会在应用初始化阶段立即发起请求避免干扰首屏加载和 Hydration检测到更新后停止轮询发现构建 ID 已变化后插件会触发app:manifest:update随后清除定时器。接下来如何刷新页面由对应的 Chunk Reload 插件或自定义逻辑处理轮询间隔可以配置轮询间隔来自outdatedBuildInterval对应nuxt.config.ts中的experimental.checkOutdatedBuildInterval3. 两种内置响应策略检测到新构建后Nuxt 会触发nuxtApp.hooks.callHook(app:manifest:update, meta)接下来使用延迟刷新还是立即刷新由experimental.emitRouteChunkError决定4. 默认策略下次导航时刷新默认加载的是chunk-reload.client.js// chunk-reload.client.js export default defineNuxtPlugin({ name: nuxt:chunk-reload, setup(nuxtApp) { const router useRouter(); const config useRuntimeConfig(); const chunkErrors new Set(); router.beforeEach(() { chunkErrors.clear(); }); nuxtApp.hook(app:chunkError, ({ error }) { chunkErrors.add(error); }); function reloadAppAtPath(to) { const path joinURL(config.app.baseURL, to.fullPath); reloadNuxtApp({ path, persistState: true }); } // 检测到新构建后在下次导航时刷新 nuxtApp.hook(app:manifest:update, () { router.beforeResolve(reloadAppAtPath); }); // Chunk 加载失败时也执行刷新 router.onError((error, to) { if (chunkErrors.has(error)) { reloadAppAtPath(to); } }); }, });这个策略不会在检测到更新后立即打断当前操作而是向router.beforeResolve注册一个路由守卫等用户主动切换路由时Nuxt 才会刷新页面并从目标地址加载最新构建它还处理了app:chunkError如果旧 Chunk 已经无法加载路由错误回调会直接调用reloadNuxtApp避免页面继续停留在不可用状态5. 立即刷新策略将emitRouteChunkError配置为automatic-immediate后Nuxt 会加载chunk-reload-immediate.client.js// chunk-reload-immediate.client.js export default defineNuxtPlugin({ name: nuxt:chunk-reload-immediate, setup(nuxtApp) { const config useRuntimeConfig(); let currentlyNavigatingTo null; addRouteMiddleware((to) { currentlyNavigatingTo to; }); function reloadAppAtPath(to) { const path joinURL(config.app.baseURL, to.fullPath); reloadNuxtApp({ path, persistState: true }); } nuxtApp.hook(app:chunkError, () reloadAppAtPath(currentlyNavigatingTo ?? nuxtApp._route) ); // 检测到新构建后立即刷新当前路由 nuxtApp.hook(app:manifest:update, () reloadAppAtPath(nuxtApp._route) ); }, });与默认策略不同这个插件不会等待用户下一次导航一旦收到app:manifest:update它就会在当前路由调用reloadNuxtApp让页面立即切换到新构建。它更适合版本一致性要求较高、不能让旧页面继续运行的场景6.reloadNuxtApp如何避免刷新循环两种内置策略最终都会调用reloadNuxtApp它不只是简单执行location.reload()还负责防止重复刷新、保存页面状态以及处理目标路径// nuxt/dist/app/composables/chunk.js export function reloadNuxtApp(options {}) { if (import.meta.server) return; const path options.path || window.location.pathname; // 从 sessionStorage 读取上次刷新记录 let handledPath {}; try { handledPath destr( sessionStorage.getItem(nuxt:reload) || {} ); } catch {} // 满足以下任一条件时执行刷新 // 1. force 强制刷新 // 2. 目标路径与上次记录不同 // 3. 上次刷新记录已经过期 if ( options.force || handledPath?.path ! path || handledPath?.expires Date.now() ) { // 记录本次刷新避免形成刷新循环 try { sessionStorage.setItem( nuxt:reload, JSON.stringify({ path, expires: Date.now() (options.ttl ?? 10000), }) ); } catch {} // 保存当前 Nuxt payload state if (options.persistState) { try { sessionStorage.setItem( nuxt:reload:state, JSON.stringify({ state: useNuxtApp().payload.state, }) ); } catch {} } // 根据目标路径决定跳转还是原地刷新 if (window.location.pathname ! path) { window.location.href path; } else { window.location.reload(); } } }这部分主要解决三个问题防止重复刷新sessionStorage会记录刷新路径和过期时间默认 TTL 为 10 秒。同一路径在记录未过期时不会重复刷新从而避免“刷新后再次检测到更新又继续刷新”的循环保留应用状态传入persistState: true后当前的useNuxtApp().payload.state会被序列化到sessionStorage为刷新后的状态恢复保留数据区分跳转与原地刷新如果目标路径和当前路径不同使用window.location.href跳转如果路径相同则调用window.location.reload()原地刷新7. 相关配置项这套机制主要由nuxt.config.ts中的三个实验性配置项控制// nuxt.config.ts export default defineNuxtConfig({ experimental: { // automatic // 使用 chunk-reload.client.js下次导航时刷新 // // automatic-immediate // 使用 chunk-reload-immediate.client.js立即刷新 // // false // 禁用内置 Chunk Reload emitRouteChunkError: automatic, // 过期构建检测间隔单位为毫秒 // 默认值为 3600000即 1 小时 // 设置为 false 可禁用过期构建检测 checkOutdatedBuildInterval: 1000 * 60 * 60, // Manifest 机制的总开关 appManifest: true, }, });插件注册逻辑可以简化为// nuxt/dist/index.mjs简化 if ( nuxt.options.experimental.emitRouteChunkError automatic ) { addPlugin(plugins/chunk-reload.client); } if ( nuxt.options.experimental.emitRouteChunkError automatic-immediate ) { addPlugin(plugins/chunk-reload-immediate.client); } if (nuxt.options.experimental.appManifest) { if ( nuxt.options.experimental.checkOutdatedBuildInterval ! false ) { addPlugin(plugins/check-outdated-build.client); } }三个配置项分别控制不同环节appManifest控制是否启用 Manifest 机制checkOutdatedBuildInterval控制是否检测过期构建以及检测频率emitRouteChunkError决定使用延迟刷新、立即刷新还是关闭内置刷新策略8. 自定义更新确认弹窗Nuxt 内置策略要么等待下一次导航要么立即刷新都不会询问用户如果业务中存在未提交表单或其他不适合被突然打断的操作可以关闭内置 Chunk Reload保留 Manifest 检测再自行监听app:manifest:update关闭内置自动刷新// nuxt.config.ts export default defineNuxtConfig({ experimental: { // 禁用内置 Chunk Reload emitRouteChunkError: false, // 每 30 分钟检测一次新构建 checkOutdatedBuildInterval: 1000 * 60 * 30, }, });创建自定义插件下面以ElMessageBox为例在检测到新版本时询问用户是否立即刷新// plugins/update-prompt.client.ts export default defineNuxtPlugin((nuxtApp) { nuxtApp.hook(app:manifest:update, () { ElMessageBox.confirm( 检测到系统已更新是否立即刷新页面加载最新版本, 版本更新提示, { confirmButtonText: 立即刷新, cancelButtonText: 稍后再说, type: info, } ) .then(() { reloadNuxtApp({ persistState: true }); }) .catch(() { // 用户暂不刷新 // 如果需要稍后再次提示需要自行保存待更新状态 }); }); });这里有一个容易忽略的细节内置检测插件发现新构建后会停止轮询因此用户选择“稍后再说”后如果还需要在下一次导航或指定时间重新提示业务代码必须自行保存待更新状态并补充后续触发逻辑。上面的示例只负责完成一次确认不包含重复提醒9. 完整执行流程┌──────────────────────────────────────────────────────┐ │ 应用启动客户端 │ └──────────────────────┬───────────────────────────────┘ ▼ onNuxtReady 触发 │ ▼ setTimeout(getLatestManifest, outdatedBuildInterval) │ ▼ 请求 builds/latest.json?timestamp │ ┌────────┴────────┐ ▼ ▼ ID 相同 ID 不同 │ │ ▼ ▼ 继续轮询 app:manifest:update │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ automatic automatic-immediate 自定义插件 │ │ │ ▼ ▼ ▼ 下次导航时刷新 立即刷新 用户确认后刷新 │ │ │ └─────────────────┴─────────────────┘ │ ▼ reloadNuxtApp({ persistState: true }) │ ┌───────────┴───────────┐ ▼ ▼ 防重复刷新检查 保存 payload state sessionStorage sessionStorage │ │ └───────────┬───────────┘ ▼ location.reload() / location.href10. 如何选择更新策略默认的automatic策略不会立即打断用户而是在下一次导航时完成刷新适合大多数普通业务场景如果页面不能长时间运行旧版本可以使用automatic-immediate在检测到新构建后立即刷新如果页面包含未提交数据或者希望让用户自己决定更新时间可以关闭内置 Chunk Reload监听app:manifest:update并实现确认弹窗需要注意的是这些行为可能随 Nuxt 版本变化。本文基于 Nuxt 3 源码中的相关实现整理实际使用时应以项目安装版本的源码和配置类型为准