Nuxt 生态 精选推荐

Nuxt 4 迁移完整指南:从 Nuxt 3 平滑升级的实战路线图

HTMLPAGE 团队
18 分钟阅读

详细的 Nuxt 3 到 Nuxt 4 迁移指南,包括准备工作、代码调整、目录重构、常见问题排查等完整步骤,帮助团队实现安全、高效的版本升级。

#Nuxt 4 #迁移指南 #版本升级 #Vue #SSR

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 迁移后,你会发现很多之前需要手动处理的事情现在变得更加自动化,很多配置变得更加简洁。这些改进在日常开发中会持续产生价值。

迁移过程可能会遇到各种问题,但大多数都有明确的解决方案。保持耐心,一步一步来,遇到问题及时查阅文档和社区资源。

祝你迁移顺利!


参考资源