Vue 3 Composition API企业级项目架构设计:状态管理、路由守卫、权限控制完整解决方案

时光倒流
时光倒流 2025-12-14T19:20:01+08:00
0 0 0

引言

随着前端技术的快速发展,Vue 3的Composition API为构建复杂的企业级应用提供了更加灵活和强大的开发模式。在现代Web应用开发中,一个良好的架构设计对于项目的可维护性、可扩展性和团队协作效率至关重要。

本文将深入探讨基于Vue 3 Composition API的企业级项目架构设计,涵盖状态管理、路由权限控制、组件通信等核心主题,提供一套完整的解决方案,帮助开发者构建高质量的前端应用。

Vue 3 Composition API基础概念

Composition API的核心优势

Vue 3的Composition API通过函数式的方式来组织和复用逻辑代码,相比传统的Options API具有以下优势:

  1. 更好的逻辑复用:通过组合函数实现跨组件的逻辑共享
  2. 更清晰的代码结构:将相关的逻辑组织在一起,避免了选项API中的分散问题
  3. 更强的类型支持:在TypeScript环境下提供更好的类型推断
  4. 更灵活的开发体验:允许开发者按照业务逻辑而非生命周期来组织代码

基本使用模式

// basic-composition-api.js
import { ref, reactive, computed, watch } from 'vue'

export default {
  setup() {
    // 响应式数据声明
    const count = ref(0)
    const user = reactive({
      name: 'John',
      age: 30
    })
    
    // 计算属性
    const doubleCount = computed(() => count.value * 2)
    
    // 方法定义
    const increment = () => {
      count.value++
    }
    
    // 监听器
    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    })
    
    return {
      count,
      user,
      doubleCount,
      increment
    }
  }
}

状态管理架构设计

Pinia状态管理方案

Pinia是Vue 3官方推荐的状态管理库,相比Vuex具有更轻量、更好的TypeScript支持和更灵活的API设计。

安装与配置

npm install pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

Store定义与组织

// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // 状态
  const userInfo = ref(null)
  const isAuthenticated = ref(false)
  
  // 计算属性
  const permissions = computed(() => {
    return userInfo.value?.permissions || []
  })
  
  const hasPermission = (permission) => {
    return permissions.value.includes(permission)
  }
  
  // 方法
  const login = async (credentials) => {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(credentials)
      })
      
      const data = await response.json()
      
      if (data.token) {
        userInfo.value = data.user
        isAuthenticated.value = true
        // 存储token到localStorage
        localStorage.setItem('token', data.token)
        return { success: true }
      }
    } catch (error) {
      return { success: false, error: error.message }
    }
  }
  
  const logout = () => {
    userInfo.value = null
    isAuthenticated.value = false
    localStorage.removeItem('token')
  }
  
  const fetchUserInfo = async () => {
    try {
      const token = localStorage.getItem('token')
      if (!token) return
      
      const response = await fetch('/api/user', {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      })
      
      const data = await response.json()
      userInfo.value = data
      isAuthenticated.value = true
    } catch (error) {
      console.error('Failed to fetch user info:', error)
      logout()
    }
  }
  
  return {
    userInfo,
    isAuthenticated,
    permissions,
    hasPermission,
    login,
    logout,
    fetchUserInfo
  }
})

多Store架构设计

// stores/index.js
import { useUserStore } from './user'
import { useAppStore } from './app'
import { usePermissionStore } from './permission'

export {
  useUserStore,
  useAppStore,
  usePermissionStore
}

状态持久化与恢复

// stores/plugins/persistence.js
import { watch } from 'vue'

