Nuxt 4 迁移完整指南
引言:为什么要迁移
从 Nuxt 3 到 Nuxt 4 的迁移,不像 Nuxt 2 到 Nuxt 3 那样是一次巨大的跨越。但它仍然带来了值得升级的改进:更清晰的项目结构、更好的性能、增强的类型系统。
这篇文章提供一份完整的迁移指南,基于实际项目迁移经验,涵盖从准备到完成的每一个步骤。
第一部分:迁移前准备
1.1 环境要求检查
# 检查 Node.js 版本
node -v
# 要求: >= 20.0.0
# 检查包管理器版本
npm -v # >= 10.0.0
# 或
pnpm -v # >= 8.0.0
# 或
yarn -v # >= 4.0.0
# 检查当前 Nuxt 版本
npx nuxi info
1.2 项目状态评估
## 迁移评估清单
### 项目复杂度
- [ ] 页面数量:___
- [ ] 组件数量:___
- [ ] Composables 数量:___
- [ ] 中间件数量:___
- [ ] 自定义模块数量:___
### 依赖情况
- [ ] 官方模块:列出使用的模块和版本
- [ ] 第三方模块:检查 Nuxt 4 兼容性
- [ ] Vue 插件:是否有不兼容的插件
### 技术债务
- [ ] 是否使用已废弃的 API
- [ ] 是否有 TypeScript 错误被忽略
- [ ] 是否有已知的性能问题
1.3 备份与分支策略
# 创建备份分支
git checkout -b backup/nuxt3-final
git push origin backup/nuxt3-final
# 创建迁移分支
git checkout main
git checkout -b feature/nuxt4-migration
# 建议:使用 Git 子模块或 Stash 保存工作进度
第二部分:依赖更新
2.1 更新核心依赖
# 方式 1:使用 nuxi upgrade(推荐)
npx nuxi upgrade --force
# 方式 2:手动更新 package.json
{
"devDependencies": {
"nuxt": "^4.0.0",
"vue": "^3.5.0",
"@nuxt/devtools": "^2.0.0"
}
}
2.2 更新官方模块
{
"dependencies": {
"@nuxtjs/i18n": "^9.0.0",
"@pinia/nuxt": "^0.6.0",
"@nuxt/content": "^3.0.0",
"@nuxt/image": "^2.0.0",
"@nuxt/ui": "^3.0.0",
"@vueuse/nuxt": "^11.0.0"
}
}
2.3 处理不兼容的依赖
// 检查模块兼容性的脚本
// scripts/check-compat.js
const modules = require('../package.json').dependencies;
const nuxt4Compatible = {
'@nuxtjs/i18n': '>= 9.0.0',
'@pinia/nuxt': '>= 0.6.0',
'@nuxt/content': '>= 3.0.0',
// ... 添加更多
};
for (const [name, version] of Object.entries(modules)) {
if (name.startsWith('@nuxt') || name.startsWith('nuxt')) {
console.log(`检查 ${name}@${version}`);
// 检查逻辑...
}
}
# 清理并重新安装
rm -rf node_modules .nuxt .output
rm package-lock.json # 或 pnpm-lock.yaml / yarn.lock
# 重新安装
npm install # 或 pnpm install / yarn install
第三部分:配置文件更新
3.1 nuxt.config.ts 更新
// 更新前 (Nuxt 3)
export default defineNuxtConfig({
// 旧配置
experimental: {
payloadExtraction: true
},
// 废弃的选项
nitro: {
preset: 'node-server'
}
});
// 更新后 (Nuxt 4)
export default defineNuxtConfig({
// 启用 Nuxt 4 兼容模式(可选,用于渐进迁移)
future: {
compatibilityVersion: 4
},
// 新的默认目录
srcDir: 'app/',
// 更新后的配置
nitro: {
// preset 不再需要手动设置
},
// 移除 experimental 中已稳定的特性
// payloadExtraction 现在默认启用
});
3.2 TypeScript 配置更新
// tsconfig.json
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ESNext",
"lib": ["ESNext", "DOM"],
// Nuxt 4 推荐设置
"skipLibCheck": true,
"noEmit": true,
// 路径别名(如果使用新目录结构)
"paths": {
"~/*": ["./app/*"],
"@/*": ["./app/*"]
}
}
}
3.3 ESLint 配置更新
// eslint.config.js (ESLint 9 扁平配置)
import { createConfigForNuxt } from '@nuxt/eslint-config/flat';
export default createConfigForNuxt({
features: {
tooling: true,
stylistic: true
}
})
.append({
rules: {
// 项目自定义规则
'vue/multi-word-component-names': 'off'
}
});
第四部分:目录结构迁移
4.1 创建新目录结构
# 创建 app 目录
mkdir -p app
# 移动文件
mv app.vue app/
mv components app/
mv composables app/
mv layouts app/
mv middleware app/
mv pages app/
mv plugins app/
# 保持不动的文件/目录
# - public/
# - server/
# - nuxt.config.ts
# - package.json
4.2 自动化迁移脚本
#!/bin/bash
# scripts/migrate-structure.sh
# 创建目标目录
mkdir -p app
# 需要移动的目录
DIRS_TO_MOVE=(
"components"
"composables"
"layouts"
"middleware"
"pages"
"plugins"
"utils"
)
# 移动目录
for dir in "${DIRS_TO_MOVE[@]}"; do
if [ -d "$dir" ]; then
echo "移动 $dir 到 app/$dir"
mv "$dir" "app/"
fi
done
# 移动文件
if [ -f "app.vue" ]; then
mv app.vue app/
fi
if [ -f "app.config.ts" ]; then
mv app.config.ts app/
fi
if [ -f "error.vue" ]; then
mv error.vue app/
fi
echo "目录结构迁移完成!"
echo "请更新 nuxt.config.ts 中的 srcDir 配置"
4.3 更新导入路径
// 更新前
import { useAuth } from '~/composables/useAuth'
import MyComponent from '~/components/MyComponent.vue'
// 更新后(如果使用新目录结构,路径仍然有效)
// ~ 和 @ 别名会自动指向 app/ 目录
import { useAuth } from '~/composables/useAuth'
import MyComponent from '~/components/MyComponent.vue'
// 但如果有硬编码的相对路径,需要更新
// 更新前
import { helper } from '../utils/helper'
// 更新后(视具体情况)
import { helper } from '~/utils/helper'
第五部分:代码调整
5.1 移除废弃 API
// ❌ 废弃的写法
import { defineNuxtRouteMiddleware } from '#app'
export default defineNuxtRouteMiddleware((to, from) => {
// ...
})
// ✅ 新的写法
export default defineNuxtRouteMiddleware((to, from) => {
// 直接使用,无需导入
})
// ❌ 废弃的写法
const nuxtApp = useNuxtApp()
nuxtApp.$router.push('/home')
// ✅ 新的写法
const router = useRouter()
router.push('/home')
// ❌ 废弃的写法
useAsyncData('key', () => fetch(), { lazy: true })
// ✅ 新的写法
useLazyAsyncData('key', () => fetch())
5.2 更新 useAsyncData 用法
// 检查项目中所有 useAsyncData 调用
// 变更 1:默认不再深度响应式
// 更新前(Nuxt 3 默认 deep: true)
const { data } = await useAsyncData('config', fetchConfig)
data.value.nested.property = 'new' // 会触发响应
// 更新后(Nuxt 4 默认 deep: false)
const { data } = await useAsyncData('config', fetchConfig, {
deep: true // 需要显式开启
})
// 变更 2:getCachedData 新签名
// 更新前
const { data } = await useAsyncData('key', fetchData, {
getCachedData: (key) => {
return nuxtApp.payload.data[key]
}
})
// 更新后
const { data } = await useAsyncData('key', fetchData, {
getCachedData: (key, nuxtApp) => { // nuxtApp 作为参数传入
return nuxtApp.payload.data[key]
}
})
5.3 更新组件语法
<!-- 更新前 -->
<template>
<div>
<!-- 废弃的 <NuxtLayout> 用法 -->
<NuxtLayout :name="layout">
<NuxtPage />
</NuxtLayout>
</div>
</template>
<script setup>
// 废弃的 layout 计算属性
const layout = computed(() => route.meta.layout || 'default')
</script>
<!-- 更新后 -->
<template>
<div>
<!-- 布局现在通过 definePageMeta 控制 -->
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<script setup>
// 在页面组件中使用 definePageMeta
// pages/dashboard.vue
definePageMeta({
layout: 'admin'
})
</script>
5.4 更新中间件
// middleware/auth.ts
// 更新前
export default defineNuxtRouteMiddleware((to, from) => {
// 使用 process.server 检查
if (process.server) return
const auth = useAuth()
if (!auth.isAuthenticated) {
return navigateTo('/login')
}
})
// 更新后
export default defineNuxtRouteMiddleware((to, from) => {
// 使用 import.meta 检查
if (import.meta.server) return
const auth = useAuth()
if (!auth.isAuthenticated) {
return navigateTo('/login')
}
})
5.5 更新服务端 API
// server/api/users.ts
// 更新前
export default defineEventHandler((event) => {
// 使用旧的 API
const body = await useBody(event)
const query = useQuery(event)
return { success: true }
})
// 更新后
export default defineEventHandler(async (event) => {
// 使用新的 API
const body = await readBody(event)
const query = getQuery(event)
return { success: true }
})
第六部分:模块迁移
6.1 @nuxtjs/i18n 迁移
// nuxt.config.ts 更新前
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: ['en', 'zh'],
defaultLocale: 'en',
vueI18n: './i18n.config.ts'
}
})
// 更新后
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'en', file: 'en.json' },
{ code: 'zh', file: 'zh.json' }
],
defaultLocale: 'en',
lazy: true,
langDir: 'locales',
// 新增配置
bundle: {
optimizeTranslations: true
}
}
})
6.2 @pinia/nuxt 迁移
// stores/user.ts 更新前
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
email: ''
}),
actions: {
async fetchUser() {
// 旧的 $fetch 用法
const user = await $fetch('/api/user')
this.$patch(user)
}
}
})
// 更新后(推荐使用 setup 语法)
export const useUserStore = defineStore('user', () => {
const name = ref('')
const email = ref('')
async function fetchUser() {
const user = await $fetch('/api/user')
name.value = user.name
email.value = user.email
}
return { name, email, fetchUser }
})
6.3 @nuxt/content 迁移
// nuxt.config.ts 更新
export default defineNuxtConfig({
modules: ['@nuxt/content'],
content: {
// v3 新配置
database: {
type: 'sqlite', // 或 'postgres' 用于生产
},
// 移除废弃选项
// documentDriven: true // 不再支持
}
})
// 查询语法更新
// 更新前
const { data: articles } = await useAsyncData('articles', () => {
return queryContent('/blog')
.where({ published: true })
.sortBy('date', 'desc')
.find()
})
// 更新后
const { data: articles } = await useAsyncData('articles', () => {
return queryCollection('blog')
.where('published', '==', true)
.order('date', 'desc')
.all()
})
第七部分:测试与验证
7.1 自动化测试
// test/migration.test.ts
import { describe, it, expect } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils'
describe('Nuxt 4 迁移验证', async () => {
await setup({
server: true
})
it('首页正常渲染', async () => {
const html = await $fetch('/')
expect(html).toContain('<!DOCTYPE html>')
})
it('API 路由正常工作', async () => {
const data = await $fetch('/api/health')
expect(data.status).toBe('ok')
})
it('SSR 数据获取正常', async () => {
const html = await $fetch('/products')
expect(html).toContain('product-list')
})
it('客户端导航正常', async () => {
// 使用 Playwright 或类似工具测试
})
})
7.2 手动检查清单
## 手动验证清单
### 页面渲染
- [ ] 首页加载正常
- [ ] 所有路由可访问
- [ ] 404 页面正常显示
- [ ] 错误页面正常显示
### 功能验证
- [ ] 用户登录/登出
- [ ] 表单提交
- [ ] 数据加载
- [ ] 图片加载
### 性能检查
- [ ] 首屏加载时间
- [ ] 客户端导航速度
- [ ] 内存使用正常
### SEO 验证
- [ ] Meta 标签正确
- [ ] OG 标签正确
- [ ] 结构化数据正确
7.3 性能基准测试
# 使用 Lighthouse
npx lighthouse http://localhost:3000 --output json --output-path ./lighthouse-report.json
# 对比迁移前后的分数
# - Performance
# - First Contentful Paint
# - Time to Interactive
# - Total Blocking Time
第八部分:常见问题与解决方案
8.1 Hydration 错误
// 问题:hydration mismatch
// 原因:服务端和客户端渲染结果不一致
// 常见场景 1:日期时间
// ❌ 会导致 mismatch
<template>
<span>{{ new Date().toLocaleString() }}</span>
</template>
// ✅ 解决方案
<template>
<ClientOnly>
<span>{{ formattedDate }}</span>
<template #fallback>
<span>加载中...</span>
</template>
</ClientOnly>
</template>
// 常见场景 2:随机数
// ❌
<template>
<div :id="`el-${Math.random()}`">...</div>
</template>
// ✅ 使用 useId()
<script setup>
const id = useId()
</script>
<template>
<div :id="id">...</div>
</template>
8.2 类型错误
// 问题:迁移后出现大量类型错误
// 解决方案 1:重新生成类型
npx nuxi prepare
// 解决方案 2:更新类型定义
// types/index.d.ts
declare module '#app' {
interface PageMeta {
// 自定义页面元数据类型
requiresAuth?: boolean
title?: string
}
}
declare module 'nuxt/schema' {
interface RuntimeConfig {
apiSecret: string
public: {
apiBase: string
}
}
}
8.3 模块加载失败
// 问题:某些模块无法加载
// 诊断
npx nuxi info
// 常见解决方案
// 1. 清理缓存
rm -rf node_modules .nuxt .output
npm install
// 2. 检查模块版本
npm ls @nuxtjs/i18n
// 3. 查看详细错误
DEBUG=nuxt:* npm run dev
// 4. 临时禁用问题模块
export default defineNuxtConfig({
modules: [
// '@problem/module', // 暂时注释
]
})
8.4 构建问题
// 问题:生产构建失败
// 诊断命令
npm run build -- --debug
// 常见问题与解决
// 1. 内存不足
NODE_OPTIONS="--max-old-space-size=8192" npm run build
// 2. 路径问题
// 检查 nuxt.config.ts 中的路径配置
export default defineNuxtConfig({
// 确保路径正确
srcDir: 'app/',
dir: {
pages: 'pages',
layouts: 'layouts'
}
})
// 3. 依赖问题
// 尝试单独构建
npm run generate -- --preset node-server
第九部分:迁移后优化
9.1 利用新特性
// 利用 Nuxt 4 新特性优化代码
// 1. 更好的类型推导
const route = useRoute('products-id')
// route.params.id 自动推导为 string
// 2. 简化的数据获取
const { data, status } = await useAsyncData('products', () =>
$fetch('/api/products')
)
// status: 'idle' | 'pending' | 'success' | 'error'
// 3. 改进的错误处理
<script setup>
const { data, error } = await useAsyncData('data', fetchData)
if (error.value) {
// error.value 包含结构化错误信息
console.error(error.value.data)
}
</script>
9.2 清理冗余代码
// 迁移完成后,清理不再需要的代码
// 1. 移除 polyfills(如果有)
// Nuxt 4 要求 Node 20+,很多 polyfill 不再需要
// 2. 简化配置
export default defineNuxtConfig({
// 移除已成为默认值的配置
// experimental: { payloadExtraction: true } // 现在默认开启
})
// 3. 清理废弃的工具函数
// 检查 utils/ 目录,移除被内置功能替代的函数
结语:迁移是投资,不是成本
完成 Nuxt 4 迁移后,你会发现很多之前需要手动处理的事情现在变得更加自动化,很多配置变得更加简洁。这些改进在日常开发中会持续产生价值。
迁移过程可能会遇到各种问题,但大多数都有明确的解决方案。保持耐心,一步一步来,遇到问题及时查阅文档和社区资源。
祝你迁移顺利!


