升级指南

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

升级 Nuxt

最新版本

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

npx nuxt upgrade

夜间发布渠道

要在最新 Nuxt 构建和测试功能正式发布前进行体验,请阅读 每日构建发布渠道 指南。

测试 Nuxt 5

Nuxt 5 目前正在开发中。在正式发布之前,您可以从 Nuxt 4.2+ 版本开始测试 Nuxt 5 的许多破坏性变更。

启用 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,请定期回来查看。

下文将列出破坏性变更或重大更改,并提供用于向后/向前兼容的迁移步骤。

迁移到 Vite 环境 API

🚦 影响程度:中

变更内容

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

过去,Nuxt 使用独立的客户端和服务端 Vite 配置。现在,Nuxt 使用共享的 Vite 配置,其中包含针对特定环境的插件,这些插件使用 applyToEnvironment() 方法来针对特定环境。

您可以通过设置 future.compatibilityVersion: 5(参见 测试 Nuxt 5)或通过显式启用 experimental.viteEnvironmentApi: true 来提前测试此功能。

关键变更

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

更改原因

Vite 环境 API 提供了以下优势:

  • 开发构建与生产构建之间更好的一致性
  • 对特定环境配置更细粒度的控制
  • 改进的性能和插件架构
  • 支持除客户端和服务端之外的自定义环境

迁移步骤

1. 迁移到使用 Vite 插件

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

// Before
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')
  }
})

// After
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    // you can set global vite configuration here
  },
  configResolved (config) {
    // you can access the fully resolved vite configuration here
  },
  configEnvironment (name, config) {
    // you can set environment-specific vite configuration here
    if (name === 'client') {
      config.optimizeDeps ||= {}
      config.optimizeDeps.include ||= []
      config.optimizeDeps.include.push('my-package')
    }
  },
  applyToEnvironment (environment) {
    return environment.name === 'client'
  },
}))
2. 迁移 Vite 插件以使用环境 API

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

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

// After
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    // you can set global vite configuration here
  },
  configResolved (config) {
    // you can access the fully resolved vite configuration here
  },
  configEnvironment (name, config) {
    // you can set environment-specific vite configuration here
    if (name === 'client') {
      config.optimizeDeps ||= {}
      config.optimizeDeps.include ||= []
      config.optimizeDeps.include.push('my-package')
    }
  },
  applyToEnvironment (environment) {
    return environment.name === 'client'
  },
}))
了解更多关于 Vite 环境 API 的信息

非异步的 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。这会导致组件挂载后作用域样式丢失。使用注释节点可以完全避免标签名冲突。

迁移步骤

如果您依赖占位符 <div> 来继承属性(如 classstyle 等)以用于布局目的(例如预留空间以防止布局偏移),请改用带有 #fallback 插槽的 <ClientOnly> 包裹组件

- <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')报错,请添加一个环境模块声明(ambient module declaration),以便 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 应用都是用组合式 API(Composition API)和 <script setup> 编写的,但 Options API 运行时仍会被打包进每个客户端产物中。移除它可以缩小客户端包体积(对于最小化应用,大约可减少 6 kB 压缩后 / 2 kB gzip 压缩后的体积)。

迁移步骤

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

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

迁移到 Nuxt 4

Nuxt 4 包含了重大的改进和变更。本指南将帮助您将现有的 Nuxt 3 应用程序迁移到 Nuxt 4。

首先,升级到 Nuxt 4

npm install nuxt@^4.0.0

升级后,大多数 Nuxt 4 行为现在已成为默认设置。但是,如果您在迁移期间需要保持向后兼容性,某些功能仍然可以进行配置。

以下各节详细介绍了升级到 Nuxt 4 所需的关键变更和迁移步骤。

下文记录了破坏性或重大的变更,以及迁移步骤和可用的配置选项。

使用 Codemods 进行迁移

为了简化升级过程,我们与 Codemod 团队合作,通过一些开源 codemod 自动化了许多迁移步骤。

