升级指南

了解如何升级到最新的 Nuxt 版本。

升级 Nuxt

最新版本

要将 Nuxt 升级到最新版本,请使用 nuxt upgrade 命令。

npx nuxt upgrade

夜版发布渠道

要使用 Nuxt 的最新构建版本并测试新功能(尚未正式发布),请阅读夜版发布渠道指南。

测试 Nuxt 5

Nuxt 5 目前正在开发中。在发布之前,可以测试许多 Nuxt 5 从 Nuxt 版本 4.2+ 的重大更改。

选择使用 Nuxt 5

首先,将 Nuxt 升级到 最新版本

然后,您可以将 future.compatibilityVersion 设置为匹配 Nuxt 5 的行为:

nuxt.config.ts
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 5,
  },
})

当您将 future.compatibilityVersion 设置为 5 时,您在 Nuxt 配置中的默认值将更改为选择 Nuxt v5 的行为,包括:

本节内容在最终发布之前可能会有所更改,因此如果您正在使用 future.compatibilityVersion: 5 测试 Nuxt 5,请定期回来查看。

重大或显著的变化将在下面注明,并附上向后/向前兼容的迁移步骤。

区分大小写的路由

🚦 影响级别:最低

变更内容

使用 compatibilityVersion: 5 时,页面路由将以区分大小写的方式匹配 URL,与 Nitro 保持一致。例如,/About 不再匹配 pages/about.vue

迁移步骤

更新链接,使其使用与页面路由相同的大小写。若要保持不区分大小写的匹配:

nuxt.config.ts
export default defineNuxtConfig({
  router: {
    options: {
      sensitive: false,
    },
  },
})

jiti 不再打包

🚦 影响级别:中等

变更内容

Nuxt 不再依赖 jiti。在 bundler 外部加载的文件(nuxt.config.tsmodules/ 中的文件以及 layer 配置)现在由运行时本身导入。

你仍然可以使用 TypeScript 编写配置。 Nuxt 5 要求 Node 22.19 或更高版本,此时类型剥离默认开启,因此 nuxt.config.ts 和 TypeScript 模块会原生加载。运行时有两件过去由 jiti 代为处理、但现在不会处理的事情:猜测文件扩展名,以及编译会生成代码的 TypeScript 语法。

否则,这两类问题只有在 Nuxt 加载文件时才会暴露出来,因此生成的 node tsconfig 现在会按照运行时所见的方式描述环境(将 modulemoduleResolution 设置为 nodenext,并加入 erasableSyntaxOnly),TypeScript 也会提前报告这两类问题。

这只涵盖 nuxt.configmodules/ 和 layer 配置。你的应用代码和 shared/ 代码会经过 Vite,并按照一直以来的方式解析。如果你需要之前的行为,可以覆盖配置:

nuxt.config.ts
export default defineNuxtConfig({
  typescript: {
    nodeTsConfig: {
      compilerOptions: {
        module: 'preserve',
        moduleResolution: 'bundler',
        erasableSyntaxOnly: false,
      },
    },
  },
})

变更原因

每次安装 Nuxt 时,都会引入 jiti 来加载少量文件,而其中大多数文件现在运行时已经可以在没有辅助的情况下加载。移除它可以减小默认安装的体积。

迁移步骤

1. 为相对导入添加文件扩展名。

nuxt.config.tsmodules/ 和 layer 配置中的相对导入需要显式指定扩展名:

nuxt.config.ts
- import { myPlugin } from './build/my-plugin'
+ import { myPlugin } from './build/my-plugin.ts'

TypeScript 会以 TS2835 报告缺少扩展名。请注意,它的快速修复建议使用 ./build/my-plugin.js;应填写文件实际使用的扩展名(.ts),Node 会直接解析该扩展名。

裸包导入(import { defu } from 'defu')不受影响。

2. 使用可擦除的 TypeScript 语法。

类型注解会被擦除,但会生成运行时代码的语法则无法使用。在配置文件和模块文件中,将以下内容替换为:

  • const 对象替换 enum Foo {}
  • 用普通导出替换 namespacemodule 代码块
  • 将构造函数参数属性(constructor(private x: string) {})替换为显式赋值
  • 实验性装饰器

TypeScript 会以 TS1294 为你报告这些问题。

3. 如果你发布 layer 或模块,请提供编译后的 JavaScript。

这一点仅适用于包。无论配置如何,运行时都拒绝从 node_modules 内的任何文件中剥离类型,因此使用 TypeScript 编写的已发布入口无法原生加载,不论 Node 版本有多新。发布前请先编译为 JavaScript;如果你的包包含 nuxt.config,请将其输出为 nuxt.config.mjs。否则,每个使用该包的项目都必须安装 jiti

