Vue 3 + TypeScript企业级项目架构设计与开发实践

LongMage
LongMage 2026-02-02T06:21:01+08:00
0 0 1

引言

随着前端技术的快速发展,Vue 3作为新一代的前端框架,凭借其强大的性能优化、更灵活的API设计以及完善的TypeScript支持,成为了构建企业级应用的理想选择。结合TypeScript的静态类型检查能力,可以显著提升代码质量和开发效率,降低维护成本。

在现代Web开发中,企业级应用面临着复杂的功能需求、严格的代码质量要求和长期的可维护性挑战。因此,合理的架构设计显得尤为重要。本文将深入探讨如何基于Vue 3和TypeScript构建一个健壮、可扩展的企业级项目架构,涵盖从基础配置到高级特性的完整实践方案。

项目初始化与基础配置

Vue 3 + TypeScript环境搭建

首先,我们需要使用Vue CLI或Vite来创建项目。推荐使用Vite,因为它具有更快的冷启动速度和更高效的开发体验。

# 使用Vite创建Vue 3 + TypeScript项目
npm create vite@latest my-enterprise-app --template vue-ts
cd my-enterprise-app
npm install

项目结构设计

一个良好的项目结构是企业级应用的基础。我们建议采用以下目录结构:

src/
├── assets/                 # 静态资源文件
├── components/             # 公共组件
├── composables/            # 可复用的逻辑组合
├── hooks/                  # 自定义Hook
├── views/                  # 页面组件
├── router/                 # 路由配置
├── store/                  # 状态管理
├── services/               # API服务层
├── types/                  # 类型定义
├── utils/                  # 工具函数
├── styles/                 # 样式文件
├── App.vue                 # 根组件
└── main.ts                 # 入口文件

TypeScript配置优化

tsconfig.json中进行详细的类型检查配置:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "types": ["vite/client"],
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.vue"],
  "exclude": ["node_modules"]
}

组件化开发模式

基础组件设计原则

在企业级应用中,组件的设计需要遵循单一职责原则和可复用性原则。每个组件应该专注于完成特定的功能,并且易于在不同场景下使用。

// components/Button.vue
<template>
  <button 
    :class="['btn', `btn--${type}`, { 'btn--disabled': disabled }]"
    :disabled="disabled"
    @click="handleClick"
  >
    <slot />
  </button>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

interface Props {
  type?: 'primary' | 'secondary' | 'danger'
  disabled?: boolean
}

interface Emits {
  (e: 'click', event: MouseEvent): void
}

const props = withDefaults(defineProps<Props>(), {
  type: 'primary',
  disabled: false
})

const emit = defineEmits<Emits>()

const handleClick = (event: MouseEvent) => {
  if (!props.disabled) {
    emit('click', event)
  }
}
</script>

<style scoped lang="scss">
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;

  &--primary {
    background-color: #007bff;
    color: white;
    
    &:hover:not(.btn--disabled) {
      background-color: #0056b3;
    }
  }

  &--secondary {
    background-color: #6c757d;
    color: white;
    
    &:hover:not(.btn--disabled) {
      background-color: #545b62;
    }
  }

  &--disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}
</style>

组件通信模式

Vue 3提供了多种组件通信方式,包括props、emit、provide/inject等。在企业级应用中,建议合理选择和使用这些通信机制:

// components/Modal.vue
<template>
  <div v-if="visible" class="modal-overlay">
    <div class="modal-content">
      <div class="modal-header">
        <h3>{{ title }}</h3>
        <button @click="closeModal">×</button>
      </div>
      <div class="modal-body">
        <slot />
      </div>
      <div class="modal-footer">
        <button @click="closeModal">取消</button>
        <button @click="confirm" class="btn-primary">确定</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

interface Props {
  visible: boolean
  title?: string
}

interface Emits {
  (e: 'close'): void
  (e: 'confirm'): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const closeModal = () => {
  emit('close')
}

const confirm = () => {
  emit('confirm')
}
</script>

状态管理架构

Pinia状态管理方案

在Vue 3中,推荐使用Pinia作为状态管理工具。相比Vuex,Pinia提供了更简洁的API和更好的TypeScript支持。

// store/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface User {
  id: number
  name: string
  email: string
  role: string
}

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const isLoggedIn = computed(() => !!user.value)

  const setUser = (userData: User) => {
    user.value = userData
  }

  const clearUser = () => {
    user.value = null
  }

  const updateProfile = (profileData: Partial<User>) => {
    if (user.value) {
      user.value = { ...user.value, ...profileData }
    }
  }

  return {
    user,
    isLoggedIn,
    setUser,
    clearUser,
    updateProfile
  }
})