export const createPersistPlugin = (storeName, keys = []) => {
  return (store) => {
    // 从localStorage恢复状态
    const savedState = localStorage.getItem(`pinia:${storeName}`)
    if (savedState) {
      try {
        const parsedState = JSON.parse(savedState)
        Object.assign(store, parsedState)
      } catch (error) {
        console.error('Failed to restore state:', error)
      }
    }
    
    // 监听状态变化并保存
    watch(
      () => store.$state,
      (newState) => {
        if (keys.length > 0) {
          const filteredState = keys.reduce((acc, key) => {
            acc[key] = newState[key]
            return acc
          }, {})
          localStorage.setItem(`pinia:${storeName}`, JSON.stringify(filteredState))
        } else {
          localStorage.setItem(`pinia:${storeName}`, JSON.stringify(newState))
        }
      },
      { deep: true }
    )
  }
}

// 使用示例
// import { createPersistPlugin } from './plugins/persistence'
// 
// const userStore = useUserStore()
// userStore.$subscribe(createPersistPlugin('user', ['userInfo', 'isAuthenticated']))

路由权限控制设计

路由配置与权限标记

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { 
      requiresAuth: true,
      permissions: ['view_dashboard']
    }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    meta: { 
      requiresAuth: true,
      permissions: ['admin_access']
    }
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('@/views/Profile.vue'),
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 全局路由守卫
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  
  // 如果需要认证但用户未登录
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    // 检查是否有token
    await userStore.fetchUserInfo()
    
    if (!userStore.isAuthenticated) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
      return
    }
  }
  
  // 权限检查
  if (to.meta.permissions && to.meta.permissions.length > 0) {
    const hasPermission = to.meta.permissions.every(permission => 
      userStore.hasPermission(permission)
    )
    
    if (!hasPermission) {
      next('/403')
      return
    }
  }
  
  next()
})

export default router

动态路由加载

// router/dynamicRoutes.js
import { useUserStore } from '@/stores/user'

export const generateDynamicRoutes = async () => {
  const userStore = useUserStore()
  
  // 根据用户权限生成动态路由
  const baseRoutes = [
    {
      path: '/dashboard',
      name: 'Dashboard',
      component: () => import('@/views/Dashboard.vue'),
      meta: { 
        requiresAuth: true,
        permissions: ['view_dashboard']
      }
    }
  ]
  
  // 根据用户角色动态添加路由
  if (userStore.hasPermission('admin_access')) {
    baseRoutes.push({
      path: '/admin',
      name: 'Admin',
      component: () => import('@/views/Admin.vue'),
      meta: { 
        requiresAuth: true,
        permissions: ['admin_access']
      }
    })
  }
  
  if (userStore.hasPermission('report_access')) {
    baseRoutes.push({
      path: '/reports',
      name: 'Reports',
      component: () => import('@/views/Reports.vue'),
      meta: { 
        requiresAuth: true,
        permissions: ['report_access']
      }
    })
  }
  
  return baseRoutes
}

路由权限组件封装

<!-- components/PermissionWrapper.vue -->
<template>
  <div v-if="hasPermission" class="permission-wrapper">
    <slot></slot>
  </div>
  <div v-else class="no-permission">
    <slot name="no-permission">
      <p>您没有访问此内容的权限</p>
    </slot>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'

const props = defineProps({
  permission: {
    type: String,
    required: true
  }
})

const userStore = useUserStore()
const hasPermission = computed(() => {
  return userStore.hasPermission(props.permission)
})
</script>

<style scoped>
.permission-wrapper {
  /* 权限组件样式 */
}

.no-permission {
  padding: 20px;
  text-align: center;
  color: #999;
  border: 1px solid #eee;
  border-radius: 4px;
}
</style>

权限控制完整解决方案

基于RBAC的权限系统