这不适用于你自己项目中的 layer:layers/*/nuxt.config.ts 会原生加载。

4. 如果仍然需要 jiti,请安装它。

jiti 现在是可选的 peer dependency。安装后,只要运行时无法自行加载文件,Nuxt 就会自动将其作为回退方案使用:

npm i -D jiti

无论使用哪个 Node 版本,nuxt.schema 文件始终需要 jiti:其中的 JSDoc 注解是通过导入时转换读取的,而不是通过导入文件读取的。

随之而来的还有一项较小的变更:postcss.plugins 中指定的 PostCSS 插件现在由运行时解析,因此仅能通过 Nuxt alias 条目解析的插件名称将不再加载。请使用包名称或路径。

迁移到 Vite 环境 API

🚦 影响级别:中等

变化内容

Nuxt 5 迁移到 Vite 6 的新 环境 API。该 API 正式化了环境的概念,并提供了对每个环境配置的更好控制。

之前,Nuxt 使用单独的客户端和服务器 Vite 配置。现在,Nuxt 使用共享的 Vite 配置,并配备特定于环境的插件。这些插件使用 applyToEnvironment() 方法来针对特定环境。

Vite 环境 API 在 Nuxt 5 中始终启用。experimental.viteEnvironmentApi 选项已被移除。

关键变化:

  1. 不再支持特定于环境的 extendViteConfig()extendViteConfig() 中的 serverclient 选项已被弃用,使用时将显示警告。
  2. 更改插件注册:使用 addVitePlugin() 注册的 Vite 插件,如果仅针对一个环境(通过传递 server: falseclient: false)将不会调用其 configconfigResolved 钩子。
  3. 共享配置vite:extendConfigvite:configResolved 钩子现在与共享配置一起工作,而不是单独的客户端/服务器配置。

变更原因

Vite 环境 API 提供了:

  • 更好的一致性:确保开发与生产构建之间差异最小化
  • 更细粒度的环境特定配置控制
  • 改进的性能与插件架构
  • 支持自定义环境,而不仅仅是客户端和服务器

迁移步骤

1. 迁移到使用 Vite 插件

我们建议您使用 Vite 插件,而不是 extendViteConfigvite:configResolvedvite:extendConfig

// 之前
extendViteConfig((config) => {
  config.optimizeDeps.include.push('my-package')
}, { server: false })

nuxt.hook('vite:extendConfig' /* or vite:configResolved */, (config, { isClient }) => {
  if (isClient) {
    config.optimizeDeps.include.push('my-package')
  }
})

// 之后
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    // 这里可以设置全局 vite 配置
  },
  configResolved (config) {
    // 这里可以访问完整解析后的 vite 配置
  },
  configEnvironment (name, config) {
    // 这里可以设置特定环境的 vite 配置
    if (name === 'client') {
      config.optimizeDeps ||= {}
      config.optimizeDeps.include ||= []
      config.optimizeDeps.include.push('my-package')
    }
  },
  applyToEnvironment (environment) {
    return environment.name === 'client'
  },
}))
2. 迁移 Vite 插件以使用环境

您可以使用插件中的新 applyToEnvironment 钩子,而不是使用 addVitePlugin 结合 server: falseclient: false

// 之前
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    config.optimizeDeps.include.push('my-package')
  },
}), { client: false })

// 之后
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    // 这里可以设置全局 vite 配置
  },
  configResolved (config) {
    // 这里可以访问完整解析后的 vite 配置
  },
  configEnvironment (name, config) {
    // 这里可以设置特定环境的 vite 配置
    if (name === 'client') {
      config.optimizeDeps ||= {}
      config.optimizeDeps.include ||= []
      config.optimizeDeps.include.push('my-package')
    }
  },
  applyToEnvironment (environment) {
    return environment.name === 'client'
  },
}))
了解更多关于 Vite 环境 API 的内容 ::、】【

giget 现为可选(远程层)

🚦 影响级别:最低

变更内容

默认不再安装 giget。现在它是 @nuxt/kit 的可选 peer dependency,仅在需要下载通过远程 URL 指定 extends 的层时才需要:
nuxt.config.ts
export default defineNuxtConfig({
  extends: ['github:my-org/my-theme'],
})
本地层、位于 ~~/layers/ 中的层,以及作为 package 安装的层不受影响。

变更原因

大多数项目都不会从远程源进行扩展,因此为所有人安装下载器,只是为了服务少数项目。在 config-resolution 时下载层也是使用层时可复现性最低的方式:获取操作发生在 package manager 之外,因此不会记录在 lockfile 中,而且除非自行固定,否则不会固定到某个 revision。

迁移步骤

首选:将层移入 package.json 每个主流 package manager 都支持 git URL,因此远程层可以作为普通 dependency。这样它会记录在 lockfile 中,固定到确切的 commit,并与其他内容一起安装:
package.json
{
  "devDependencies": {
    "my-theme": "github:my-org/my-theme#4a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b"
  }
}
然后通过 package name 扩展它:
nuxt.config.ts
export default defineNuxtConfig({
  extends: ['my-theme'],
})
# 后面的部分可以是任意 commit-ish,因此 tag(#v1.2.0)或 #semver:^1.2.0 也可以使用;完整的 commit SHA 是唯一真正不可变的形式。否则,请安装 giget,继续使用远程 extends 条目:
npm install -D giget

如果在未安装 giget 的情况下解析远程层,Nuxt 会报告哪个 extends 条目需要它。

非异步 callHook

🚦 影响级别:最小

变化内容

随着升级到 hookable v6callHook 现在可能返回 void,而不是始终返回 Promise<void>。这是一项重要的性能改进:当没有注册的钩子,或所有钩子都是同步时,避免了不必要的 Promise 分配。

默认情况下(当 compatibilityVersion: 4),Nuxt 会用 Promise.resolve() 包装 callHook,以确保现有的 .then().catch() 链式调用继续正常工作。当 compatibilityVersion: 5 时,这个包装将被移除。

这会影响构建时的 Nuxt 钩子(由 Nuxt 模块使用)和运行时的 Nuxt 钩子(你可能在应用代码中使用)。

变更原因

Hookable v6 的 callHook 快了 20–40 倍,因为它在不需要时不会创建 Promise。这对拥有很多钩子调用点的应用有利。

迁移步骤

如果你或你的模块在使用 callHook 时配合 .then().catch() 链式调用,请改为使用 await

- nuxtApp.callHook('my:hook', data).then(() => { ... })
+ await nuxtApp.callHook('my:hook', data)
- nuxtApp.hooks.callHook('my:hook', data).catch(err => { ... })
+ try { await nuxtApp.hooks.callHook('my:hook', data) } catch (err) { ... }
你可以通过设置 future.compatibilityVersion: 5 来更早测试此功能(参见测试 Nuxt 5),或通过设置 experimental.asyncCallHook: false 显式启用它。

