升级指南

了解如何升级到最新的 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 v1nuxt/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、removeResponseHeaderevent.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>/server
  • layers/、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。

变更原因

  1. 性能 - 将所有代码放在仓库根目录会导致 .git/ 和 node_modules/ 文件夹被文件系统监视器扫描/包含,这在非 Mac OS 上会显著延迟启动。
  2. IDE 类型安全 - server/ 和应用其余部分运行在两个完全不同的上下文中,有不同的全局导入可用,确保 server/ 不在应用其余部分的同一个文件夹中是确保 IDE 自动补全良好的重要第一步。

迁移步骤

  1. 创建名为 app/ 的新目录。
  2. 将 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,这些路径保持不变。
  3. 确保 nuxt.config.ts、content/、layers/、modules/、public/、shared/ 和 server/ 文件夹留在 app/ 文件夹外,在项目根目录。
  4. 记得更新任何第三方配置文件以适配新的目录结构,比如 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)已大幅重组,以获得更好的性能和一致性:

  1. 相同键的共享 refs:所有使用相同键的 useAsyncData 或 useFetch 调用现在共享相同的 data、error 和 status refs。这意味着重要的是所有使用显式键的调用不能有冲突的 deep、transform、pick、getCachedData 或 default 选项。
  2. 对 getCachedData 更多控制:getCachedData 函数现在在每次获取数据时都会被调用,即使是由监视器或调用 refreshNuxtData 引起的。(之前,新数据总是被获取,这些情况下不会调用此函数。)为了更好地控制何时使用缓存数据和何时重新获取,该函数现在接收一个带有请求原因的上下文对象。
  3. 响应式键支持:现在可以使用 computed refs、普通 refs 或 getter 函数作为键,实现自动数据重新获取(并单独存储数据)。
  4. 数据清理:当最后一个使用 useAsyncData 获取数据的组件被卸载时,Nuxt 会移除该数据,以避免内存使用不断增长。

变更原因

这些变更旨在改善内存使用,并提高 useAsyncData 各调用间加载状态的一致性。

迁移步骤

  1. 检查不一致的选项:审查任何使用相同键但有不同选项或获取函数的组件。
// 这现在会触发警告
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() }),
    },
  )
}
  1. 更新 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 的模块之前加载,这与预期行为相反。

现在模块按正确顺序加载:

  1. Layer 模块优先(按扩展顺序 - 更深的 layer 优先)
  2. 项目模块最后(最高优先级)

这影响:

  • nuxt.config.ts 中 modules 数组定义的模块
  • 从 modules/ 目录自动发现的模块

变更原因

此变更确保:

  • 扩展 layer 的优先级低于消费项目
  • 模块执行顺序符合直观的 layer 继承模式
  • 模块配置和 hooks 在多层设置中按预期工作

迁移步骤

大多数项目不需要变更,因为这修正了加载顺序以匹配预期行为。

不过,如果你的项目依赖之前错误的顺序,你可能需要:

  1. 审查模块依赖:检查是否有模块依赖特定的加载顺序
  2. 调整模块配置:如果模块被配置为绕过错误的顺序
  3. 充分测试:确保所有功能在修正后的顺序下按预期工作

新正确顺序的示例:

// 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'
  }]
})
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 也应该是不可变的。

迁移步骤

在大多数情况下,不需要迁移步骤,但如果你依赖数据对象的响应式,有两个选择:

  1. 可以按 composable 粒度选择启用深度响应式:
- const { data } = useFetch('/api/test')
+ const { data } = useFetch('/api/test', { deep: true })
  1. 可以在项目范围内更改默认行为(不推荐):
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 的推荐。

迁移步骤

有两种方法:

  1. 对应用运行类型检查并修复任何新错误(推荐)。
  2. 在 nuxt.config.ts 中覆盖新默认值:
export default defineNuxtConfig({
  typescript: {
    tsConfig: {
      compilerOptions: {
        noUncheckedIndexedAccess: false,
      },
    },
  },
})

TypeScript 配置拆分

🚦 影响级别:最小

变更内容

Nuxt 现在为不同上下文生成单独的 TypeScript 配置,以提供更好的类型检查体验:

  1. 新的 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 - 用于向后兼容的旧配置
  1. 向后兼容:扩展 .nuxt/tsconfig.json 的现有项目将继续像以前一样工作。
  2. 选择启用项目引用:新项目或想要更好类型检查的项目可以采用 TypeScript 的项目引用功能。
  3. 上下文特定类型检查:每个上下文现在有适当的编译器选项和包含/排除,以适应其特定环境。
  4. 新的 typescript.nodeTsConfig 选项:现在可以自定义 Node.js 构建时代码的 TypeScript 配置。

变更原因

此变更提供多个好处:

  1. 更好的类型安全:每个上下文(应用、服务端、构建时)获得适当的类型检查,带有上下文特定的全局和 API。
  2. 改进的 IDE 体验:为代码库的不同部分提供更好的 IntelliSense 和错误报告。
  3. 更清晰的分离:服务端代码不会错误地建议客户端 API,反之亦然。
  4. 性能:TypeScript 可以更高效地检查具有适当范围配置的代码。

例如,自动导入在你的 nuxt.config.ts 中不可用(但之前 TypeScript 没有标记这个)。虽然 IDE 识别 server/ 目录中 tsconfig.json 暗示的单独上下文,但这没有反映在类型检查中(需要单独的步骤)。

迁移步骤

不需要迁移 - 现有项目将继续像以前一样工作。

不过,要利用改进的类型检查,可以选择启用新的项目引用方法:

  1. 更新根 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" }
  ]
}
  1. 移除任何手动的 server tsconfig.json 文件(如 server/tsconfig.json),它们扩展了 .nuxt/tsconfig.server.json。
  2. 更新类型检查脚本 以使用项目引用的构建标志:
- "typecheck": "nuxt prepare && vue-tsc --noEmit"
+ "typecheck": "nuxt prepare && vue-tsc -b --noEmit"
  1. 将所有类型增强移到适当的上下文:
  • 如果在为应用上下文增强类型,将文件移到 app/ 目录。
  • 如果在为服务端上下文增强类型,将文件移到 server/ 目录。
  • 如果在增强应用和服务端之间共享的类型,将文件移到 shared/ 目录。

从 app/、server/ 或 shared/ 目录之外增强类型在新的项目引用设置中将不起作用。

5. **配置 TypeScript 选项**(如果需要): ```ts export default defineNuxtConfig({ typescript: { // 自定义 tsconfig.app.json tsConfig: { // ... }, // 自定义 tsconfig.shared.json sharedTsConfig: { // ... }, // 自定义 tsconfig.node.json nodeTsConfig: { // ... }, }, nitro: { typescript: { // 自定义 tsconfig.server.json tsConfig: { // ... }, }, }, }) ``` 6. **更新任何运行 TypeScript 检查的 CI/构建脚本**,确保它们使用新的项目引用方法。

新配置为选择启用的项目提供更好的类型安全和 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 2Nuxt BridgeNuxt 3+
Vue223
稳定性😊 稳定😊 稳定😊 稳定
性能🏎 快✈️ 更快🚀 最快
Nitro 引擎❌✅✅
ESM 支持🌙 部分👍 更好✅
TypeScript☑️ 选择启用🚧 部分✅
Composition API❌🚧 部分✅
Options API✅✅✅
组件自动导入✅✅✅
<script setup> 语法❌🚧 部分✅
自动导入❌✅✅
webpack445
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