如果您遇到任何问题,请通过 npx codemod feedback 向 Codemod 团队反馈 🙏

有关 Nuxt 4 codemod 的完整列表、每个 codemod 的详细信息、其来源以及运行它们的各种方法,请访问 Codemod 注册表

您可以使用以下 codemod 配方运行本指南中提到的所有 codemod

# Using pinned version due to https://github.com/codemod/codemod/issues/1710
npx 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.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. 性能 - 将所有代码放在仓库的根目录下会导致文件系统监视器(FS watchers)扫描/包含 .git/node_modules/ 文件夹,这在非 Mac 操作系统上会显著延迟启动时间。
  2. IDE 类型安全 - server/ 与应用的其余部分运行在两个完全不同的上下文中,并且可用的全局导入也不同。确保 server/ 不在应用其余部分所在的同一文件夹内部,是确保你在 IDE 中获得良好自动补全的重要第一步。

迁移步骤

  1. 创建一个名为 app/ 的新目录。
  2. 将您的 assets/components/composables/app/layouts/app/middleware/app/pages/app/plugins/utils/ 文件夹移动到该目录下,还有 app.vueerror.vueapp.config.ts。如果您有 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({
  // This reverts the new srcDir default from `app` back to your root directory
  srcDir: '.',
  // This specifies the directory prefix for `router.options.ts` and `spa-loading-template.html`
  dir: {
    app: 'app',
  },
})

单例数据获取层

🚦 影响程度:中等

变更内容

Nuxt 的数据获取系统(useAsyncDatauseFetch)经过了重大重组,以获得更好的性能和一致性

  1. 相同键共享引用:所有使用相同键调用 useAsyncDatauseFetch 的地方现在共享相同的 dataerrorstatus 引用。这意味着,所有带有显式键的调用绝不能有冲突的 deeptransformpickgetCachedDatadefault 选项,这一点非常重要。
  2. getCachedData 的更多控制:现在,每次获取数据时都会调用 getCachedData 函数,即使这是由 watcher 或调用 refreshNuxtData 引起的(此前,在这些情况下总是会获取新数据且不会调用此函数)。为了更好地控制何时使用缓存数据以及何时重新获取,该函数现在会接收一个包含请求原因的上下文对象。
  3. 响应式键支持:您现在可以使用计算属性引用(computed refs)、普通引用(plain refs)或 getter 函数作为键,这支持了自动重新获取数据(并单独存储数据)。
  4. 数据清理:当使用通过 useAsyncData 获取的数据的最后一个组件被卸载时,Nuxt 将删除该数据,以避免内存使用量不断增长。

更改原因

这些更改旨在改善内存使用情况,并增强跨 useAsyncData 调用的加载状态的一致性。

迁移步骤

  1. 检查不一致的选项:检查所有使用相同键但带有不同选项或获取函数的组件。
    // This will now trigger a warning
    const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
    const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
    

    将任何共享显式键(且具有自定义选项)的 useAsyncData 调用提取到它们自己的组合式函数(composable)中可能会更有利
    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 - can be 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
    +    
    +    // Example: Don't use cache on manual refresh
    +    if (ctx.cause === 'refresh:manual') return undefined
    +    
    +    return cachedData[key]
    +  }
    })
    

或者,目前您可以通过以下方式禁用此行为:

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

纠正了图层(Layers)中的模块加载顺序

🚦 影响程度:极小

变更内容

使用 Nuxt 图层(Layers)时加载模块的顺序已被纠正。此前,项目根目录中的模块会在扩展图层中的模块之前加载,这与预期的行为相反。

现在模块将按正确的顺序加载

  1. 图层模块优先(按扩展顺序——更深层的图层优先)
  2. 项目模块最后(优先级最高)

这会影响以下两方面:

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

更改原因

此更改确保了:

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

迁移步骤

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

但是,如果您的项目依赖于以前的不正确顺序,您可能需要:

  1. 审查模块依赖项:检查是否有任何模块依赖于特定的加载顺序
  2. 调整模块配置:如果模块是为了绕过不正确的顺序而进行配置的
  3. 充分测试:确保所有功能在纠正顺序后按预期工作

