Vue 3企业级项目架构设计:Composition API状态管理与模块化开发实践

D
dashi78 2025-09-12T20:33:07+08:00
0 0 239

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)