Vue 3 Composition API实战:从零构建企业级后台管理系统架构

橙色阳光
橙色阳光 2026-02-27T08:04:09+08:00
0 0 0

引言

随着前端技术的快速发展,Vue 3的Composition API为开发者提供了更加灵活和强大的组件开发方式。本文将通过一个完整的后台管理系统项目,深入探讨如何利用Vue 3的Composition API、TypeScript以及Element Plus构建现代化的企业级后台管理系统。

项目概述

项目背景

企业后台管理系统通常需要处理复杂的业务逻辑、数据管理、权限控制等需求。传统的Vue 2选项式API在处理复杂组件时存在代码分散、难以维护等问题。Vue 3的Composition API通过函数式编程的方式,让开发者能够更好地组织和复用代码。

技术栈选择

  • Vue 3: 最新的Vue版本,提供更好的性能和开发体验
  • Composition API: 组件逻辑复用的核心技术
  • TypeScript: 提供类型安全和更好的开发体验
  • Element Plus: 基于Vue 3的UI组件库
  • Vue Router: 路由管理
  • Vuex 4: 状态管理

环境搭建与项目初始化

项目初始化

# 使用Vite创建Vue 3项目
npm create vite@latest admin-system --template vue-ts
cd admin-system
npm install

依赖安装

npm install element-plus vue-router vuex@4 @types/node

项目结构设计

src/
├── assets/              # 静态资源
├── components/          # 公共组件
├── views/               # 页面组件
├── router/              # 路由配置
├── store/               # 状态管理
├── services/            # API服务
├── utils/               # 工具函数
├── types/               # 类型定义
├── styles/              # 样式文件
└── App.vue              # 根组件

路由系统设计

路由配置基础结构

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/store/user'

// 路由元信息类型定义
interface RouteMeta {
  title: string
  icon?: string
  requiresAuth?: boolean
  permission?: string[]
}

// 路由类型定义
type RouteRecordRawWithMeta = RouteRecordRaw & {
  meta: RouteMeta
}

// 路由配置
const routes: RouteRecordRawWithMeta[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登录', requiresAuth: false }
  },
  {
    path: '/',
    name: 'Layout',
    component: () => import('@/layouts/Layout.vue'),
    redirect: '/dashboard',
    meta: { title: '首页', requiresAuth: true },
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/Dashboard.vue'),
        meta: { title: '仪表盘', icon: 'el-icon-home' }
      },
      {
        path: 'users',
        name: 'Users',
        component: () => import('@/views/users/Users.vue'),
        meta: { title: '用户管理', icon: 'el-icon-user' }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  
  if (to.meta.requiresAuth && !userStore.token) {
    next({ name: 'Login' })
  } else if (to.meta.permission && !userStore.hasPermission(to.meta.permission)) {
    next({ name: 'Dashboard' })
  } else {
    next()
  }
})

export default router

状态管理设计

用户状态管理

// src/store/user.ts
import { defineStore } from 'vuex'
import { User } from '@/types/user'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    userInfo: null as User | null,
    permissions: [] as string[]
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token,
    hasPermission: (state) => (permission: string) => {
      return state.permissions.includes(permission)
    }
  },
  
  actions: {
    setToken(token: string) {
      this.token = token
      localStorage.setItem('token', token)
    },
    
    setUserInfo(userInfo: User) {
      this.userInfo = userInfo
    },
    
    setPermissions(permissions: string[]) {
      this.permissions = permissions
    },
    
    logout() {
      this.token = ''
      this.userInfo = null
      this.permissions = []
      localStorage.removeItem('token')
    }
  }
})

Composition API核心实践

用户管理逻辑封装

// src/composables/useUser.ts
import { ref, reactive } from 'vue'
import { User, UserForm } from '@/types/user'
import { useUserStore } from '@/store/user'
import { userService } from '@/services/user'

