Vue3 + TypeScript + Pinia企业级项目架构设计:从0到1搭建完整开发框架

CleverSpirit
CleverSpirit 2026-02-28T06:02:06+08:00
0 0 3

引言

随着前端技术的快速发展,企业级应用开发对前端架构的要求越来越高。Vue3作为新一代的前端框架,结合TypeScript的类型安全和Pinia的状态管理,为构建大型企业级应用提供了强大的技术支持。本文将详细介绍如何从零开始搭建一个基于Vue3 + TypeScript + Pinia的企业级项目框架,涵盖项目结构设计、开发规范、最佳实践等核心内容。

Vue3 + TypeScript + Pinia技术栈概述

Vue3核心特性

Vue3作为Vue.js的下一个主要版本,带来了多项重要改进。其核心特性包括:

  • Composition API:提供更灵活的组件逻辑组织方式
  • 更好的性能:通过Tree-shaking减少包体积
  • 多根节点支持:组件可以返回多个根节点
  • 更好的TypeScript支持:原生支持TypeScript类型推导

TypeScript在企业级开发中的价值

TypeScript作为JavaScript的超集,为前端开发带来了显著优势:

  • 类型安全:在编译时发现类型错误
  • 代码提示:IDE提供更智能的代码补全
  • 重构安全:支持安全的代码重构
  • 团队协作:清晰的类型定义便于团队成员理解

Pinia状态管理的优势

Pinia是Vue官方推荐的状态管理库,相比Vuex 4具有以下优势:

  • 更轻量级:体积更小,性能更好
  • 更好的TypeScript支持:原生支持TypeScript
  • 模块化设计:易于组织和维护
  • 热重载支持:开发时支持热重载

项目初始化与基础配置

使用Vite创建项目

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

# 进入项目目录
cd my-enterprise-app

# 安装依赖
npm install

项目结构设计

my-enterprise-app/
├── public/
├── src/
│   ├── assets/                 # 静态资源
│   ├── components/             # 公共组件
│   ├── views/                  # 页面组件
│   ├── layouts/                # 布局组件
│   ├── router/                 # 路由配置
│   ├── store/                  # 状态管理
│   ├── services/               # API服务
│   ├── utils/                  # 工具函数
│   ├── types/                  # 类型定义
│   ├── hooks/                  # 自定义Hook
│   ├── plugins/                # 插件
│   ├── styles/                 # 样式文件
│   ├── App.vue                 # 根组件
│   └── main.ts                 # 入口文件
├── tests/                      # 测试文件
├── .env.*                      # 环境变量配置
├── vite.config.ts              # Vite配置
└── tsconfig.json               # TypeScript配置

TypeScript配置优化

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

Pinia状态管理架构设计

Store目录结构

src/
└── store/
    ├── index.ts            # Store初始化
    ├── modules/            # 模块化Store
    │   ├── user/
    │   │   ├── index.ts
    │   │   ├── types.ts
    │   │   └── actions.ts
    │   ├── app/
    │   │   ├── index.ts
    │   │   ├── types.ts
    │   │   └── actions.ts
    └── types/              # Store类型定义
        └── index.ts

用户模块Store实现

// src/store/modules/user/index.ts
import { defineStore } from 'pinia'
import { UserState, LoginCredentials, UserProfile } from './types'
import { login, getUserProfile, logout } from '@/services/user'

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    profile: null,
    token: localStorage.getItem('token') || '',
    isAuthenticated: false,
    loading: false,
    error: null
  }),

  getters: {
    hasPermission: (state) => (permission: string): boolean => {
      return state.profile?.permissions?.includes(permission) || false
    },
    isAdmin: (state) => state.profile?.role === 'admin'
  },

  actions: {
    async login(credentials: LoginCredentials) {
      this.loading = true
      this.error = null
      
      try {
        const response = await login(credentials)
        const { token, user } = response
        
        this.token = token
        this.profile = user
        this.isAuthenticated = true
        
        // 存储token到localStorage
        localStorage.setItem('token', token)
        
        return { success: true }
      } catch (error) {
        this.error = error as string
        return { success: false, error }
      } finally {
        this.loading = false
      }
    },

    async fetchProfile() {
      if (!this.token) {
        return
      }
      
      try {
        const profile = await getUserProfile()
        this.profile = profile
      } catch (error) {
        console.error('Failed to fetch user profile:', error)
      }
    },

    logout() {
      this.token = ''
      this.profile = null
      this.isAuthenticated = false
      localStorage.removeItem('token')
      // 重定向到登录页
      window.location.href = '/login'
    }
  }
})