或者,你可以通过以下方式确保 callHook 始终返回一个 Promise

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    asyncCallHook: true,
  },
})

仅客户端注释占位符

🚦 影响级别:最小

变化内容

compatibilityVersion: 5 时,仅客户端组件(.client.vue 文件和 createClientOnly() 的封装)现在会在服务器端渲染一个 HTML 注释(<!--placeholder-->),而不是渲染一个空的 <div> 元素。

变更原因

当占位符 <div> 和实际组件根节点使用相同的标签名时,Vue 的运行时在 hydration 过程中会跳过重新应用 setScopeId。这会导致组件挂载后 scoped styles 缺失。使用注释节点可以完全避免标签名冲突。

迁移步骤

如果你依赖占位符 <div> 继承属性(classstyle 等)以用于布局目的(例如:预留空间以防止布局抖动),请改为使用 <ClientOnly> 并配合 #fallback 插槽来包裹该组件:

- <MyComponent class="placeholder" style="min-height: 200px" />
+ <ClientOnly>
+   <MyComponent />
+   <template #fallback>
+     <div class="placeholder" style="min-height: 200px"></div>
+   </template>
+ </ClientOnly>
你可以通过设置 future.compatibilityVersion: 5 来更早测试此功能(参见测试 Nuxt 5),或通过设置 experimental.clientNodePlaceholder: true 显式启用它。

或者,你可以通过以下方式恢复到之前的 <div> 占位符行为:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    clientNodePlaceholder: false,
  },
})

更严格的副作用导入

🚦 影响级别:最小

变化内容

当使用 compatibilityVersion: 5 时,Nuxt 生成的 tsconfig.json 会启用 noUncheckedSideEffectImports。这是 TypeScript 7 中的默认设置,因此尽早采用它可以让您的项目在升级前保持一致。

启用此选项后,TypeScript 无法解析到模块的仅副作用导入(import './setup')现在会成为类型错误,而之前会被忽略。这只影响类型检查(nuxt typecheck 和您的编辑器),不影响运行时行为。

变更原因

无法解析的副作用导入会被静默忽略,因此拼写错误或已删除文件可能仍能通过类型检查。将其标记出来可以捕获这些错误,并与 TypeScript 7 默认行为保持一致。

迁移步骤

如果类型检查现在对非代码资源的副作用导入报错(例如 import '~/assets/styles.css'),请添加一个环境模块声明,以便 TypeScript 知道该导入是有效的:

types.d.ts
declare module '*.css' {}
您可以通过在 nuxt.config 中禁用该选项来恢复到之前的行为:
nuxt.config.ts
export default defineNuxtConfig({
  typescript: {
    tsConfig: {
      compilerOptions: {
        noUncheckedSideEffectImports: false,
      },
    },
  },
})

默认禁用 Vue Options API

🚦 影响级别:最小

变化内容

compatibilityVersion: 5 时,Nuxt 会将 Vue 的 __VUE_OPTIONS_API__ 特性标志设置为 false,从而将 Vue 的 Options API 运行时代码从客户端包中编译移除。

变更原因

尽管大多数 Nuxt 应用都使用 Composition API 和 <script setup> 编写,但 Options API 运行时代码仍会出现在每个客户端包中。移除它可以缩小客户端包体积(在一个最小化应用中约减少 6 kB,压缩后约 2 kB)。

迁移步骤

如果您的任何组件(或依赖中的组件)使用了 Options API(export default { data() {}, methods: {}, ... }),请在您的 nuxt.config 中重新启用它:

nuxt.config.ts
export default defineNuxtConfig({
  vue: {
    optionsApi: true,
  },
})
defineNuxtComponent 不受影响:其 asyncDatahead 选项通过 setup() 处理,而不是通过 Vue Options API,因此无论此标志如何,它都能正常工作。 :::

默认启用 Typed Pages

🚦 影响级别:最低

变更内容

使用 compatibilityVersion: 5 时,默认启用 experimental.typedPages。Nuxt 会根据你的 pages/ 目录生成类型化的路由名称和路径,因此 useRoutenavigateTo<NuxtLink>router.push 等可组合式函数会根据实际路由进行类型检查。

变更原因

类型化路由会在类型检查时而不是运行时捕获失效链接和过时的路由名称,并且现在已原生支持于 vue-router,因此将其设为默认行为是合理的。

迁移步骤

如果你引用了不存在的路由(例如 to 属性中存在拼写错误,或在 pages/ 目录之外动态定义的路由),类型检查现在会报错。请修复该引用,或为你在运行时添加的路由扩展生成的路由类型。
你可以通过设置 future.compatibilityVersion: 5 提前测试此功能(请参阅测试 Nuxt 5),或通过显式启用 experimental.typedPages: true 来使用此功能。
或者,你可以选择退出并恢复之前的行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    typedPages: false,
  },
})

迁移到 Nuxt 4

🚦 影响级别:中等

变化内容

Nuxt 5 从 Vite 7 升级到 Vite 8,其中用 Rolldown 替换了 esbuild 和 Rollup 作为底层打包器。这带来了显著更快的构建速度,但也包含一些破坏性更改。
与 Vite 环境 API 迁移不同,此更改无法通过设置 future.compatibilityVersion: 5 提前启用。如果想提前测试 Vite 8 兼容性,可在 package.json 中添加 "vite": "^8.0.0-beta.15" 的依赖覆盖。
大多数迁移由 Nuxt 内部处理,但有些面向用户的变化需要注意:
  • vite.esbuildvite.optimizeDeps.esbuildOptions 已弃用,替换为 vite.oxcvite.optimizeDeps.rolldownOptions。Vite 8 目前会自动转换这些选项,但未来将移除旧选项。
  • build.rollupOptions 弃用,改用 build.rolldownOptions
  • CommonJS 互操作行为改变。如果使用 CJS 模块,请查阅 Vite 8 迁移指南