// stores/permission.js
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const usePermissionStore = defineStore('permission', () => {
  // 角色和权限数据
  const roles = ref([])
  const permissions = ref([])
  
  // 用户角色映射
  const userRoles = ref([])
  
  // 计算属性:获取用户所有权限
  const userPermissions = computed(() => {
    const allPermissions = new Set()
    
    userRoles.value.forEach(role => {
      const roleInfo = roles.value.find(r => r.name === role)
      if (roleInfo && roleInfo.permissions) {
        roleInfo.permissions.forEach(permission => {
          allPermissions.add(permission)
        })
      }
    })
    
    return Array.from(allPermissions)
  })
  
  // 权限检查方法
  const hasPermission = (permission) => {
    return userPermissions.value.includes(permission)
  }
  
  const hasAnyPermission = (permissionsArray) => {
    return permissionsArray.some(permission => 
      userPermissions.value.includes(permission)
    )
  }
  
  const hasAllPermissions = (permissionsArray) => {
    return permissionsArray.every(permission => 
      userPermissions.value.includes(permission)
    )
  }
  
  // 角色检查方法
  const hasRole = (role) => {
    return userRoles.value.includes(role)
  }
  
  // 初始化权限系统
  const initPermissionSystem = async () => {
    try {
      // 获取用户角色和权限信息
      const response = await fetch('/api/permission/user')
      const data = await response.json()
      
      roles.value = data.roles || []
      permissions.value = data.permissions || []
      userRoles.value = data.userRoles || []
      
      return { success: true }
    } catch (error) {
      console.error('Failed to initialize permission system:', error)
      return { success: false, error: error.message }
    }
  }
  
  return {
    roles,
    permissions,
    userRoles,
    userPermissions,
    hasPermission,
    hasAnyPermission,
    hasAllPermissions,
    hasRole,
    initPermissionSystem
  }
})

权限路由配置工具

// utils/permission.js
export const filterRoutesByPermission = (routes, permissions) => {
  return routes.filter(route => {
    // 如果不需要权限验证,直接通过
    if (!route.meta || !route.meta.requiresAuth) {
      return true
    }
    
    // 检查是否需要认证
    if (route.meta.requiresAuth && !permissions.isAuthenticated) {
      return false
    }
    
    // 检查权限要求
    if (route.meta.permissions && route.meta.permissions.length > 0) {
      const hasRequiredPermissions = route.meta.permissions.every(permission => 
        permissions.userPermissions.includes(permission)
      )
      
      return hasRequiredPermissions
    }
    
    return true
  })
}

export const buildPermissionTree = (permissions) => {
  const tree = {}
  
  permissions.forEach(permission => {
    const parts = permission.split('.')
    let current = tree
    
    parts.forEach((part, index) => {
      if (!current[part]) {
        current[part] = {}
      }
      
      if (index === parts.length - 1) {
        current[part].permission = permission
      }
      
      current = current[part]
    })
  })
  
  return tree
}

权限控制指令封装

// directives/permission.js
import { usePermissionStore } from '@/stores/permission'

export default {
  mounted(el, binding, vnode) {
    const permissionStore = usePermissionStore()
    const permission = binding.value
    
    if (!permissionStore.hasPermission(permission)) {
      el.style.display = 'none'
      el.setAttribute('disabled', true)
    }
  },
  
  updated(el, binding, vnode) {
    const permissionStore = usePermissionStore()
    const permission = binding.value
    
    if (!permissionStore.hasPermission(permission)) {
      el.style.display = 'none'
      el.setAttribute('disabled', true)
    } else {
      el.style.display = ''
      el.removeAttribute('disabled')
    }
  }
}
<!-- 使用权限指令 -->
<template>
  <div>
    <button v-permission="'user.create'">创建用户</button>
    <button v-permission="'user.delete'">删除用户</button>
    <div v-permission="'admin.view'">
      管理员专属内容
    </div>
  </div>
</template>

<script setup>
import { usePermissionStore } from '@/stores/permission'

const permissionStore = usePermissionStore()
</script>

组件通信最佳实践

基于Pinia的组件间通信

// components/GlobalMessage.vue
<template>
  <div class="message-container">
    <transition-group name="message" tag="div">
      <div 
        v-for="message in messages" 
        :key="message.id"
        class="message-item"
        :class="message.type"
      >
        {{ message.content }}
        <button @click="removeMessage(message.id)">×</button>
      </div>
    </transition-group>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useAppStore } from '@/stores/app'

