Vue 3 + TypeScript企业级项目架构设计:从零搭建完整的开发框架

Ulysses566
Ulysses566 2026-01-31T12:02:00+08:00
0 0 1

前言

随着前端技术的快速发展,Vue 3作为新一代的前端框架,结合TypeScript的强大类型系统,为构建企业级应用提供了坚实的基础。本文将详细介绍如何从零开始搭建一个完整的Vue 3 + TypeScript企业级项目架构,涵盖项目结构设计、状态管理、路由配置、组件封装、构建优化等核心内容。

项目初始化与基础配置

创建项目结构

首先,我们使用Vue CLI或Vite来创建项目。这里以Vite为例:

npm create vue@latest my-enterprise-app
cd my-enterprise-app
npm install

在创建过程中选择TypeScript支持,这样会自动配置好TypeScript环境。

项目目录结构设计

一个良好的企业级项目架构应该具备清晰的分层结构:

src/
├── assets/                 # 静态资源
│   ├── images/
│   ├── styles/
│   └── icons/
├── components/             # 公共组件
│   ├── common/
│   ├── layout/
│   └── ui/
├── composables/            # 可复用逻辑
├── hooks/                  # 自定义Hook
├── views/                  # 页面组件
├── router/                 # 路由配置
├── store/                  # 状态管理
├── services/               # API服务
├── utils/                  # 工具函数
├── types/                  # 类型定义
├── layouts/                # 布局组件
└── App.vue

TypeScript配置优化

tsconfig.json中进行详细的配置:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

状态管理架构设计

Pinia状态管理方案

在Vue 3中,Pinia是推荐的状态管理库。首先安装:

npm install pinia

创建Store基础结构

// src/store/index.ts
import { createPinia } from 'pinia'
import { App } from 'vue'

export function setupStore(app: App) {
  const pinia = createPinia()
  app.use(pinia)
}

export * from './modules/user'
export * from './modules/app'

用户状态模块示例

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

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

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const isAuthenticated = computed(() => !!user.value)
  
  const setUser = (userData: User) => {
    user.value = userData
  }
  
  const clearUser = () => {
    user.value = null
  }
  
  return {
    user,
    isAuthenticated,
    setUser,
    clearUser
  }
})

应用状态模块

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

export const useAppStore = defineStore('app', () => {
  const loading = ref(false)
  const theme = ref<'light' | 'dark'>('light')
  const language = ref<'zh' | 'en'>('zh')
  
  const setLoading = (status: boolean) => {
    loading.value = status
  }
  
  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }
  
  return {
    loading,
    theme,
    language,
    setLoading,
    toggleTheme
  }
})

路由系统设计

路由配置基础结构

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { App } from 'vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  }
]

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

export function setupRouter(app: App) {
  app.use(router)
}

export default router

路由守卫实现

// src/router/guard.ts
import { router } from './index'
import { useUserStore } from '@/store/modules/user'

router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

组件化开发实践

公共组件设计原则

// src/components/common/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
}

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

const emit = defineEmits<{
  (e: 'click'): void
}>()

const handleClick = () => {
  if (!props.disabled) {
    emit('click')
  }
}
</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>

布局组件封装

// src/layouts/MainLayout.vue
<template>
  <div class="main-layout">
    <header class="layout-header">
      <slot name="header" />
    </header>
    <aside class="layout-aside">
      <slot name="sidebar" />
    </aside>
    <main class="layout-main">
      <slot />
    </main>
  </div>
</template>

<script setup lang="ts">
// 布局组件逻辑
</script>

<style scoped lang="scss">
.main-layout {
  display: grid;
  grid-template-areas: 
    "header header"
    "sidebar main";
  grid-template-rows: 60px 1fr;
  grid-template-columns: 250px 1fr;
  min-height: 100vh;
}

.layout-header {
  grid-area: header;
  background-color: #f8f9fa;
  border-bottom: 1px solid #dee2e6;
}

.layout-aside {
  grid-area: sidebar;
  background-color: #f8f9fa;
  border-right: 1px solid #dee2e6;
}

.layout-main {
  grid-area: main;
  padding: 20px;
}
</style>

API服务层设计

HTTP客户端封装

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

class ApiService {
  private client: AxiosInstance
  
  constructor() {
    this.client = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL,
      timeout: 10000
    })
    
    this.setupInterceptors()
  }
  
  private setupInterceptors() {
    // 请求拦截器
    this.client.interceptors.request.use(
      (config) => {
        const userStore = useUserStore()
        if (userStore.isAuthenticated && config.headers) {
          config.headers.Authorization = `Bearer ${userStore.user?.token}`
        }
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.client.interceptors.response.use(
      (response) => response.data,
      (error) => {
        if (error.response?.status === 401) {
          // 处理未授权
          const userStore = useUserStore()
          userStore.clearUser()
          window.location.href = '/login'
        }
        return Promise.reject(error)
      }
    )
  }
  
  public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.get(url, config)
  }
  
  public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post(url, data, config)
  }
  
  public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.put(url, data, config)
  }
  
  public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.delete(url, config)
  }
}