查看完整 Vite 8 迁移指南及破坏性更改

非异步 callHook

🚦 影响级别:最小

变化内容

升级到 hookable v6 后,callHook 现在可能返回 void,而非一直返回 Promise<void>。这大幅提升性能:避免无钩子或同步钩子时不必要的 Promise 分配。默认(兼容版本 4)下,Nuxt 会用 Promise.resolve() 包装 callHook,以保证现有的 .then().catch() 链式调用正常。设置兼容版本为 5 时,移除此包装。
此变动影响构建时的 Nuxt 钩子(模块使用)和运行时的钩子(应用代码中使用)。

变更原因

Hookable v6 的 callHook 会避免不必要的 Promise 创建,使钩子调用性能提升 20–40 倍,利于拥有大量钩子调用点的应用。

迁移步骤

若你或模块使用 callHook.then().catch() 链式调用,请改用 await
- nuxtApp.callHook('my:hook', data).then(() => { ... })
+ await nuxtApp.callHook('my:hook', data)
- nuxtApp.hooks.callHook('my:hook', data).catch(err => { ... })
+ try { await nuxtApp.hooks.callHook('my:hook', data) } catch (err) { ... }
你可以通过设置 future.compatibilityVersion: 5 或开启 experimental.asyncCallHook: false 提前测试此功能。
或者,确保 callHook 始终返回 Promise:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    asyncCallHook: true,
  },
})

迁移到 Nuxt 4

Nuxt 4 包含了显著的改进和变化。本指南将帮助你将现有的 Nuxt 3 应用迁移到 Nuxt 4。首先,升级到 Nuxt 4:
npm install nuxt@^4.0.0
升级完成后,大多数 Nuxt 4 的行为已成为默认。但如果在迁移过程中需要保持向后兼容,某些特性仍可以配置。以下章节详细描述了升级到 Nuxt 4 时的主要变化和迁移步骤。重大或破坏性更改均已记录,并附带迁移步骤及可用配置选项。

使用 Codemods 迁移

为了简化升级流程,我们与 Codemod 团队合作,提供了多个开源的自动化迁移脚本。
如果遇到任何问题,请使用 npx codemod feedback 向 Codemod 团队反馈 🙏
关于 Nuxt 4 Codemods 的完整列表、详细信息、源码及多种运行方式,请访问 Codemod 注册表。你可以使用以下 codemod 脚本运行本指南中提及的所有 codemod:
# 由于 https://github.com/codemod/codemod/issues/1710,使用固定版本
npx codemod@0.18.7 nuxt/4/migration-recipe
该命令会依次执行所有 codemods,并允许你取消选择不希望运行的项。每个 codemod 也在下面按其对应更改列出,可单独运行。

新的目录结构

🚦 影响级别:重大Nuxt 现在默认采用新的目录结构,同时保持向后兼容(如果 Nuxt 检测到你使用的是旧结构,比如存在顶层的 app/pages/ 目录,则不会应用此新结构)。👉 查看完整 RFC

变化内容

  • 新的 Nuxt 默认 srcDir 变为 app/,大多数资源都会从这里解析。
  • serverDir 默认值由 <srcDir>/server 改为 <rootDir>/server
  • layers/modules/public/ 目录默认相对于 <rootDir> 解析
  • 使用 Nuxt Content v2.13+ 时,content/ 目录默认相对于 <rootDir> 解析
  • 新增 dir.app 配置项,用于指定查找 router.options.tsspa-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/layouts/middleware/pages/plugins/utils/ 文件夹,以及 app.vueerror.vueapp.config.ts 移动到 app/ 目录下。如果你有 app/router-options.tsapp/spa-loading-template.html,路径保持不变。
  3. 确保 nuxt.config.tscontent/layers/modules/public/shared/server/ 这些文件夹依然在项目根目录,且未移入 app/
  4. 记得更新任何第三方配置文件以适配新的目录结构,例如 tailwindcsseslint 配置(如需要,@nuxtjs/tailwindcss 应会自动正确配置 tailwindcss)。
你可以运行 npx codemod@latest nuxt/4/file-structure 自动完成该迁移。
不过,迁移 非强制性。如果你希望保持现有目录结构,Nuxt 会自动检测。(如果未检测到,请提交 issue。)唯一例外是如果你已有自定义的 srcDir,此时 modules/public/shared/server/ 文件夹将默认从 rootDir 解析,而非自定义的 srcDir。你可以通过配置 dir.modulesdir.publicserverDir 来覆盖。你也可以通过以下配置强制使用 v3 的目录结构:
nuxt.config.ts
export default defineNuxtConfig({
  // 将新默认的 srcDir 从 `app` 改回根目录
  srcDir: '.',
  // 指定 `router.options.ts` 和 `spa-loading-template.html` 的目录前缀
  dir: {
    app: 'app',
  },
})

单例数据获取层

🚦 影响级别:中等

变化内容

