升级指南
了解如何升级到最新的 Nuxt 版本。
升级 Nuxt
最新版本
要将 Nuxt 升级到最新版本,使用 nuxt upgrade 命令。
npx nuxt upgrade
yarn nuxt upgrade
pnpm nuxt upgrade
bun x nuxt upgrade
deno x nuxt upgrade
Nightly 发布渠道
要使用最新的 Nuxt 构建并在发布前测试新功能,阅读 nightly 发布渠道 指南。
测试 Nuxt 5
Nuxt 5 目前正在开发中。从 Nuxt 4.2+ 开始,你可以通过将 future.compatibilityVersion 设为 5 来测试许多破坏性变更:
export default defineNuxtConfig({
future: {
compatibilityVersion: 5,
},
})
Nuxt 配置中的默认值随后会切换为 Nuxt 5 的行为。在 Nuxt 4 上,这包括:
- Vite Environment API:
experimental.viteEnvironmentApi启用,改变 Vite 插件的注册和配置方式 - 区分大小写路由:页面路由精确匹配 URL 大小写,与 Nitro 一致,所以
/About不再匹配pages/about.vue - 规范化页面名称:页面组件名称匹配路由名称,以实现一致的
<KeepAlive>行为 - 可序列化页面 meta:
experimental.extractSerializablePageMeta在构建时提取静态可分析的definePageMeta值 - 客户端 payload 提取:
experimental.payloadExtraction变为'client',将 payload 内联到预渲染页面的 HTML 中 - clearNuxtState 重置为默认值:
clearNuxtState将状态重置为useState的初始值,而不是设为undefined - 非异步 callHook:
callHook可能返回void而不是总是返回Promise - navigateTo 提前返回:
experimental.navigateToEarlyReturn在中间件重定向后停止继续执行 - 仅服务端 head composable:
useServerHead、useServerHeadSafe和useServerSeoMeta不再自动导入,unhead.legacy被忽略,Unhead 的旧版插件不再注册,Capo 排序始终开启 - PostCSS 默认关闭:
autoprefixer和cssnano不再配置,除非你在postcss.plugins中启用 - 键控 composable 需要 source:
optimization.keyedComposables条目不提供字符串source时不再通过自动导入匹配 - 注释节点占位符:仅客户端组件使用注释节点代替
<div>作为 SSR 占位符 - 类型化页面:
experimental.typedPages启用,用于类型检查路由 - 类型化 $fetch:
experimental.routeTypedFetch从生成的路由类型为$fetch和useFetch提供类型 - Nitro 自动导入关闭:服务端代码显式导入 Nitro 辅助工具,而不是依赖自动导入兼容层
- Vue Options API 禁用:Options API 从客户端包中编译移除,以减小体积
- 更严格的副作用导入:生成的
tsconfig.json启用noUncheckedSideEffectImports - TypeScript baseUrl 忽略:生成的 TypeScript 配置不再使用
compilerOptions.baseUrl解析 Nuxt 别名
此列表描述了该标志在 Nuxt 4 上的变更。Nuxt 5 本身还有更多变更,此处没有标志可以启用,比如升级到 Nitro v3。
每个变更的迁移步骤以及升级带来的其他内容,都记录在 Nuxt 5 升级指南中。
迁移到 nuxt/server
从 Nuxt 4.6 开始,服务端代码可以从 nuxt/server 导入 handler 和辅助工具,而不是从 h3 导入,这样同一个文件可以在 Nuxt 4(nitropack v2 和 h3 v1)和 Nuxt 5(Nitro v3 和 h3 v2)上运行。
在 Nuxt 4.6 上,从 nuxt/server 显式导入 defineEventHandler 和辅助工具。相同拼写的自动导入名称是 h3 v1 的,h3 的 defineEventHandler 传给你的 handler 的 event 与 nuxt/server 辅助工具读取的 event 不同。
+ import { defineEventHandler, getQuery } from 'nuxt/server'
+
export default defineEventHandler((event) => {
const { name } = getQuery<{ name?: string }>(event)
return { message: `Hello, ${name ?? 'world'}!` }
})
在 Nuxt 5 上,相同的名称会从 nuxt/server 自动导入,所以在 Nuxt 4.6 上写的显式导入保持不变。
与 h3 v1 的差异
| h3 v1 | nuxt/server |
|---|---|
sendRedirect(event, location) 直接发送响应。 | sendRedirect(event, location) 返回响应体:return sendRedirect(event, '/login')。 |
getRouterParams(event) 返回解码后的值。 | getRouterParams(event) 返回 URL 中原始的值。传 { decode: true } 来解码。 |
handleCors(event, options) 响应预检请求时返回 true。 | handleCors(event, options) 返回要从 handler 返回的预检 Response,或 false。 |
readValidatedBody 和 getValidatedQuery 接受验证函数。 | 它们接受 Standard Schema 或函数。 |
createError({ statusCode, statusMessage }) | createError({ status, statusText }) |
h3 的 useSession、getSession、updateSession 和 clearSession 用你传入的 password 密封一个 h3 cookie。 | 它们密封一个 nuxt-session cookie,默认使用从 appSecret 派生的密钥。两者不可互换:一个发出的 session 不能用另一个解封。 |
getResponseHeader、setResponseHeader、appendResponseHeader、removeResponseHeader | event.res.headers.get()、.set()、.append()、.delete() |
getMethod(event)、getRequestHost(event) | event.req.method、event.url.host |
nuxt/server 之外的辅助工具
对于需要 nuxt/server 未提供的 h3 或 Nitro 辅助工具的 handler,比如 readMultipartFormData、proxyRequest、useStorage 或 defineCachedEventHandler,该 handler 继续从 h3(或 nitropack/runtime)导入 defineEventHandler 和这些辅助工具。这样写的代码与服务端运行时绑定,在 Nuxt 5 上需要重新审视。
import { defineEventHandler, readMultipartFormData } from 'h3'
export default defineEventHandler(async (event) => {
const parts = await readMultipartFormData(event)
return { received: parts?.length ?? 0 }
})
NUXT_E8012
当 nuxt/server 辅助工具如 readValidatedBody、getValidatedQuery、handleCors 或 useSession 收到 h3 event 时,请求会失败并返回 500,消息以 [NUXT_E8012] 开头。这意味着 handler 是用 h3 的 defineEventHandler 定义的(通常是自动导入的那个)。在同一个文件中从 nuxt/server 导入 defineEventHandler。
应用密钥
nuxt/server session 辅助工具和 deriveSecret 读取 runtimeConfig.appSecret,你通过 NUXT_APP_SECRET 设置。在 Nuxt 4 上,nitropack v2 解析环境覆盖,所以纯数字的密钥会以数字形式到达;参见应用密钥了解如何避免。
迁移到 Nuxt 4
Nuxt 4 包含重大改进和变更。本指南将帮助你将现有的 Nuxt 3 应用迁移到 Nuxt 4。
首先,升级到 Nuxt 4:
npm install nuxt@^4.0.0
yarn add nuxt@^4.0.0
pnpm add nuxt@^4.0.0
bun add nuxt@^4.0.0
deno add npm:nuxt@^4.0.0
升级后,大多数 Nuxt 4 行为现在是默认的。不过,如果你在迁移过程中需要保持向后兼容,某些功能仍然可以配置。
以下各节详细介绍升级到 Nuxt 4 时需要的关键变更和迁移步骤。
破坏性或重大变更记录在下面,附带迁移步骤和可用的配置选项。
使用 Codemod 迁移
为了简化升级过程,我们与 Codemod 团队合作,通过一些开源 codemod 自动化了许多迁移步骤。
如果遇到任何问题,请通过 npx codemod feedback 向 Codemod 团队报告 🙏
要查看完整的 Nuxt 4 codemod 列表、每个的详细信息、来源以及各种运行方式,访问 Codemod Registry。
你可以使用以下 codemod recipe 运行本指南中提到的所有 codemod:
# 由于 https://github.com/codemod/codemod/issues/1710 使用固定版本
npx codemod@0.18.7 nuxt/4/migration-recipe
# 由于 https://github.com/codemod/codemod/issues/1710 使用固定版本
yarn dlx codemod@0.18.7 nuxt/4/migration-recipe
# 由于 https://github.com/codemod/codemod/issues/1710 使用固定版本
pnpm dlx codemod@0.18.7 nuxt/4/migration-recipe
# 由于 https://github.com/codemod/codemod/issues/1710 使用固定版本
bun x codemod@0.18.7 nuxt/4/migration-recipe
# 由于 https://github.com/codemod/codemod/issues/1710 使用固定版本
deno x codemod@0.18.7 nuxt/4/migration-recipe
此命令将按顺序执行所有 codemod,你可以取消选择不想运行的。每个 codemod 也列在下面对应的变更旁边,可以独立执行。
新目录结构
🚦 影响级别:重大
Nuxt 现在默认使用新的目录结构,具有向后兼容(所以如果 Nuxt 检测到你在使用旧结构,比如顶层的 app/pages/ 目录,这个新结构不会生效)。
👉 查看完整 RFC
变更内容
- 新的 Nuxt 默认
srcDir是app/,大多数内容从这里解析。 serverDir现在默认为<rootDir>/server而不是<srcDir>/serverlayers/、modules/和public/默认相对于<rootDir>解析- 如果使用 Nuxt Content v2.13+,
content/相对于<rootDir>解析 - 新增
dir.app,这是我们查找router.options.ts和spa-loading-template.html的目录 - 默认为<srcDir>/ - 新的
shared/目录可用于 Vue 应用和 Nitro 服务端之间共享的代码,shared/utils/和shared/types/有自动导入
v4 目录结构示例。
.output/
.nuxt/
app/
assets/
components/
composables/
layouts/
middleware/
pages/
plugins/
utils/
app.config.ts
app.vue
router.options.ts
content/
layers/
modules/
node_modules/
public/
shared/
types/
utils/
server/
api/
middleware/
plugins/
routes/
utils/
nuxt.config.ts
使用这个新结构,~ 别名现在默认指向 app/ 目录(你的 srcDir)。这意味着 ~/components 解析为 app/components/,~/pages 解析为 app/pages/,等等。
👉 更多细节参见实现此变更的 PR。
变更原因
- 性能 - 将所有代码放在仓库根目录会导致
.git/和node_modules/文件夹被文件系统监视器扫描/包含,这在非 Mac OS 上会显著延迟启动。 - IDE 类型安全 -
server/和应用其余部分运行在两个完全不同的上下文中,有不同的全局导入可用,确保server/不在应用其余部分的同一个文件夹中是确保 IDE 自动补全良好的重要第一步。
迁移步骤
- 创建名为
app/的新目录。 - 将
assets/、components/、composables/、app/layouts/、app/middleware/、app/pages/、app/plugins/和utils/文件夹移到它下面,以及app.vue、error.vue、app.config.ts。如果有app/router-options.ts或app/spa-loading-template.html,这些路径保持不变。 - 确保
nuxt.config.ts、content/、layers/、modules/、public/、shared/和server/文件夹留在app/文件夹外,在项目根目录。 - 记得更新任何第三方配置文件以适配新的目录结构,比如
tailwindcss或eslint配置(如果需要 -@nuxtjs/tailwindcss应该会自动正确配置tailwindcss)。
你可以通过运行 npx codemod@latest nuxt/4/file-structure 自动化此迁移
不过,迁移不是必须的。如果你想保留当前的文件夹结构,Nuxt 应该自动检测。(如果没有,请提 issue。)唯一的例外是如果你已经有自定义的 srcDir。在这种情况下,你应该注意 modules/、public/、shared/ 和 server/ 文件夹将从你的 rootDir 而不是自定义 srcDir 解析。如果需要,可以配置 dir.modules、dir.public 和 serverDir 来覆盖。
你也可以用以下配置强制使用 v3 文件夹结构:
export default defineNuxtConfig({
// 这会将新的 srcDir 默认值从 `app` 改回你的根目录
srcDir: '.',
// 这指定 `router.options.ts` 和 `spa-loading-template.html` 的目录前缀
dir: {
app: 'app',
},
})
单例数据获取层
🚦 影响级别:中等
变更内容
Nuxt 的数据获取系统(useAsyncData 和 useFetch)已大幅重组,以获得更好的性能和一致性:
- 相同键的共享 refs:所有使用相同键的
useAsyncData或useFetch调用现在共享相同的data、error和statusrefs。这意味着重要的是所有使用显式键的调用不能有冲突的deep、transform、pick、getCachedData或default选项。 - 对 getCachedData 更多控制:
getCachedData函数现在在每次获取数据时都会被调用,即使是由监视器或调用refreshNuxtData引起的。(之前,新数据总是被获取,这些情况下不会调用此函数。)为了更好地控制何时使用缓存数据和何时重新获取,该函数现在接收一个带有请求原因的上下文对象。 - 响应式键支持:现在可以使用 computed refs、普通 refs 或 getter 函数作为键,实现自动数据重新获取(并单独存储数据)。
- 数据清理:当最后一个使用
useAsyncData获取数据的组件被卸载时,Nuxt 会移除该数据,以避免内存使用不断增长。
变更原因
这些变更旨在改善内存使用,并提高 useAsyncData 各调用间加载状态的一致性。
迁移步骤
- 检查不一致的选项:审查任何使用相同键但有不同选项或获取函数的组件。
// 这现在会触发警告
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
将共享显式键(并有自定义选项)的 useAsyncData 调用提取到它们自己的 composable 中可能是有益的:
export function useUserData (userId: string) {
return useAsyncData(
`user-${userId}`,
() => fetchUser(userId),
{
deep: true,
transform: user => ({ ...user, lastAccessed: new Date() }),
},
)
}
- 更新 getCachedData 实现:
useAsyncData('key', fetchFunction, {
- getCachedData: (key, nuxtApp) => {
- return cachedData[key]
- }
+ getCachedData: (key, nuxtApp, ctx) => {
+ // ctx.cause - 可以是 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
+
+ // 示例:手动刷新时不使用缓存
+ if (ctx.cause === 'refresh:manual') return undefined
+
+ return cachedData[key]
+ }
})
或者,目前你可以用以下方式禁用此行为:
export default defineNuxtConfig({
experimental: {
granularCachedData: false,
purgeCachedData: false,
},
})
修正 Layers 中的模块加载顺序
🚦 影响级别:最小
变更内容
使用 Nuxt layers 时模块加载的顺序已修正。之前,项目根目录的模块在扩展 layer 的模块之前加载,这与预期行为相反。
现在模块按正确顺序加载:
- Layer 模块优先(按扩展顺序 - 更深的 layer 优先)
- 项目模块最后(最高优先级)
这影响:
nuxt.config.ts中modules数组定义的模块- 从
modules/目录自动发现的模块
变更原因
此变更确保:
- 扩展 layer 的优先级低于消费项目
- 模块执行顺序符合直观的 layer 继承模式
- 模块配置和 hooks 在多层设置中按预期工作
迁移步骤
大多数项目不需要变更,因为这修正了加载顺序以匹配预期行为。
不过,如果你的项目依赖之前错误的顺序,你可能需要:
- 审查模块依赖:检查是否有模块依赖特定的加载顺序
- 调整模块配置:如果模块被配置为绕过错误的顺序
- 充分测试:确保所有功能在修正后的顺序下按预期工作
新正确顺序的示例:
// Layer: my-layer/nuxt.config.ts
export default defineNuxtConfig({
modules: ['layer-module-1', 'layer-module-2'],
})
// 项目: nuxt.config.ts
export default defineNuxtConfig({
extends: ['./my-layer'],
modules: ['project-module-1', 'project-module-2'],
})
// 加载顺序(已修正):
// 1. layer-module-1
// 2. layer-module-2
// 3. project-module-1(可以覆盖 layer 模块)
// 4. project-module-2(可以覆盖 layer 模块)
如果你因为需要注册 hook 而遇到模块顺序依赖的问题,考虑为需要调用 hook 的模块使用 modules:done hook。这个 hook 在所有其他模块加载后运行,所以使用是安全的。
👉 更多细节参见 PR #31507 和 issue #25719。
路由元数据去重
🚦 影响级别:最小
变更内容
可以使用 definePageMeta 设置一些路由元数据,如 name、path 等。之前这些在路由和路由元数据上都可用(例如 route.name 和 route.meta.name)。
现在,它们只能在路由对象上访问。
变更原因
这是默认启用 experimental.scanPageMeta 的结果,是性能优化。
迁移步骤
迁移应该很直接:
const route = useRoute()
- console.log(route.meta.name)
+ console.log(route.name)
规范化组件名称
🚦 影响级别:中等
Vue 现在将生成与 Nuxt 组件命名模式匹配的组件名称。
变更内容
默认情况下,如果你没有手动设置,Vue 会分配一个与组件文件名匹配的组件名称。
├─ components/
├─── SomeFolder/
├───── MyComponent.vue
在这种情况下,就 Vue 而言,组件名称是 MyComponent。如果你想对它使用 <KeepAlive>,或在 Vue DevTools 中识别它,需要使用这个名称。
但为了自动导入,你需要使用 SomeFolderMyComponent。
有了这个变更,这两个值将匹配,Vue 将生成与 Nuxt 组件命名模式匹配的组件名称。
迁移步骤
确保在任何使用 @vue/test-utils 的 findComponent 的测试中,以及在任何依赖组件名称的 <KeepAlive> 中使用更新后的名称。
或者,目前你可以用以下方式禁用此行为:
export default defineNuxtConfig({
experimental: {
normalizeComponentNames: false,
},
})
Unhead v2
🚦 影响级别:最小
变更内容
用于生成 <head> 标签的 Unhead 已更新到 v2。虽然大部分兼容,但包含几个对底层 API 的破坏性变更。
- 移除的 props:
vmid、hid、children、body。 - 不再支持 Promise 输入。
- 标签现在默认使用 Capo.js 排序。
迁移步骤
以上变更对你的应用影响应该很小。
如果有问题,你应该验证:
- 你没有使用任何移除的 props。
useHead({
meta: [{
name: 'description',
// meta 标签不需要 vmid 或 key
- vmid: 'description'
- hid: 'description'
}]
})
- 如果你在使用 Template Params 或 Alias Tag Sorting,现在需要显式选择启用这些功能。
import { AliasSortingPlugin, TemplateParamsPlugin } from '@unhead/vue/plugins'
export default defineNuxtPlugin({
setup () {
const unhead = injectHead()
unhead.use(TemplateParamsPlugin)
unhead.use(AliasSortingPlugin)
},
})
虽然不强制,但建议将 @unhead/vue 的任何导入更新为 #imports 或 nuxt/app。
-import { useHead } from '@unhead/vue'
+import { useHead } from '#imports'
如果仍然有问题,可以通过启用 head.legacy 配置回退到 v1 行为。
export default defineNuxtConfig({
unhead: {
legacy: true,
},
})
SPA 加载屏幕的新 DOM 位置
🚦 影响级别:最小
变更内容
渲染仅客户端页面时(ssr: false),我们可选地渲染加载屏幕(来自 ~/app/spa-loading-template.html - 注意在 Nuxt 4 中这也变成了 ~/spa-loading-template.html),在 Nuxt 应用根内:
<div id="__nuxt">
<!-- spa 加载模板 -->
</div>
现在,我们默认将模板渲染在 Nuxt 应用根旁边:
<div id="__nuxt"></div>
<!-- spa 加载模板 -->
变更原因
这允许 spa 加载模板在 Vue 应用 suspense resolve 之前保持在 DOM 中,防止白屏闪烁。
迁移步骤
如果你用 CSS 或 document.queryElement 定位 spa 加载模板,需要更新选择器。为此,可以使用新的 app.spaLoaderTag 和 app.spaLoaderAttrs 配置选项。
或者,可以用以下方式回退到之前的行为:
export default defineNuxtConfig({
experimental: {
spaLoadingTemplateLocation: 'within',
},
})
解析后的 error.data
🚦 影响级别:最小
可以抛出带 data 属性的错误,但之前不会被解析。现在,它会被解析并在 error 对象中可用。虽然是修复,但如果你之前依赖之前的行为并手动解析,这在技术上是破坏性变更。
迁移步骤
更新你的自定义 error.vue,移除对 error.data 的任何额外解析:
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError
})
- const data = JSON.parse(error.data)
+ const data = error.data
</script>
更细粒度的内联样式
🚦 影响级别:中等
Nuxt 现在只为 Vue 组件内联样式,不为全局 CSS 内联。
变更内容
之前,Nuxt 会内联所有 CSS,包括全局样式,并移除指向单独 CSS 文件的 <link> 元素。现在,Nuxt 只为 Vue 组件这样做(之前这些会生成单独的 CSS 块)。我们认为这是减少单独网络请求(和之前一样,初始加载时不会为每个页面或组件单独请求 .css 文件)与允许缓存单个全局 CSS 文件并减少初始请求的文档下载大小之间的更好平衡。
迁移步骤
此功能完全可配置,你可以通过设置 inlineStyles: true 来回退到之前的行为,同时内联全局 CSS 和组件 CSS。
export default defineNuxtConfig({
features: {
inlineStyles: true,
},
})
解析后扫描页面 Meta
🚦 影响级别:最小
变更内容
我们现在在调用 pages:extend hook 之后扫描页面元数据(在 definePageMeta 中定义),而不是之前。
变更原因
这是为了允许扫描用户想在 pages:extend 中添加的页面的元数据。我们仍然在新的 pages:resolved hook 中提供更改或覆盖页面元数据的机会。
迁移步骤
如果你想覆盖页面元数据,在 pages:resolved 中而不是 pages:extend 中执行。
export default defineNuxtConfig({
hooks: {
- 'pages:extend'(pages) {
+ 'pages:resolved'(pages) {
const myPage = pages.find(page => page.path === '/')
myPage.meta ||= {}
myPage.meta.layout = 'overridden-layout'
}
}
})
或者,可以用以下方式回退到之前的行为:
export default defineNuxtConfig({
experimental: {
scanPageMeta: true,
},
})
共享预渲染数据
🚦 影响级别:中等
变更内容
我们启用了一个之前实验性的功能,跨不同页面共享 useAsyncData 和 useFetch 调用的数据。参见原始 PR。
变更原因
此功能自动在预渲染的页面之间共享 payload 数据。这可以在预渲染使用 useAsyncData 或 useFetch 并在不同页面获取相同数据的站点时显著提升性能。
例如,如果你的站点每个页面都需要 useFetch 调用(比如获取菜单导航数据,或从 CMS 获取站点设置),这些数据只会在预渲染第一个使用它的页面时获取一次,然后缓存供预渲染其他页面时使用。
迁移步骤
确保你的数据的任何唯一键始终可解析到相同数据。例如,如果你使用 useAsyncData 获取与特定页面相关的数据,应该提供一个唯一匹配该数据的键。(useFetch 应该会自动为你做。)
// 这在动态页面(如 `[slug].vue`)中不安全,因为路由 slug 影响获取的数据
// 但 Nuxt 无法知道,因为它没有反映在键中
const route = useRoute()
const { data } = await useAsyncData(async () => {
return await $fetch(`/api/my-page/${route.params.slug}`)
})
// 相反,应该使用唯一标识获取数据的键。
const { data } = await useAsyncData(route.params.slug, async () => {
return await $fetch(`/api/my-page/${route.params.slug}`)
})
或者,可以用以下方式禁用此功能:
export default defineNuxtConfig({
experimental: {
sharedPrerenderData: false,
},
})
useAsyncData 和 useFetch 中的默认 data 和 error 值
🚦 影响级别:最小
变更内容
useAsyncData 返回的 data 和 error 对象现在默认为 undefined。
变更原因
之前 data 初始化为 null 但在 clearNuxtData 中重置为 undefined。error 初始化为 null。此变更为了更好的一致性。
迁移步骤
如果你之前检查 data.value 或 error.value 是否为 null,可以将这些检查更新为检查 undefined。
你可以通过运行 npx codemod@latest nuxt/4/default-data-error-value 自动化此步骤
移除 useAsyncData 和 useFetch 中调用 refresh 时 dedupe 选项已弃用的 boolean 值
🚦 影响级别:最小
变更内容
之前可以向 refresh 传递 dedupe: boolean。这些是 cancel(true)和 defer(false)的别名。
// @errors: 2322
const { refresh } = await useAsyncData(() => Promise.resolve({ message: 'Hello, Nuxt!' }))
async function refreshData () {
await refresh({ dedupe: true })
}
变更原因
这些别名被移除,为了更清晰。
在将 dedupe 添加为 useAsyncData 的选项时出现了这个问题,我们移除了布尔值,因为它们最终是相反的。
refresh({ dedupe: false }) 意思是不要为了这个新请求取消现有请求。但在 useAsyncData 的选项中传 dedupe: true 意思是如果有待处理请求,不要发起新请求。(参见 PR。)
迁移步骤
迁移应该很直接:
const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' }))
async function refreshData () {
- await refresh({ dedupe: true })
+ await refresh({ dedupe: 'cancel' })
- await refresh({ dedupe: false })
+ await refresh({ dedupe: 'defer' })
}
你可以通过运行 npx codemod@latest nuxt/4/deprecated-dedupe-value 自动化此步骤
清除 useAsyncData 和 useFetch 中的 data 时尊重默认值
🚦 影响级别:最小
变更内容
如果你为 useAsyncData 提供了自定义 default 值,现在调用 clear 或 clearNuxtData 时会使用它,数据会重置为默认值而不是简单地取消设置。
变更原因
用户经常设置适当的空值,比如空数组,以避免迭代时检查 null/undefined。重置/清除数据时应该尊重这个。
清除 useState 时尊重默认值
🚦 影响级别:最小
变更内容
使用 compatibilityVersion: 5 时,clearNuxtState 会将状态重置为初始值(由 useState 的 init 函数提供),而不是设为 undefined。这使 clearNuxtState 的行为与 clearNuxtData 对齐,后者已经重置为默认值。
变更原因
当 clearNuxtState 将状态设为 undefined 时,依赖该状态的 composable 可能崩溃,因为它们期望状态始终有有效的形状(比如访问 undefined 上的属性)。重置为 init 值确保状态始终有可用的默认值。
迁移步骤
如果你依赖 clearNuxtState 将状态设为 undefined,可以显式传递 { reset: false }:
- clearNuxtState('myKey')
+ clearNuxtState('myKey', { reset: false })
或者,可以用以下方式回退到之前的行为:
export default defineNuxtConfig({
experimental: {
defaults: {
useState: {
resetOnClear: false,
},
},
},
})
你也可以现在就选择启用此行为,而不设置 compatibilityVersion: 5:
export default defineNuxtConfig({
experimental: {
defaults: {
useState: {
resetOnClear: true,
},
},
},
})
useAsyncData 和 useFetch 中 pending 值的对齐
🚦 影响级别:中等
从 useAsyncData、useFetch、useLazyAsyncData 和 useLazyFetch 返回的 pending 对象现在是一个 computed 属性,仅当 status 也是 pending 时为 true。
变更内容
现在,传入 immediate: false 时,pending 在第一次请求之前为 false。这与之前的行为不同,之前 pending 在第一次请求之前始终为 true。
变更原因
这使 pending 的含义与 status 属性对齐,后者在请求进行中也是 pending。
迁移步骤
如果你依赖 pending 属性,确保你的逻辑考虑了新行为:pending 仅在 status 也是 pending 时为 true。
<template>
- <div v-if="!pending">
+ <div v-if="status === 'success'">
<p>Data: {{ data }}</p>
</div>
<div v-else>
<p>Loading...</p>
</div>
</template>
<script setup lang="ts">
const { data, pending, execute, status } = await useAsyncData(() => fetch('/api/data'), {
immediate: false
})
onMounted(() => execute())
</script>
或者,可以暂时用以下方式回退到之前的行为:
export default defineNuxtConfig({
experimental: {
pendingWhenIdle: true,
},
})
useAsyncData 和 useFetch 中的键变更行为
🚦 影响级别:中等
变更内容
在 useAsyncData 或 useFetch 中使用响应式键时,Nuxt 在键变化时自动重新获取数据。设置 immediate: false 时,useAsyncData 仅在数据已经获取过一次后才在键变化时获取。
之前,useFetch 的行为略有不同。它总是在键变化时获取数据。
现在,useFetch 和 useAsyncData 行为一致 - 仅在数据已经获取过一次后才在键变化时获取。
变更原因
这确保 useAsyncData 和 useFetch 之间行为一致,防止意外获取。如果你设置了 immediate: false,必须调用 refresh 或 execute,否则数据永远不会在 useFetch 或 useAsyncData 中获取。
迁移步骤
此变更通常会改善预期行为,但如果你之前期望改变非立即 useFetch 的键或选项会触发获取,现在需要手动触发第一次。
const id = ref('123')
const { data, execute } = await useFetch('/api/test', {
query: { id },
immediate: false
)
+ watch(id, () => execute(), { once: true })
要选择退出此行为:
// 或在 Nuxt 配置中全局设置
export default defineNuxtConfig({
experimental: {
alwaysRunFetchOnKeyChange: true,
},
})
useAsyncData 和 useFetch 中的浅数据响应式
🚦 影响级别:最小
从 useAsyncData、useFetch、useLazyAsyncData 和 useLazyFetch 返回的 data 对象现在是 shallowRef 而不是 ref。
变更内容
获取新数据时,依赖 data 的任何内容仍然是响应式的,因为整个对象被替换。但如果你的代码更改数据结构内部的属性,这不会触发应用中的任何响应式。
变更原因
这为深层嵌套对象和数组带来了显著的性能提升,因为 Vue 不需要监视每个属性/数组的修改。在大多数情况下,data 也应该是不可变的。
迁移步骤
在大多数情况下,不需要迁移步骤,但如果你依赖数据对象的响应式,有两个选择:
- 可以按 composable 粒度选择启用深度响应式:
- const { data } = useFetch('/api/test')
+ const { data } = useFetch('/api/test', { deep: true })
- 可以在项目范围内更改默认行为(不推荐):
export default defineNuxtConfig({
experimental: {
defaults: {
useAsyncData: {
deep: true,
},
},
},
})
如果需要,可以通过运行 npx codemod@latest nuxt/4/shallow-function-reactivity 自动化此步骤
builder:watch 中的绝对监视路径
🚦 影响级别:最小
变更内容
Nuxt 的 builder:watch hook 现在发出的是绝对路径,而不是相对于项目 srcDir 的路径。
变更原因
这允许我们支持监视 srcDir 之外的路径,并为 layers 和其他更复杂的模式提供更好的支持。
迁移步骤
我们已经主动迁移了我们知道使用此 hook 的公开 Nuxt 模块。参见 issue #25339。
不过,如果你是模块作者,使用 builder:watch hook 并希望保持向后/向前兼容,可以使用以下代码确保你的代码在 Nuxt v3 和 Nuxt v4 中工作方式相同:
+ import { relative, resolve } from 'node:fs'
// ...
nuxt.hook('builder:watch', async (event, path) => {
+ path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path))
// ...
})
你可以通过运行 npx codemod@latest nuxt/4/absolute-watch-path 自动化此步骤
移除 window.__NUXT__ 对象
变更内容
我们在应用水合完成后移除全局 window.__NUXT__ 对象。
变更原因
这为多应用模式开辟了道路(#21635),并使我们能够专注于单一访问 Nuxt 应用数据的方式 - useNuxtApp()。
迁移步骤
数据仍然可用,但可以通过 useNuxtApp().payload 访问:
- console.log(window.__NUXT__)
+ console.log(useNuxtApp().payload)
目录索引扫描
🚦 影响级别:中等
变更内容
app/middleware/ 文件夹中的子文件夹也会被扫描 index 文件,这些现在也注册为项目中的中间件。
变更原因
Nuxt 自动扫描多个文件夹,包括 app/middleware/ 和 app/plugins/。
app/plugins/ 文件夹中的子文件夹会被扫描 index 文件,我们希望使这种行为在扫描目录间保持一致。
迁移步骤
可能不需要迁移,但如果想回退到之前的行为,可以添加 hook 过滤掉这些中间件:
export default defineNuxtConfig({
hooks: {
'app:resolve' (app) {
app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path))
},
},
})
模板编译变更
🚦 影响级别:最小
变更内容
之前,Nuxt 使用 lodash/template 编译文件系统上的 .ejs 文件格式/语法的模板。
此外,我们提供了一些模板工具(serialize、importName、importSources),可用于这些模板内的代码生成,现在正在移除。
变更原因
在 Nuxt v3 中,我们转向了带有 getContents() 函数的'虚拟'语法,这更加灵活和高性能。
此外,lodash/template 有一系列安全问题。这些实际上不适用于 Nuxt 项目,因为它在构建时而不是运行时使用,且由可信代码使用。不过,它们仍然会出现在安全审计中。此外,lodash 是一个庞大的依赖,大多数项目不使用。
最后,直接在 Nuxt 中提供代码序列化函数并不理想。相反,我们维护像 unjs/knitwork 这样的项目,它们可以作为你项目的依赖,安全问题可以直接在那里报告/解决,而不需要升级 Nuxt 本身。
迁移步骤
我们已经提 PR 更新使用 EJS 语法的模块,但如果你需要自己做,有三个向后/向前兼容的替代方案:
- 将字符串插值逻辑直接移到
getContents()中。 - 使用自定义函数处理替换,比如 https://github.com/nuxt-modules/color-mode/pull/240。
- 使用
es-toolkit/compat(lodash template 的直接替代品),作为你项目而不是 Nuxt 的依赖:
+ import { readFileSync } from 'node:fs'
+ import { template } from 'es-toolkit/compat'
// ...
addTemplate({
fileName: 'appinsights-vue.js'
options: { /* 一些选项 */ },
- src: resolver.resolve('./runtime/plugin.ejs'),
+ getContents({ options }) {
+ const contents = readFileSync(resolver.resolve('./runtime/plugin.ejs'), 'utf-8')
+ return template(contents)({ options })
+ },
})
最后,如果你在使用模板工具(serialize、importName、importSources),可以用 knitwork 的工具如下替换:
import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\}"(?=,?$)/gm, r => JSON.parse(r).replace(/^\{(.*)\}$/, '$1'))
const importSources = (sources: string | string[], { lazy = false } = {}) => {
return toArray(sources).map((src) => {
if (lazy) {
return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
}
return genImport(src, genSafeVariableName(src))
}).join('\n')
}
const importName = genSafeVariableName
你可以通过运行 npx codemod@latest nuxt/4/template-compilation-changes 自动化此步骤
默认 TypeScript 配置变更
🚦 影响级别:最小
变更内容
compilerOptions.noUncheckedIndexedAccess 现在是 true 而不是 false。
变更原因
此变更是之前 3.12 配置更新的后续,我们改进了默认值,主要遵循 TotalTypeScript 的推荐。
迁移步骤
有两种方法:
- 对应用运行类型检查并修复任何新错误(推荐)。
- 在
nuxt.config.ts中覆盖新默认值:
export default defineNuxtConfig({
typescript: {
tsConfig: {
compilerOptions: {
noUncheckedIndexedAccess: false,
},
},
},
})
TypeScript 配置拆分
🚦 影响级别:最小
变更内容
Nuxt 现在为不同上下文生成单独的 TypeScript 配置,以提供更好的类型检查体验:
- 新的 TypeScript 配置文件:Nuxt 现在生成额外的 TypeScript 配置:
.nuxt/tsconfig.app.json- 用于你的应用代码(Vue 组件、composables 等).nuxt/tsconfig.server.json- 用于你的服务端代码(Nitro/server 目录).nuxt/tsconfig.node.json- 用于你的构建时代码(模块、nuxt.config.ts等).nuxt/tsconfig.shared.json- 用于应用和服务端上下文之间共享的代码(如类型和非环境特定工具).nuxt/tsconfig.json- 用于向后兼容的旧配置
- 向后兼容:扩展
.nuxt/tsconfig.json的现有项目将继续像以前一样工作。 - 选择启用项目引用:新项目或想要更好类型检查的项目可以采用 TypeScript 的项目引用功能。
- 上下文特定类型检查:每个上下文现在有适当的编译器选项和包含/排除,以适应其特定环境。
- 新的 typescript.nodeTsConfig 选项:现在可以自定义 Node.js 构建时代码的 TypeScript 配置。
变更原因
此变更提供多个好处:
- 更好的类型安全:每个上下文(应用、服务端、构建时)获得适当的类型检查,带有上下文特定的全局和 API。
- 改进的 IDE 体验:为代码库的不同部分提供更好的 IntelliSense 和错误报告。
- 更清晰的分离:服务端代码不会错误地建议客户端 API,反之亦然。
- 性能:TypeScript 可以更高效地检查具有适当范围配置的代码。
例如,自动导入在你的 nuxt.config.ts 中不可用(但之前 TypeScript 没有标记这个)。虽然 IDE 识别 server/ 目录中 tsconfig.json 暗示的单独上下文,但这没有反映在类型检查中(需要单独的步骤)。
迁移步骤
不需要迁移 - 现有项目将继续像以前一样工作。
不过,要利用改进的类型检查,可以选择启用新的项目引用方法:
- 更新根 tsconfig.json 以使用项目引用:
如果你的 tsconfig.json 当前有 "extends": "./.nuxt/tsconfig.json" 行,在添加引用之前移除它。项目引用和 extends 是互斥的。
{
// 如果存在 "extends": "./.nuxt/tsconfig.json",移除它
"files": [],
"references": [
{ "path": "./.nuxt/tsconfig.app.json" },
{ "path": "./.nuxt/tsconfig.server.json" },
{ "path": "./.nuxt/tsconfig.shared.json" },
{ "path": "./.nuxt/tsconfig.node.json" }
]
}
- 移除任何手动的 server tsconfig.json 文件(如
server/tsconfig.json),它们扩展了.nuxt/tsconfig.server.json。 - 更新类型检查脚本 以使用项目引用的构建标志:
- "typecheck": "nuxt prepare && vue-tsc --noEmit"
+ "typecheck": "nuxt prepare && vue-tsc -b --noEmit"
- 将所有类型增强移到适当的上下文:
- 如果在为应用上下文增强类型,将文件移到
app/目录。 - 如果在为服务端上下文增强类型,将文件移到
server/目录。 - 如果在增强应用和服务端之间共享的类型,将文件移到
shared/目录。
从 app/、server/ 或 shared/ 目录之外增强类型在新的项目引用设置中将不起作用。
新配置为选择启用的项目提供更好的类型安全和 IntelliSense,同时为现有设置保持完全向后兼容。
移除实验性功能
🚦 影响级别:最小
变更内容
四个实验性功能在 Nuxt 4 中不再可配置:
experimental.treeshakeClientOnly将为true(自 v3.0 起默认)experimental.configSchema将为true(自 v3.3 起默认)experimental.polyfillVueUseHead将为false(自 v3.4 起默认)experimental.respectNoSSRHeader将为false(自 v3.4 起默认)vite.devBundler不再可配置 - 默认使用vite-node
变更原因
这些选项已经设置为当前值一段时间了,我们没有理由认为它们需要保持可配置。
迁移步骤
移除顶层 generate 配置
🚦 影响级别:最小
变更内容
顶层 generate 配置选项在 Nuxt 4 中不再可用。这包括其所有属性:
generate.exclude- 用于从预渲染中排除路由generate.routes- 用于指定要预渲染的路由
变更原因
顶层 generate 配置是 Nuxt 2 的遗留。我们已经支持 nitro.prerender 一段时间了,它是 Nuxt 3+ 中配置预渲染的首选方式。
迁移步骤
用相应的 nitro.prerender 选项替换 generate 配置:
export default defineNuxtConfig({
- generate: {
- exclude: ['/admin', '/private'],
- routes: ['/sitemap.xml', '/robots.txt']
- }
+ nitro: {
+ prerender: {
+ ignore: ['/admin', '/private'],
+ routes: ['/sitemap.xml', '/robots.txt']
+ }
+ }
})
阅读更多关于 Nitro 的预渲染配置选项。
规范化页面组件名称
🚦 影响级别:最小
变更内容
当 future.compatibilityVersion 设为 5(或启用 experimental.normalizePageNames)时,页面组件名称匹配其路由名称而不是使用文件名。例如,pages/foo/index.vue 的组件名称将是 foo 而不是 index。
变更原因
之前,Vue 根据文件名分配组件名称。这意味着多个页面如 pages/foo/index.vue 和 pages/bar/index.vue 都会有组件名称 index。这使得带 include/exclude 过滤器的 <KeepAlive> 不可靠,需要手动为每个页面添加 defineOptions({ name: '...' })。
迁移步骤
如果你依赖当前的组件名称(比如在 <KeepAlive> include/exclude 列表中),将它们更新为使用路由名称而不是文件名。
<template>
<NuxtPage :keepalive="{
- include: ['index']
+ include: ['foo']
}" />
</template>
要禁用此行为:
export default defineNuxtConfig({
experimental: {
normalizePageNames: false,
},
})
Nuxt 2 vs. Nuxt 3+
下表是 Nuxt 三个版本的快速对比:
| 特性 / 版本 | Nuxt 2 | Nuxt Bridge | Nuxt 3+ |
|---|---|---|---|
| Vue | 2 | 2 | 3 |
| 稳定性 | 😊 稳定 | 😊 稳定 | 😊 稳定 |
| 性能 | 🏎 快 | ✈️ 更快 | 🚀 最快 |
| Nitro 引擎 | ❌ | ✅ | ✅ |
| ESM 支持 | 🌙 部分 | 👍 更好 | ✅ |
| TypeScript | ☑️ 选择启用 | 🚧 部分 | ✅ |
| Composition API | ❌ | 🚧 部分 | ✅ |
| Options API | ✅ | ✅ | ✅ |
| 组件自动导入 | ✅ | ✅ | ✅ |
<script setup> 语法 | ❌ | 🚧 部分 | ✅ |
| 自动导入 | ❌ | ✅ | ✅ |
| webpack | 4 | 4 | 5 |
| Vite | ⚠️ 部分 | 🚧 部分 | ✅ |
| Nuxt CLI | ❌ 旧版 | ✅ nuxt | ✅ nuxt |
| 静态站点 | ✅ | ✅ | ✅ |
Nuxt 2 到 Nuxt 3+
迁移指南提供 Nuxt 2 功能与 Nuxt 3+ 功能的逐步比较,以及适配当前应用的指导。
查看从 Nuxt 2 迁移到 Nuxt 3 的指南。
Nuxt 2 到 Nuxt Bridge
如果你想逐步将 Nuxt 2 应用迁移到 Nuxt 3,可以使用 Nuxt Bridge。Nuxt Bridge 是一个兼容层,允许你在 Nuxt 2 中以选择启用机制使用 Nuxt 3+ 功能。
从 Nuxt 2 迁移到 Nuxt Bridge