复杂状态管理示例

对于更复杂的状态管理需求,可以创建多个store模块:

// store/app.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAppStore = defineStore('app', () => {
  const loading = ref(false)
  const error = ref<string | null>(null)
  const theme = ref<'light' | 'dark'>('light')
  const language = ref<'zh-CN' | 'en-US'>('zh-CN')

  const isLoading = computed(() => loading.value)
  const hasError = computed(() => !!error.value)

  const setLoading = (status: boolean) => {
    loading.value = status
  }

  const setError = (message: string | null) => {
    error.value = message
  }

  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }

  return {
    loading,
    error,
    theme,
    language,
    isLoading,
    hasError,
    setLoading,
    setError,
    toggleTheme
  }
})

// store/products.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface Product {
  id: number
  name: string
  price: number
  category: string
  description: string
}

export const useProductStore = defineStore('products', () => {
  const products = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  const filteredProducts = computed(() => products.value)
  const productCount = computed(() => products.value.length)

  const fetchProducts = async () => {
    try {
      setLoading(true)
      // 模拟API调用
      const response = await fetch('/api/products')
      const data = await response.json()
      products.value = data
    } catch (err) {
      setError('获取产品列表失败')
      console.error(err)
    } finally {
      setLoading(false)
    }
  }

  const addProduct = (product: Product) => {
    products.value.push(product)
  }

  const updateProduct = (id: number, updates: Partial<Product>) => {
    const index = products.value.findIndex(p => p.id === id)
    if (index !== -1) {
      products.value[index] = { ...products.value[index], ...updates }
    }
  }

  const deleteProduct = (id: number) => {
    products.value = products.value.filter(p => p.id !== id)
  }

  const setLoading = (status: boolean) => {
    loading.value = status
  }

  const setError = (message: string | null) => {
    error.value = message
  }

  return {
    products,
    loading,
    error,
    filteredProducts,
    productCount,
    fetchProducts,
    addProduct,
    updateProduct,
    deleteProduct
  }
})

路由配置与权限管理

路由结构设计

企业级应用通常需要复杂的路由结构,包括基础路由、动态路由和权限路由:

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

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/dashboard'
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/products',
    name: 'Products',
    component: () => import('@/views/Products.vue'),
    meta: { requiresAuth: true, permission: 'product:view' }
  },
  {
    path: '/users',
    name: 'Users',
    component: () => import('@/views/Users.vue'),
    meta: { requiresAuth: true, permission: 'user:view' }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true, permission: 'admin:access' },
    children: [
      {
        path: 'settings',
        name: 'Settings',
        component: () => import('@/views/admin/Settings.vue')
      }
    ]
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  },
  {
    path: '/:pathMatch(.*)*',
    redirect: '/404'
  }
]

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

// 路由守卫
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  const requiredPermission = to.meta.permission as string | undefined

  if (requiresAuth && !userStore.isLoggedIn) {
    next('/login')
    return
  }

  if (requiredPermission && !userStore.user?.role) {
    next('/404')
    return
  }

  // 权限检查逻辑
  if (requiredPermission && userStore.user?.role) {
    const hasPermission = checkPermission(userStore.user.role, requiredPermission)
    if (!hasPermission) {
      next('/404')
      return
    }
  }

  next()
})

function checkPermission(userRole: string, requiredPermission: string): boolean {
  // 实际项目中应从后端获取权限列表进行比对
  const permissionsMap: Record<string, string[]> = {
    'admin': ['product:view', 'product:edit', 'user:view', 'user:edit', 'admin:access'],
    'manager': ['product:view', 'product:edit', 'user:view'],
    'user': ['product:view']
  }

  return permissionsMap[userRole]?.includes(requiredPermission) || false
}

export default router

API服务层设计

统一API服务封装

良好的API服务层能够提供统一的请求处理、错误处理和拦截器功能:

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

class ApiService {
  private instance: AxiosInstance