Nuxt 的数据获取系统(useAsyncDatauseFetch)经过重大重组,以提升性能与一致性:
  1. 相同 key 共享 refs:所有使用相同 key 调用 useAsyncDatauseFetch 的地方都会共享相同的 dataerrorstatus 引用。这意味着带有显式 key 的调用,其 deeptransformpickgetCachedDatadefault 选项必须一致,以避免冲突。
  2. 更灵活的 getCachedDatagetCachedData 函数现在每次数据获取时都会调用,包括由侦听器触发或手动调用 refreshNuxtData 时。(之前这些情况下不会调用该函数。)为更灵活地控制何时使用缓存数据、何时重新请求数据,函数会接收一个包含请求原因的上下文对象。
  3. 支持响应式 key:现在支持使用计算 Ref、普通 Ref 或 getter 函数作为 key,可以基于 key 自动重新请求数据(并且独立存储数据)。
  4. 数据自动清理:当最后一个使用 useAsyncData 数据的组件卸载时,Nuxt 会自动移除相关数据,避免内存占用不断增长。

变更原因

此改动提升内存利用效率,同时统一 useAsyncData 各调用处的加载状态表现。

迁移步骤

  1. 检查不一致的选项:查找所有使用同一 key 但带不同选项或 fetch 函数的组件。
    // 现在会触发警告
    const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
    const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
    

    建议将带有显式 key(且自定义选项)的调用提取为独立的组合函数:
    app/composables/useUserData.ts
    export function useUserData (userId: string) {
      return useAsyncData(
        `user-${userId}`,
        () => fetchUser(userId),
        {
          deep: true,
          transform: user => ({ ...user, lastAccessed: new Date() }),
        },
      )
    }
    
  2. 更新 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]
    +  }
    })
    
或者,你现在也可以通过以下配置禁用该行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    granularCachedData: false,
    purgeCachedData: false,
  },
})

Layers 中修正的模块加载顺序

🚦 影响级别:最小

变化内容

当使用Nuxt 层级系统时,之前模块加载顺序错误:项目根目录的模块先加载,扩展层的模块后加载,顺序与预期相反。现在模块加载顺序已被正确修正:
  1. 先加载层级模块(按照扩展顺序,最深层优先)
  2. 后加载项目模块(优先级最高)
这会影响:
  • nuxt.config.tsmodules 数组中定义的模块
  • modules/ 目录中自动发现的模块

变更原因

此改动确保:
  • 扩展层的优先级低于使用它们的项目
  • 模块执行顺序符合层继承的直观预期
  • 在多层架构下,模块配置和钩子能正常工作

迁移步骤

大多数项目无需更改,此改动只是修正了模块加载顺序,使其与预期一致。但如果你的项目依赖之前错误的顺序,可能需要:
  1. 检查模块依赖关系:查看是否有模块依赖特定的加载顺序
  2. 调整模块配置:如果你此前为应对顺序问题做过配置调整
  3. 充分测试:确保修正顺序后功能正常
新的正确示例顺序:
// 层级: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 (可覆盖层模块)
// 4. project-module-2 (可覆盖层模块)
如果你的模块顺序依赖于注册钩子,建议使用 modules:done 钩子,该钩子在所有模块加载完成后触发,这样更安全。👉 参见 PR #31507issue #25719 获取更多细节。

路由元数据去重

🚦 影响级别:最小

变化内容

可以使用 definePageMeta 设置某些路由元数据,如 namepath 等。之前这些元数据同时存在于路由对象和路由元数据对象中(例如 route.nameroute.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 中识别该组件,需要使用这个名称。但 Nuxt 的自动导入会使用 SomeFolderMyComponent 作为名称。此次变更后,这两个名称将保持一致,Vue 生成的组件名将匹配 Nuxt 的命名模式。

迁移步骤

确保你在使用 @vue/test-utilsfindComponent 进行测试,以及任何依赖组件名的 <KeepAlive> 中使用更新后的名称。或者,你现在可以通过以下配置禁用此行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    normalizeComponentNames: false,
  },
})

Unhead v2

🚦 影响级别:最小

变化内容

用于生成 <head> 标签的 Unhead 升级到版本 2。尽管大多数兼容,但底层 API 有若干破坏性变更:
  • 移除属性:vmidhidchildrenbody
  • 不再支持 Promise 类型的输入
  • 默认使用 Capo.js 对标签进行排序

迁移步骤

上述变更对应用影响较小。如果遇到问题,请检查:
  • 你是否使用了已移除的属性
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 的导入改为 #importsnuxt/app
-import { useHead } from '@unhead/vue'
+import { useHead } from '#imports'
如果仍有问题,可通过启用 head.legacy 配置回退到 v1 行为:
export default defineNuxtConfig({
  unhead: {
    legacy: true,
  },
})

SPA Loading 屏幕的新 DOM 位置

🚦 影响级别:最小

变化内容

客户端渲染页面(ssr: false)时,加载屏幕(默认值为 ~/app/spa-loading-template.html;在 Nuxt 4 中改为 ~/spa-loading-template.html)渲染在 Nuxt 应用根节点内:
<div id="__nuxt">
  <!-- spa loading template -->
</div>
现在,加载屏幕默认渲染在 Nuxt 应用根节点旁边:
<div id="__nuxt"></div>
<!-- spa loading template -->

变更原因

这样能确保加载屏幕在 Vue 应用的 Suspense 解析完前一直存在,避免出现闪白。

迁移步骤

如果你曾用 CSS 或 document.querySelector 定位加载屏幕元素,请更新选择器。你可以利用新的 app.spaLoaderTagapp.spaLoaderAttrs 配置来辅助定位。或者,你可以通过以下配置恢复到旧行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    spaLoadingTemplateLocation: 'within',
  },
})

解析后的 error.data

🚦 影响级别:最小之前抛出的错误对象中若有 data 属性,未自动解析。现在已自动解析,并可通过错误对象访问。虽然这是一项修复,但如果你依赖旧行为并手动解析,这将属于破坏性变更。

迁移步骤