新的正确顺序示例

// Layer: my-layer/nuxt.config.ts
export default defineNuxtConfig({
  modules: ['layer-module-1', 'layer-module-2'],
})

// Project: nuxt.config.ts
export default defineNuxtConfig({
  extends: ['./my-layer'],
  modules: ['project-module-1', 'project-module-2'],
})

// Loading order (corrected):
// 1. layer-module-1
// 2. layer-module-2
// 3. project-module-1 (can override layer modules)
// 4. project-module-2 (can override layer modules)

如果您由于需要注册钩子而遇到模块顺序依赖问题,请考虑对需要调用钩子的模块使用 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 中识别它,您就需要使用这个名称。

但为了自动导入它,您需要使用 SomeFolderMyComponent

通过此更改,这两个值将匹配,并且 Vue 将生成与 Nuxt 组件命名模式相匹配的组件名称。

迁移步骤

请确保在所有使用 @vue/test-utils 中的 findComponent 的测试中以及任何依赖于组件名称的 <KeepAlive> 中使用更新后的名称。

或者,目前您可以通过以下方式禁用此行为:

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

Unhead v2

🚦 影响程度:极小

变更内容

用于生成 <head> 标签的 Unhead 已更新至 2.0 版本。虽然它大部分向后兼容,但包含对底层 API 的若干破坏性更改。

  • 移除了以下属性:vmidhidchildrenbody
  • 不再支持 Promise 输入。
  • 现在默认使用 Capo.js 对标签进行排序。

迁移步骤

上述更改对您的应用影响应该极小。

如果您遇到问题,您应该进行以下验证:

  • 您没有使用任何被移除的属性。
