正文
Nuxt 4 不是一次“推倒重来”的升级,而是一次更偏向开发体验、项目组织、数据获取一致性和类型安全的稳定型大版本演进。官方在 Nuxt 4.0 发布说明中提到,它重点改进了新的 app/ 目录结构、更聪明的数据获取、更好的 TypeScript 支持,以及更快的 CLI 和开发体验。
不过,越是“看起来平滑”的升级,越容易让团队低估细节风险。真正开始升级前,建议先查四件事:目录结构有没有受影响、useFetch 行为有没有改变、TypeScript 是否会暴露新错误、模块和本地扩展是否兼容 Nuxt 4。
为什么 Nuxt 4 升级前不能直接改版本号?
很多项目从 Nuxt 3 升到 Nuxt 4,第一反应是执行:
pnpm add nuxt@^4.0.0或者:
npm install nuxt@^4.0.0这确实是官方升级指南给出的第一步,但它不应该是你的第一项工作。官方指南也明确列出了 Nuxt 4 的关键迁移点,包括新目录结构、数据获取层、TypeScript 配置变化、模板编译变化、useFetch 默认值变化等。
更安全的顺序是:
- 先在独立分支升级。
- 跑完整测试、类型检查和构建。
- 检查目录别名、服务端 API、模块、插件。
- 再处理业务代码中的行为差异。
换句话说,Nuxt 4 升级前不是先“改”,而是先“查”。
先查目录结构:app/ 不是小改名,而是上下文变化
Nuxt 4 最明显的变化是新的目录组织方式。应用层代码默认放到 app/ 目录中,例如:
my-nuxt-app/
├─ app/
│ ├─ assets/
│ ├─ components/
│ ├─ composables/
│ ├─ layouts/
│ ├─ middleware/
│ ├─ pages/
│ ├─ plugins/
│ ├─ utils/
│ ├─ app.vue
│ ├─ app.config.ts
│ └─ error.vue
├─ server/
├─ shared/
├─ public/
├─ content/
└─ nuxt.config.ts官方目录结构文档说明,app/ 是 Nuxt 应用的主目录,包含组件、页面、布局、插件、组合式函数、工具函数、app.vue、error.vue 等应用层文件;server/ 用于服务端代码;shared/ 用于同时被 Vue 应用和 Nitro 服务端使用的共享代码。
升级前你要重点查这些位置:
pages/、components/、layouts/是否还在根目录。- 代码里是否大量使用
~/或@/路径别名。 server/是否被错误放进了旧的srcDir。public/、modules/、layers/是否依赖旧路径。- 共享类型和工具函数是否应该移到
shared/。
Nuxt 4 的升级指南提到,新默认 srcDir 是 app/,serverDir 默认指向 <rootDir>/server,而 layers/、modules/、public/ 默认相对 <rootDir> 解析;同时新增 shared/,并支持 shared/utils/ 和 shared/types/ 自动导入。
这意味着,目录迁移不是简单把文件拖进 app/。你还要检查所有路径引用。例如旧项目中常见写法:
import { formatDate } from '~/utils/date'迁移后,如果 utils/date.ts 被放进 app/utils/date.ts,问题不大;但如果它是前后端都要用的工具函数,就更适合放到:
shared/utils/date.ts这样可以避免服务端和客户端边界混乱。
旧结构还能不能继续用?可以,但要知道代价
Nuxt 4 不是强制所有项目立刻迁移到 app/ 目录。官方说明,Nuxt 会检测旧结构并保持向后兼容;如果你暂时不想迁移,也可以通过配置把 srcDir 改回根目录。
示例:
export default defineNuxtConfig({
srcDir: '.',
dir: {
app: 'app'
}
})这对大型项目很有用,尤其是你有复杂 CI、多人协作、模块封装或大量路径别名时。
但长期来看,建议逐步迁移到新结构。原因很简单:Nuxt 4 的 app/、server/、shared/ 分层更清楚,IDE 更容易理解当前代码运行在哪个上下文,团队成员也更容易判断“这段代码能不能访问浏览器 API”或“这段代码能不能跑在服务端”。
再查 useFetch:同一个 key 会共享数据引用
Nuxt 4 的数据获取层变化很关键,尤其是 useFetch 和 useAsyncData。官方说明,Nuxt 4 重组了数据获取系统:相同 key 的 useFetch 或 useAsyncData 会共享同一组 data、error 和 status refs。
这听起来是优化,但也可能暴露问题。
例如你在两个组件里这样写:
const { data } = await useFetch('/api/user', {
key: 'user',
pick: ['name']
})另一个地方:
const { data } = await useFetch('/api/user', {
key: 'user',
transform: user => ({
label: user.name
})
})如果 key 相同,但 pick、transform、deep、default 等选项不同,就可能出现数据形态不一致的问题。升级前最好全局搜索:
useFetch(
useAsyncData(
key:然后检查显式 key 是否被重复使用。我的建议是:同一个 key 只表达同一种数据形态。如果返回结构不同,就换 key。
查 data 和 error:不要再只判断 null
Nuxt 4 中,useAsyncData 和 useFetch 返回的 data、error 默认值现在会是 undefined,而不是旧行为里更常见的 null。官方建议,如果你以前检查 data.value === null 或 error.value === null,应改为检查 undefined。
旧写法:
if (data.value === null) {
// no data
}建议改成:
if (data.value === undefined) {
// no data
}模板里也一样:
<template>
<div v-if="data === undefined">
暂无数据
</div>
<div v-else>
{{ data }}
</div>
</template>这类问题不一定会让构建失败,却会导致空状态、错误状态、加载状态显示异常。所以升级前要查所有 null 判断,尤其是列表页、详情页、搜索页和后台管理页面。
查 pending:它不再等于“还没请求过”
Nuxt 4 中,pending 的含义更严格。官方说明,pending 现在是计算属性,只有当 status 也是 pending 时才为 true;当你设置 immediate: false 时,在第一次请求发出前,pending 会是 false。
这会影响很多手动触发请求的场景:
const { data, pending, execute, status } = await useFetch('/api/report', {
immediate: false
})如果旧代码用:
<div v-if="!pending">
{{ data }}
</div>升级后可能逻辑不准。更稳的写法是根据 status 判断:
<template>
<button @click="execute">加载报表</button>
<p v-if="status === 'idle'">请点击加载</p>
<p v-else-if="status === 'pending'">加载中...</p>
<pre v-else-if="status === 'success'">{{ data }}</pre>
<p v-else>加载失败</p>
</template>升级前请重点检查所有 pending、status、immediate: false、execute()、refresh() 的组合。
查深层响应式:data 现在默认是 shallowRef
Nuxt 4 中,useFetch、useAsyncData、useLazyFetch 和 useLazyAsyncData 返回的 data 默认变为 shallowRef。官方解释是,当新数据整体替换时仍然响应式,但如果你直接修改数据对象内部属性,不会触发深层响应式更新;这样做能改善深层对象和数组的性能。
这类旧代码要特别小心:
const { data: user } = await useFetch('/api/user')
user.value.profile.name = 'New Name'更好的做法是整体替换:
user.value = {
...user.value,
profile: {
...user.value.profile,
name: 'New Name'
}
}如果你确实依赖深层响应式,可以单次开启:
const { data } = await useFetch('/api/user', {
deep: true
})但别为了省事全局开启。Nuxt 4 默认浅响应式是为了性能,尤其对大型列表、嵌套 JSON、CMS 内容和后台表格更友好。
查 refresh({ dedupe }):布尔值该改成字符串
Nuxt 4 移除了 refresh 中 dedupe: boolean 的旧写法。官方迁移示例显示,应把 true 改为 'cancel',把 false 改为 'defer'。
旧写法:
await refresh({ dedupe: true })
await refresh({ dedupe: false })新写法:
await refresh({ dedupe: 'cancel' })
await refresh({ dedupe: 'defer' })这类问题 TypeScript 通常能帮你抓出来,但前提是你真的跑了类型检查。
查 TypeScript:Nuxt 4 会让隐藏问题浮出水面
Nuxt 4 的 TypeScript 体验更强,但它也会让旧代码中“之前没报错”的问题变得明显。
官方升级指南提到,compilerOptions.noUncheckedIndexedAccess 现在默认是 true,这意味着通过索引访问数组或对象时,TypeScript 会更谨慎地认为结果可能是 undefined。
例如:
const first = users[0]
console.log(first.name)升级后可能需要改成:
const first = users[0]
if (first) {
console.log(first.name)
}或者:
const firstName = users[0]?.name这不是 Nuxt 在“找麻烦”,而是在帮你提前发现潜在运行时错误。尤其是接口数据、CMS 数据、搜索结果、分页列表,都很容易出现数组为空的问题。
查 TypeScript 配置拆分:客户端、服务端、共享代码要分清
Nuxt 4 会生成更细的 TypeScript 配置,包括 app、server、node、shared 等不同上下文的配置文件。官方说明,这样能让应用代码、服务端代码、构建时代码和共享代码获得更准确的类型检查、自动补全和错误提示。
你要重点检查这几类问题:
// server/ 中误用 window
window.localStorage.getItem('token')// app/ 中误用仅服务端可用的逻辑
import { readFileSync } from 'node:fs'// shared/ 中混入浏览器或 Node 专属 API
export const isDark = window.matchMedia('(prefers-color-scheme: dark)').matchesshared/ 的核心价值是“前后端都能安全使用”。所以这里最好只放:
- 类型定义。
- 纯函数。
- 常量。
- 不依赖浏览器或 Node 专属 API 的工具函数。
如果某个函数需要 window,放 app/utils。如果某个函数需要 fs、数据库、私钥或服务端环境变量,放 server/utils。
查模块兼容性:不要只看项目能启动
模块兼容性是 Nuxt 4 升级前最容易被低估的一项。官方 Nuxt 4 发布说明提到,少数模块可能需要进一步更新才能完整兼容 Nuxt 4,部分旧工具和废弃特性也已被清理。
升级前请列出项目中的模块:
export default defineNuxtConfig({
modules: [
'@nuxtjs/i18n',
'@nuxt/image',
'@pinia/nuxt',
'@nuxt/content',
// ...
]
})然后逐个检查:
- 是否有支持 Nuxt 4 的版本。
- 是否有迁移指南。
- 是否依赖旧目录结构。
- 是否修改了 Nitro、Vite、路由、自动导入或 TypeScript 配置。
- 是否在构建时和运行时表现一致。
官方 Nuxt Modules 页面可以作为检查入口,用来确认模块生态状态和维护情况。
本地模块也别忘了查。很多团队会在 modules/ 里写自己的 Nuxt 模块,用来注册组件、插件、API、运行时配置或自动导入。Nuxt 模块会在开发启动或生产构建时运行,适合封装可复用能力,但升级后也要确认 hooks、路径、类型声明和模板生成是否还正常。
建议的 Nuxt 4 升级前检查清单
| 检查项 | 要查什么 | 风险 |
|---|---|---|
| 目录结构 | app/、server/、shared/、public/ 是否放对 | 路径别名失效、自动导入异常 |
useFetch key | 相同 key 是否用了不同 transform、pick、default | 数据共享后结果不一致 |
| 空值判断 | 是否还在判断 null | 空状态或错误状态不显示 |
pending | 是否依赖旧的 pending 行为 | 加载状态异常 |
| 深层响应式 | 是否直接修改 data.value.xxx | UI 不更新 |
| TypeScript | 是否新增索引访问错误 | 构建失败或隐藏 bug 暴露 |
shared/ | 是否混入浏览器或 Node 专属 API | 客户端或服务端运行时报错 |
| 模块 | 第三方模块和本地模块是否兼容 | 开发能跑,构建或部署失败 |
一个更稳的升级流程
推荐按下面节奏做:
git checkout -b upgrade/nuxt-4
pnpm add nuxt@^4.0.0
pnpm install
pnpm nuxi prepare
pnpm typecheck
pnpm build
pnpm test如果你想先自动处理一部分迁移,Nuxt 官方升级指南也提供了 codemod 迁移方式,可以运行 Nuxt 4 migration recipe 来批量执行多个迁移步骤。
但要记住:codemod 只能处理可机械替换的部分,不能替你判断业务语义。像 useFetch key 是否冲突、共享函数应该放哪里、模块是否真的兼容生产构建,这些仍然需要人工 review。
常见问题 FAQ
1. Nuxt 4 一定要使用 app/ 目录吗?
不一定。Nuxt 4 对旧结构有向后兼容能力,也可以通过配置继续使用旧结构。但新项目或中长期维护项目更建议迁移到 app/,因为它能让应用、服务端和共享代码边界更清晰。
2. useFetch 升级后最容易出什么问题?
最常见的是相同 key 复用导致数据形态冲突,其次是 data、error 默认值从旧习惯中的 null 判断变成更适合判断 undefined,以及 pending、浅响应式行为变化。
3. 为什么我的数据改了,但页面没更新?
因为 Nuxt 4 中 useFetch 返回的 data 默认是 shallowRef。如果你直接修改深层属性,可能不会触发更新。建议整体替换对象,或者在确实需要时设置 { deep: true }。
4. TypeScript 报更多错是坏事吗?
不一定。Nuxt 4 的 TypeScript 配置更严格、更分上下文,很多新增错误其实是在提醒你处理潜在的 undefined、客户端服务端 API 混用、共享代码边界不清等问题。
5. 第三方模块怎么确认是否支持 Nuxt 4?
先查模块官方文档、npm 版本、GitHub issue/release,再看 Nuxt Modules 官方目录。升级时最好逐个启用模块并测试,而不是一次性全部打开后再排查。
6. 小项目可以直接升级 Nuxt 4 吗?
可以,但仍建议至少跑 typecheck、build 和主要页面回归测试。小项目风险低,不代表没有 useFetch 状态判断、路径别名或模块兼容问题。
结论:Nuxt 4 升级前,先查边界,再改代码
Nuxt 4 升级前先查什么:目录结构、useFetch、TypeScript 和模块兼容性?答案很明确:先查代码运行边界,再查数据行为,再查类型系统,最后查生态模块。
app/ 目录让项目更清楚,shared/ 让前后端共享更规范,useFetch 的新行为让数据层更一致,TypeScript 拆分让错误更早暴露。只要你在升级前把这些点查清楚,Nuxt 4 迁移通常不会是灾难,反而会是一次清理技术债、提升项目可维护性的好机会。