Store类型定义

// src/store/modules/user/types.ts
export interface UserProfile {
  id: string
  username: string
  email: string
  role: string
  permissions: string[]
  avatar?: string
}

export interface UserState {
  profile: UserProfile | null
  token: string
  isAuthenticated: boolean
  loading: boolean
  error: string | null
}

export interface LoginCredentials {
  username: string
  password: string
}

Store初始化配置

// src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia
export * from './modules/user'

组件化架构设计

组件目录结构

src/
└── components/
    ├── common/         # 通用组件
    │   ├── Button/
    │   ├── Input/
    │   ├── Modal/
    │   └── Table/
    ├── layout/         # 布局组件
    │   ├── Header/
    │   ├── Sidebar/
    │   └── Footer/
    ├── business/       # 业务组件
    │   ├── UserCard/
    │   ├── OrderList/
    │   └── Dashboard/
    └── index.ts        # 组件导出

通用组件示例 - Button

<!-- src/components/common/Button/Button.vue -->
<template>
  <button
    :class="[
      'btn',
      `btn--${variant}`,
      `btn--${size}`,
      { 'btn--disabled': disabled, 'btn--loading': loading }
    ]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="btn__spinner"></span>
    <slot />
  </button>
</template>

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

interface Props {
  variant?: 'primary' | 'secondary' | 'danger' | 'outline'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
  loading?: boolean
}

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

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'medium',
  disabled: false,
  loading: false
})

const emit = defineEmits<Emits>()

const handleClick = (event: MouseEvent) => {
  emit('click', event)
}
</script>

<style scoped lang="scss">
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s ease;
  font-weight: 500;
  
  &--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;
    }
  }
  
  &--danger {
    background-color: #dc3545;
    color: white;
    
    &:hover:not(.btn--disabled) {
      background-color: #c82333;
    }
  }
  
  &--outline {
    background-color: transparent;
    border: 1px solid #007bff;
    color: #007bff;
    
    &:hover:not(.btn--disabled) {
      background-color: #007bff;
      color: white;
    }
  }
  
  &--small {
    padding: 4px 8px;
    font-size: 12px;
  }
  
  &--medium {
    padding: 8px 16px;
    font-size: 14px;
  }
  
  &--large {
    padding: 12px 24px;
    font-size: 16px;
  }
  
  &--disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
  
  &__spinner {
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 2px solid #f3f3f3;
    border-top: 2px solid #007bff;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin-right: 8px;
  }
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

组件导出配置

// src/components/index.ts
import Button from './common/Button/Button.vue'
import Input from './common/Input/Input.vue'
import Modal from './common/Modal/Modal.vue'
import Table from './common/Table/Table.vue'

export { Button, Input, Modal, Table }

// 也可以统一导出
export * from './common'
export * from './layout'
export * from './business'

路由配置与权限管理

路由目录结构

src/
└── router/
    ├── index.ts            # 路由配置入口
    ├── routes/             # 路由配置文件
    │   ├── index.ts        # 路由配置
    │   ├── auth.ts         # 认证相关路由
    │   └── dashboard.ts    # 仪表板路由
    └── middleware/         # 路由中间件
        └── auth.ts         # 认证中间件

路由配置实现

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

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

// 路由守卫
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  
  // 检查是否需要认证
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  } else if (to.meta.requiresAdmin && !userStore.isAdmin) {
    next('/403')
  } else {
    // 如果已认证但访问登录页,重定向到首页
    if (to.path === '/login' && userStore.isAuthenticated) {
      next('/')
    } else {
      next()
    }
  }
})

export default router

路由配置示例

// src/router/routes/index.ts
import { RouteRecordRaw } from 'vue-router'
import Layout from '@/layouts/default.vue'

