升级指南

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

升级 Nuxt

最新版本

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

npx nuxi upgrade

夜间发布通道

要使用最新的 Nuxt 构建并在发布前测试功能,请参阅 夜间发布通道 指南。

夜间发布通道的 latest 标签目前跟踪的是 Nuxt v4 分支,这意味着目前尤其可能包含重大变更——请小心!你可以选择使用 3.x 分支的夜间版本,方法是设置 "nuxt": "npm:nuxt-nightly@3x"

测试 Nuxt 4

Nuxt 4 的发布日期 待定。它取决于 Nitro 重大发布后有足够的时间在社区中进行充分测试。你可以在 此 PR 中跟踪 Nitro 发布进展。

在发布之前,可以从 Nuxt 版本 3.12+ 开始测试 Nuxt 4 的许多重大变更。

然而,迁移 并非必需。如果你希望保留当前文件夹结构,Nuxt 应该会自动检测。(如果没有,请提出问题。)唯一的例外是如果你 已经 自定义了 srcDir。在这种情况下,你应注意你的 modules/public/server/ 文件夹将从 rootDir 而不是自定义的 srcDir 解析。你可以通过配置 dir.modulesdir.publicserverDir 来覆盖此行为。

你还可以使用以下配置强制使用 v3 文件夹结构:

nuxt.config.ts
export default defineNuxtConfig({
  // 这将新的 srcDir 默认值从 `app` 恢复为你的根目录
  srcDir: '.',
  // 这指定了 `app/router.options.ts` 和 `app/spa-loading-template.html` 的目录前缀
  dir: {
    app: 'app'
  }
})

单例数据获取层

🚦 影响级别: 中等

变更内容

Nuxt 的数据获取系统(useAsyncDatauseFetch)经过了重大重构,以提高性能和一致性:

  1. 相同 key 共享 refs:所有使用相同 key 调用 useAsyncDatauseFetch 的请求现在共享相同的 dataerrorstatus 引用。这意味着所有使用显式 key 的调用必须确保 deeptransformpickgetCachedDatadefault 选项没有冲突。
  2. getCachedData 的更多控制:即使是由观察者或调用 refreshNuxtData 引发的请求,每次数据获取时,getCachedData 函数都会被调用。(之前,只有在这些情况下数据被获取时,getCachedData 函数才不会被调用。)为了提供更多控制权来决定何时使用缓存数据以及何时重新获取数据,函数现在接收一个包含请求原因的上下文对象。
  3. 响应式 key 支持:现在可以使用计算引用、普通引用或 getter 函数作为 key,这使得自动数据重新获取成为可能,并且每个请求的数据将被单独存储。
  4. 数据清理:当最后一个使用 useAsyncData 获取数据的组件被卸载时,Nuxt 将删除该数据,以避免内存使用不断增长。

变更原因

这些改动旨在提高内存使用效率,并增强跨调用的一致性,尤其是在加载状态的处理上。

迁移步骤

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

    可能需要将共享显式 key 的(并且有自定义选项的) useAsyncData 调用提取到单独的组合式函数中:
    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
  }
})

路由元数据的去重

🚦 影响级别:轻微

变更内容

使用 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-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', 
    // 元标签不需要 vmid 或 key    
-   vmid: 'description' 
-   hid: 'description'
  }]
})
import { TemplateParamsPlugin, AliasSortingPlugin } 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)时,我们可以选择渲染加载屏幕(来自 app/spa-loading-template.html),位于 Nuxt 应用根目录内:

<div id="__nuxt">
  <!-- spa 加载模板 -->
</div>

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

<div id="__nuxt"></div>
<!-- spa 加载模板 -->

变更原因

这允许 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.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.config.ts
export default defineNuxtConfig({
  experimental: {
    parseErrorData: false
  },
})

更细粒度的内联样式

🚦 影响级别:中等

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
// 在动态页面(例如 `[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}`)
})

或者,你可以通过以下方式禁用此功能:

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 自动化此步骤。

如果遇到任何问题,你可以暂时恢复到之前的行为:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    defaults: {
      useAsyncData: {
        value: 'null',
        errorValue: 'null'
      }
    }
  }
})

如果你在这样做,请报告问题,因为我们不打算保持此配置。

移除 useAsyncDatauseFetchrefresh 调用时已弃用的 dedupe 选项的布尔值

🚦 影响级别:轻微

变更内容

之前可以在 refresh 中传递 dedupe: boolean。这些是 canceltrue)和 deferfalse)的别名。

app.vue
const { refresh } = await useAsyncData(async () => ({ message: '你好,Nuxt!' }))

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

变更原因

为了更清晰,移除了这些别名。

在为 useAsyncData 添加 dedupe 选项时出现了问题,我们移除了布尔值,因为它们最终是 相反的

refresh({ dedupe: false }) 表示“不要 取消 现有请求以优先于这个新请求”。但在 useAsyncData 的选项中传递 dedupe: true 表示“如果有现有待处理请求,不要发起新请求。”(参见 PR。)

迁移步骤

迁移应该很简单:

  const { refresh } = await useAsyncData(async () => ({ message: '你好,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。在重置/清除数据时应尊重这一点。

迁移步骤

如果遇到任何问题,你可以暂时恢复到之前的行为:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    resetAsyncDataToUndefined: 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 模块。参见 问题 #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)

目录索引扫描

🚦 影响级别:中等

变更内容

你的 middleware/ 文件夹中的子文件夹现在也会被扫描以查找 index 文件,这些文件现在也会在你的项目中注册为中间件。

变更原因

Nuxt 自动扫描多个文件夹,包括 middleware/plugins/

你的 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 自动化此步骤。

移除实验性功能

🚦 影响级别:轻微

变更内容

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 可以在用户端通过 服务器中间件 实现。

Nuxt 2 vs. Nuxt 3+

下表对 Nuxt 的三个版本进行了快速比较:

功能/版本Nuxt 2Nuxt BridgeNuxt 3+
Vue223
稳定性😊 稳定😊 稳定😊 稳定
性能🏎 快✈️ 更快🚀 最快
Nitro 引擎
ESM 支持🌙 部分👍 更好
TypeScript☑️ 可选🚧 部分
组合式 API🚧 部分
选项式 API
组件自动导入
<script setup> 语法🚧 部分
自动导入
webpack445
Vite⚠️ 部分🚧 部分
Nuxi CLI❌ 旧版✅ nuxi✅ nuxi
静态站点

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