  constructor() {
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })

    this.setupInterceptors()
  }

  private setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        const userStore = useUserStore()
        const token = userStore.user?.token
        
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }

        const appStore = useAppStore()
        appStore.setLoading(true)

        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        const appStore = useAppStore()
        appStore.setLoading(false)
        return response.data
      },
      (error) => {
        const appStore = useAppStore()
        appStore.setLoading(false)
        
        if (error.response?.status === 401) {
          // 处理未授权错误
          const userStore = useUserStore()
          userStore.clearUser()
          window.location.href = '/login'
        }
        
        appStore.setError(error.message)
        return Promise.reject(error)
      }
    )
  }

  public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get<T>(url, config)
  }

  public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post<T>(url, data, config)
  }

  public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.put<T>(url, data, config)
  }

  public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.delete<T>(url, config)
  }
}

export const apiService = new ApiService()

API服务使用示例

// services/userService.ts
import { apiService } from './api'
import { User } from '@/types/user'

export class UserService {
  static async login(credentials: { email: string; password: string }) {
    return apiService.post<{ token: string; user: User }>('/auth/login', credentials)
  }

  static async getCurrentUser() {
    return apiService.get<User>('/users/me')
  }

  static async getUsers() {
    return apiService.get<User[]>('/users')
  }

  static async createUser(userData: Omit<User, 'id'>) {
    return apiService.post<User>('/users', userData)
  }

  static async updateUser(id: number, userData: Partial<User>) {
    return apiService.put<User>(`/users/${id}`, userData)
  }

  static async deleteUser(id: number) {
    return apiService.delete(`/users/${id}`)
  }
}

类型系统与接口定义

统一的类型定义规范

在企业级项目中,良好的类型系统是保证代码质量的关键。我们需要建立一套完整的类型定义规范:

// types/user.ts
export interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'manager' | 'user'
  avatar?: string
  createdAt: string
  updatedAt: string
}

export interface LoginCredentials {
  email: string
  password: string
}

export interface AuthResponse {
  token: string
  user: User
}

// types/product.ts
export interface Product {
  id: number
  name: string
  price: number
  category: string
  description: string
  stock: number
  imageUrl?: string
  createdAt: string
  updatedAt: string
}

export interface ProductFilter {
  category?: string
  minPrice?: number
  maxPrice?: number
  search?: string
}

// types/response.ts
export interface ApiResponse<T> {
  success: boolean
  data: T
  message?: string
  code?: number
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number
    limit: number
    total: number
    totalPages: number
  }
}

类型工具函数

为了提高开发效率,我们可以创建一些常用的类型工具函数:

// utils/types.ts
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

export type Nullable<T> = T | null

export type Optional<T> = T | undefined

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

// 用于API响应的类型安全处理
export function isApiResponse<T>(data: any): data is ApiResponse<T> {
  return (
    typeof data === 'object' &&
    data !== null &&
    typeof data.success === 'boolean' &&
    'data' in data
  )
}

样式管理与组件库设计

CSS模块化方案

在企业级项目中,合理的样式管理至关重要:

// styles/variables.scss
:root {
  --primary-color: #007bff;
  --secondary-color: #6c757d;
  --success-color: #28a745;
  --danger-color: #dc3545;
  --warning-color: #ffc107;
  --info-color: #17a2b8;
  
  --border-radius: 4px;
  --box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  --transition: all 0.3s ease;
}