export const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/dashboard'
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/auth/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Layout,
    meta: { requiresAuth: true },
    children: [
      {
        path: '',
        name: 'DashboardHome',
        component: () => import('@/views/dashboard/Home.vue')
      }
    ]
  },
  {
    path: '/users',
    name: 'Users',
    component: Layout,
    meta: { requiresAuth: true, requiresAdmin: true },
    children: [
      {
        path: '',
        name: 'UserList',
        component: () => import('@/views/users/List.vue')
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
]

API服务层设计

服务目录结构

src/
└── services/
    ├── index.ts            # 服务导出
    ├── api.ts              # API基础配置
    ├── auth.ts             # 认证服务
    ├── user.ts             # 用户服务
    └── http.ts             # HTTP客户端

HTTP客户端实现

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

class HttpClient {
  private client: AxiosInstance

  constructor() {
    this.client = 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.client.interceptors.request.use(
      (config) => {
        const userStore = useUserStore()
        const token = userStore.token
        
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )

    // 响应拦截器
    this.client.interceptors.response.use(
      (response: AxiosResponse) => {
        return response.data
      },
      (error) => {
        if (error.response?.status === 401) {
          // token过期,清除用户状态
          const userStore = useUserStore()
          userStore.logout()
        }
        
        return Promise.reject(error)
      }
    )
  }

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

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

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

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

export const http = new HttpClient()

用户服务实现

// src/services/user.ts
import { http } from './http'
import { UserProfile, LoginCredentials } from '@/store/modules/user/types'

export const login = async (credentials: LoginCredentials) => {
  return http.post<{ token: string; user: UserProfile }>('/auth/login', credentials)
}

export const getUserProfile = async (): Promise<UserProfile> => {
  return http.get<UserProfile>('/users/profile')
}

export const logout = async () => {
  return http.post('/auth/logout')
}

export const getUsers = async (params: { page: number; limit: number }) => {
  return http.get<{ users: UserProfile[]; total: number }>('/users', { params })
}

工具函数与Hook设计

工具函数目录结构

src/
└── utils/
    ├── index.ts            # 工具函数导出
    ├── helpers.ts          # 辅助函数
    ├── validators.ts       # 验证函数
    ├── format.ts           # 格式化函数
    └── storage.ts          # 存储工具

自定义Hook实现

// src/hooks/useAuth.ts
import { ref, computed } from 'vue'
import { useUserStore } from '@/store/modules/user'

export function useAuth() {
  const userStore = useUserStore()
  
  const isAuthenticated = computed(() => userStore.isAuthenticated)
  const currentUser = computed(() => userStore.profile)
  const isAdmin = computed(() => userStore.isAdmin)
  
  const login = async (credentials: LoginCredentials) => {
    return userStore.login(credentials)
  }
  
  const logout = () => {
    userStore.logout()
  }
  
  const fetchProfile = async () => {
    await userStore.fetchProfile()
  }
  
  return {
    isAuthenticated,
    currentUser,
    isAdmin,
    login,
    logout,
    fetchProfile
  }
}

// src/hooks/usePagination.ts
import { ref, computed, watch } from 'vue'

export function usePagination<T>(data: T[], pageSize: number = 10) {
  const currentPage = ref(1)
  const _pageSize = ref(pageSize)
  
  const paginatedData = computed(() => {
    const start = (currentPage.value - 1) * _pageSize.value
    const end = start + _pageSize.value
    return data.slice(start, end)
  })
  
  const total = computed(() => data.length)
  const totalPages = computed(() => Math.ceil(total.value / _pageSize.value))
  
  const goToPage = (page: number) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }
  
  const nextPage = () => {
    if (currentPage.value < totalPages.value) {
      currentPage.value++
    }
  }
  
  const prevPage = () => {
    if (currentPage.value > 1) {
      currentPage.value--
    }
  }
  
  // 监听数据变化,重置到第一页
  watch(data, () => {
    currentPage.value = 1
  })
  
  return {
    currentPage,
    totalPages,
    paginatedData,
    total,
    goToPage,
    nextPage,
    prevPage
  }
}

开发规范与最佳实践

代码风格规范

// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
    'plugin:vue/vue3-recommended'
  ],
  rules: {
    'vue/multi-word-component-names': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/explicit-function-return-type': 'off',
    'no-console': 'warn',
    'no-debugger': 'error'
  }
}

组件命名规范

<!-- 推荐的组件命名 -->
<template>
  <UserCard :user="user" @edit="handleEdit" />
</template>

<script setup lang="ts">
// 组件命名使用PascalCase
interface UserCardProps {
  user: UserProfile
  showActions?: boolean
}

defineProps<UserCardProps>()

const emit = defineEmits<{
  (e: 'edit', user: UserProfile): void
}>()
</script>

状态管理最佳实践

// src/store/modules/app/index.ts
import { defineStore } from 'pinia'
import { AppStatus, Theme } from './types'

export const useAppStore = defineStore('app', {
  state: (): AppStatus => ({
    theme: 'light',
    loading: false,
    error: null,
    language: 'zh-CN'
  }),

  getters: {
    isDarkTheme: (state) => state.theme === 'dark',
    isLoading: (state) => state.loading
  },

  actions: {
    setLoading(loading: boolean) {
      this.loading = loading
    },

    setError(error: string | null) {
      this.error = error
    },

    setTheme(theme: Theme) {
      this.theme = theme
      // 应用主题到DOM
      document.body.className = `theme-${theme}`
    },

    async initialize() {
      // 应用初始化逻辑
      const savedTheme = localStorage.getItem('app-theme') as Theme | null
      if (savedTheme) {
        this.setTheme(savedTheme)
      }
    }
  }
})

测试策略与质量保证

单元测试配置

// src/__tests__/user.store.spec.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useUserStore } from '@/store/modules/user'
import { mock } from 'vitest-mock-extended'

describe('User Store', () => {
  beforeEach(() => {
    // 重置store
    const store = useUserStore()
    store.$reset()
  })

  it('should login successfully', async () => {
    const store = useUserStore()
    
    // Mock API调用
    const mockLogin = vi.fn().mockResolvedValue({
      token: 'mock-token',
      user: { id: '1', username: 'test', email: 'test@example.com' }
    })
    
    const result = await store.login({ username: 'test', password: 'password' })
    
    expect(result.success).toBe(true)
    expect(store.isAuthenticated).toBe(true)
    expect(store.token).toBe('mock-token')
  })

  it('should handle login error', async () => {
    const store = useUserStore()
    
    const mockLogin = vi.fn().mockRejectedValue(new Error('Invalid credentials'))
    
    const result = await store.login({ username: 'test', password: 'wrong' })
    
    expect(result.success).toBe(false)
    expect(store.error).toBe('Invalid credentials')
  })
})

端到端测试

// src/e2e/login.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Login Page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login')
  })

  test('should login with valid credentials', async ({ page }) => {
    await page.fill('input[name="username"]', 'admin')
    await page.fill('input[name="password"]', 'password')
    await page.click('button[type="submit"]')
    
    // 验证登录成功
    await expect(page).toHaveURL('/dashboard')
    await expect(page.locator('text=Welcome')).toBeVisible()
  })

  test('should show error with invalid credentials', async ({ page }) => {
    await page.fill('input[name="username"]', 'invalid')
    await page.fill('input[name="password"]', 'wrong')
    await page.click('button[type="submit"]')
    
    // 验证错误提示
    await expect(page.locator('.error-message')).toBeVisible()
  })
})

