Nuxt 3 vs Nuxt 2 迁移指南:平滑升级的实战路径
Nuxt 3 带来了全新的架构和开发体验——Vue 3、Vite、TypeScript 原生支持。但迁移并非一键完成,特别是对于大型项目。本文将帮你理清迁移路径,避开常见陷阱。
核心差异对比
技术栈变化
| 特性 | Nuxt 2 | Nuxt 3 |
|---|---|---|
| Vue 版本 | Vue 2 | Vue 3 |
| 构建工具 | Webpack | Vite(默认) |
| 服务端引擎 | Connect | Nitro |
| 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 页面):
- 创建新 Nuxt 3 项目
- 逐个迁移组件和页面
- 一次性切换
策略二:渐进迁移
适用于大型项目:
- 使用 Nuxt Bridge 过渡
- 逐步替换 API
- 分批迁移页面
策略三:并行运行
适用于关键业务系统:
- 新功能用 Nuxt 3 开发
- 老功能维持 Nuxt 2
- 通过反向代理路由
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 迁移的核心要点:
- 评估优先:了解项目复杂度,选择合适策略
- Bridge 过渡:大型项目用 Nuxt Bridge 渐进迁移
- API 替换:掌握核心 API 的新旧对应关系
- 依赖更新:确保第三方库兼容 Vue 3
- 充分测试:迁移后全面验证功能和性能
迁移是一次技术升级的机会——不仅仅是版本号的改变,更是开发体验和应用性能的全面提升。