export const apiService = new ApiService()

业务API服务

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

export class UserService {
  static async getCurrentUser(): Promise<User> {
    return apiService.get('/user/me')
  }
  
  static async updateUser(userData: Partial<User>): Promise<User> {
    return apiService.put('/user', userData)
  }
  
  static async getUserList(page: number, size: number): Promise<{users: User[], total: number}> {
    return apiService.get('/users', {
      params: { page, size }
    })
  }
}

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

工具函数与类型定义

常用工具函数

// src/utils/index.ts
import { Ref } from 'vue'

export const formatDate = (date: Date | string, format: string = 'YYYY-MM-DD'): string => {
  const d = new Date(date)
  const year = d.getFullYear()
  const month = String(d.getMonth() + 1).padStart(2, '0')
  const day = String(d.getDate()).padStart(2, '0')
  
  return format
    .replace('YYYY', year.toString())
    .replace('MM', month)
    .replace('DD', day)
}

export const debounce = <T extends (...args: any[]) => any>(
  func: T,
  wait: number
): ((...args: Parameters<T>) => void) => {
  let timeoutId: NodeJS.Timeout | null = null
  
  return function (...args: Parameters<T>) {
    if (timeoutId) {
      clearTimeout(timeoutId)
    }
    timeoutId = setTimeout(() => func(...args), wait)
  }
}

export const throttle = <T extends (...args: any[]) => any>(
  func: T,
  limit: number
): ((...args: Parameters<T>) => void) => {
  let inThrottle: boolean
  
  return function (...args: Parameters<T>) {
    if (!inThrottle) {
      func(...args)
      inThrottle = true
      setTimeout(() => (inThrottle = false), limit)
    }
  }
}

export const deepClone = <T>(obj: T): T => {
  return JSON.parse(JSON.stringify(obj))
}

类型定义文件

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

export interface PaginationParams {
  page: number
  size: number
  sortBy?: string
  sortOrder?: 'asc' | 'desc'
}

export interface PaginationResponse<T> {
  items: T[]
  total: number
  page: number
  size: number
}

构建优化与性能调优

Vite配置优化

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
          utils: ['lodash-es']
        }
      }
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

代码分割与懒加载

// src/router/index.ts (优化版本)
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true, roles: ['admin'] },
    children: [
      {
        path: 'users',
        name: 'Users',
        component: () => import('@/views/admin/Users.vue')
      },
      {
        path: 'settings',
        name: 'Settings',
        component: () => import('@/views/admin/Settings.vue')
      }
    ]
  }
]

开发环境与测试配置

ESLint和Prettier配置

// .eslintrc.json
{
  "extends": [
    "@vue/typescript/recommended",
    "@vue/prettier"
  ],
  "rules": {
    "no-console": "warn",
    "no-debugger": "error"
  }
}

单元测试配置

// src/__tests__/components/Button.test.ts
import { mount } from '@vue/test-utils'
import Button from '@/components/common/Button.vue'

describe('Button', () => {
  it('renders correctly with default props', () => {
    const wrapper = mount(Button)
    expect(wrapper.classes()).toContain('btn--primary')
  })
  
  it('emits click event when clicked', async () => {
    const wrapper = mount(Button)
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toHaveLength(1)
  })
})

部署与CI/CD流程

构建脚本优化

// package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "vitest",
    "lint": "eslint src --ext .ts,.vue --fix",
    "type-check": "vue-tsc --noEmit"
  }
}

Docker部署配置

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "preview"]

最佳实践总结

代码规范与团队协作

  1. 组件命名规范:使用PascalCase,如UserProfile.vue
  2. 状态管理原则:单一职责,避免过度嵌套
  3. API调用规范:统一错误处理和拦截器配置
  4. 类型安全:充分利用TypeScript的类型系统

性能优化要点

  1. 懒加载组件:减少初始包大小
  2. 代码分割:按路由或功能模块分割代码
  3. 缓存策略:合理使用computed和watch
  4. 虚拟滚动:大数据量列表优化

安全性考虑

  1. API安全:Token验证和权限控制
  2. 输入验证:表单数据校验
  3. XSS防护:内容转义处理
  4. CORS配置:合理的跨域策略

结语

通过本文的详细介绍,我们构建了一个完整的Vue 3 + TypeScript企业级项目架构。这个架构具备良好的扩展性、可维护性和性能表现,能够满足大多数企业级应用的需求。在实际开发中,还需要根据具体业务场景进行调整和优化。

记住,好的架构不是一蹴而就的,需要在实践中不断迭代和完善。希望本文能为你的Vue 3项目开发提供有价值的参考和指导。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000