更新自定义 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>。现在仅对 Vue 组件相关的 CSS 内联(此前这些样式会产生独立的 CSS Chunk)。这能更好地平衡以下目标:减少网络请求数量(首屏加载时不会为每个页面或组件单独请求 .css 文件)、提升全局 CSS 缓存效率,以及减少初始请求体积。

迁移步骤

此特性可通过配置完全控制。你可以通过设置 inlineStyles: true 恢复之前的行为,即对全局 CSS 也进行内联:
nuxt.config.ts
export default defineNuxtConfig({
  features: {
    inlineStyles: true,
  },
})

解析后扫描页面元数据

🚦 影响级别:最小

变化内容

我们现在在调用 pages:extend 钩子之后扫描页面元数据(在 definePageMeta 中定义),而不是在调用之前。

变更原因

这样做是为了允许扫描用户希望在 pages:extend 中添加的页面的元数据。我们仍然提供了一个机会,即在新的 pages:resolved 钩子中更改或覆盖页面元数据。

迁移步骤

如果你想覆盖页面元数据,请在 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'
      }
    }
  })
或者,你可以通过以下方式恢复之前的行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    scanPageMeta: true,
  },
})

共享预渲染数据

🚦 影响级别:中等

变化内容

我们启用了一个此前的实验功能,用于在不同页面之间共享 useAsyncDatauseFetch 调用的数据。详见原始 PR

变更原因

该功能会自动在预渲染页面之间共享负载中的 数据。当预渲染使用 useAsyncDatauseFetch,并且在不同页面请求相同数据的站点时,能显著提升性能。例如,如果你的网站在每个页面都需要调用 useFetch(比如用于获取菜单导航数据,或 CMS 中的站点设定),这些数据会在首次预渲染使用它的页面时只被请求一次,并缓存起来供其他页面的预渲染使用。

迁移步骤

确保你数据的唯一键总是能解析到相同的数据。例如,如果你使用 useAsyncData 请求与特定页面相关的数据,应提供一个唯一匹配该数据的键。(useFetch 会自动为你处理)
app/pages/test/[slug].vue
// 在动态页面(例如 `[slug].vue`)中,这样使用是不安全的,因为路由的 slug 会影响请求的数据,
// 而 Nuxt 无法感知这一点,因为它未反映在 key 中。
const route = useRoute()
const { data } = await useAsyncData(async () => {
  return await $fetch(`/api/my-page/${route.params.slug}`)
})
// 正确做法是使用唯一标识请求数据的 key。
const { data } = await useAsyncData(route.params.slug, async () => {
  return await $fetch(`/api/my-page/${route.params.slug}`)
})
或者,你可以禁用此功能:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    sharedPrerenderData: false,
  },
})

useAsyncDatauseFetch 中默认的 dataerror

🚦 影响级别:最小

变化内容

useAsyncData 返回的 dataerror 对象默认现在为 undefined

变更原因

之前 data 初始化为 null,但在 clearNuxtData 中被重置为 undefinederror 初始化为 null。此更改旨在提升一致性。

迁移步骤

如果你之前检查过 data.valueerror.value 是否为 null,可以改为检查其是否为 undefined
你可以通过运行 npx codemod@latest nuxt/4/default-data-error-value 自动完成此步骤

移除调用 useAsyncDatauseFetchrefreshdedupe 选项的布尔值支持

🚦 影响级别:最小

变化内容

之前调用 refresh 时可以传递 dedupe: boolean。它们分别是 canceltrue)和 deferfalse)的别名。
app/app.vue
// @errors: 2322
const { refresh } = await useAsyncData(() => Promise.resolve({ message: 'Hello, Nuxt!' }))

async function refreshData () {
  await refresh({ dedupe: true })
}

变更原因

为使语义更清晰,我们移除了这些布尔别名。该问题产生于为 useAsyncData 添加 dedupe 选项时,布尔值最终变成了相反的含义。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 自动完成此步骤 :::

useAsyncDatauseFetch 中清空 data 时尊重默认值

🚦 影响级别:最小

变化内容

如果你为 useAsyncData 提供了自定义的 default 值,那么在调用 clearclearNuxtData 时,将会使用该默认值重置,而不仅仅是简单地置为空。

变更原因

用户通常会设置合适的空值(如空数组),以避免在遍历时检查 nullundefined。清空数据时应尊重这一设定。

清空 useState 时尊重默认值

🚦 影响级别:最小

变化内容

当设置 compatibilityVersion: 5 时,clearNuxtState 会将状态重置为由 useStateinit 函数提供的初始值,而不是将其设置为 undefined。这使得 clearNuxtState 的行为与已重置默认值的 clearNuxtData 保持一致。

变更原因

clearNuxtState 将状态设为 undefined 时,依赖该状态的组合式函数可能会崩溃,因为它们期望状态始终具有有效形态(例如,不能对 undefined 访问属性)。重置为 init 提供的值可确保状态始终有可用的默认值。

迁移步骤

如果你依赖 clearNuxtState 把状态设置为 undefined,可以显式传递 { reset: false }
- clearNuxtState('myKey')
+ clearNuxtState('myKey', { reset: false })
或者,你可以通过以下配置恢复之前行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    defaults: {
      useState: {
        resetOnClear: false,
      },
    },
  },
})
你也可以在不设置 compatibilityVersion: 5 的情况下,当前选择启用此行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    defaults: {
      useState: {
        resetOnClear: true,
      },
    },
  },
})

useAsyncDatauseFetchpending 值的对齐

🚦 影响级别:中等useAsyncDatauseFetchuseLazyAsyncDatauseLazyFetch 返回的 pending 现在是计算属性,仅当 status 也处于挂起状态时才为 true

变化内容

