Nuxt 3 vs Nuxt 2 迁移指南:平滑升级的实战路径

HTMLPAGE 团队
22 分钟阅读

详细对比 Nuxt 2 与 Nuxt 3 的核心差异,提供循序渐进的迁移步骤、常见问题解决方案和兼容性处理技巧,帮助项目顺利过渡到新版本。

#Nuxt迁移 #Nuxt 2 #Nuxt 3 #版本升级 #Vue 3

Nuxt 3 vs Nuxt 2 迁移指南:平滑升级的实战路径

Nuxt 3 带来了全新的架构和开发体验——Vue 3、Vite、TypeScript 原生支持。但迁移并非一键完成,特别是对于大型项目。本文将帮你理清迁移路径,避开常见陷阱。

核心差异对比

技术栈变化

特性Nuxt 2Nuxt 3
Vue 版本Vue 2Vue 3
构建工具WebpackVite(默认)
服务端引擎ConnectNitro
TypeScript可选(配置繁琐)原生支持
Composition API需插件内置
自动导入部分全面

API 变化概览

// Nuxt 2
export default {
  asyncData({ $axios, params }) {
    return $axios.$get(`/api/posts/${params.id}`)
  },
  head() {
    return { title: this.post.title }
  }
}

// Nuxt 3
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
useHead({ title: post.value?.title })

迁移前准备

评估迁移成本

因素低成本高成本
代码量<50 个页面>200 个页面
Vue 版本已用 Composition API全是 Options API
依赖库均有 Vue 3 版本依赖 Vue 2 专属库
TypeScript已使用未使用
测试覆盖有完整测试无测试

迁移策略选择

策略一:全量迁移

适用于小型项目(<30 页面):

  1. 创建新 Nuxt 3 项目
  2. 逐个迁移组件和页面
  3. 一次性切换

策略二:渐进迁移

适用于大型项目:

  1. 使用 Nuxt Bridge 过渡
  2. 逐步替换 API
  3. 分批迁移页面

策略三:并行运行

适用于关键业务系统:

  1. 新功能用 Nuxt 3 开发
  2. 老功能维持 Nuxt 2
  3. 通过反向代理路由

Nuxt Bridge:渐进迁移的桥梁

Nuxt Bridge 让你在 Nuxt 2 项目中体验 Nuxt 3 的部分特性:

安装 Bridge

# 移除 nuxt 依赖
npm remove nuxt

# 安装 nuxt-bridge
npm install nuxt@npm:nuxt-bridge

更新配置

// nuxt.config.js
import { defineNuxtConfig } from '@nuxt/bridge'

export default defineNuxtConfig({
  bridge: {
    nitro: true,  // 启用 Nitro 引擎
    vite: true,   // 使用 Vite 构建
    meta: true,   // 启用新的 useHead
    capi: true    // 启用 Composition API
  }
})

Bridge 支持的特性

特性支持状态
Composition API
useAsyncData/useFetch
useHead
Nitro 服务端
Vite
TypeScript
自动导入
<script setup>

核心 API 迁移

asyncData → useFetch/useAsyncData

// Nuxt 2
export default {
  async asyncData({ $axios, params, error }) {
    try {
      const post = await $axios.$get(`/api/posts/${params.id}`)
      return { post }
    } catch (e) {
      error({ statusCode: 404, message: 'Post not found' })
    }
  }
}

// Nuxt 3
const route = useRoute()
const { data: post, error } = await useFetch(`/api/posts/${route.params.id}`)

if (error.value) {
  throw createError({ statusCode: 404, message: 'Post not found' })
}

fetch → useFetch(组件级)

// Nuxt 2
export default {
  data: () => ({ posts: [] }),
  async fetch() {
    this.posts = await this.$axios.$get('/api/posts')
  }
}

// Nuxt 3
const { data: posts, pending, refresh } = await useFetch('/api/posts')

// 刷新数据
await refresh()

head → useHead/useSeoMeta

// Nuxt 2
export default {
  head() {
    return {
      title: this.post.title,
      meta: [
        { hid: 'description', name: 'description', content: this.post.excerpt }
      ]
    }
  }
}

// Nuxt 3
const post = ref({ title: 'Hello', excerpt: 'World' })

useHead({
  title: () => post.value.title
})

useSeoMeta({
  description: () => post.value.excerpt,
  ogTitle: () => post.value.title
})

context → Composables

// Nuxt 2 - 通过 context 访问
export default {
  asyncData({ $axios, route, store, redirect }) {
    // ...
  }
}

// Nuxt 3 - 使用 Composables
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
const { $axios } = useNuxtApp()  // 如果仍需 axios

Store:Vuex → Pinia

// Nuxt 2 Vuex
// store/counter.js
export const state = () => ({
  count: 0
})
export const mutations = {
  increment(state) {
    state.count++
  }
}
export const actions = {
  async fetchCount({ commit }) {
    const count = await this.$axios.$get('/api/count')
    commit('setCount', count)
  }
}

// Nuxt 3 Pinia
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++
    },
    async fetchCount() {
      this.count = await $fetch('/api/count')
    }
  }
})

// 或 Setup 风格
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  
  function increment() {
    count.value++
  }
  
  async function fetchCount() {
    count.value = await $fetch('/api/count')
  }
  
  return { count, increment, fetchCount }
})

Middleware 迁移

