目录结构与配置详解
理解Nuxt.js的目录结构与约定规范,掌握企业级应用的项目组织最佳实践
引言
Nuxt 采用约定胜于配置的设计理念,通过 pages/
目录结构自动生成路由配置。这种方式极大简化了路由管理,同时提供了强大的动态路由能力。
核心优势: 文件系统路由消除了手动配置的复杂性,通过目录结构即可清晰地表达应用的路由层次结构。
核心目录
pages 目录与路由
pages
目录是Nuxt应用的核心,负责自动生成路由系统,所有放在此目录下的 Vue 文件都会自动生成对应的路由。
基本结构
pages/
├── index.vue # / 路由
├── about.vue # /about 路由
├── user/
│ ├── index.vue # /user 路由
│ ├── profile.vue # /user/profile 路由
│ └── [id].vue # /user/:id 动态路由
└── blog/
├── index.vue # /blog 路由
├── [slug].vue # /blog/:slug 动态路由
└── [...slug].vue # /blog/* 捕获所有路由
动态路由
创建动态路由页面:
<!-- pages/users/[id].vue -->
<template>
<div>
<h1>用户详情页</h1>
<p>用户ID: {{ $route.params.id }}</p>
</div>
</template>
<script setup lang="ts">
// 获取路由参数
const route = useRoute()
const userId = route.params.id
// 类型安全的路由参数
definePageMeta({
validate: (route) => {
return /^\d+$/.test(route.params.id as string)
}
})
</script>
嵌套路由
通过文件夹结构创建嵌套路由:
pages/
└── users/
├── index.vue # 父路由组件
└── [id]/
├── index.vue # /users/:id
├── edit.vue # /users/:id/edit
└── posts/
└── [postId].vue # /users/:id/posts/:postId
<!-- pages/users/index.vue -->
<template>
<div>
<h1>用户管理</h1>
<!-- 子路由渲染出口 -->
<NuxtPage />
</div>
</template>
捕获路由
用于匹配未明确声明的路由路径,支持两种匹配模式:
pages/
├── [...slug].vue # 必需参数捕获 (必须包含参数)
└── [[...slug]].vue # 可选参数捕获 (参数可选)
工作原理:
[...slug]
:匹配任意层级路径,参数会以数组形式保存在$route.params.slug
[[...slug]]
:同上,但允许零个参数(匹配父级路由)
典型应用场景:
- 自定义404页面
- 全站路由监控
- 动态面包屑导航
- 兼容旧路由结构的重定向
配置示例:
<script setup lang="ts">
definePageMeta({
// 自定义正则匹配规则
path: '/:slug(.*)*'
})
</script>
最佳实践
模块化组织:按功能模块组织页面,避免扁平化结构
pages/
├── auth/ # 认证模块
│ ├── login.vue
│ └── register.vue
├── dashboard/ # 控制台模块
│ ├── index.vue
│ └── analytics.vue
└── admin/ # 管理模块
├── users/
└── settings/
路由元信息:使用 definePageMeta
定义页面元信息
<script setup lang="ts">
definePageMeta({
title: '用户管理',
description: '企业用户管理系统',
requiresAuth: true,
layout: 'admin'
})
</script>
路由工作机制
文件系统路由的实现原理
Nuxt 的文件系统路由是通过构建时的静态分析实现的。其核心工作流程如下:
- 构建时扫描:Nuxt 在构建过程中使用
@nuxt/kit
的scanDir
函数递归扫描pages
目录 - 路由树构建:基于文件路径和命名约定,构建内部路由树数据结构
- 动态路由处理:识别
[param]
和[...slug]
等动态路由模式,生成对应的路由参数配置 - 路由代码生成:在
.nuxt/routes.mjs
中生成最终的路由配置代码
// 内部生成的路由配置示例
export const routes = [
{
path: '/blog/:slug',
component: () => import('~/pages/blog/[slug].vue'),
meta: { /* 从 definePageMeta 提取的元数据 */ }
}
]
性能优化机制:
- 懒加载:每个页面组件都被包装为动态导入,实现代码分割
- 预加载:Nuxt 会预加载链接页面的组件,提升导航速度
- 路由缓存:路由配置在构建时确定,运行时无需重新计算
与 Vue Router 的集成: Nuxt 在底层使用 Vue Router,但通过文件系统抽象简化了路由配置。构建时生成的路由配置会被传递给 Vue Router 实例,保持了 Vue 生态系统的兼容性。
components 目录与组件开发
components
目录实现了零配置的组件自动导入,提高开发效率。
目录结构设计
components/
├── Base/ # 基础组件
│ ├── Button.vue # <BaseButton>
│ ├── Input.vue # <BaseInput>
│ └── Modal.vue # <BaseModal>
├── Form/ # 表单组件
│ ├── UserForm.vue # <FormUserForm>
│ └── LoginForm.vue # <FormLoginForm>
├── Layout/ # 布局组件
│ ├── Header.vue # <LayoutHeader>
│ ├── Sidebar.vue # <LayoutSidebar>
│ └── Footer.vue # <LayoutFooter>
└── Common/ # 通用组件
├── Loading.vue # <CommonLoading>
└── Empty.vue # <CommonEmpty>
组件命名约定
<!-- components/Base/Button.vue -->
<template>
<button
:class="buttonClasses"
:disabled="disabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
const buttonClasses = computed(() => {
return [
'btn',
`btn-${props.variant}`,
`btn-${props.size}`,
{
'btn-disabled': props.disabled
}
]
})
const handleClick = (event: MouseEvent) => {
if (!props.disabled) {
emit('click', event)
}
}
</script>
<!-- components/Form/UserForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="user-form">
<BaseInput
v-model="form.name"
label="用户名"
:error="errors.name"
required
/>
<BaseInput
v-model="form.email"
type="email"
label="邮箱"
:error="errors.email"
required
/>
<BaseButton
type="submit"
:loading="isSubmitting"
variant="primary"
>
提交
</BaseButton>
</form>
</template>
<script setup lang="ts">
import { z } from 'zod'
// 类型定义
interface UserForm {
name: string
email: string
}
// 表单验证schema
const userSchema = z.object({
name: z.string().min(2, '用户名至少2个字符'),
email: z.string().email('请输入有效的邮箱地址')
})
// 响应式数据
const form = reactive<UserForm>({
name: '',
email: ''
})
const errors = reactive<Partial<UserForm>>({})
const isSubmitting = ref(false)
// 事件定义
const emit = defineEmits<{
submit: [data: UserForm]
}>()
// 表单提交处理
const handleSubmit = async () => {
try {
// 清除之前的错误
Object.keys(errors).forEach(key => {
errors[key as keyof UserForm] = undefined
})
// 验证表单数据
const validatedData = userSchema.parse(form)
isSubmitting.value = true
emit('submit', validatedData)
} catch (error) {
if (error instanceof z.ZodError) {
// 处理验证错误
error.errors.forEach(err => {
const field = err.path[0] as keyof UserForm
errors[field] = err.message
})
}
} finally {
isSubmitting.value = false
}
}
</script>
组件开发规范
组件设计原则
- 单一职责: 每个组件只负责一个功能
- 可复用性: 通过 props 和 slots 提供灵活性
- 类型安全: 使用 TypeScript 定义清晰的接口
- 可测试性: 组件逻辑独立,便于单元测试
composables 目录与 Hooks
composables
目录用于存放可复用的组合式函数,实现逻辑复用和状态管理。
目录结构规范
composables/
├── api/ # API相关hooks
│ ├── useAuth.ts # 认证逻辑
│ ├── useUser.ts # 用户数据管理
│ └── useProducts.ts # 产品数据管理
├── ui/ # UI交互hooks
│ ├── useModal.ts # 模态框控制
│ ├── useToast.ts # 消息提示
│ └── useLoading.ts # 加载状态
├── utils/ # 工具类hooks
│ ├── useLocalStorage.ts
│ ├── useDebounce.ts
│ └── useThrottle.ts
└── business/ # 业务逻辑hooks
├── useCart.ts # 购物车逻辑
├── useOrder.ts # 订单处理
└── usePayment.ts # 支付流程
Composables 编写规范
- 命名规范
- 使用
use
前缀 + 功能描述 (例:useCart
) - 文件名采用小驼峰式命名 (例:
useProductList.ts
) - 类型定义使用 PascalCase 命名 (例:
AuthState
)
- 函数结构
// 1. 类型定义优先
interface ReturnType {
state: Ref<DataType>
actions: {
fetchData: () => Promise<void>
updateData: (payload: PayloadType) => void
}
computed: {
formattedData: ComputedRef<string>
}
}
// 2. 主函数实现
export const useFeature = (): ReturnType => {
// 状态声明 (优先用 ref 替代 reactive)
const state = ref<DataType>(initialValue)
// 计算属性
const formattedData = computed(() => format(state.value))
// 方法实现
const fetchData = async () => {
try {
// 业务逻辑...
} catch (error) {
// 统一错误处理
}
}
// 返回结构化对象
return {
state,
actions: { fetchData },
computed: { formattedData }
}
}
- 最佳实践
- 类型安全: 为参数、返回值和内部状态提供完整类型定义
- 响应式隔离: 在函数内部创建独立响应式状态
- 生命周期: 使用
onMounted
/onUnmounted
管理副作用 - 自动导入: 确保 composables 目录符合 Nuxt 自动导入规范
- 依赖注入: 复杂场景使用
provide
/inject
实现层级通信 - 测试友好: 通过参数注入替代直接导入依赖
- 性能优化
- 避免在 composables 中创建不必要的响应式对象
- 使用
debounce
/throttle
包装高频操作 - 大数据量使用
shallowRef
替代 ref - 及时清理事件监听器和定时器
- 使用
computedAsync
处理异步计算
- 文档规范
/**
* 用户认证功能 Hook
* @param config 认证配置项
* @returns {
* user: 当前用户信息,
* login: 登录方法,
* logout: 退出登录方法
* }
* @example
* const { user, login } = useAuth()
*/
// composables/api/useAuth.ts
import type { User } from '~/types'
export interface LoginCredentials {
email: string
password: string
}
export interface AuthState {
user: Ref<User | null>
isAuthenticated: ComputedRef<boolean>
isLoading: Ref<boolean>
error: Ref<string | null>
}
export const useAuth = (): AuthState & {
login: (credentials: LoginCredentials) => Promise<void>
logout: () => Promise<void>
refresh: () => Promise<void>
} => {
// 状态管理
const user = ref<User | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// 计算属性
const isAuthenticated = computed(() => !!user.value)
// 登录方法
const login = async (credentials: LoginCredentials) => {
try {
isLoading.value = true
error.value = null
const { data } = await $fetch<{ user: User; token: string }>('/api/auth/login', {
method: 'POST',
body: credentials
})
// 存储用户信息
user.value = data.user
// 存储token到cookie
const token = useCookie('auth-token', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7 // 7天
})
token.value = data.token
// 跳转到首页
await navigateTo('/')
} catch (err: any) {
error.value = err.data?.message || '登录失败'
throw err
} finally {
isLoading.value = false
}
}
// 退出登录
const logout = async () => {
try {
await $fetch('/api/auth/logout', { method: 'POST' })
} catch (err) {
console.warn('登出请求失败:', err)
} finally {
// 清除本地状态
user.value = null
await navigateTo('/auth/login')
}
}
// 刷新用户信息
const refresh = async () => {
try {
const { data } = await $fetch<{ user: User }>('/api/auth/me')
user.value = data.user
} catch (err) {
console.warn('获取用户信息失败:', err)
user.value = null
}
}
return {
user: readonly(user),
isAuthenticated,
isLoading: readonly(isLoading),
error: readonly(error),
login,
logout,
refresh
}
}
// composables/ui/useModal.ts
export interface ModalState {
isOpen: Ref<boolean>
data: Ref<any>
}
export const useModal = <T = any>() => {
const isOpen = ref(false)
const data = ref<T | null>(null)
const open = (payload?: T) => {
data.value = payload || null
isOpen.value = true
}
const close = () => {
isOpen.value = false
// 延迟清除数据,等待动画完成
setTimeout(() => {
data.value = null
}, 300)
}
const toggle = (payload?: T) => {
if (isOpen.value) {
close()
} else {
open(payload)
}
}
// 监听 ESC 键关闭
useEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen.value) {
close()
}
})
return {
isOpen: readonly(isOpen),
data: readonly(data),
open,
close,
toggle
}
}
// composables/business/useCart.ts
import type { Product, CartItem } from '~/types'
export const useCart = () => {
// 使用 localStorage 持久化购物车数据
const cartItems = useLocalStorage<CartItem[]>('cart-items', [])
// 计算属性
const totalItems = computed(() =>
cartItems.value.reduce((sum, item) => sum + item.quantity, 0)
)
const totalPrice = computed(() =>
cartItems.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
)
const isEmpty = computed(() => cartItems.value.length === 0)
// 添加商品到购物车
const addItem = (product: Product, quantity: number = 1) => {
const existingItem = cartItems.value.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity += quantity
} else {
cartItems.value.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity
})
}
// 显示成功提示
const toast = useToast()
toast.success(`${product.name} 已添加到购物车`)
}
// 更新商品数量
const updateQuantity = (productId: string, quantity: number) => {
const item = cartItems.value.find(item => item.id === productId)
if (item) {
if (quantity <= 0) {
removeItem(productId)
} else {
item.quantity = quantity
}
}
}
// 移除商品
const removeItem = (productId: string) => {
const index = cartItems.value.findIndex(item => item.id === productId)
if (index > -1) {
cartItems.value.splice(index, 1)
}
}
// 清空购物车
const clearCart = () => {
cartItems.value = []
}
return {
items: readonly(cartItems),
totalItems,
totalPrice,
isEmpty,
addItem,
updateQuantity,
removeItem,
clearCart
}
}
layouts 目录与布局系统
layouts
目录定义页面布局模板,提供一致的页面结构。
布局结构
layouts/
├── default.vue # 默认布局
├── auth.vue # 认证页面布局
├── admin.vue # 管理后台布局
├── blog.vue # 博客页面布局
└── error.vue # 错误页面布局
布局组件实现
<!-- layouts/default.vue -->
<template>
<div class="min-h-screen bg-gray-50">
<!-- 顶部导航 -->
<LayoutHeader />
<!-- 主要内容区域 -->
<main class="container mx-auto px-4 py-8">
<slot />
</main>
<!-- 底部 -->
<LayoutFooter />
<!-- 全局组件 -->
<CommonLoading v-if="$route.meta.loading" />
<NotificationToast />
</div>
</template>
<script setup lang="ts">
// 布局级别的逻辑
const { user } = useAuth()
// 监听路由变化,重置页面状态
watch(() => $route.path, () => {
// 清除之前的错误状态
clearError()
})
</script>
<!-- layouts/admin.vue -->
<template>
<div class="admin-layout">
<!-- 侧边栏 -->
<aside class="admin-sidebar">
<AdminSidebar />
</aside>
<!-- 主内容区 -->
<div class="admin-main">
<!-- 顶部工具栏 -->
<header class="admin-header">
<AdminHeader />
</header>
<!-- 面包屑导航 -->
<nav class="admin-breadcrumb">
<AdminBreadcrumb />
</nav>
<!-- 页面内容 -->
<main class="admin-content">
<slot />
</main>
</div>
</div>
</template>
<script setup lang="ts">
// 权限检查
const { user, isAuthenticated } = useAuth()
// 检查管理员权限
watchEffect(() => {
if (!isAuthenticated.value || !user.value?.isAdmin) {
throw createError({
statusCode: 403,
statusMessage: '权限不足'
})
}
})
</script>
<style scoped>
.admin-layout {
@apply min-h-screen flex;
}
.admin-sidebar {
@apply w-64 bg-gray-900 text-white;
}
.admin-main {
@apply flex-1 flex flex-col;
}
.admin-header {
@apply h-16 bg-white border-b border-gray-200;
}
.admin-content {
@apply flex-1 p-6 bg-gray-50;
}
</style>
middleware 目录与中间件
middleware
目录存放路由中间件,用于路由跳转前的逻辑处理。
中间件类型与命名
middleware/
├── auth.ts # 认证中间件
├── guest.ts # 游客访问中间件
├── admin.ts # 管理员权限中间件
├── subscription.ts # 订阅验证中间件
└── maintenance.ts # 维护模式中间件
中间件实现示例
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value) {
return navigateTo('/auth/login', {
redirectTo: to.fullPath
})
}
})
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { user } = useAuth()
if (!user.value?.isAdmin) {
throw createError({
statusCode: 403,
statusMessage: '需要管理员权限'
})
}
})
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// 页面访问统计
if (process.client) {
const { trackPageView } = useAnalytics()
trackPageView(to.path)
}
})
plugins 目录与插件系统
plugins
目录用于注册全局插件和第三方库,在应用初始化阶段执行。
插件核心特性
插件执行顺序
- 按文件名数字前缀顺序执行 (
01.xxx.ts
→02.yyy.ts
) - 客户端插件自动添加
.client
后缀 - 服务端插件自动添加
.server
后缀 - 通用插件同时作用于客户端和服务端
插件开发规范
- 命名规范
- 使用数字前缀控制执行顺序 (例:
01.pinia.ts
) - 客户端插件添加
.client.ts
后缀 - 服务端插件添加
.server.ts
后缀 - 通用插件使用
.ts
扩展名
- 最佳实践
- 优先使用 Nuxt 的
provide
/inject
实现依赖注入 - 避免在插件中执行耗时操作
- 第三方库集成时添加必要的类型声明
- 客户端插件应检查
process.client
环境 - 服务端插件应避免污染全局对象
插件使用注意事项
- 避免在插件中直接修改 Vue 原型链
- 谨慎使用全局状态,优先使用 Pinia
- 服务端插件不应包含浏览器 API 调用
- 大型第三方库建议使用 Nuxt 模块集成
插件分类组织
plugins/
├── 01.pinia.client.ts # 状态管理 (仅客户端)
├── 02.axios.ts # HTTP客户端
├── 03.dayjs.ts # 日期处理
├── 04.element-plus.client.ts # UI组件库 (仅客户端)
├── 05.analytics.client.ts # 分析工具 (仅客户端)
└── directives.ts # 自定义指令
插件实现规范
// plugins/axios.ts
import axios from 'axios'
export default defineNuxtPlugin(() => {
// 创建axios实例
const api = axios.create({
baseURL: useRuntimeConfig().public.apiBase,
timeout: 10000,
withCredentials: true
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加认证token
const token = useCookie('auth-token')
if (token.value) {
config.headers.Authorization = `Bearer ${token.value}`
}
// 添加请求ID用于追踪
config.headers['X-Request-ID'] = crypto.randomUUID()
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
api.interceptors.response.use(
(response) => response,
async (error) => {
const { status } = error.response || {}
// 处理认证失效
if (status === 401) {
const { logout } = useAuth()
await logout()
return Promise.reject(error)
}
// 处理服务器错误
if (status >= 500) {
const toast = useToast()
toast.error('服务器错误,请稍后重试')
}
return Promise.reject(error)
}
)
// 提供给应用使用
return {
provide: {
api
}
}
})
// plugins/directives.ts
export default defineNuxtPlugin((nuxtApp) => {
// 图片懒加载指令
nuxtApp.vueApp.directive('lazy', {
mounted(el: HTMLImageElement, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement
img.src = binding.value
img.onload = () => img.classList.add('loaded')
observer.unobserve(img)
}
})
})
observer.observe(el)
}
})
// 权限控制指令
nuxtApp.vueApp.directive('permission', {
mounted(el: HTMLElement, binding) {
const { user } = useAuth()
const requiredPermission = binding.value
if (!user.value?.permissions?.includes(requiredPermission)) {
el.style.display = 'none'
}
}
})
})
utils 目录与工具方法
utils
目录存放纯函数工具方法,提供可复用的业务逻辑。
工具方法分类
utils/
├── format/ # 格式化工具
│ ├── date.ts # 日期格式化
│ ├── number.ts # 数字格式化
│ └── text.ts # 文本处理
├── validation/ # 验证工具
│ ├── rules.ts # 验证规则
│ └── schemas.ts # 验证模式
├── api/ # API工具
│ ├── request.ts # 请求封装
│ └── cache.ts # 缓存管理
└── business/ # 业务工具
├── permissions.ts # 权限判断
├── pricing.ts # 价格计算
└── analytics.ts # 数据分析
工具方法实现
// utils/format/date.ts
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import timezone from 'dayjs/plugin/timezone'
dayjs.extend(relativeTime)
dayjs.extend(timezone)
/**
* 格式化日期
*/
export const formatDate = (
date: string | Date | dayjs.Dayjs,
format: string = 'YYYY-MM-DD HH:mm:ss'
): string => {
return dayjs(date).format(format)
}
/**
* 相对时间
*/
export const timeAgo = (date: string | Date | dayjs.Dayjs): string => {
return dayjs(date).fromNow()
}
/**
* 是否为今天
*/
export const isToday = (date: string | Date | dayjs.Dayjs): boolean => {
return dayjs(date).isSame(dayjs(), 'day')
}
// utils/validation/rules.ts
/**
* 邮箱验证
*/
export const isEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
/**
* 手机号验证(中国大陆)
*/
export const isPhone = (phone: string): boolean => {
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
/**
* 身份证验证(简化版)
*/
export const isIdCard = (idCard: string): boolean => {
const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
return idCardRegex.test(idCard)
}
server 目录与后端 API
server
目录提供全栈开发能力,可以创建API路由和服务端逻辑。
Server 目录结构
server/
├── api/ # API路由
│ ├── auth/
│ │ ├── login.post.ts
│ │ ├── logout.post.ts
│ │ └── me.get.ts
│ ├── users/
│ │ ├── index.get.ts
│ │ ├── [id].get.ts
│ │ └── [id].put.ts
│ └── upload.post.ts
├── middleware/ # 服务端中间件
│ ├── auth.ts
│ └── cors.ts
└── utils/ # 服务端工具
├── jwt.ts
├── db.ts
└── validation.ts
核心功能
- 文件式 API 路由:自动将
server/api
下的文件映射为 RESTful 端点 - 类型安全:自动生成 API 请求/响应类型定义
- HTTP 方法支持:通过文件后缀指定请求方法(
.get.ts
,.post.ts
) - 服务端中间件:实现跨路由的通用逻辑处理
- 服务端工具:封装数据库操作、认证逻辑等可复用模块
API 路由开发规范
- 路由组织原则
- 按业务模块创建子目录(如
/users
) - 单个文件处理单一资源操作
- 文件命名遵循
[param].method.ts
格式 - 复杂参数使用
[...].ts
捕获剩余参数
- 按业务模块创建子目录(如
- 请求处理规范
- 使用
defineEventHandler
包裹业务逻辑 - 通过
getRouterParam
获取路径参数 - 使用
readBody
读取请求体内容 - 异步操作必须使用
async/await
- 使用
- 错误处理标准
- 使用
createError
创建标准错误响应 - HTTP 状态码遵循 REST 规范:
- 200: 成功请求
- 400: 参数错误
- 401: 未授权
- 404: 资源不存在
- 500: 服务端错误
- 错误信息需脱敏处理
- 使用
- 数据验证要求
- 使用
validation.ts
中的校验工具 - 路径参数必须验证有效性
- 请求体需通过 Zod 模式校验
- 响应数据需类型断言(as Type)
- 使用
- 安全规范
- 敏感操作需通过
auth
中间件 - 数据库查询使用参数化查询
- 响应数据必须脱敏处理
- 使用
useRuntimeConfig
获取敏感配置
- 敏感操作需通过
- 性能优化
- 数据库查询需添加缓存机制(使用
useStorage
) - 批量操作实现分页处理
- 复杂计算使用 Worker 线程
- 避免同步阻塞操作
- 数据库查询需添加缓存机制(使用
- 类型安全
- 请求/响应类型需定义在
~/types
下 - 使用
$fetch
替代axios
获得类型提示 - 接口响应必须实现类型校验(如
isValidUser
) - 使用泛型定义 API 响应结构
- 请求/响应类型需定义在
// server/api/users/[id].get.ts
import { User } from '~/types'
export default defineEventHandler(async (event) => {
// 获取运行时配置
const config = useRuntimeConfig()
// 从路径参数获取用户ID
const userId = getRouterParam(event, 'id')
// 数据库查询
const user = await $fetch(`${config.dbBaseUrl}/users/${userId}`, {
headers: { Authorization: `Bearer ${config.dbToken}` }
})
// 类型校验
if (!isValidUser(user)) {
throw createError({
statusCode: 404,
statusMessage: '用户不存在'
})
}
// 数据脱敏
return {
id: user.id,
name: user.name,
avatar: user.avatar
} as User
})
// 用户类型校验
function isValidUser(data: any): boolean {
return !!data?.id && !!data?.name
}
// server/api/users/[id].put.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const userId = getRouterParam(event, 'id')
// 请求验证
if (!body.name) {
throw createError({
statusCode: 400,
statusMessage: '用户名不能为空'
})
}
// 更新逻辑
return updateUser(userId, body).catch((error) => {
throw createError({
statusCode: 500,
statusMessage: '更新用户失败',
data: error
})
})
})
中间件开发实践
// server/middleware/auth.ts
export default defineEventHandler((event) => {
// JWT 验证逻辑
const token = getHeader(event, 'Authorization')?.split(' ')[1]
if (!token) {
throw createError({
statusCode: 401,
statusMessage: '需要认证令牌'
})
}
try {
const payload = verifyJWT(token)
event.context.user = payload
} catch (error) {
throw createError({
statusCode: 403,
statusMessage: '无效的认证令牌'
})
}
})
最佳实践
- 分层架构:保持路由处理简洁,复杂逻辑移至
server/utils
- 配置管理:敏感信息通过
.env
管理,使用useRuntimeConfig()
获取 - 错误处理:统一错误格式,包含状态码和可读错误信息
- 性能优化:对高频接口添加缓存策略
- 安全防护:实施请求验证、速率限制和 SQL 注入防护
Nitro 引擎优势
- 支持跨平台部署(Node.js, Serverless, Edge 等)
- 自动生成 OpenAPI 文档
- 内置请求/响应验证
- 服务端热更新能力
资源管理目录
assets 目录与静态资源
assets/
目录用于存放需要被构建工具处理的静态资源。
资源目录结构
assets/
├── css/
│ ├── main.css # 主样式文件
│ ├── variables.css # CSS变量
│ └── components.css # 组件样式
├── scss/
│ ├── _variables.scss # SCSS变量
│ ├── _mixins.scss # SCSS混入
│ └── main.scss # 主SCSS文件
├── images/
│ ├── logo.svg # 矢量图标
│ ├── hero-bg.jpg # 背景图片
│ └── icons/ # 图标集合
└── fonts/
├── custom-font.woff2 # 自定义字体
└── icons.ttf # 图标字体
样式文件最佳实践
/* assets/css/variables.css */
:root {
/* 主色系 */
--color-primary: #3b82f6;
--color-primary-dark: #1d4ed8;
--color-primary-light: #93c5fd;
/* 语义色彩 */
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #06b6d4;
/* 中性色 */
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-900: #111827;
/* 间距系统 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* 字体系统 */
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
/* 圆角系统 */
--border-radius-sm: 0.25rem;
--border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem;
--border-radius-xl: 0.75rem;
/* 阴影系统 */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
在组件中使用资源
<template>
<div class="hero">
<!-- 使用assets中的图片 -->
<img src="~/assets/images/hero-bg.jpg" alt="Hero Background" />
<!-- 使用CSS变量 -->
<button class="custom-button">点击我</button>
</div>
</template>
<style scoped>
.hero {
background-image: url('~/assets/images/hero-bg.jpg');
background-size: cover;
background-position: center;
}
.custom-button {
background-color: var(--color-primary);
color: white;
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--border-radius-md);
font-size: var(--font-size-base);
box-shadow: var(--shadow-md);
transition: all 0.2s ease;
}
.custom-button:hover {
background-color: var(--color-primary-dark);
box-shadow: var(--shadow-lg);
}
</style>
public 目录与公共文件
public/
目录中的文件会被直接复制到网站根目录,无需构建处理。
public目录结构
public/
├── favicon.ico # 网站图标
├── robots.txt # 搜索引擎爬虫规则
├── sitemap.xml # 网站地图
├── manifest.json # PWA清单文件
├── images/
│ ├── og-image.jpg # 社交媒体分享图片
│ └── screenshots/ # 应用截图
├── icons/
│ ├── icon-192.png # PWA图标
│ └── icon-512.png # PWA图标
PWA配置示例
// public/manifest.json
{
"name": "Nuxt 企业级应用",
"short_name": "NuxtApp",
"description": "基于Nuxt3的企业级应用",
"theme_color": "#3b82f6",
"background_color": "#ffffff",
"display": "standalone",
"orientation": "portrait",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
SEO配置文件
# public/robots.txt
User-agent: *
Allow: /
# 禁止爬取管理后台
Disallow: /admin/
Disallow: /api/
# 网站地图位置
Sitemap: https://your-domain.com/sitemap.xml
配置文件详解
nuxt.config.ts 配置文件
nuxt.config.ts
是Nuxt应用的核心配置文件,用于自定义应用行为。
基础配置示例
// nuxt.config.ts
export default defineNuxtConfig({
// 开发工具
devtools: { enabled: true },
// TypeScript配置
typescript: {
strict: true,
typeCheck: true
},
// CSS框架
css: [
'~/assets/css/main.css'
],
// 模块配置
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@nuxt/content',
'@nuxtjs/i18n'
],
// 应用配置
app: {
// 全局头部配置
head: {
title: 'Nuxt 企业级应用',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: '基于Nuxt3的企业级应用框架' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
},
// 路由配置
router: {
options: {
scrollBehaviorType: 'smooth'
}
},
// 运行时配置
runtimeConfig: {
// 服务端环境变量
apiSecret: process.env.API_SECRET,
// 公开的环境变量
public: {
apiBase: process.env.API_BASE_URL || '/api',
appName: 'Nuxt App'
}
},
// 构建配置
build: {
transpile: ['@headlessui/vue']
},
// Vite配置
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "~/assets/scss/variables.scss";'
}
}
}
},
// 服务端渲染配置
ssr: true,
// 静态生成配置
nitro: {
prerender: {
routes: ['/sitemap.xml', '/robots.txt']
}
}
})
性能优化配置
// nuxt.config.ts - 性能优化版本
export default defineNuxtConfig({
// 实验性功能
experimental: {
payloadExtraction: false,
renderJsonPayloads: true
},
// 构建优化
build: {
// 代码分割
splitChunks: {
layouts: true,
pages: true,
commons: true
}
},
// 资源优化
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
chunks: 'all'
}
}
}
},
// 压缩配置
compression: {
gzip: true,
brotli: true
},
// 缓存配置
routeRules: {
'/': { prerender: true },
'/admin/**': { ssr: false },
'/api/**': { cors: true },
'/blog/**': {
headers: { 'Cache-Control': 's-maxage=3600' }
}
}
})
app.vue 根组件
app.vue
是应用的根组件,所有页面都会在其内部渲染。
基础根组件
<!-- app.vue -->
<template>
<div id="app">
<!-- 全局加载状态 -->
<LazyLoadingSpinner v-if="pending" />
<!-- 页面内容 -->
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<!-- 全局组件 -->
<LazyNotifications />
<LazyModals />
</div>
</template>
<script setup lang="ts">
// 全局META配置
useSeoMeta({
titleTemplate: '%s | Nuxt 企业级应用',
description: '基于Nuxt3构建的现代化企业级应用框架',
ogImage: '/og-image.jpg',
twitterCard: 'summary_large_image'
})
// 全局样式
useHead({
htmlAttrs: {
lang: 'zh-CN'
},
bodyAttrs: {
class: 'min-h-screen bg-gray-50'
}
})
// 全局状态管理
const { pending } = useLazyAsyncData('app-init', async () => {
// 应用初始化逻辑
const { initUser } = useAuth()
await initUser()
return true
})
// 全局错误处理
onErrorCaptured((error) => {
console.error('应用错误:', error)
// 发送错误到监控系统
return false
})
</script>
<style>
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 全局过渡效果 */
.page-enter-active,
.page-leave-active {
transition: all 0.3s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
transform: translateY(10px);
}
</style>
环境变量配置
.env文件配置
# .env
# 数据库配置
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# API配置
API_SECRET="your-super-secret-key"
API_BASE_URL="https://api.example.com"
# 第三方服务
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
# 邮件服务
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="your-email@gmail.com"
SMTP_PASS="your-app-password"
# 对象存储
AWS_ACCESS_KEY_ID="your-access-key"
AWS_SECRET_ACCESS_KEY="your-secret-key"
AWS_REGION="us-east-1"
AWS_BUCKET="your-bucket-name"
# 分析和监控
GOOGLE_ANALYTICS_ID="GA_MEASUREMENT_ID"
SENTRY_DSN="your-sentry-dsn"
环境变量使用示例
// 在服务端API中使用
// server/api/config.get.ts
export default defineEventHandler(() => {
const config = useRuntimeConfig()
return {
apiSecret: config.apiSecret, // 服务端变量
publicApiBase: config.public.apiBase // 公开变量
}
})
// 在组件中使用公开变量
// components/ApiStatus.vue
<script setup lang="ts">
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
const { data: status } = await $fetch(`${apiBase}/health`)
</script>
多环境配置
# .env.development
API_BASE_URL="http://localhost:3001/api"
NUXT_PUBLIC_APP_ENV="development"
# .env.staging
API_BASE_URL="https://staging-api.example.com"
NUXT_PUBLIC_APP_ENV="staging"
# .env.production
API_BASE_URL="https://api.example.com"
NUXT_PUBLIC_APP_ENV="production"