useHead({
  meta: [{ 
    name: 'description', 
    // meta tags don't need a vmid, or a 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 加载屏幕的新 DOM 位置

🚦 影响程度:极小

变更内容

在渲染仅客户端页面(设置了 ssr: false)时,我们会在 Nuxt 应用根目录内有选择地渲染加载屏幕(来自 ~/app/spa-loading-template.html——请注意,在 Nuxt 4 中这也已更改为 ~/spa-loading-template.html

<div id="__nuxt">
  <!-- spa loading template -->
</div>

现在,我们默认将模板渲染在 Nuxt 应用根目录的旁边

<div id="__nuxt"></div>
<!-- spa loading template -->

更改原因

这允许 spa 加载模板在 DOM 中保留,直到 Vue 应用的 suspense 解析完毕,从而防止白屏闪烁。

迁移步骤

如果您过去使用 CSS 或 document.queryElement 来定位 spa 加载模板,则需要更新您的选择器。为此,您可以使用新的 app.spaLoaderTagapp.spaLoaderAttrs 配置选项。

或者,您可以通过以下方式恢复到以前的行为

nuxt.config.ts
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,从而恢复到以前的行为。

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
// This would be unsafe in a dynamic page (e.g. `[slug].vue`) because the route slug makes a difference
// to the data fetched, but Nuxt can't know that because it's not reflected in the key.
const route = useRoute()
const { data } = await useAsyncData(async () => {
  return await $fetch(`/api/my-page/${route.params.slug}`)
})
// Instead, you should use a key that uniquely identifies the data fetched.
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 来自动化此步骤

移除在 useAsyncDatauseFetch 中调用 refresh 时针对 dedupe 选项的已弃用 boolean

🚦 影响程度:极小

变更内容

过去可以向 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 })
}

更改原因

为了更清晰起见,这些别名已被移除。

这个问题在将 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 来自动化此步骤

useAsyncDatauseFetch 中清除 data 时遵循默认值

🚦 影响程度:极小

变更内容

如果您为 useAsyncData 提供了自定义的 default 值,现在在调用 clearclearNuxtData 时将使用该值,它将被重置为默认值,而不仅仅是取消设置。

更改原因

通常用户会设置一个适当的空值(例如空数组),以避免在迭代它时检查 null/undefined。在重置/清除数据时,应该尊重这一点。

在清除 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 也为 pending(挂起)时,它才为 true

变更内容

现在,当传递 immediate: false 时,在发出第一个请求之前,pending 将为 false。这与以前的行为有所不同,以前在发出第一个请求之前,pending 总是 true

更改原因

这使 pending 的含义与 status 属性保持一致,当请求正在进行时,status 也为 pending

迁移步骤

如果您依赖 pending 属性,请确保您的逻辑适应新行为,即只有当状态也为 pending 时,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>

或者,您可以通过以下方式临时恢复到以前的行为

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

Key Change Behavior in useAsyncDatauseFetch 中的键变更行为

🚦 影响程度:中

变更内容

当在 useAsyncDatauseFetch 中使用响应式键时,当键更改时,Nuxt 会自动重新获取数据。当设置了 immediate: false 时,useAsyncData 只有在数据已经获取过一次的情况下,才会在键更改时获取数据。

以前,useFetch 的行为略有不同。无论如何,只要键更改,它就会获取数据。

现在,useFetchuseAsyncData 的行为保持一致——只有在数据已经获取过一次的情况下,才会在键更改时获取数据。

更改原因

这确保了 useAsyncDatauseFetch 之间的行为一致,并防止了意外的数据获取。如果你设置了 immediate: false,那么你必须调用 refreshexecute,否则数据将永远不会在 useFetchuseAsyncData 中被获取。

迁移步骤

这一更改通常会改善预期行为,但如果你原本期望通过更改非即时执行的 useFetch 的键或选项来触发获取,现在你需要首次手动触发它。

  const id = ref('123')
  const { data, execute } = await useFetch('/api/test', {
    query: { id },
    immediate: false
  )
+ watch(id, () => execute(), { once: true })

要退出此行为

// Or globally in your Nuxt config
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

但是,如果你是一名使用 builder:watch 钩子并希望保持向后/向前兼容的模块作者,你可以使用以下代码来确保你的代码在 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 文件,我们希望在各个被扫描的目录之间保持这种行为一致。

迁移步骤

可能不需要进行迁移,但如果你希望恢复到以前的行为,你可以添加一个钩子来过滤掉这些中间件

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 本身。

迁移步骤

我们已经提交了 PR 以更新使用 EJS 语法的模块,但如果你需要自己动手,你有三种向后/向前兼容的替代方案

  • 将你的字符串插值逻辑直接移入 getContents() 中。
  • 使用自定义函数来处理替换,例如在 https://github.com/nuxt-modules/color-mode/pull/240 中所示。
  • 使用 es-toolkit/compat(lodash 模板的直接替代品)作为项目的依赖项,而不是作为 Nuxt 的依赖项
+ import { readFileSync } from 'node:fs'
+ import { template } from 'es-toolkit/compat'
  // ...
  addTemplate({
    fileName: 'appinsights-vue.js'
    options: { /* some 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 现在默认为 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 组件、组合式函数等)
    • .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. 可选的项目引用:新项目或希望获得更好类型检查的项目可以采用 TypeScript 的项目引用功能。
  4. 特定上下文的类型检查:现在,每个上下文都为其特定环境拥有适当的编译器选项以及 include/exclude 设置。
  5. 新的 typescript.nodeTsConfig 选项:你现在可以自定义用于 Node.js 构建时代码的 TypeScript 配置。

更改原因

这一更改带来了几个好处

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

例如,自动导入在你的 nuxt.config.ts 中不可用(但以前 TypeScript 并未标记这一点)。虽然 IDE 识别到了由 server/ 目录中的 tsconfig.json 提示的独立上下文,但这并未反映在类型检查中(需要单独的步骤)。

迁移步骤

无需迁移 - 现有项目将像以前一样继续运行。

但是,为了充分利用改进后的类型检查,你可以选择启用新的项目引用方法

  1. 更新你的根目录 tsconfig.json 以使用项目引用
    如果你的 tsconfig.json 当前包含 "extends": "./.nuxt/tsconfig.json" 行,请在添加引用之前将其删除。项目引用和 extends 是互斥的。
    {
      // Remove "extends": "./.nuxt/tsconfig.json" if present
      "files": [],
      "references": [
        { "path": "./.nuxt/tsconfig.app.json" },
        { "path": "./.nuxt/tsconfig.server.json" },
        { "path": "./.nuxt/tsconfig.shared.json" },
        { "path": "./.nuxt/tsconfig.node.json" }
      ]
    }
    
  2. 删除任何继承自 .nuxt/tsconfig.server.json 的手动服务器 tsconfig.json 文件(例如 server/tsconfig.json)。
  3. 更新你的类型检查脚本以使用项目引用的构建标志
    - "typecheck": "nuxt prepare && vue-tsc --noEmit"
    + "typecheck": "nuxt prepare && vue-tsc -b --noEmit"
    
  4. 将所有类型扩充移动到其适当的上下文中:
    • 如果你正在为应用上下文扩充类型,请将文件移动到 app/ 目录。
    • 如果你正在为服务器上下文扩充类型,请将文件移动到 server/ 目录。
    • 如果你正在扩充在应用和服务器之间共享的类型,请将文件移动到 shared/ 目录。
    app/server/shared/ 目录之外扩充类型将无法在新项目引用设置下工作。
  5. 如果需要,配置 TypeScript 选项
    export default defineNuxtConfig({
      typescript: {
        // customize tsconfig.app.json
        tsConfig: {
          // ...
        },
        // customize tsconfig.shared.json
        sharedTsConfig: {
          // ...
        },
        // customize tsconfig.node.json
        nodeTsConfig: {
          // ...
        },
      },
      nitro: {
        typescript: {
          // customize tsconfig.server.json
          tsConfig: {
            // ...
          },
        },
      },
    })
    
  6. 更新运行 TypeScript 检查的任何 CI/构建脚本,以确保它们使用新的项目引用方法。

新配置为选择加入的项目提供了更好的类型安全性和智能感知,同时为现有设置保持了完全的向后兼容性。

移除实验性功能

🚦 影响程度:极小

变更内容

在 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

更改原因

这些选项维持当前值已经有一段时间了,我们没有理由认为它们还需要保持可配置状态。

迁移步骤

  • polyfillVueUseHead 可以在用户空间通过此插件来实现
  • respectNoSSRHeader 可以在用户空间通过服务端中间件来实现

移除顶级 generate 配置

🚦 影响程度:极小

变更内容

在 Nuxt 4 中,顶级 generate 配置选项已不再可用。这包括它的所有属性:

  • generate.exclude - 用于从预渲染中排除路由
  • generate.routes - 用于指定要预渲染的路由

更改原因

顶级 generate 配置是 Nuxt 2 的遗留产物。我们支持 nitro.prerender 已经有一段时间了,它是 Nuxt 3+ 中配置预渲染的首选方式。

迁移步骤

generate 配置替换为相应的 nitro.prerender 选项

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.vuepages/bar/index.vue 这样的多个页面的组件名称都将是 index。这使得带有 include/exclude 过滤器的 <KeepAlive> 变得不可靠,并且需要手动向每个页面添加 defineOptions({ name: '...' })

迁移步骤

如果你依赖当前的组件名称(例如在 <KeepAlive>include/exclude 列表中),请将其更新为使用路由名称而不是文件名。

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

要禁用此行为

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

Nuxt 2 对比 Nuxt 3+

下表是对 3 个版本 Nuxt 的简要对比

特性 / 版本Nuxt 2Nuxt BridgeNuxt 3+
Vue223
稳定性😊 稳定😊 稳定😊 稳定
性能🏎 快✈️ 更快🚀 最快
Nitro 引擎
ESM 支持🌙 部分支持👍 更好
TypeScript☑️ 可选🚧 部分支持
组合式 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