Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Nuxt.js框架教程,详解Nuxt3安装配置、项目结构、路由系统。包含完整代码示例,适合前端开发者快速掌握Vue3 SSR框架开发技巧。
核心关键词:Nuxt.js 2024、Nuxt3框架、Vue3 SSR框架、Nuxt.js教程、Vue SSR开发、Nuxt3配置
长尾关键词:Nuxt.js怎么使用、Nuxt3项目搭建、Nuxt.js vs Next.js、Vue3 SSR框架选择、Nuxt.js最佳实践
通过本节Nuxt.js框架,你将系统性掌握:
Nuxt.js是什么?这是Vue生态系统中最成熟的全栈框架。Nuxt.js是基于Vue3构建的现代化全栈框架,提供服务端渲染、静态生成、API路由等功能,大大简化了Vue3 SSR应用的开发复杂度。
💡 框架定位:Nuxt.js之于Vue,就像Next.js之于React,是Vue生态系统中的全栈解决方案
# 🎉 Nuxt3项目创建
# 1. 使用官方脚手架创建项目
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
# 2. 安装依赖
npm install
# 3. 启动开发服务器
npm run dev
# 4. 构建生产版本
npm run build
# 5. 预览生产版本
npm run preview// 🎉 nuxt.config.ts - Nuxt3配置文件
export default defineNuxtConfig({
// 基础配置
app: {
head: {
title: 'My Nuxt App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: 'My amazing Nuxt3 application' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
},
// 渲染模式配置
ssr: true, // 启用服务端渲染
// 开发工具配置
devtools: { enabled: true },
// CSS框架配置
css: [
'~/assets/css/main.css',
'@/assets/scss/global.scss'
],
// 模块配置
modules: [
'@nuxtjs/tailwindcss', // Tailwind CSS
'@pinia/nuxt', // Pinia状态管理
'@nuxtjs/color-mode', // 主题切换
'@nuxt/content', // 内容管理
'@nuxtjs/google-fonts' // Google字体
],
// 插件配置
plugins: [
'~/plugins/api.client.ts',
'~/plugins/auth.ts'
],
// 运行时配置
runtimeConfig: {
// 服务端环境变量
apiSecret: process.env.API_SECRET,
// 公共环境变量(客户端可访问)
public: {
apiBase: process.env.API_BASE_URL || 'http://localhost:3001',
appVersion: process.env.npm_package_version
}
},
// 构建配置
build: {
transpile: ['@headlessui/vue']
},
// Vite配置
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "~/assets/scss/variables.scss" as *;'
}
}
}
},
// 路由配置
router: {
options: {
scrollBehaviorType: 'smooth'
}
},
// 实验性功能
experimental: {
payloadExtraction: false,
inlineSSRStyles: false
},
// TypeScript配置
typescript: {
strict: true,
typeCheck: true
}
})# 🎉 .env - 环境变量配置
# 数据库配置
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
# API配置
API_BASE_URL=https://api.example.com
API_SECRET=your-secret-key
# 第三方服务
GOOGLE_ANALYTICS_ID=GA_MEASUREMENT_ID
STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
# 开发配置
NODE_ENV=development
DEBUG=truemy-nuxt-app/
├── .nuxt/ # 构建输出目录(自动生成)
├── .output/ # 生产构建输出
├── assets/ # 静态资源(需要处理)
│ ├── css/
│ ├── images/
│ └── scss/
├── components/ # Vue组件
│ ├── ui/ # UI组件
│ ├── layout/ # 布局组件
│ └── feature/ # 功能组件
├── composables/ # 组合函数
│ ├── useAuth.ts
│ ├── useApi.ts
│ └── useState.ts
├── content/ # 内容文件(Markdown等)
│ ├── blog/
│ └── docs/
├── layouts/ # 布局模板
│ ├── default.vue
│ ├── admin.vue
│ └── auth.vue
├── middleware/ # 路由中间件
│ ├── auth.ts
│ └── guest.ts
├── pages/ # 页面组件(自动路由)
│ ├── index.vue # 首页 /
│ ├── about.vue # 关于页 /about
│ ├── blog/
│ │ ├── index.vue # 博客列表 /blog
│ │ └── [slug].vue # 博客详情 /blog/:slug
│ └── user/
│ ├── index.vue # 用户列表 /user
│ ├── [id].vue # 用户详情 /user/:id
│ └── settings.vue # 用户设置 /user/settings
├── plugins/ # 插件
│ ├── api.client.ts
│ └── auth.ts
├── public/ # 静态文件(直接访问)
│ ├── favicon.ico
│ └── images/
├── server/ # 服务端代码
│ ├── api/ # API路由
│ │ ├── users.get.ts
│ │ └── auth/
│ │ ├── login.post.ts
│ │ └── logout.post.ts
│ └── middleware/ # 服务端中间件
├── stores/ # Pinia状态管理
│ ├── auth.ts
│ └── user.ts
├── types/ # TypeScript类型定义
│ ├── api.ts
│ └── user.ts
├── utils/ # 工具函数
│ ├── format.ts
│ └── validation.ts
├── app.vue # 根组件
├── nuxt.config.ts # Nuxt配置
├── package.json
└── tsconfig.json<!-- 🎉 components/ui/Button.vue -->
<template>
<button
:class="buttonClasses"
:disabled="disabled"
@click="$emit('click', $event)"
>
<Icon v-if="icon" :name="icon" class="mr-2" />
<slot />
</button>
</template>
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
icon?: string
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false
})
defineEmits<{
click: [event: MouseEvent]
}>()
const buttonClasses = computed(() => [
'btn',
`btn-${props.variant}`,
`btn-${props.size}`,
{
'btn-disabled': props.disabled
}
])
</script><!-- 🎉 pages/index.vue - 自动导入组件 -->
<template>
<div>
<h1>欢迎使用Nuxt3</h1>
<!-- 自动导入的组件,无需手动import -->
<UiButton variant="primary" @click="handleClick">
点击我
</UiButton>
<UiButton variant="secondary" icon="user">
用户按钮
</UiButton>
</div>
</template>
<script setup>
// 组件会自动导入,无需手动导入
function handleClick() {
console.log('按钮被点击')
}
</script><!-- 🎉 pages/index.vue - 首页路由 / -->
<template>
<div>
<h1>首页</h1>
<NuxtLink to="/about">关于我们</NuxtLink>
<NuxtLink to="/blog">博客</NuxtLink>
</div>
</template>
<script setup>
// 页面元数据
definePageMeta({
title: '首页',
description: '这是网站首页',
layout: 'default'
})
// SEO配置
useSeoMeta({
title: '我的网站首页',
ogTitle: '我的网站首页',
description: '这是一个使用Nuxt3构建的现代化网站',
ogDescription: '这是一个使用Nuxt3构建的现代化网站',
ogImage: '/images/og-image.jpg',
twitterCard: 'summary_large_image'
})
</script><!-- 🎉 pages/blog/[slug].vue - 动态路由 /blog/:slug -->
<template>
<div>
<article v-if="post">
<h1>{{ post.title }}</h1>
<p class="meta">
发布于 {{ formatDate(post.createdAt) }}
作者:{{ post.author }}
</p>
<div class="content" v-html="post.content"></div>
</article>
<div v-else>
<h1>文章未找到</h1>
<p>抱歉,您访问的文章不存在。</p>
<NuxtLink to="/blog">返回博客列表</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
// 获取路由参数
const route = useRoute()
const slug = route.params.slug as string
// 数据获取
const { data: post, error } = await $fetch(`/api/posts/${slug}`)
// 错误处理
if (error.value) {
throw createError({
statusCode: 404,
statusMessage: '文章未找到'
})
}
// 页面元数据
definePageMeta({
validate: async (route) => {
// 路由验证
return typeof route.params.slug === 'string'
}
})
// 动态SEO
useSeoMeta({
title: () => post.value?.title,
description: () => post.value?.excerpt,
ogTitle: () => post.value?.title,
ogDescription: () => post.value?.excerpt,
ogImage: () => post.value?.featuredImage
})
// 工具函数
function formatDate(date: string) {
return new Date(date).toLocaleDateString('zh-CN')
}
</script><!-- 🎉 pages/admin.vue - 父路由 -->
<template>
<div class="admin-layout">
<AdminSidebar />
<main class="admin-content">
<!-- 子路由出口 -->
<NuxtPage />
</main>
</div>
</template>
<script setup>
// 中间件保护
definePageMeta({
middleware: 'auth',
layout: 'admin'
})
</script><!-- 🎉 pages/admin/users.vue - 子路由 /admin/users -->
<template>
<div>
<h1>用户管理</h1>
<UserTable :users="users" />
</div>
</template>
<script setup>
// 数据获取
const { data: users } = await $fetch('/api/admin/users')
</script>// 🎉 middleware/auth.ts - 认证中间件
export default defineNuxtRouteMiddleware((to, from) => {
const { $auth } = useNuxtApp()
if (!$auth.isAuthenticated) {
return navigateTo('/login')
}
})// 🎉 middleware/guest.ts - 访客中间件
export default defineNuxtRouteMiddleware((to, from) => {
const { $auth } = useNuxtApp()
if ($auth.isAuthenticated) {
return navigateTo('/dashboard')
}
})
### 数据获取策略:服务端和客户端数据管理
#### 服务端数据获取
```vue
<!-- 🎉 pages/products/index.vue - 服务端数据获取 -->
<template>
<div>
<h1>产品列表</h1>
<div class="filters">
<select v-model="selectedCategory" @change="refreshProducts">
<option value="">所有分类</option>
<option v-for="category in categories" :key="category.id" :value="category.id">
{{ category.name }}
</option>
</select>
</div>
<div class="products-grid">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
/>
</div>
<Pagination
:current-page="currentPage"
:total-pages="totalPages"
@page-change="handlePageChange"
/>
</div>
</template>
<script setup lang="ts">
// 响应式查询参数
const route = useRoute()
const router = useRouter()
const currentPage = computed(() => parseInt(route.query.page as string) || 1)
const selectedCategory = ref(route.query.category as string || '')
// 服务端数据获取
const { data: productsData, refresh: refreshProducts } = await $fetch('/api/products', {
query: {
page: currentPage,
category: selectedCategory,
limit: 12
},
server: true, // 确保在服务端执行
default: () => ({ products: [], total: 0, categories: [] })
})
const products = computed(() => productsData.value.products)
const totalPages = computed(() => Math.ceil(productsData.value.total / 12))
const categories = computed(() => productsData.value.categories)
// 页面变化处理
async function handlePageChange(page: number) {
await router.push({
query: { ...route.query, page }
})
}
// 监听分类变化
watch(selectedCategory, (newCategory) => {
router.push({
query: { ...route.query, category: newCategory, page: 1 }
})
})
// SEO优化
useSeoMeta({
title: `产品列表 - 第${currentPage.value}页`,
description: '浏览我们的产品目录,找到您需要的商品'
})
</script>// 🎉 composables/useApi.ts - API组合函数
export const useApi = () => {
const config = useRuntimeConfig()
// 基础请求函数
const $api = $fetch.create({
baseURL: config.public.apiBase,
headers: {
'Content-Type': 'application/json'
},
// 请求拦截器
onRequest({ request, options }) {
const token = useCookie('auth-token')
if (token.value) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token.value}`
}
}
},
// 响应拦截器
onResponse({ response }) {
// 处理响应
},
// 错误处理
onResponseError({ response }) {
if (response.status === 401) {
// 清除认证信息并重定向
const token = useCookie('auth-token')
token.value = null
navigateTo('/login')
}
}
})
// 用户相关API
const userApi = {
async getProfile() {
return await $api('/user/profile')
},
async updateProfile(data: any) {
return await $api('/user/profile', {
method: 'PUT',
body: data
})
},
async uploadAvatar(file: File) {
const formData = new FormData()
formData.append('avatar', file)
return await $api('/user/avatar', {
method: 'POST',
body: formData
})
}
}
// 产品相关API
const productApi = {
async getProducts(params: any = {}) {
return await $api('/products', { query: params })
},
async getProduct(id: string) {
return await $api(`/products/${id}`)
},
async createProduct(data: any) {
return await $api('/products', {
method: 'POST',
body: data
})
}
}
return {
$api,
userApi,
productApi
}
}// 🎉 stores/auth.ts - Pinia状态管理
export const useAuthStore = defineStore('auth', () => {
// 状态
const user = ref<User | null>(null)
const token = useCookie('auth-token', {
default: () => null,
httpOnly: true,
secure: true,
sameSite: 'strict'
})
// 计算属性
const isAuthenticated = computed(() => !!user.value && !!token.value)
const isAdmin = computed(() => user.value?.role === 'admin')
// 操作
async function login(credentials: LoginCredentials) {
try {
const { userApi } = useApi()
const response = await userApi.login(credentials)
user.value = response.user
token.value = response.token
// 重定向到仪表板
await navigateTo('/dashboard')
return { success: true }
} catch (error) {
return {
success: false,
error: error.message || '登录失败'
}
}
}
async function logout() {
try {
const { userApi } = useApi()
await userApi.logout()
} catch (error) {
console.error('登出错误:', error)
} finally {
user.value = null
token.value = null
await navigateTo('/login')
}
}
async function fetchUser() {
if (!token.value) return
try {
const { userApi } = useApi()
user.value = await userApi.getProfile()
} catch (error) {
console.error('获取用户信息失败:', error)
await logout()
}
}
async function updateProfile(data: Partial<User>) {
try {
const { userApi } = useApi()
const updatedUser = await userApi.updateProfile(data)
user.value = { ...user.value, ...updatedUser }
return { success: true }
} catch (error) {
return {
success: false,
error: error.message || '更新失败'
}
}
}
return {
// 状态
user: readonly(user),
isAuthenticated,
isAdmin,
// 操作
login,
logout,
fetchUser,
updateProfile
}
})// 🎉 plugins/api.client.ts - 客户端插件
export default defineNuxtPlugin(() => {
// 全局API实例
const api = $fetch.create({
baseURL: '/api',
onRequest({ options }) {
// 添加认证头
const token = useCookie('auth-token')
if (token.value) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token.value}`
}
}
},
onResponseError({ response }) {
// 全局错误处理
if (response.status === 401) {
navigateTo('/login')
}
}
})
// 提供给应用使用
return {
provide: {
api
}
}
})// 🎉 plugins/toast.client.ts - Toast通知插件
import { createApp } from 'vue'
import ToastContainer from '~/components/ui/ToastContainer.vue'
export default defineNuxtPlugin(() => {
// 创建Toast容器
const toastContainer = createApp(ToastContainer)
const toastInstance = toastContainer.mount(document.createElement('div'))
document.body.appendChild(toastInstance.$el)
// Toast服务
const toast = {
success(message: string, options = {}) {
toastInstance.add({
type: 'success',
message,
duration: 3000,
...options
})
},
error(message: string, options = {}) {
toastInstance.add({
type: 'error',
message,
duration: 5000,
...options
})
},
warning(message: string, options = {}) {
toastInstance.add({
type: 'warning',
message,
duration: 4000,
...options
})
},
info(message: string, options = {}) {
toastInstance.add({
type: 'info',
message,
duration: 3000,
...options
})
}
}
return {
provide: {
toast
}
}
})// 🎉 server/api/users.get.ts - 用户列表API
export default defineEventHandler(async (event) => {
// 获取查询参数
const query = getQuery(event)
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 10
const search = query.search as string || ''
try {
// 数据库查询(示例)
const users = await getUsersFromDatabase({
page,
limit,
search
})
return {
success: true,
data: users,
pagination: {
page,
limit,
total: users.total,
pages: Math.ceil(users.total / limit)
}
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: '获取用户列表失败'
})
}
})// 🎉 server/api/auth/login.post.ts - 登录API
export default defineEventHandler(async (event) => {
// 获取请求体
const body = await readBody(event)
const { email, password } = body
// 验证输入
if (!email || !password) {
throw createError({
statusCode: 400,
statusMessage: '邮箱和密码不能为空'
})
}
try {
// 验证用户凭据
const user = await validateUserCredentials(email, password)
if (!user) {
throw createError({
statusCode: 401,
statusMessage: '邮箱或密码错误'
})
}
// 生成JWT令牌
const token = await generateJWT(user.id)
// 设置安全Cookie
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7 // 7天
})
return {
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
},
token
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: '登录失败'
})
}
})// 🎉 nuxt.config.ts - 生产优化配置
export default defineNuxtConfig({
// 构建优化
build: {
// 分析包大小
analyze: process.env.ANALYZE === 'true',
// 代码分割
splitChunks: {
layouts: true,
pages: true,
commons: true
}
},
// 渲染优化
render: {
// 压缩HTML
compressor: { threshold: 0 },
// 资源提示
resourceHints: true,
// 预加载
preloadLinks: true
},
// 性能优化
optimization: {
// 图片优化
images: {
formats: ['webp', 'avif'],
quality: 80,
sizes: [320, 640, 768, 1024, 1280, 1920]
},
// 字体优化
fonts: {
preload: true,
display: 'swap'
}
},
// 缓存策略
routeRules: {
// 首页预渲染
'/': { prerender: true },
// 产品页面ISR
'/products/**': { isr: 60 },
// API路由缓存
'/api/**': {
headers: {
'Cache-Control': 's-maxage=60'
}
},
// 管理页面SPA模式
'/admin/**': { ssr: false },
// 静态页面
'/about': { prerender: true }
}
})# 🎉 Dockerfile - 生产环境部署
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 生产运行阶段
FROM base AS runner
WORKDIR /app
# 创建非root用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nuxtjs
# 复制构建产物
COPY --from=builder --chown=nuxtjs:nodejs /app/.output /app/.output
USER nuxtjs
EXPOSE 3000
ENV PORT 3000
ENV NODE_ENV production
CMD ["node", ".output/server/index.mjs"]通过本节Nuxt.js框架的学习,你已经掌握:
A: Vue3是前端框架,Nuxt3是基于Vue3的全栈框架。Nuxt3提供了SSR、路由、状态管理等开箱即用的功能,而Vue3需要手动配置这些功能。
A: 当项目需要SSR、SEO优化、快速开发、或者是内容驱动的网站时,选择Nuxt3。如果是简单的SPA或对框架有特殊要求,可以选择纯Vue3。
A: 如果熟悉Vue3,Nuxt3的学习曲线相对平缓。主要需要理解文件系统路由、SSR概念和Nuxt3的约定。
A: Nuxt3性能优秀,支持多种渲染模式、自动代码分割、图片优化等。正确配置下,可以获得很好的Core Web Vitals分数。
A: Nuxt3有重大变化,建议参考官方迁移指南,逐步迁移。主要变化包括Composition API、新的目录结构、配置文件格式等。
// 问题:页面文件创建后路由不生效
// 解决:检查文件命名和位置
// ❌ 错误:文件名包含特殊字符
pages/user-profile.vue // 应该使用 [id].vue 或 profile.vue
// ✅ 正确:使用标准命名
pages/user/profile.vue // 路由: /user/profile
pages/user/[id].vue // 路由: /user/:id// 问题:客户端状态与服务端不匹配
// 解决:确保数据一致性
// ❌ 错误:服务端和客户端数据不一致
const data = ref(Math.random()) // 每次渲染都不同
// ✅ 正确:使用一致的数据源
const { data } = await $fetch('/api/data') // 确保数据一致"掌握Nuxt.js框架,让Vue3 SSR开发变得简单高效。Nuxt3是现代全栈开发的强大工具!"