Vue 3企业级项目架构设计:Composition API状态管理与模块化开发实践
引言
随着前端技术的快速发展,Vue 3的推出为企业级应用开发带来了全新的可能性。Composition API作为Vue 3的核心特性,提供了更灵活、更可复用的代码组织方式。结合Pinia状态管理、模块化路由设计等现代前端技术,我们可以构建出更加健壮、可维护的企业级Vue 3项目架构。
本文将深入探讨基于Vue 3 Composition API的企业级项目架构设计,涵盖状态管理、模块化开发、权限控制等核心技术,为开发团队提供一套完整的开发规范和最佳实践。
项目架构概览
整体架构设计
企业级Vue 3项目通常采用分层架构模式,将业务逻辑、状态管理、UI组件等进行合理分离:
src/
├── api/ # API接口层
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # Composition API函数
├── layouts/ # 页面布局
├── pages/ # 页面组件
├── plugins/ # 插件配置
├── router/ # 路由配置
├── store/ # 状态管理
├── styles/ # 样式文件
├── utils/ # 工具函数
└── main.js # 入口文件
这种架构设计遵循单一职责原则,每个目录都有明确的职责边界,便于团队协作和代码维护。
Pinia状态管理深度实践
Pinia核心概念
Pinia是Vue 3官方推荐的状态管理库,相比Vuex具有更简洁的API和更好的TypeScript支持。Pinia的核心概念包括:
- Store: 状态容器,包含状态、getters和actions
- State: 存储应用状态
- Getters: 计算属性,用于派生状态
- Actions: 业务逻辑处理函数
模块化Store设计
在企业级项目中,我们需要将状态管理按业务模块进行划分:
// store/modules/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
token: localStorage.getItem('token') || '',
permissions: []
}),
getters: {
isLoggedIn: (state) => !!state.token,
userRole: (state) => state.userInfo?.role || 'guest'
},
actions: {
async login(credentials) {
try {
const response = await api.login(credentials)
this.token = response.data.token
this.userInfo = response.data.user
localStorage.setItem('token', this.token)
return response
} catch (error) {
throw new Error(error.message)
}
},
logout() {
this.token = ''
this.userInfo = null
this.permissions = []
localStorage.removeItem('token')
},
async fetchPermissions() {
if (!this.token) return
try {
const response = await api.getUserPermissions()
this.permissions = response.data
} catch (error) {
console.error('获取权限失败:', error)
}
}
}
})
// store/modules/product.js
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
categories: [],
loading: false,
filters: {
category: '',
priceRange: [0, 1000]
}
}),
getters: {
filteredProducts: (state) => {
return state.products.filter(product => {
const categoryMatch = !state.filters.category ||
product.category === state.filters.category
const priceMatch = product.price >= state.filters.priceRange[0] &&
product.price <= state.filters.priceRange[1]
return categoryMatch && priceMatch
})
}
},
actions: {
async fetchProducts() {
this.loading = true
try {
const response = await api.getProducts()
this.products = response.data
} catch (error) {
console.error('获取产品列表失败:', error)
} finally {
this.loading = false
}
},
async fetchCategories() {
try {
const response = await api.getCategories()
this.categories = response.data
} catch (error) {
console.error('获取分类失败:', error)
}
},
updateFilters(newFilters) {
this.filters = { ...this.filters, ...newFilters }
}
}
})
Store持久化策略
为了提升用户体验,我们需要实现状态的持久化存储:
// plugins/pinia-persist.js
import { watch } from 'vue'
export function createPersistPlugin(options = {}) {
return ({ store }) => {
const { key = store.$id, paths = [], storage = localStorage } = options
// 恢复状态
const savedState = storage.getItem(key)
if (savedState) {
try {
const parsedState = JSON.parse(savedState)
store.$patch(parsedState)
} catch (error) {
console.error('恢复状态失败:', error)
}
}
// 监听状态变化并保存
watch(
() => {
const state = {}
paths.forEach(path => {
state[path] = store[path]
})
return state
},
(newState) => {
try {
storage.setItem(key, JSON.stringify(newState))
} catch (error) {
console.error('保存状态失败:', error)
}
},
{ deep: true }
)
}
}
使用持久化插件:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createPersistPlugin } from './plugins/pinia-persist'
const pinia = createPinia()
pinia.use(createPersistPlugin({
key: 'user-store',
paths: ['token', 'userInfo']
}))
const app = createApp(App)
app.use(pinia)
app.mount('#app')
Composition API模块化开发
可复用逻辑封装
Composition API的最大优势在于能够将可复用的逻辑封装成独立的函数:
// composables/useApi.js
import { ref, reactive } from 'vue'
import { useLoading } from './useLoading'
export function useApi(apiFunction) {
const { loading, startLoading, stopLoading } = useLoading()
const data = ref(null)
const error = ref(null)
const execute = async (...args) => {
startLoading()
error.value = null
try {
const response = await apiFunction(...args)
data.value = response.data
return response
} catch (err) {
error.value = err.message
throw err
} finally {
stopLoading()
}
}
return {
data,
error,
loading,
execute
}
}
// composables/usePagination.js
import { reactive, computed } from 'vue'
export function usePagination(options = {}) {
const {
pageSize = 10,
currentPage = 1,
total = 0
} = options
const pagination = reactive({
currentPage,
pageSize,
total
})
const totalPages = computed(() =>
Math.ceil(pagination.total / pagination.pageSize)
)
const hasNextPage = computed(() =>
pagination.currentPage < totalPages.value
)
const hasPrevPage = computed(() =>
pagination.currentPage > 1
)
const setPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
pagination.currentPage = page
}
}
const setPageSize = (size) => {
pagination.pageSize = size
pagination.currentPage = 1 // 重置到第一页
}
const setTotal = (total) => {
pagination.total = total
}
return {
pagination,
totalPages,
hasNextPage,
hasPrevPage,
setPage,
setPageSize,
setTotal
}
}
表单处理封装
企业级应用中表单处理是常见需求,我们可以封装通用的表单处理逻辑:
// composables/useForm.js
import { reactive, ref } from 'vue'
export function useForm(initialValues = {}, validationRules = {}) {
const formData = reactive({ ...initialValues })
const errors = reactive({})
const isSubmitting = ref(false)
const validateField = (field, value) => {
const rules = validationRules[field]
if (!rules) return true
for (const rule of rules) {
const result = rule.validator(value, formData)
if (!result) {
errors[field] = rule.message
return false
}
}
delete errors[field]
return true
}
const validateForm = () => {
let isValid = true
Object.keys(validationRules).forEach(field => {
if (!validateField(field, formData[field])) {
isValid = false
}
})
return isValid
}
const resetForm = () => {
Object.keys(initialValues).forEach(key => {
formData[key] = initialValues[key]
})
Object.keys(errors).forEach(key => {
delete errors[key]
})
}
const setFieldValue = (field, value) => {
formData[field] = value
if (errors[field]) {
validateField(field, value)
}
}
const submitForm = async (submitHandler) => {
if (!validateForm()) return false
isSubmitting.value = true
try {
const result = await submitHandler(formData)
return result
} catch (error) {
throw error
} finally {
isSubmitting.value = false
}
}
return {
formData,
errors,
isSubmitting,
validateField,
validateForm,
resetForm,
setFieldValue,
submitForm
}
}
使用示例:
<template>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>用户名</label>
<input
v-model="formData.username"
@blur="() => validateField('username', formData.username)"
:class="{ error: errors.username }"
/>
<span v-if="errors.username" class="error-message">
{{ errors.username }}
</span>
</div>
<div class="form-group">
<label>邮箱</label>
<input
v-model="formData.email"
@blur="() => validateField('email', formData.email)"
:class="{ error: errors.email }"
/>
<span v-if="errors.email" class="error-message">
{{ errors.email }}
</span>
</div>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</form>
</template>
<script setup>
import { useForm } from '@/composables/useForm'
const { formData, errors, isSubmitting, validateField, submitForm } = useForm({
username: '',
email: ''
}, {
username: [
{
validator: (value) => value.length >= 3,
message: '用户名至少3个字符'
}
],
email: [
{
validator: (value) => /\S+@\S+\.\S+/.test(value),
message: '请输入有效的邮箱地址'
}
]
})
const handleSubmit = async () => {
try {
await submitForm(async (data) => {
// 提交逻辑
await api.createUser(data)
alert('创建成功')
})
} catch (error) {
alert('提交失败: ' + error.message)
}
}
</script>
模块化路由设计
动态路由配置
企业级应用通常需要根据用户权限动态加载路由:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
// 基础路由
const basicRoutes = [
{
path: '/login',
name: 'Login',
component: () => import('@/pages/auth/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/pages/error/404.vue'),
meta: { requiresAuth: false }
}
]
// 异步路由
const asyncRoutes = [
{
path: '/',
name: 'Dashboard',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Home',
component: () => import('@/pages/dashboard/Home.vue'),
meta: { title: '首页', icon: 'home' }
},
{
path: 'products',
name: 'Products',
component: () => import('@/pages/products/Index.vue'),
meta: { title: '产品管理', icon: 'product', permission: 'product:view' }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes: basicRoutes
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 不需要认证的路由直接通过
if (!to.meta.requiresAuth) {
next()
return
}
// 检查是否已登录
if (!userStore.isLoggedIn) {
next('/login')
return
}
// 如果还没有加载权限,先加载权限
if (userStore.permissions.length === 0) {
await userStore.fetchPermissions()
}
// 检查权限
if (to.meta.permission && !userStore.permissions.includes(to.meta.permission)) {
next('/404')
return
}
next()
})
// 动态添加路由
export function addRoutes(routes) {
routes.forEach(route => {
router.addRoute(route)
})
}
export default router
路由权限控制
基于用户权限的路由控制:
// utils/permission.js
export function checkPermission(userPermissions, requiredPermission) {
if (!requiredPermission) return true
if (!userPermissions || userPermissions.length === 0) return false
return userPermissions.includes(requiredPermission)
}
export function filterAsyncRoutes(routes, permissions) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(permissions, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, permissions)
}
res.push(tmp)
}
})
return res
}
function hasPermission(permissions, route) {
if (route.meta && route.meta.permission) {
return permissions.includes(route.meta.permission)
}
return true
}
路由懒加载优化
为了提升应用性能,我们需要实现路由的懒加载:
// router/modules/product.js
const ProductRoutes = [
{
path: '/products',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{
path: '',
name: 'ProductList',
component: () => import(
/* webpackChunkName: "product-list" */
'@/pages/products/List.vue'
),
meta: { title: '产品列表' }
},
{
path: 'create',
name: 'ProductCreate',
component: () => import(
/* webpackChunkName: "product-create" */
'@/pages/products/Create.vue'
),
meta: { title: '创建产品' }
},
{
path: ':id/edit',
name: 'ProductEdit',
component: () => import(
/* webpackChunkName: "product-edit" */
'@/pages/products/Edit.vue'
),
meta: { title: '编辑产品' }
}
]
}
]
export default ProductRoutes
组件库封装与设计
基础组件封装
企业级项目需要一套统一的UI组件库:
<!-- components/BaseButton.vue -->
<template>
<button
:class="buttonClass"
:disabled="disabled || loading"
@click="handleClick"
>
<BaseSpinner v-if="loading" size="small" />
<slot v-else />
</button>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
},
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
block: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
const buttonClass = computed(() => [
'base-button',
`base-button--${props.type}`,
`base-button--${props.size}`,
{
'base-button--disabled': props.disabled,
'base-button--loading': props.loading,
'base-button--block': props.block
}
])
const handleClick = (event) => {
if (!props.disabled && !props.loading) {
emit('click', event)
}
}
</script>
<style scoped>
.base-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
background-color: #fff;
}
.base-button--primary {
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
.base-button--success {
background-color: #67c23a;
border-color: #67c23a;
color: #fff;
}
.base-button--warning {
background-color: #e6a23c;
border-color: #e6a23c;
color: #fff;
}
.base-button--danger {
background-color: #f56c6c;
border-color: #f56c6c;
color: #fff;
}
.base-button--small {
padding: 6px 12px;
font-size: 12px;
}
.base-button--large {
padding: 12px 24px;
font-size: 16px;
}
.base-button--disabled,
.base-button--loading {
cursor: not-allowed;
opacity: 0.6;
}
.base-button--block {
display: flex;
width: 100%;
}
</style>
业务组件抽象
基于基础组件构建业务组件:
<!-- components/ProductCard.vue -->
<template>
<div class="product-card">
<div class="product-image">
<img :src="product.image" :alt="product.name" />
</div>
<div class="product-info">
<h3 class="product-name">{{ product.name }}</h3>
<p class="product-description">{{ product.description }}</p>
<div class="product-price">¥{{ product.price }}</div>
<div class="product-actions">
<BaseButton
type="primary"
size="small"
@click="handleAddToCart"
>
加入购物车
</BaseButton>
<BaseButton
type="default"
size="small"
@click="handleViewDetail"
>
查看详情
</BaseButton>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const props = defineProps({
product: {
type: Object,
required: true
}
})
const emit = defineEmits(['add-to-cart'])
const router = useRouter()
const handleAddToCart = () => {
emit('add-to-cart', props.product)
}
const handleViewDetail = () => {
router.push(`/products/${props.product.id}`)
}
</script>
<style scoped>
.product-card {
border: 1px solid #ebeef5;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
}
.product-card:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.product-image img {
width: 100%;
height: 200px;
object-fit: cover;
}
.product-info {
padding: 16px;
}
.product-name {
margin: 0 0 8px;
font-size: 16px;
font-weight: 500;
}
.product-description {
margin: 0 0 12px;
color: #606266;
font-size: 14px;
line-height: 1.5;
}
.product-price {
margin: 0 0 16px;
color: #f56c6c;
font-size: 18px;
font-weight: 500;
}
.product-actions {
display: flex;
gap: 8px;
}
</style>
权限控制体系
基于角色的权限管理
企业级应用通常采用RBAC(基于角色的访问控制)模型:
// composables/usePermission.js
import { computed } from 'vue'
import { useUserStore } from '@/store/modules/user'
export function usePermission() {
const userStore = useUserStore()
const hasPermission = computed(() => (permission) => {
return userStore.permissions.includes(permission)
})
const hasRole = computed(() => (role) => {
return userStore.userRole === role
})
const canAccess = computed(() => (requiredPermissions) => {
if (!requiredPermissions) return true
if (Array.isArray(requiredPermissions)) {
return requiredPermissions.every(permission =>
userStore.permissions.includes(permission)
)
}
return userStore.permissions.includes(requiredPermissions)
})
return {
hasPermission,
hasRole,
canAccess
}
}
指令级权限控制
通过自定义指令实现元素级权限控制:
// directives/permission.js
import { useUserStore } from '@/store/modules/user'
export const permission = {
mounted(el, binding) {
const { value } = binding
const userStore = useUserStore()
if (value && typeof value === 'string') {
const hasPermission = userStore.permissions.includes(value)
if (!hasPermission) {
el.style.display = 'none'
}
} else if (value && Array.isArray(value)) {
const hasPermission = value.some(permission =>
userStore.permissions.includes(permission)
)
if (!hasPermission) {
el.style.display = 'none'
}
}
},
updated(el, binding) {
const { value } = binding
const userStore = useUserStore()
if (value && typeof value === 'string') {
const hasPermission = userStore.permissions.includes(value)
el.style.display = hasPermission ? '' : 'none'
} else if (value && Array.isArray(value)) {
const hasPermission = value.some(permission =>
userStore.permissions.includes(permission)
)
el.style.display = hasPermission ? '' : 'none'
}
}
}
注册指令:
// main.js
import { createApp } from 'vue'
import { permission } from './directives/permission'
const app = createApp(App)
app.directive('permission', permission)
app.mount('#app')
使用示例:
<template>
<div>
<BaseButton
v-permission="'product:create'"
type="primary"
@click="handleCreate"
>
创建产品
</BaseButton>
<BaseButton
v-permission="['product:edit', 'product:delete']"
type="danger"
@click="handleDelete"
>
删除产品
</BaseButton>
</div>
</template>
API接口层设计
统一请求封装
企业级项目需要统一的API请求封装:
// api/request.js
import axios from 'axios'
import { useUserStore } from '@/store/modules/user'
import { ElMessage } from 'element-plus'
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response) => {
const { code, data, message } = response.data
if (code === 200) {
return response.data
} else {
ElMessage.error(message || '请求失败')
return Promise.reject(new Error(message || '请求失败'))
}
},
(error) => {
const { response } = error
if (response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
window.location.href = '/login'
} else if (response?.status === 403) {
ElMessage.error('权限不足')
} else if (response?.status >= 500) {
ElMessage.error('服务器内部错误')
} else {
ElMessage.error(error.message || '网络错误')
}
return Promise.reject(error)
}
)
export default service
API模块化管理
按照业务模块组织API接口:
// api/modules/auth.js
import request from '../request'
export const authApi = {
login(data) {
return request.post('/auth/login', data)
},
logout() {
return request.post('/auth/logout')
},
register(data) {
return request.post('/auth/register', data)
},
refreshToken() {
return request.post('/auth/refresh')
}
}
// api/modules/user.js
import request from '../request'
export const userApi = {
getUserInfo() {
return request.get('/user/info')
},
getUserPermissions() {
return request.get('/user/permissions')
},
updateUserProfile(data) {
return request.put('/user/profile', data)
},
changePassword(data) {
return request.put('/user/password',
评论 (0)