当传入 immediate: false 时,pending 会在首次请求发起之前保持为 false,这与之前的行为不同(之前首次请求前 pending 总是为 true)。

变更原因

此变更使 pending 的含义与 status 属性一致:当请求进行时,两者均为挂起状态。

迁移步骤

如果你依赖 pending 属性,请确保逻辑考虑到新行为:pending 仅在 status 也处于挂起时为 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>
你也可以暂时使用下列配置恢复旧行为:
nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    pendingWhenIdle: true,
  },
})

useAsyncDatauseFetch 中 key 变更行为

🚦 影响级别:中等

变化内容

useAsyncDatauseFetch 中使用响应式 key 时,当 key 变化会自动重新请求数据。设置 immediate: false 时,useAsyncData 只有在数据已请求过一次后,key 变化才会重新请求。之前,useFetch 的行为略有不同:在 key 变化时它会始终请求数据。现在,useFetchuseAsyncData 行为一致——只有在数据已请求过一次后,key 变化才会重新请求。

变更原因

确保 useAsyncDatauseFetch 行为一致,避免意外请求。若设置了 immediate: false,需要调用 refreshexecute,否则数据永远不会被请求。

迁移步骤

此更改通常会提升预期行为,但如果你希望在非立即加载的 useFetch 中通过改变 key 或选项自动请求,现在需要手动首次触发。
  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,
  },
})

useAsyncDatauseFetch 中数据浅层响应性

🚦 影响级别:最小从 useAsyncDatauseFetchuseLazyAsyncDatauseLazyFetch 返回的 data 对象现在是 shallowRef,而非普通的 ref

变化内容

新数据到达时,依赖 data 的内容仍然是响应的,因为整个对象会被替换。但如果代码修改了数据对象内部的属性,则不会触发响应性更新。

变更原因

这对于深层嵌套的对象和数组带来显著的性能提升,因为 Vue 无需监控每个属性或数组项。且通常 data 应该是不可变的。

迁移步骤

大多数情况下无需变化,但如果你依赖数据对象内部的响应性,有两种方案:
  1. 在具体的组合式函数调用中开启深层响应性:
    - const { data } = useFetch('/api/test')
    + const { data } = useFetch('/api/test', { deep: true })
    
  2. 在项目中全局更改默认行为(不推荐):
    nuxt.config.ts
    export default defineNuxtConfig({
      experimental: {
        defaults: {
          useAsyncData: {
            deep: true,
          },
        },
      },
    })
    
你可以通过运行 npx codemod@latest nuxt/4/shallow-function-reactivity 自动完成此步骤

builder:watch 中使用绝对路径监听

🚦 影响级别:最小

变化内容

Nuxt 的 builder:watch 钩子现在发出的路径是绝对路径,而不是相对于项目 srcDir 的相对路径。

变更原因

此变更支持监听 srcDir 之外的路径,并且更好地支持图层等复杂模式。

迁移步骤

我们已经主动迁移了已知使用该钩子的官方 Nuxt 模块,详见 issue #25339

如果你是模块作者,并想同时兼容 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__ 对象

变化内容

在应用完成 hydration 后,我们移除了全局的 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 文件,我们希望在扫描目录时行为保持一致。

迁移步骤

通常无需迁移,但如果你想恢复之前的行为,可以添加钩子过滤掉这些中间件:

export default defineNuxtConfig({
  hooks: {
    'app:resolve' (app) {
      app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path))
    },
  },
})

模板编译更改

🚦 影响级别:最小

变化内容

之前 Nuxt 使用 lodash/template 编译位于文件系统中的模板,且采用 .ejs 文件格式/语法。

此外,我们还提供了一些模板工具函数(如 serializeimportNameimportSources),用于这些模板中的代码生成,但现在不再提供。

变更原因

在 Nuxt v3 中,我们使用了更灵活高效的“虚拟”语法和 getContents() 函数。

另外,lodash/template 曾出现多次安全问题。尽管对 Nuxt 项目影响不大(用于构建时,非运行时,且使用可信代码),但仍可能在安全审计中被提及。lodash 本身依赖较重,大多数项目并未使用它。

最后,在 Nuxt 内置代码序列化函数并不理想。更推荐使用像 unjs/knitwork 这样的项目:它们可以作为你的项目依赖,在其中可以直接上报和修复安全问题,而不需要更新 Nuxt。

迁移步骤

我们已经针对使用 EJS 语法的模块提交了 PR,但如果你需要自行处理,有三种兼容方案:

+ 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 })
+   },
  })

最后,如果你使用了模板工具函数(如 serializeimportNameimportSources),可以用 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 默认值由 false 改为 true

变更原因

这是之前 3.12 配置更新 的后续。改进默认配置主要遵循 TotalTypeScript 的建议

迁移步骤

两种方案:
  1. 对应用运行类型检查并修复新报错(推荐)。
  2. nuxt.config.ts 中覆盖默认配置:
    export default defineNuxtConfig({
      typescript: {
        tsConfig: {
          compilerOptions: {
            noUncheckedIndexedAccess: false,
          },
        },
      },
    })
    

TypeScript 配置拆分

🚦 影响级别:最小

变化内容

Nuxt 现在为不同环境生成独立的 TypeScript 配置文件,以提升类型检测体验:
  1. 新的 TypeScript 配置文件
    • .nuxt/tsconfig.app.json - 用于应用代码(Vue 组件、组合式函数等)
    • .nuxt/tsconfig.server.json - 用于服务端代码(Nitro/server 目录)
    • .nuxt/tsconfig.node.json - 用于构建时代码(模块、nuxt.config.ts 等)
    • .nuxt/tsconfig.shared.json - 用于共享代码(如类型和非环境依赖工具)
    • .nuxt/tsconfig.json - 旧版配置,保持向后兼容
  2. 向后兼容性:扩展 .nuxt/tsconfig.json 的项目仍将继续正常使用。
  3. 项目引用选项(Project References):新项目或需要更好类型检查的可以采用。
  4. 针对不同环境的上下文类型检查:每个环境都拥有适合的编译选项和包含/排除规则。
  5. 新增 typescript.nodeTsConfig 选项:可定制 Node.js 构建时的 TypeScript 配置。

