Nuxt3 → Nuxt4 完整迁移手册
Nuxt4 不是颠覆性重写,而是 Nuxt3 的渐进式升级。大部分 Breaking Changes 可以通过
compatibilityVersion: 4提前预览和修复。本章提供一份可执行的迁移 Checklist——从评估、预览、目录迁移到回归测试,确保你的项目平滑升级到 Nuxt4。
1. 迁移评估
1.1 Nuxt4 的核心变化
| 变化 | 影响范围 | 难度 |
|---|---|---|
| 新目录结构 | app/ 替代项目根目录 | 中 |
| 默认 TypeScript 严格模式 | 类型错误变编译错误 | 低-高 |
| useAsyncData 默认 deep: false | 深层响应性变化 | 低 |
| Composition API 为默认 | Options API 需要显式启用 | 低 |
| 移除旧版兼容 API | 部分 Nuxt2 遗留 API 移除 | 低 |
| Nitro v3 | 服务端 API 微调 | 低 |
| Vue 3.5+ | 新的响应式优化 | 无(自动生效) |
1.2 评估清单
迁移前评估:
- [ ] 当前 Nuxt 版本:_____(建议先升级到 Nuxt 3.15+)
- [ ] 项目页面数量:_____
- [ ] 第三方模块数量:_____(逐一检查 Nuxt4 兼容性)
- [ ] 是否使用 Options API:_____(需要迁移到 Composition API)
- [ ] TypeScript strict 模式:_____(Nuxt4 默认开启)
- [ ] CI/CD 测试覆盖率:_____(建议 > 60% 再迁移)
- [ ] 预计迁移时间:小型项目 1-2 天,大型项目 1-2 周2. compatibilityVersion 预览
2.1 开启 Nuxt4 兼容模式
在 Nuxt3 项目中提前体验 Nuxt4 的所有变化:
// nuxt.config.ts
export default defineNuxtConfig({
future: {
compatibilityVersion: 4,
},
})效果:开启后,Nuxt3 会按照 Nuxt4 的行为运行。所有 Breaking Changes 都会生效,你可以逐个修复。
2.2 逐项开启
如果一次性开启太多变化,可以逐项启用:
export default defineNuxtConfig({
future: {
compatibilityVersion: 3, // 保持 v3 行为
},
experimental: {
// 逐项开启 v4 特性
scanPageMeta: true,
sharedPrerenderData: true,
compileTemplate: true,
templateUtils: true,
defaults: {
useAsyncData: { deep: false },
},
},
})2.3 Nuxt Codemods
官方提供了自动迁移工具:
npx nuxi@latest upgrade --dedupe
npx @nuxt/codemods@latest <codemod-name> <path>3. 目录结构迁移
3.1 新旧对比
Nuxt 3 目录结构: Nuxt 4 目录结构:
├── components/ ├── app/
├── composables/ │ ├── components/
├── layouts/ │ ├── composables/
├── middleware/ │ ├── layouts/
├── pages/ │ ├── middleware/
├── plugins/ │ ├── pages/
├── utils/ │ ├── plugins/
├── app.vue │ ├── utils/
├── error.vue │ ├── app.vue
├── nuxt.config.ts │ └── error.vue
├── server/ ├── server/
└── public/ ├── public/
├── shared/ ← 新增
└── nuxt.config.ts
核心变化:前端代码全部移入 app/ 目录,shared/ 目录用于前后端共享代码。
3.2 迁移脚本
#!/bin/bash
# migrate-to-nuxt4.sh
# 1. 创建 app 目录
mkdir -p app
# 2. 移动前端目录
for dir in components composables layouts middleware pages plugins utils assets; do
if [ -d "$dir" ]; then
mv "$dir" app/
echo "Moved $dir → app/$dir"
fi
done
# 3. 移动入口文件
for file in app.vue error.vue app.config.ts; do
if [ -f "$file" ]; then
mv "$file" app/
echo "Moved $file → app/$file"
fi
done
# 4. 创建 shared 目录(前后端共享类型/工具)
mkdir -p shared/types shared/utils
echo "Migration complete. Run 'pnpm dev' to verify."3.3 路径别名更新
// nuxt.config.ts — 新的别名映射
export default defineNuxtConfig({
alias: {
'~': './app',
'@': './app',
'~~': './',
'@@': './',
},
})注意:~ 和 @ 现在指向 app/ 而不是项目根目录。如果代码中有 ~/server/ 这类路径,需要改为 ~~/server/。
3.4 tsconfig 更新
// tsconfig.json
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true, // Nuxt4 默认 strict
"paths": {
"~/*": ["./app/*"],
"@/*": ["./app/*"],
"~~/*": ["./*"],
"@@/*": ["./*"],
"#shared/*": ["./shared/*"]
}
}
}4. Breaking Changes 逐项修复
4.1 useAsyncData deep: false
变化:Nuxt4 中 useAsyncData 和 useFetch 的 data 默认不是深层响应式。
// Nuxt 3 行为:data 是 deep reactive
const { data } = await useFetch('/api/videos')
data.value.items[0].title = 'new title' // ✅ 自动触发重渲染
// Nuxt 4 行为:data 是 shallow reactive
const { data } = await useFetch('/api/videos')
data.value.items[0].title = 'new title' // ❌ 不会触发重渲染
// 修复方式 1:显式设置 deep
const { data } = await useFetch('/api/videos', { deep: true })
// 修复方式 2(推荐):替换整个 data
data.value = { ...data.value, items: updatedItems }4.2 共享的 types 和 utils
// shared/types/index.ts — 前后端共享类型
export interface Video {
id: string
title: string
description: string
createdAt: string
}
export interface ApiResponse<T> {
data: T
meta?: { total: number; page: number }
}// shared/utils/format.ts — 前后端共享工具
export function formatDuration(seconds: number): string {
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m}:${s.toString().padStart(2, '0')}`
}使用方式(前端和后端都可以导入):
import type { Video } from '#shared/types'
import { formatDuration } from '#shared/utils/format'4.3 移除的 API
// ❌ 移除:useHead() 的 object syntax 中 hid 属性
useHead({ meta: [{ hid: 'description', name: 'description', content: '...' }] })
// ✅ Nuxt4:直接用 name 去重(自动处理)
useHead({ meta: [{ name: 'description', content: '...' }] })// ❌ 移除:definePageMeta 中 keepalive 字符串
definePageMeta({ keepalive: 'my-page' })
// ✅ Nuxt4:使用布尔值或对象
definePageMeta({ keepalive: true })4.4 Nitro v3 变化
// ❌ Nuxt3:getQuery 返回的类型可能不准确
const query = getQuery(event)
// ✅ Nuxt4:推荐使用 getValidatedQuery 配合 Zod
import { z } from 'zod'
const schema = z.object({ page: z.coerce.number().default(1) })
const query = await getValidatedQuery(event, schema.parse)5. 第三方模块兼容性
5.1 检查模块兼容性
# 检查所有已安装模块的 Nuxt4 兼容性
npx nuxi module search --compatibility 45.2 常见模块兼容状态
| 模块 | Nuxt4 兼容 | 说明 |
|---|---|---|
@nuxt/ui | ✅ v3+ | 原生支持 |
@nuxt/image | ✅ | 无变化 |
@nuxt/fonts | ✅ | 无变化 |
@pinia/nuxt | ✅ | 需要 v0.8+ |
nuxt-auth-utils | ✅ | 无变化 |
@sidebase/nuxt-auth | ⚠️ | 检查最新版本 |
@nuxtjs/i18n | ⚠️ | 需要 v9+ |
5.3 不兼容模块的处理
// 临时降级:对不兼容的模块保持 v3 行为
export default defineNuxtConfig({
future: { compatibilityVersion: 4 },
// 但某些模块路径还在旧位置
modules: ['@some/legacy-module'],
// 手动配置兼容路径
alias: {
'@some/legacy-module': '@some/legacy-module/dist/nuxt3-compat',
},
})6. 回归测试
6.1 迁移后检查清单
功能验证:
- [ ] 所有页面可正常访问(SSR + CSR)
- [ ] 页面导航正常(NuxtLink + 编程式导航)
- [ ] 数据获取正常(useFetch / useAsyncData)
- [ ] 表单提交正常
- [ ] 认证流程正常(登录 / 登出 / 权限)
- [ ] 错误页面正常(404 / 500 / error.vue)
构建验证:
- [ ] `pnpm dev` 启动无错误
- [ ] `pnpm build` 构建成功
- [ ] `pnpm typecheck` 无类型错误
- [ ] `pnpm lint` 无规范错误
- [ ] `pnpm test` 所有测试通过
性能验证:
- [ ] Lighthouse 分数不低于迁移前
- [ ] Bundle 大小不超过迁移前 10%
- [ ] SSR 响应时间无明显增长6.2 自动化回归测试
// tests/e2e/migration.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Post-migration sanity checks', () => {
test('homepage renders', async ({ page }) => {
await page.goto('/')
await expect(page.locator('h1')).toBeVisible()
})
test('navigation works', async ({ page }) => {
await page.goto('/')
await page.click('a[href="/about"]')
await expect(page).toHaveURL('/about')
})
test('API routes respond', async ({ request }) => {
const response = await request.get('/api/health')
expect(response.ok()).toBe(true)
})
test('SSR data fetching works', async ({ page }) => {
await page.goto('/videos')
const content = await page.textContent('body')
expect(content).not.toContain('undefined')
expect(content).not.toContain('null')
})
test('auth flow works', async ({ page }) => {
await page.goto('/login')
await page.fill('input[name="email"]', '[email protected]')
await page.fill('input[name="password"]', 'password')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})
})7. 迁移步骤总结
7.1 推荐迁移顺序
第 1 步:升级到 Nuxt 3.15+
↓
第 2 步:开启 compatibilityVersion: 4
↓
第 3 步:修复 TypeScript strict 模式错误
↓
第 4 步:修复 useAsyncData deep: false 相关问题
↓
第 5 步:执行目录迁移脚本(→ app/ 结构)
↓
第 6 步:更新路径别名和 import
↓
第 7 步:升级第三方模块到 Nuxt4 兼容版本
↓
第 8 步:运行完整测试套件
↓
第 9 步:正式升级到 Nuxt 4.x
7.2 回滚方案
// 如果遇到严重问题,可以快速回滚
export default defineNuxtConfig({
future: {
compatibilityVersion: 3, // 回退到 v3 行为
},
})目录结构回滚需要逆向移动文件,建议在迁移前创建 Git 分支。
本章小结
- 渐进式迁移:通过
compatibilityVersion: 4在 Nuxt3 中提前体验所有 v4 变化 - 核心变化:app/ 目录结构、deep: false 默认值、strict TypeScript、shared/ 目录
- 迁移脚本:一键移动前端文件到 app/ 目录
- 模块兼容:官方模块基本兼容,社区模块需逐一验证
- 回归测试:功能 + 构建 + 性能三维度验证,Playwright E2E 自动化
- 回滚方案:
compatibilityVersion: 3一键回退