const appStore = useAppStore()

const messages = computed(() => appStore.messages)

const removeMessage = (id) => {
  appStore.removeMessage(id)
}
</script>

<style scoped>
.message-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
}

.message-item {
  padding: 12px 16px;
  margin-bottom: 8px;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.message-item.success {
  background-color: #f6ffed;
  border: 1px solid #b7eb8f;
  color: #52c41a;
}

.message-item.error {
  background-color: #fff2f0;
  border: 1px solid #ffccc7;
  color: #ff4d4f;
}

.message-item.warning {
  background-color: #fffbe6;
  border: 1px solid #ffe58f;
  color: #faad14;
}

.message-item.info {
  background-color: #e6f7ff;
  border: 1px solid #91d5ff;
  color: #1890ff;
}

.message-item button {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
  margin-left: 8px;
}
</style>

事件总线模式

// utils/eventBus.js
import { createApp } from 'vue'

const EventBus = {
  install(app) {
    const eventBus = createApp({}).config.globalProperties.$eventBus = {}
    
    // 实现事件监听
    eventBus.on = (event, callback) => {
      if (!eventBus._events) {
        eventBus._events = {}
      }
      
      if (!eventBus._events[event]) {
        eventBus._events[event] = []
      }
      
      eventBus._events[event].push(callback)
    }
    
    // 实现事件触发
    eventBus.emit = (event, data) => {
      if (eventBus._events && eventBus._events[event]) {
        eventBus._events[event].forEach(callback => callback(data))
      }
    }
    
    // 实现事件移除
    eventBus.off = (event, callback) => {
      if (eventBus._events && eventBus._events[event]) {
        if (callback) {
          eventBus._events[event] = eventBus._events[event].filter(cb => cb !== callback)
        } else {
          delete eventBus._events[event]
        }
      }
    }
    
    app.config.globalProperties.$eventBus = eventBus
  }
}

export default EventBus

代码分割与性能优化

动态导入和懒加载

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { 
      requiresAuth: true,
      permissions: ['view_dashboard']
    }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    meta: { 
      requiresAuth: true,
      permissions: ['admin_access']
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

组件懒加载

<!-- components/LazyComponent.vue -->
<template>
  <div v-if="loaded">
    <component :is="dynamicComponent" v-bind="componentProps" />
  </div>
  <div v-else class="loading-placeholder">
    加载中...
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const props = defineProps({
  componentName: {
    type: String,
    required: true
  },
  componentProps: {
    type: Object,
    default: () => ({})
  }
})

const loaded = ref(false)
const dynamicComponent = ref(null)

onMounted(async () => {
  try {
    // 动态导入组件
    const componentModule = await import(`@/components/${props.componentName}.vue`)
    dynamicComponent.value = componentModule.default
    loaded.value = true
  } catch (error) {
    console.error('Failed to load component:', error)
  }
})
</script>

<style scoped>
.loading-placeholder {
  padding: 20px;
  text-align: center;
  color: #999;
}
</style>

项目结构设计

标准化项目目录结构

src/
├── assets/                    # 静态资源
│   ├── images/
│   ├── styles/
│   └── icons/
├── components/                # 公共组件
│   ├── layout/
│   ├── ui/
│   └── shared/
├── composables/               # 组合式函数
│   ├── useAuth.js
│   ├── useApi.js
│   └── usePermission.js
├── hooks/                     # 自定义钩子
│   ├── useLocalStorage.js
│   └── useDebounce.js
├── views/                     # 页面组件
│   ├── Home.vue
│   ├── Login.vue
│   ├── Dashboard/
│   └── Admin/
├── stores/                    # 状态管理
│   ├── index.js
│   ├── user.js
│   ├── app.js
│   └── permission.js
├── router/                    # 路由配置
│   ├── index.js
│   └── routes.js
├── services/                  # API服务
│   ├── api.js
│   ├── auth.js
│   └── user.js
├── utils/                     # 工具函数
│   ├── helpers.js
│   ├── validators.js
│   └── constants.js
├── plugins/                   # 插件
│   ├── eventBus.js
│   └── permission.js
└── App.vue                    # 根组件

环境配置管理

// config/index.js
const config = {
  development: {
    apiUrl: 'http://localhost:3000/api',
    debug: true,
    enableMock: true
  },
  
  production: {
    apiUrl: 'https://api.yourapp.com/api',
    debug: false,
    enableMock: false
  },
  
  staging: {
    apiUrl: 'https://staging-api.yourapp.com/api',
    debug: true,
    enableMock: false
  }
}

export default config[process.env.NODE_ENV] || config.development

完整示例应用

应用入口文件

<!-- App.vue -->
<template>
  <div id="app">
    <router-view />
    <GlobalMessage />
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'

const userStore = useUserStore()
const appStore = useAppStore()

onMounted(async () => {
  // 应用启动时初始化用户信息
  await userStore.fetchUserInfo()
  
  // 初始化应用状态
  appStore.initApp()
})
</script>

<style>
/* 全局样式 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  background-color: #f5f5f5;
}

#app {
  min-height: 100vh;
}
</style>

权限控制的完整流程

// composables/usePermission.js
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
import { usePermissionStore } from '@/stores/permission'

export function usePermission() {
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()
  
  const hasPermission = (permission) => {
    return userStore.hasPermission(permission)
  }
  
  const hasRole = (role) => {
    return userStore.hasRole(role)
  }
  
  const checkPermissions = (permissions) => {
    if (!Array.isArray(permissions)) {
      permissions = [permissions]
    }
    
    return permissions.every(permission => 
      userStore.hasPermission(permission)
    )
  }
  
  const checkRoles = (roles) => {
    if (!Array.isArray(roles)) {
      roles = [roles]
    }
    
    return roles.some(role => userStore.hasRole(role))
  }
  
  // 权限检查组合式函数
  const usePermissionCheck = (permission, options = {}) => {
    const { 
      redirect = true,
      redirectPath = '/403',
      showComponent = true 
    } = options
    
    const hasAccess = computed(() => {
      return userStore.hasPermission(permission)
    })
    
    const checkAndRedirect = () => {
      if (!hasAccess.value && redirect) {
        // 这里可以使用路由跳转
        console.log(`Redirecting to ${redirectPath}`)
      }
    }
    
    return {
      hasAccess,
      checkAndRedirect
    }
  }
  
  return {
    hasPermission,
    hasRole,
    checkPermissions,
    checkRoles,
    usePermissionCheck
  }
}

总结与最佳实践

架构设计原则

  1. 单一职责原则:每个模块只负责特定的功能领域
  2. 可扩展性设计:预留扩展点,便于功能增加
  3. 可维护性考虑:代码结构清晰,文档完整
  4. 性能优化:合理使用缓存、懒加载等技术

性能优化建议

  1. 路由懒加载:减少初始包大小
  2. 组件按需加载:避免不必要的组件引入
  3. 状态管理优化:只监听必要的状态变化
  4. 代码分割:合理划分代码块

安全性考虑

  1. 前端权限控制:作为用户体验的增强,不是安全防护的唯一手段
  2. 后端验证:所有权限检查都应该在后端进行验证
  3. 敏感数据处理:避免在前端存储敏感信息
  4. HTTPS使用:确保所有API通信都是加密的

通过本文介绍的基于Vue 3 Composition API的企业级项目架构设计,开发者可以构建出具有良好可维护性、可扩展性和安全性的现代化前端应用。这套方案结合了Pinia状态管理、路由权限控制、组件通信等核心技术,为实际项目开发提供了完整的解决方案。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000