性能优化策略

组件懒加载

// src/router/routes/dashboard.ts
export const dashboardRoutes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/dashboard/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/analytics',
    name: 'Analytics',
    component: () => import('@/views/analytics/Analytics.vue'),
    meta: { requiresAuth: true }
  }
]

代码分割

// src/utils/lazyLoad.ts
export function lazyLoad(component: () => Promise<any>) {
  return defineAsyncComponent({
    loader: component,
    loadingComponent: () => import('@/components/common/Loading.vue'),
    errorComponent: () => import('@/components/common/Error.vue'),
    delay: 200,
    timeout: 3000
  })
}

部署与环境配置

环境变量配置

# .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_NAME=Enterprise App
VITE_APP_VERSION=1.0.0

# .env.production
VITE_API_BASE_URL=https://api.yourapp.com
VITE_APP_NAME=Enterprise App
VITE_APP_VERSION=1.0.0

构建配置

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

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, './src')
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['@element-plus', 'element-plus'],
          utils: ['axios', 'lodash']
        }
      }
    }
  }
})

总结

本文详细介绍了如何基于Vue3 + TypeScript + Pinia构建企业级前端应用的完整框架。通过合理的项目结构设计、类型安全的TypeScript实现、模块化的Pinia状态管理、组件化开发模式以及完善的测试策略,我们构建了一个既高效又可维护的开发框架。

关键要点包括:

  1. 项目架构:采用模块化设计,清晰的目录结构便于团队协作
  2. 类型安全:充分利用TypeScript的类型系统,提升代码质量和开发体验
  3. 状态管理:使用Pinia实现灵活的状态管理,支持持久化和热重载
  4. 组件化:建立完整的组件库,提高代码复用率
  5. 开发规范:制定统一的编码规范和最佳实践
  6. **
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000