// styles/mixins.scss
@mixin button-style($bg-color, $text-color: white) {
  background-color: $bg-color;
  color: $text-color;
  border: none;
  padding: 8px 16px;
  border-radius: var(--border-radius);
  cursor: pointer;
  transition: var(--transition);
  
  &:hover:not(:disabled) {
    opacity: 0.9;
  }
  
  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

// styles/components/_button.scss
.btn {
  @include button-style(var(--primary-color));
  
  &--secondary {
    @include button-style(var(--secondary-color));
  }
  
  &--danger {
    @include button-style(var(--danger-color));
  }
  
  &--success {
    @include button-style(var(--success-color));
  }
}

组件库的可复用性设计

// components/DataTable.vue
<template>
  <div class="data-table">
    <div class="table-header">
      <h3>{{ title }}</h3>
      <div class="actions">
        <button 
          v-if="showAddButton" 
          @click="handleAdd"
          class="btn btn--primary"
        >
          添加
        </button>
        <input 
          v-model="searchQuery" 
          placeholder="搜索..." 
          class="search-input"
        />
      </div>
    </div>
    
    <div class="table-container">
      <table class="table">
        <thead>
          <tr>
            <th 
              v-for="column in columns" 
              :key="column.key"
              @click="handleSort(column.key)"
            >
              {{ column.title }}
              <span v-if="sortKey === column.key" class="sort-icon">
                {{ sortDirection === 'asc' ? '↑' : '↓' }}
              </span>
            </th>
          </tr>
        </thead>
        <tbody>
          <tr 
            v-for="row in filteredData" 
            :key="getRowKey(row)"
            @click="handleRowClick(row)"
          >
            <td v-for="column in columns" :key="column.key">
              <component 
                :is="column.component || 'span'" 
                :value="row[column.key]"
                :data="row"
              />
            </td>
          </tr>
        </tbody>
      </table>
      
      <div v-if="filteredData.length === 0" class="no-data">
        暂无数据
      </div>
    </div>
    
    <div class="pagination" v-if="showPagination">
      <button 
        @click="changePage(currentPage - 1)" 
        :disabled="currentPage <= 1"
      >
        上一页
      </button>
      
      <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
      
      <button 
        @click="changePage(currentPage + 1)" 
        :disabled="currentPage >= totalPages"
      >
        下一页
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'

interface Column {
  key: string
  title: string
  component?: string
}

interface Props {
  data: any[]
  columns: Column[]
  title?: string
  showAddButton?: boolean
  showPagination?: boolean
  pageSize?: number
  rowKey?: string
}

const props = withDefaults(defineProps<Props>(), {
  data: () => [],
  title: '',
  showAddButton: false,
  showPagination: true,
  pageSize: 10,
  rowKey: 'id'
})

const emit = defineEmits<{
  (e: 'add'): void
  (e: 'row-click', row: any): void
  (e: 'sort', key: string, direction: 'asc' | 'desc'): void
}>()

const searchQuery = ref('')
const sortKey = ref('')
const sortDirection = ref<'asc' | 'desc'>('asc')
const currentPage = ref(1)

const filteredData = computed(() => {
  let result = [...props.data]
  
  // 搜索过滤
  if (searchQuery.value) {
    const query = searchQuery.value.toLowerCase()
    result = result.filter(item =>
      Object.values(item).some(value =>
        String(value).toLowerCase().includes(query)
      )
    )
  }
  
  // 排序
  if (sortKey.value) {
    result.sort((a, b) => {
      const aVal = a[sortKey.value]
      const bVal = b[sortKey.value]
      
      if (aVal < bVal) return sortDirection.value === 'asc' ? -1 : 1
      if (aVal > bVal) return sortDirection.value === 'asc' ? 1 : -1
      return 0
    })
  }
  
  return result
})

const totalPages = computed(() => {
  return Math.ceil(filteredData.value.length / props.pageSize)
})

const paginatedData = computed(() => {
  if (!props.showPagination) return filteredData.value
  
  const start = (currentPage.value - 1) * props.pageSize
  const end = start + props.pageSize
  return filteredData.value.slice(start, end)
})

const handleAdd = () => {
  emit('add')
}

const handleRowClick = (row: any) => {
  emit('row-click', row)
}

const handleSort = (key: string) => {
  if (sortKey.value === key) {
    sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
  } else {
    sortKey.value = key
    sortDirection.value = 'asc'
  }
  
  emit('sort', key, sortDirection.value)
}

const changePage = (page: number) => {
  if (page >= 1 && page <= totalPages.value) {
    currentPage.value = page
  }
}

const getRowKey = (row: any) => {
  return row[props.rowKey]
}

// 监听数据变化重置分页
watch(() => props.data, () => {
  currentPage.value = 1
})
</script>

<style scoped lang="scss">
.data-table {
  background: white;
  border-radius: var(--border-radius);
  box-shadow: var(--box-shadow);
  overflow: hidden;
}

.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid #eee;
  
  h3 {
    margin: 0;
    font-size: 1.2em;
  }
  
  .actions {
    display: flex;
    gap: 8px;
    align-items: center;
  }
}

.table-container {
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  
  th, td {
    padding: 12px 16px;
    text-align: left;
    border-bottom: 1px solid #eee;
  }
  
  th {
    background-color: #f8f9fa;
    font-weight: 600;
    cursor: pointer;
    
    &:hover {
      background-color: #e9ecef;
    }
    
    .sort-icon {
      margin-left: 4px;
      font-size: 0.8em;
    }
  }
  
  tbody tr:hover {
    background-color: #f8f9fa;
  }
  
  tbody tr {
    cursor: pointer;
  }
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 16px;
  gap: 16px;
  
  button {
    @include button-style(var(--secondary-color), white);
    padding: 8px 16px;
  }
}

.search-input {
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000