export function useUser() {
  const userStore = useUserStore()
  const loading = ref(false)
  const users = ref<User[]>([])
  const userForm = reactive<UserForm>({
    name: '',
    email: '',
    phone: '',
    role: ''
  })
  const dialogVisible = ref(false)
  const currentUserId = ref<number | null>(null)

  // 获取用户列表
  const fetchUsers = async () => {
    loading.value = true
    try {
      const response = await userService.getUsers()
      users.value = response.data
    } catch (error) {
      console.error('获取用户列表失败:', error)
    } finally {
      loading.value = false
    }
  }

  // 创建用户
  const createUser = async (formData: UserForm) => {
    try {
      await userService.createUser(formData)
      await fetchUsers()
      dialogVisible.value = false
    } catch (error) {
      console.error('创建用户失败:', error)
      throw error
    }
  }

  // 更新用户
  const updateUser = async (id: number, formData: UserForm) => {
    try {
      await userService.updateUser(id, formData)
      await fetchUsers()
      dialogVisible.value = false
    } catch (error) {
      console.error('更新用户失败:', error)
      throw error
    }
  }

  // 删除用户
  const deleteUser = async (id: number) => {
    try {
      await userService.deleteUser(id)
      await fetchUsers()
    } catch (error) {
      console.error('删除用户失败:', error)
      throw error
    }
  }

  // 打开编辑对话框
  const openEditDialog = (user: User) => {
    currentUserId.value = user.id
    Object.assign(userForm, user)
    dialogVisible.value = true
  }

  // 打开创建对话框
  const openCreateDialog = () => {
    currentUserId.value = null
    Object.assign(userForm, {
      name: '',
      email: '',
      phone: '',
      role: ''
    })
    dialogVisible.value = true
  }

  // 提交表单
  const handleSubmit = async () => {
    if (currentUserId.value) {
      await updateUser(currentUserId.value, userForm)
    } else {
      await createUser(userForm)
    }
  }

  return {
    loading,
    users,
    userForm,
    dialogVisible,
    fetchUsers,
    createUser,
    updateUser,
    deleteUser,
    openEditDialog,
    openCreateDialog,
    handleSubmit
  }
}

表格组件封装

// src/components/Table.vue
<template>
  <el-table 
    :data="tableData" 
    :loading="loading"
    border
    style="width: 100%"
  >
    <el-table-column prop="id" label="ID" width="80"></el-table-column>
    <el-table-column prop="name" label="姓名"></el-table-column>
    <el-table-column prop="email" label="邮箱"></el-table-column>
    <el-table-column prop="phone" label="电话"></el-table-column>
    <el-table-column prop="role" label="角色"></el-table-column>
    <el-table-column label="操作" width="200">
      <template #default="scope">
        <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
        <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>
  
  <el-pagination
    v-model:current-page="currentPage"
    v-model:page-size="pageSize"
    :total="total"
    :page-sizes="[10, 20, 50, 100]"
    layout="total, sizes, prev, pager, next, jumper"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
  />
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { User } from '@/types/user'

interface TableProps {
  data: User[]
  loading: boolean
  total: number
  currentPage: number
  pageSize: number
}

interface TableEmits {
  (e: 'edit', row: User): void
  (e: 'delete', row: User): void
  (e: 'size-change', size: number): void
  (e: 'current-change', page: number): void
}

const props = withDefaults(defineProps<TableProps>(), {
  data: () => [],
  loading: false,
  total: 0,
  currentPage: 1,
  pageSize: 10
})

const emit = defineEmits<TableEmits>()

const handleEdit = (row: User) => {
  emit('edit', row)
}

const handleDelete = (row: User) => {
  emit('delete', row)
}

const handleSizeChange = (size: number) => {
  emit('size-change', size)
}

const handleCurrentChange = (page: number) => {
  emit('current-change', page)
}

const tableData = ref<User[]>(props.data)
watch(() => props.data, (newData) => {
  tableData.value = newData
})
</script>

权限控制实现

权限指令封装

// src/directives/permission.ts
import { DirectiveBinding } from 'vue'
import { useUserStore } from '@/store/user'

export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const userStore = useUserStore()
    const permission = binding.value
    
    if (!userStore.hasPermission(permission)) {
      el.style.display = 'none'
    }
  },
  
  updated(el: HTMLElement, binding: DirectiveBinding) {
    const userStore = useUserStore()
    const permission = binding.value
    
    if (!userStore.hasPermission(permission)) {
      el.style.display = 'none'
    } else {
      el.style.display = ''
    }
  }
}

权限菜单渲染

// src/components/SidebarMenu.vue
<template>
  <el-menu
    :default-active="activeMenu"
    :collapse="isCollapse"
    router
    background-color="#545c64"
    text-color="#fff"
    active-text-color="#ffd04b"
  >
    <template v-for="route in filteredRoutes" :key="route.path">
      <el-submenu v-if="route.children && route.children.length > 0" :index="route.path">
        <template #title>
          <i :class="route.meta?.icon"></i>
          <span>{{ route.meta?.title }}</span>
        </template>
        <el-menu-item
          v-for="child in route.children"
          :key="child.path"
          :index="child.path"
        >
          {{ child.meta?.title }}
        </el-menu-item>
      </el-submenu>
      <el-menu-item v-else :index="route.path">
        <i :class="route.meta?.icon"></i>
        <template #title>{{ route.meta?.title }}</template>
      </el-menu-item>
    </template>
  </el-menu>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/store/user'

const route = useRoute()
const userStore = useUserStore()

// 过滤有权限的路由
const filteredRoutes = computed(() => {
  const routes = []
  
  // 这里可以根据实际的路由配置进行过滤
  // 简化示例
  return routes
})

const activeMenu = computed(() => {
  return route.path
})