变更原因

此变更带来多重好处:
  1. 更好类型安全:各环境具有匹配的全局变量和 API 检查。
  2. 提升 IDE 体验:针对不同代码部分提供更好的智能提示和错误报告。
  3. 清晰分离:避免服务端代码误引用客户端 API,反之亦然。
  4. 性能提升:TypeScript 能更高效地检查限定范围的代码。
例如:你的 nuxt.config.ts 中不再支持自动导入,之前 TS 不会报错。IDE 虽识别服务端目录的 tsconfig.json,但类型检查并未反映。此变化补足了这一差异。

迁移步骤

无需迁移,现有项目保持不变即可继续工作。若想利用更好的类型检查,可采用新的项目引用方式:
  1. 更新根目录 tsconfig.json,添加项目引用:
如果你的 tsconfig.json 当前有一行 "extends": "./.nuxt/tsconfig.json"在添加引用之前请删除它。项目引用与扩展是互斥的。
{
  // 如果存在则移除 "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. 移除任何手写的服务端 tsconfig.json(例如 server/tsconfig.json)中对 .nuxt/tsconfig.server.json 的继承。
  2. 更新类型检查脚本以使用项目引用构建:
    - "typecheck": "nuxt prepare && vue-tsc --noEmit"
    + "typecheck": "nuxt prepare && vue-tsc -b --noEmit"
    
  3. 将所有类型增强代码归入相应上下文:
    • 应用端的增强放入 app/ 目录。
    • 服务端的增强放入 server/ 目录。
    • 应用与服务端共享的增强放入 shared/ 目录。
    增强代码如果放在 app/server/shared/ 目录之外,将无法兼容新的项目引用机制。
  4. 如需定制 TypeScript 配置,在配置中添加:
    export default defineNuxtConfig({
      typescript: {
        // 自定义 tsconfig.app.json
        tsConfig: {
          // ...
        },
        // 自定义 tsconfig.shared.json
        sharedTsConfig: {
          // ...
        },
        // 自定义 tsconfig.node.json
        nodeTsConfig: {
          // ...
        },
      },
      nitro: {
        typescript: {
          // 自定义 tsconfig.server.json
          tsConfig: {
            // ...
          },
        },
      },
    })
    
  5. 更新 CI/构建脚本,确保使用新的项目引用方式。

新配置为可选项:提供更好的类型安全与智能提示,同时对现有项目保持完整兼容。

移除实验性功能配置

🚦 影响级别:最小

变化内容

Nuxt 4 不再允许配置以下四个实验性功能:

  • experimental.treeshakeClientOnly:将默认开启(自 v3.0 起即默认)
  • experimental.configSchema:将默认开启(自 v3.3 起即默认)
  • experimental.polyfillVueUseHead:将默认关闭(自 v3.4 起即默认)
  • experimental.respectNoSSRHeader:将默认关闭(自 v3.4 起即默认)
  • experimental.viteEnvironmentApi:现在始终启用,且不可禁用

变更原因

这些选项长期默认开启或关闭,目前没有理由保留为可配置状态。

迁移步骤

  • polyfillVueUseHead 可通过用户插件实现,示例
  • respectNoSSRHeader 可通过服务器中间件实现,示例

移除顶层 generate 配置项

🚦 影响级别:最小

变化内容

Nuxt 4 不再支持顶层的 generate 配置,包括其所有属性:

  • generate.exclude —— 排除预渲染路由
  • generate.routes —— 指定预渲染路由

变更原因

顶层 generate 配置是 Nuxt 2 的遗留,现在优先使用 nitro.prerender 配置。

迁移步骤

使用对应的 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 prerender 配置选项。

标准化页面组件名称

🚦 影响级别:最小

变化内容

当设置 future.compatibilityVersion5(或启用 experimental.normalizePageNames)时,页面组件名称将匹配其路由名称,而不是使用文件名。例如,pages/foo/index.vue 组件名将变为 foo,而不是 index

变更原因

之前,Vue 会根据文件名赋予组件名,导致多个页面(如 pages/foo/index.vuepages/bar/index.vue)都拥有组件名 index。这会导致 <KeepAlive> 结合 include/exclude 过滤器时效果不可靠,并且需要在每个页面手动添加 defineOptions({ name: '...' })

迁移步骤

如果你依赖当前的组件名(例如在 <KeepAlive>includeexclude 列表中),请更新为使用路由名称,而非文件名。

<template>
  <NuxtPage :keepalive="{
-   include: ['index']
+   include: ['foo']
  }" />
</template>

若要禁用此行为:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    normalizePageNames: false,
  },
})

Nuxt 2 与 Nuxt 3+

下表简要比较了 Nuxt 的三个版本:

特性 / 版本Nuxt 2Nuxt BridgeNuxt 3+
Vue223
稳定性😊 稳定😊 稳定😊 稳定
性能🏎 快✈️ 更快🚀 最快
Nitro 引擎
ESM 支持🌙 部分支持👍 改进
TypeScript☑️ 选用引入🚧 部分支持
组合式 API🚧 部分支持
选项式 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 2 中使用部分 Nuxt 3+ 特性,需主动开启。

从 Nuxt 2 迁移到 Nuxt Bridge :::