// Nuxt 2
// middleware/auth.js
export default function ({ store, redirect }) {
  if (!store.state.authenticated) {
    return redirect('/login')
  }
}

// Nuxt 3
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const auth = useAuthStore()
  
  if (!auth.isAuthenticated) {
    return navigateTo('/login')
  }
})

Plugins 迁移

// Nuxt 2
// plugins/axios.js
export default function ({ $axios, redirect }) {
  $axios.onError(error => {
    if (error.response.status === 401) {
      redirect('/login')
    }
  })
}

// Nuxt 3
// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
  const api = $fetch.create({
    baseURL: '/api',
    onResponseError({ response }) {
      if (response.status === 401) {
        navigateTo('/login')
      }
    }
  })
  
  return {
    provide: {
      api
    }
  }
})

// 使用
const { $api } = useNuxtApp()
await $api('/users')

目录结构变化

Nuxt 2 → Nuxt 3

Nuxt 2:                     Nuxt 3:
├── assets/                 ├── assets/
├── components/             ├── components/      (自动导入)
├── layouts/                ├── layouts/
├── middleware/             ├── middleware/
├── pages/                  ├── pages/
├── plugins/                ├── plugins/
├── static/          →      ├── public/
├── store/           →      ├── stores/          (Pinia)
├── nuxt.config.js   →      ├── nuxt.config.ts
└── vue.config.js           └── (不再需要)

新增目录:
                            ├── composables/     (自动导入)
                            ├── server/          (API 路由)
                            └── utils/           (自动导入)

Server API 路由

// Nuxt 2 - 需要 serverMiddleware
// serverMiddleware/api.js
const express = require('express')
const app = express()
app.get('/posts', (req, res) => {
  res.json([])
})
module.exports = app

// Nuxt 3 - 内置 server/
// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
  return await db.query('SELECT * FROM posts')
})

// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  return await db.insert('posts', body)
})

常见问题与解决

$axios 不再内置

# 安装 ofetch(推荐)或 axios
npm install ofetch
# 或
npm install @nuxtjs/axios@next
// 使用 $fetch(内置)
const data = await $fetch('/api/posts')

// 或封装 axios
// plugins/axios.ts
import axios from 'axios'

export default defineNuxtPlugin(() => {
  const instance = axios.create({
    baseURL: '/api'
  })
  
  return {
    provide: {
      axios: instance
    }
  }
})

this.$router/this.$route

// Nuxt 2
this.$router.push('/about')
this.$route.params.id

// Nuxt 3
const router = useRouter()
const route = useRoute()

router.push('/about')
route.params.id

第三方库兼容性

// 检查 Vue 3 兼容版本
// ❌ vue-awesome-swiper(Vue 2 only)
// ✅ swiper(Vue 3 兼容)

// ❌ vue-lazyload
// ✅ @vueuse/components 的 vLazy

// ❌ vuex
// ✅ pinia

process.client/process.server

// Nuxt 2
if (process.client) {
  // 客户端代码
}

// Nuxt 3(仍可用,但推荐)
import { isClient } from '#imports'

if (import.meta.client) {
  // 客户端代码
}

Layouts 语法变化

<!-- Nuxt 2 -->
<template>
  <Nuxt />
</template>

<!-- Nuxt 3 -->
<template>
  <slot />
</template>
<!-- Nuxt 2 页面 -->
<script>
export default {
  layout: 'dashboard'
}
</script>

<!-- Nuxt 3 页面 -->
<script setup>
definePageMeta({
  layout: 'dashboard'
})
</script>

迁移检查清单

准备阶段

☐ 评估项目规模和复杂度
☐ 盘点第三方依赖兼容性
☐ 决定迁移策略(全量/渐进/并行)
☐ 搭建测试环境
☐ 准备回滚方案

配置迁移

☐ nuxt.config.js → nuxt.config.ts
☐ 更新 runtimeConfig 格式
☐ 迁移 buildModules 配置
☐ 更新 CSS 预处理器配置
☐ 迁移环境变量

代码迁移

☐ asyncData → useFetch/useAsyncData
☐ fetch → useFetch
☐ head → useHead/useSeoMeta
☐ Vuex → Pinia
☐ Middleware 语法更新
☐ Plugin 语法更新
☐ Layout 语法更新
☐ 组件迁移到 Composition API

测试验证

☐ 所有页面可访问
☐ API 调用正常
☐ 认证流程正常
☐ SEO 元信息正确
☐ 性能无明显下降
☐ 无控制台错误
☐ SSR 水合正常

迁移收益

成功迁移后你将获得:

收益说明
构建速度Vite 冷启动快 10-100 倍
HMR 速度毫秒级热更新
包体积Tree-shaking 更彻底
TypeScript零配置完整支持
开发体验自动导入、更好的 IDE 支持
服务端Nitro 引擎更灵活高效
生态Vue 3 生态持续发展

总结

Nuxt 2 → Nuxt 3 迁移的核心要点:

  1. 评估优先:了解项目复杂度,选择合适策略
  2. Bridge 过渡:大型项目用 Nuxt Bridge 渐进迁移
  3. API 替换:掌握核心 API 的新旧对应关系
  4. 依赖更新:确保第三方库兼容 Vue 3
  5. 充分测试:迁移后全面验证功能和性能

迁移是一次技术升级的机会——不仅仅是版本号的改变,更是开发体验和应用性能的全面提升。

延伸阅读