一个完整的中大型 Vue 3 项目通常会有 5-7 层的文件结构。以下是一个完整的架构图。
🏗️ 完整的 Vue 3 项目文件结构
src/
├── views/ # 页面视图层
├── components/ # 组件层(可复用 UI)
├── composables/ # 组合式函数层(逻辑复用)
├── stores/ # 状态管理层(Pinia/Vuex)
├── api/ # API 请求层
├── utils/ # 工具函数层
├── router/ # 路由层
├── directives/ # 自定义指令层
├── plugins/ # 插件层
├── constants/ # 常量定义层
├── types/ # TypeScript 类型定义
├── assets/ # 静态资源
└── styles/ # 全局样式
📁 各层详细说明
1. API 层 (api/)
封装所有 HTTP 请求,统一管理接口地址和请求配置。
javascript
// api/request.js - axios 实例配置
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
})
// 请求拦截器
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
response => response.data,
error => {
// 统一错误处理
if (error.response?.status === 401) {
// 跳转登录
}
return Promise.reject(error)
}
)
export default request
javascript
// api/user.js - 用户相关 API
import request from './request'
export const userApi = {
// 获取用户列表
getUsers(params) {
return request.get('/users', { params })
},
// 获取用户详情
getUserById(id) {
return request.get(`/users/${id}`)
},
// 创建用户
createUser(data) {
return request.post('/users', data)
},
// 更新用户
updateUser(id, data) {
return request.put(`/users/${id}`, data)
},
// 删除用户
deleteUser(id) {
return request.delete(`/users/${id}`)
}
}
javascript
// api/product.js - 商品相关 API
import request from './request'
export const productApi = {
getProducts(params) {
return request.get('/products', { params })
},
getProductById(id) {
return request.get(`/products/${id}`)
}
}
2. 状态管理层 (stores/) - Pinia
管理全局共享状态,跨组件的数据。
javascript
// stores/userStore.js - 用户全局状态
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
token: localStorage.getItem('token'),
permissions: []
}),
getters: {
isLoggedIn: (state) => !!state.token,
hasPermission: (state) => (permission) => state.permissions.includes(permission)
},
actions: {
async login(credentials) {
const { data } = await userApi.login(credentials)
this.token = data.token
this.userInfo = data.user
localStorage.setItem('token', data.token)
},
logout() {
this.token = null
this.userInfo = null
localStorage.removeItem('token')
}
}
})
javascript
// stores/appStore.js - 应用全局状态
export const useAppStore = defineStore('app', {
state: () => ({
theme: 'light', // 主题
sidebarCollapsed: false, // 侧边栏状态
language: 'zh-CN' // 语言
}),
actions: {
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed
},
setTheme(theme) {
this.theme = theme
localStorage.setItem('theme', theme)
}
}
})
3. 工具函数层 (utils/)
纯函数,无副作用的工具方法。
javascript
// utils/date.js - 日期处理工具
export const formatDate = (date, format = 'YYYY-MM-DD') => {
// 日期格式化逻辑
return dayjs(date).format(format)
}
export const getRelativeTime = (timestamp) => {
// 获取相对时间(刚刚、几分钟前)
const diff = Date.now() - new Date(timestamp).getTime()
const minutes = Math.floor(diff / 60000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
// ...
}
javascript
// utils/validator.js - 表单验证工具
export const validators = {
// 手机号验证
phone: (value) => /^1[3-9]\d{9}$/.test(value),
// 邮箱验证
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
// 身份证验证
idCard: (value) => /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/.test(value)
}
javascript
// utils/storage.js - 本地存储封装
export const storage = {
set(key, value) {
localStorage.setItem(key, JSON.stringify(value))
},
get(key) {
const value = localStorage.getItem(key)
return value ? JSON.parse(value) : null
},
remove(key) {
localStorage.removeItem(key)
},
clear() {
localStorage.clear()
}
}
4. 常量定义层 (constants/)
避免魔法数字和字符串。
javascript
// constants/roles.js - 角色常量
export const USER_ROLES = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
}
export const ROLE_PERMISSIONS = {
[USER_ROLES.ADMIN]: ['*'],
[USER_ROLES.USER]: ['read', 'write'],
[USER_ROLES.GUEST]: ['read']
}
javascript
// constants/api.js - API 端点常量
export const API_ENDPOINTS = {
AUTH: {
LOGIN: '/auth/login',
LOGOUT: '/auth/logout',
REGISTER: '/auth/register'
},
USER: {
LIST: '/users',
DETAIL: (id) => `/users/${id}`,
UPDATE: (id) => `/users/${id}`
}
}
javascript
// constants/options.js - 下拉选项等常量
export const GENDER_OPTIONS = [
{ label: '男', value: 1 },
{ label: '女', value: 2 }
]
export const ORDER_STATUS = {
PENDING: { value: 0, label: '待支付', color: 'warning' },
PAID: { value: 1, label: '已支付', color: 'success' },
CANCELLED: { value: 2, label: '已取消', color: 'danger' }
}
5. 类型定义层 (types/) - TypeScript
定义 TypeScript 接口和类型。
typescript
// types/user.ts
export interface User {
id: number
name: string
email: string
age: number
role: 'admin' | 'user' | 'guest'
createdAt: Date
}
export interface LoginRequest {
username: string
password: string
}
export interface LoginResponse {
token: string
user: User
}
typescript
// types/api.ts
export interface ApiResponse<T = any> {
code: number
message: string
data: T
}
export interface PaginationParams {
page: number
pageSize: number
}
export interface PaginationResponse<T> {
list: T[]
total: number
page: number
pageSize: number
}
6. 自定义指令层 (directives/)
封装 DOM 操作逻辑。
javascript
// directives/permission.js - 权限指令
export const permission = {
mounted(el, binding) {
const { value } = binding
const permissions = useUserStore().permissions
if (value && !permissions.includes(value)) {
el.parentNode?.removeChild(el)
}
}
}
javascript
// directives/loading.js - 加载指令
export const loading = {
mounted(el, binding) {
const div = document.createElement('div')
div.className = 'loading-mask'
div.innerHTML = '<div class="loading-spinner"></div>'
el.loadingMask = div
if (binding.value) {
el.appendChild(div)
}
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
if (binding.value) {
el.appendChild(el.loadingMask)
} else {
el.loadingMask?.remove()
}
}
}
}
7. 路由层 (router/)
页面路由配置。
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { title: '首页', requiresAuth: true }
},
{
path: 'users',
name: 'Users',
component: () => import('@/views/Users.vue'),
meta: { title: '用户管理', permission: 'user:list' }
}
]
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录', requiresGuest: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next('/login')
} else {
next()
}
})
export default router
📊 完整的数据流关系图
用户操作
↓
Vue 组件 (views/components)
↓ 调用
Composables (组合式函数)
↓ 调用 ↓ 读取/写入
API 层 ←────────────── Stores (状态管理)
↓ ↓
后端接口 组件响应式更新
↓
返回数据
🎯 实战示例:完整的用户列表功能
文件结构
src/
├── views/
│ └── Users.vue # 用户列表页面
├── components/
│ └── UserTable.vue # 用户表格组件
├── composables/
│ └── useUserList.js # 用户列表逻辑
├── stores/
│ └── userStore.js # 用户全局状态
├── api/
│ ├── request.js # axios 配置
│ └── user.js # 用户 API
├── utils/
│ ├── date.js # 日期格式化
│ └── validator.js # 验证工具
├── constants/
│ └── user.js # 用户常量
└── types/
└── user.ts # 用户类型定义
各层代码实现
typescript
// types/user.ts
export interface User {
id: number
name: string
email: string
status: 'active' | 'inactive'
createdAt: string
}
export interface UserQueryParams {
page: number
pageSize: number
keyword?: string
status?: string
}
javascript
// constants/user.js
export const USER_STATUS = {
ACTIVE: { value: 'active', label: '启用', color: 'success' },
INACTIVE: { value: 'inactive', label: '禁用', color: 'danger' }
}
export const DEFAULT_PAGE_SIZE = 10
javascript
// api/user.js
import request from './request'
export const userApi = {
getUsers(params) {
return request.get('/users', { params })
},
updateUserStatus(id, status) {
return request.patch(`/users/${id}/status`, { status })
},
deleteUser(id) {
return request.delete(`/users/${id}`)
}
}
javascript
// stores/userStore.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
selectedUsers: [], // 选中的用户
filters: {} // 筛选条件
}),
actions: {
toggleSelectUser(userId) {
const index = this.selectedUsers.indexOf(userId)
if (index > -1) {
this.selectedUsers.splice(index, 1)
} else {
this.selectedUsers.push(userId)
}
},
clearSelected() {
this.selectedUsers = []
}
}
})
javascript
// composables/useUserList.js
import { ref, computed } from 'vue'
import { userApi } from '@/api/user'
import { DEFAULT_PAGE_SIZE } from '@/constants/user'
import { useUserStore } from '@/stores/userStore'
import { formatDate } from '@/utils/date'
export function useUserList() {
const users = ref([])
const loading = ref(false)
const pagination = ref({
page: 1,
pageSize: DEFAULT_PAGE_SIZE,
total: 0
})
const userStore = useUserStore()
// 获取用户列表
const fetchUsers = async () => {
loading.value = true
try {
const { data } = await userApi.getUsers({
page: pagination.value.page,
pageSize: pagination.value.pageSize,
...userStore.filters
})
users.value = data.list
pagination.value.total = data.total
} finally {
loading.value = false
}
}
// 格式化显示(使用工具函数)
const formattedUsers = computed(() => {
return users.value.map(user => ({
...user,
createdAtFormatted: formatDate(user.createdAt),
statusLabel: USER_STATUS[user.status]?.label
}))
})
// 删除用户
const deleteUser = async (id) => {
await userApi.deleteUser(id)
await fetchUsers() // 刷新列表
userStore.clearSelected() // 清空选中状态
}
return {
users: formattedUsers,
loading,
pagination,
fetchUsers,
deleteUser
}
}
vue
<!-- views/Users.vue -->
<template>
<div class="users-page">
<!-- 使用 composable 获取数据 -->
<UserTable
:users="users"
:loading="loading"
@delete="handleDelete"
/>
<!-- 使用 store 管理选中状态 -->
<div v-if="selectedUsers.length > 0">
已选中 {{ selectedUsers.length }} 条数据
</div>
</div>
</template>
<script setup>
import { useUserList } from '@/composables/useUserList'
import { useUserStore } from '@/stores/userStore'
import { onMounted } from 'vue'
const { users, loading, fetchUsers, deleteUser } = useUserList()
const userStore = useUserStore()
const handleDelete = (id) => {
deleteUser(id)
}
onMounted(() => {
fetchUsers()
})
</script>
📝 总结:什么时候用什么文件
| 文件/目录 | 用途 | 典型内容 |
|---|---|---|
| views/ | 页面级别组件 | 路由对应的页面 |
| components/ | 可复用 UI 组件 | 按钮、表格、弹窗 |
| composables/ | 可复用逻辑(带状态) | 数据获取、业务逻辑 |
| stores/ | 全局共享状态 | 用户信息、主题、权限 |
| api/ | 接口请求封装 | axios 配置、API 方法 |
| utils/ | 纯函数工具 | 日期格式化、验证函数 |
| constants/ | 常量定义 | 枚举值、配置项 |
| types/ | TypeScript 类型 | 接口定义、类型声明 |
| directives/ | 自定义指令 | 权限、加载、水印 |
| router/ | 路由配置 | 路由表、路由守卫 |
| plugins/ | 插件 | 第三方库配置 |
| assets/ | 静态资源 | 图片、字体、图标 |
| styles/ | 全局样式 | CSS 变量、重置样式 |
🎯 核心原则
- UI 放 components/views
- 逻辑放 composables/stores
- 数据放 api/
- 工具放 utils/
- 配置放 constants/
- 类型放 types/