const isCollapse = computed(() => {
  return false // 可以根据实际需求调整
})
</script>

API服务封装

通用API服务

// src/services/api.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/user'

// 创建axios实例
const apiClient: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
apiClient.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers = {
        ...config.headers,
        Authorization: `Bearer ${userStore.token}`
      }
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
apiClient.interceptors.response.use(
  (response: AxiosResponse) => {
    return response.data
  },
  (error) => {
    if (error.response?.status === 401) {
      const userStore = useUserStore()
      userStore.logout()
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default apiClient

用户服务实现

// src/services/user.ts
import apiClient from './api'
import { User, UserForm } from '@/types/user'

export const userService = {
  // 获取用户列表
  getUsers(params?: any) {
    return apiClient.get('/users', { params })
  },
  
  // 获取用户详情
  getUserById(id: number) {
    return apiClient.get(`/users/${id}`)
  },
  
  // 创建用户
  createUser(data: UserForm) {
    return apiClient.post('/users', data)
  },
  
  // 更新用户
  updateUser(id: number, data: UserForm) {
    return apiClient.put(`/users/${id}`, data)
  },
  
  // 删除用户
  deleteUser(id: number) {
    return apiClient.delete(`/users/${id}`)
  },
  
  // 用户登录
  login(credentials: { email: string; password: string }) {
    return apiClient.post('/auth/login', credentials)
  }
}

类型定义设计

用户类型定义

// src/types/user.ts
export interface User {
  id: number
  name: string
  email: string
  phone: string
  role: string
  createdAt: string
  updatedAt: string
}

export interface UserForm {
  name: string
  email: string
  phone: string
  role: string
}

export interface LoginCredentials {
  email: string
  password: string
}

export interface LoginResponse {
  token: string
  user: User
}

响应类型定义

// src/types/response.ts
export interface ApiResponse<T> {
  code: number
  message: string
  data: T
  timestamp: number
}

export interface PaginatedResponse<T> {
  data: T[]
  total: number
  page: number
  pageSize: number
}

组件化开发实践

表单组件封装

// src/components/UserForm.vue
<template>
  <el-form 
    :model="form" 
    :rules="rules" 
    ref="formRef"
    label-width="100px"
  >
    <el-form-item label="姓名" prop="name">
      <el-input v-model="form.name" placeholder="请输入姓名"></el-input>
    </el-form-item>
    
    <el-form-item label="邮箱" prop="email">
      <el-input v-model="form.email" placeholder="请输入邮箱"></el-input>
    </el-form-item>
    
    <el-form-item label="电话" prop="phone">
      <el-input v-model="form.phone" placeholder="请输入电话"></el-input>
    </el-form-item>
    
    <el-form-item label="角色" prop="role">
      <el-select v-model="form.role" placeholder="请选择角色">
        <el-option label="管理员" value="admin"></el-option>
        <el-option label="普通用户" value="user"></el-option>
      </el-select>
    </el-form-item>
    
    <el-form-item>
      <el-button type="primary" @click="submitForm">提交</el-button>
      <el-button @click="resetForm">重置</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { UserForm } from '@/types/user'

interface FormEmits {
  (e: 'submit', data: UserForm): void
  (e: 'reset'): void
}

const emit = defineEmits<FormEmits>()

const formRef = ref()
const form = reactive<UserForm>({
  name: '',
  email: '',
  phone: '',
  role: ''
})

const rules = {
  name: [
    { required: true, message: '请输入姓名', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
  ],
  phone: [
    { required: true, message: '请输入电话', trigger: 'blur' }
  ]
}

const submitForm = () => {
  formRef.value.validate((valid: boolean) => {
    if (valid) {
      emit('submit', form)
    }
  })
}

const resetForm = () => {
  formRef.value.resetFields()
  emit('reset')
}
</script>

页面组件实现

// src/views/users/Users.vue
<template>
  <div class="user-page">
    <el-card class="box-card">
      <div class="header">
        <h2>用户管理</h2>
        <el-button type="primary" @click="openCreateDialog">
          新增用户
        </el-button>
      </div>
      
      <el-table 
        :data="users" 
        :loading="loading"
        border
        style="width: 100%"
      >
        <el-table-column prop="id" label="ID" width="80"></el-table-column>
        <el-table-column prop="name" label="姓名"></el-table-column>
        <el-table-column prop="email" label="邮箱"></el-table-column>
        <el-table-column prop="phone" label="电话"></el-table-column>
        <el-table-column prop="role" label="角色"></el-table-column>
        <el-table-column label="操作" width="200">
          <template #default="scope">
            <el-button size="small" @click="openEditDialog(scope.row)">编辑</el-button>
            <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </el-card>
    
    <el-dialog
      v-model="dialogVisible"
      :title="currentUserId ? '编辑用户' : '新增用户'"
      width="500px"
    >
      <UserForm 
        @submit="handleSubmit"
        @reset="dialogVisible = false"
      />
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { useUser } from '@/composables/useUser'

const {
  loading,
  users,
  dialogVisible,
  fetchUsers,
  openEditDialog,
  openCreateDialog,
  handleSubmit
} = useUser()

const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)

onMounted(() => {
  fetchUsers()
})

const handleSizeChange = (size: number) => {
  pageSize.value = size
  fetchUsers()
}

const handleCurrentChange = (page: number) => {
  currentPage.value = page
  fetchUsers()
}
</script>

<style scoped>
.user-page {
  padding: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
</style>

性能优化策略

组件懒加载

// src/router/index.ts
const routes: RouteRecordRawWithMeta[] = [
  {
    path: '/users',
    name: 'Users',
    component: () => import('@/views/users/Users.vue'),
    meta: { title: '用户管理', icon: 'el-icon-user' }
  },
  {
    path: '/products',
    name: 'Products',
    component: () => import('@/views/products/Products.vue'),
    meta: { title: '产品管理', icon: 'el-icon-goods' }
  }
]

数据缓存机制

// src/composables/useCache.ts
import { ref, watch } from 'vue'

export function useCache<T>(key: string, initialValue: T) {
  const cachedValue = ref<T>(initialValue)
  
  // 从localStorage获取缓存
  const loadFromCache = () => {
    const cached = localStorage.getItem(key)
    if (cached) {
      try {
        cachedValue.value = JSON.parse(cached)
      } catch (error) {
        console.error('缓存解析失败:', error)
      }
    }
  }
  
  // 保存到localStorage
  const saveToCache = (value: T) => {
    cachedValue.value = value
    localStorage.setItem(key, JSON.stringify(value))
  }
  
  // 监听值变化并保存
  watch(cachedValue, (newValue) => {
    saveToCache(newValue)
  }, { deep: true })
  
  loadFromCache()
  
  return {
    value: cachedValue,
    save: saveToCache,
    load: loadFromCache
  }
}

错误处理与日志记录

全局错误处理

// src/utils/errorHandler.ts
import { ElMessage, ElMessageBox } from 'element-plus'

export function handleError(error: any, context?: string) {
  console.error('错误发生:', { error, context })
  
  if (error.response) {
    // 服务器响应错误
    const { status, data } = error.response
    switch (status) {
      case 401:
        ElMessage.error('登录已过期,请重新登录')
        // 跳转到登录页
        window.location.href = '/login'
        break
      case 403:
        ElMessage.error('权限不足')
        break
      case 404:
        ElMessage.error('请求的资源不存在')
        break
      default:
        ElMessage.error(data.message || '请求失败')
    }
  } else if (error.request) {
    // 网络错误
    ElMessage.error('网络连接失败,请检查网络')
  } else {
    // 其他错误
    ElMessage.error(error.message || '未知错误')
  }
  
  // 发送错误到监控系统(可选)
  // sendErrorToMonitoring(error, context)
}

请求拦截器增强

// src/services/api.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { handleError } from '@/utils/errorHandler'

// 添加请求拦截器
apiClient.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // 添加请求时间戳
    config.params = {
      ...config.params,
      _t: Date.now()
    }
    
    // 添加请求标识
    const requestId = Math.random().toString(36).substr(2, 9)
    config.headers = {
      ...config.headers,
      'X-Request-ID': requestId
    }
    
    return config
  },
  (error) => {
    handleError(error, '请求拦截器错误')
    return Promise.reject(error)
  }
)

// 添加响应拦截器
apiClient.interceptors.response.use(
  (response: AxiosResponse) => {
    // 可以在这里处理统一的响应格式
    return response.data
  },
  (error) => {
    handleError(error, '响应拦截器错误')
    return Promise.reject(error)
  }
)

测试策略

单元测试示例

// src/composables/__tests__/useUser.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { useUser } from '../useUser'

describe('useUser composable', () => {
  it('should initialize with correct default values', () => {
    const { loading, users, dialogVisible } = useUser()
    
    expect(loading.value).toBe(false)
    expect(users.value).toEqual([])
    expect(dialogVisible.value).toBe(false)
  })
  
  it('should handle loading state correctly', async () => {
    const { loading, fetchUsers } = useUser()
    
    expect(loading.value).toBe(false)
    
    // 模拟异步操作
    const mockUsers = [{ id: 1, name: 'Test User' }]
    
    // 这里可以添加mock的实现来测试loading状态
    // 由于是组合式API,需要更复杂的测试环境
  })
})

部署与构建优化

构建配置

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    vue(),
    vueJsx()
  ],
  resolve: {
    alias: {
      '@': resolve(__dirname, './src')
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'vuex', 'element-plus'],
          api: ['axios'],
          utils: ['lodash',
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000