Vue3 + TypeScript + Vite企业级项目架构设计:从零搭建现代化前端工程化体系

OldTears
OldTears 2026-02-27T01:08:10+08:00
0 0 0

前言

随着前端技术的快速发展,企业级前端应用对工程化、可维护性和开发效率的要求越来越高。Vue3的Composition API、TypeScript的类型安全以及Vite的现代化构建工具,为构建高质量的前端应用提供了强大的技术支撑。本文将详细介绍如何从零开始搭建一个基于Vue3、TypeScript和Vite的企业级前端项目架构,涵盖项目初始化、组件设计、状态管理、构建优化等完整流程。

项目初始化与基础配置

1.1 使用Vite创建项目

首先,我们使用Vite来创建Vue3项目。Vite作为新一代前端构建工具,具有快速的冷启动和热更新特性。

# 使用npm创建项目
npm create vite@latest my-vue3-app --template vue-ts

# 或使用yarn
yarn create vite my-vue3-app --template vue-ts

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

创建完成后,项目会包含以下基本结构:

my-vue3-app/
├── public/
│   └── favicon.ico
├── src/
│   ├── assets/
│   ├── components/
│   ├── views/
│   ├── router/
│   ├── store/
│   ├── utils/
│   ├── styles/
│   ├── App.vue
│   └── main.ts
├── vite.config.ts
├── tsconfig.json
├── package.json
└── README.md

1.2 TypeScript配置优化

tsconfig.json中,我们需要进行详细的配置以确保TypeScript的最佳实践:

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

1.3 Vite配置优化

vite.config.ts中配置项目:

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

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  server: {
    port: 3000,
    host: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
          utils: ['axios', 'lodash']
        }
      }
    }
  }
})

组件设计与架构模式

2.1 组件结构设计

在企业级项目中,我们采用组件化架构设计,将组件分为以下几类:

// src/components/base/BaseButton.vue
<template>
  <button 
    :class="['base-button', `base-button--${type}`, { 'is-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">
.base-button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
  
  &--primary {
    background-color: #409eff;
    color: white;
    
    &:hover {
      background-color: #66b2ff;
    }
  }
  
  &--secondary {
    background-color: #f5f5f5;
    color: #333;
    
    &:hover {
      background-color: #e0e0e0;
    }
  }
  
  &.is-disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
}
</style>

2.2 组件通信模式

采用Props、Emits和Provide/Inject的组合模式:

// src/components/advanced/DataTable.vue
<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <th v-for="column in columns" :key="column.key">
            {{ column.title }}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in data" :key="row.id">
          <td v-for="column in columns" :key="column.key">
            <component 
              :is="column.component" 
              v-bind="column.props"
              :value="row[column.key]"
              @update="handleUpdate"
            />
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

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

interface Column {
  key: string
  title: string
  component?: string
  props?: Record<string, any>
}

interface Row {
  id: string | number
  [key: string]: any
}

interface Props {
  columns: Column[]
  data: Row[]
}

interface Emits {
  (e: 'update', key: string, value: any, row: Row): void
}

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

const handleUpdate = (key: string, value: any, row: Row) => {
  emit('update', key, value, row)
}
</script>

状态管理设计

3.1 Pinia状态管理

使用Pinia作为状态管理工具,相比Vuex更加轻量且TypeScript支持更好:

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

export const useUserStore = defineStore('user', () => {
  const userInfo = ref<User | null>(null)
  const isLoggedIn = computed(() => !!userInfo.value)
  
  const login = (user: User) => {
    userInfo.value = user
    localStorage.setItem('userInfo', JSON.stringify(user))
  }
  
  const logout = () => {
    userInfo.value = null
    localStorage.removeItem('userInfo')
  }
  
  const initUser = () => {
    const storedUser = localStorage.getItem('userInfo')
    if (storedUser) {
      userInfo.value = JSON.parse(storedUser)
    }
  }
  
  return {
    userInfo,
    isLoggedIn,
    login,
    logout,
    initUser
  }
})

3.2 复杂状态管理示例

// src/store/complex.ts
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
import { api } from '@/utils/request'

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

interface FilterState {
  category: string
  minPrice: number
  maxPrice: number
}

export const useProductStore = defineStore('product', () => {
  const products = ref<Product[]>([])
  const loading = ref(false)
  const filter = ref<FilterState>({
    category: '',
    minPrice: 0,
    maxPrice: 10000
  })
  
  const filteredProducts = computed(() => {
    return products.value.filter(product => {
      const categoryMatch = !filter.value.category || 
        product.category === filter.value.category
      const priceMatch = product.price >= filter.value.minPrice && 
        product.price <= filter.value.maxPrice
      return categoryMatch && priceMatch
    })
  })
  
  const fetchProducts = async () => {
    loading.value = true
    try {
      const response = await api.get<Product[]>('/products')
      products.value = response.data
    } catch (error) {
      console.error('Failed to fetch products:', error)
    } finally {
      loading.value = false
    }
  }
  
  const updateFilter = (newFilter: Partial<FilterState>) => {
    filter.value = { ...filter.value, ...newFilter }
  }
  
  // 监听过滤条件变化
  watch(filter, () => {
    console.log('Filter changed:', filter.value)
  }, { deep: true })
  
  return {
    products,
    filteredProducts,
    loading,
    filter,
    fetchProducts,
    updateFilter
  }
})

路由设计与权限控制

4.1 路由配置

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

const routes: Array<RouteRecordRaw> = [
  {
    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 }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true, roles: ['admin'] }
  }
]

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

// 路由守卫
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  
  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    next('/login')
  } else if (to.meta.roles && !to.meta.roles.includes(userStore.userInfo?.role || '')) {
    next('/403')
  } else {
    next()
  }
})

export default router

4.2 权限控制组件

// src/components/auth/PermissionWrapper.vue
<template>
  <div v-if="hasPermission" class="permission-wrapper">
    <slot />
  </div>
  <div v-else class="permission-denied">
    <slot name="denied" />
  </div>
</template>

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

interface Props {
  permission?: string
  role?: string
}

const props = defineProps<Props>()
const userStore = useUserStore()

const hasPermission = computed(() => {
  if (!props.permission && !props.role) {
    return true
  }
  
  if (props.role && userStore.userInfo?.role) {
    return userStore.userInfo.role === props.role
  }
  
  return true
})
</script>

<style scoped>
.permission-wrapper {
  display: block;
}

.permission-denied {
  padding: 16px;
  background-color: #f5f5f5;
  border-radius: 4px;
  color: #999;
  text-align: center;
}
</style>

API封装与请求处理

5.1 Axios封装

// src/utils/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus'

// 创建axios实例
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

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

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const res = response.data
    
    if (res.code !== 200) {
      ElMessage({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      
      if (res.code === 401) {
        const userStore = useUserStore()
        userStore.logout()
        location.reload()
      }
      
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  (error) => {
    console.error('Response error:', error)
    ElMessage({
      message: error.message || 'Network Error',
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export { service as api }

5.2 API服务层设计

// src/api/user.ts
import { api } from '@/utils/request'
import { User } from '@/types/user'

export interface LoginParams {
  username: string
  password: string
}

export interface LoginResponse {
  token: string
  user: User
}

export const userApi = {
  login(params: LoginParams) {
    return api.post<LoginResponse>('/auth/login', params)
  },
  
  getUserInfo() {
    return api.get<User>('/user/info')
  },
  
  updateUserInfo(data: Partial<User>) {
    return api.put<User>('/user/info', data)
  },
  
  getUsers(params?: { page?: number; limit?: number }) {
    return api.get<User[]>('/users', { params })
  }
}

工程化优化与构建配置

6.1 构建优化配置

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

export default defineConfig({
  plugins: [
    vue(),
    visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
      brotliSize: true
    })
  ],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
          utils: ['axios', 'lodash', 'dayjs']
        }
      }
    },
    assetsInlineLimit: 4096,
    chunkSizeWarningLimit: 1000,
    sourcemap: false
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
})

6.2 环境变量管理

// .env
VITE_API_BASE_URL=http://localhost:8080
VITE_APP_TITLE=My Vue App
VITE_APP_VERSION=1.0.0

// .env.production
VITE_API_BASE_URL=https://api.myapp.com
VITE_APP_TITLE=My Production App

6.3 性能监控

// src/utils/performance.ts
export class PerformanceMonitor {
  private startTime: number = 0
  private endTime: number = 0
  
  start() {
    this.startTime = performance.now()
  }
  
  end() {
    this.endTime = performance.now()
    const duration = this.endTime - this.startTime
    console.log(`Performance: ${duration.toFixed(2)}ms`)
    return duration
  }
  
  static measure<T>(fn: () => T, name: string): T {
    const monitor = new PerformanceMonitor()
    monitor.start()
    const result = fn()
    monitor.end()
    console.log(`${name} took ${monitor.endTime - monitor.startTime}ms`)
    return result
  }
}

// 使用示例
// PerformanceMonitor.measure(() => {
//   // 你的代码逻辑
// }, 'Component Render')

TypeScript类型系统最佳实践

7.1 类型定义规范

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

export interface Pagination {
  page: number
  limit: number
  total: number
  pages: number
}

export interface ApiResponse<T> {
  code: number
  message: string
  data: T
  pagination?: Pagination
}

// src/types/common.ts
export type Nullable<T> = T | null | undefined

export type Partial<T> = {
  [P in keyof T]?: T[P]
}

export type Required<T> = {
  [P in keyof T]-?: T[P]
}

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

export type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

7.2 组件类型定义

// src/components/advanced/SmartForm.vue
import { defineComponent, ref, watch } from 'vue'

interface FormField {
  name: string
  label: string
  type: 'text' | 'number' | 'select' | 'date'
  required?: boolean
  options?: Array<{ label: string; value: any }>
  rules?: Array<(value: any) => boolean | string>
}

interface FormData {
  [key: string]: any
}

export default defineComponent({
  name: 'SmartForm',
  props: {
    fields: {
      type: Array as () => FormField[],
      required: true
    },
    modelValue: {
      type: Object as () => FormData,
      default: () => ({})
    }
  },
  emits: ['update:modelValue', 'submit'],
  setup(props, { emit }) {
    const formData = ref<FormData>({ ...props.modelValue })
    
    watch(
      () => props.modelValue,
      (newVal) => {
        formData.value = { ...newVal }
      },
      { deep: true }
    )
    
    const handleChange = (field: string, value: any) => {
      formData.value[field] = value
      emit('update:modelValue', formData.value)
    }
    
    const handleSubmit = () => {
      emit('submit', formData.value)
    }
    
    return {
      formData,
      handleChange,
      handleSubmit
    }
  }
})

测试策略与质量保证

8.1 单元测试配置

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

describe('BaseButton', () => {
  it('renders correctly with default props', () => {
    const wrapper = mount(BaseButton)
    expect(wrapper.classes()).toContain('base-button')
    expect(wrapper.classes()).toContain('base-button--primary')
  })
  
  it('renders correctly with custom props', () => {
    const wrapper = mount(BaseButton, {
      props: {
        type: 'secondary',
        disabled: true
      }
    })
    
    expect(wrapper.classes()).toContain('base-button--secondary')
    expect(wrapper.classes()).toContain('is-disabled')
  })
  
  it('emits click event', async () => {
    const wrapper = mount(BaseButton)
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toHaveLength(1)
  })
})

8.2 端到端测试

// src/__tests__/e2e/login.cy.ts
describe('Login Page', () => {
  beforeEach(() => {
    cy.visit('/login')
  })
  
  it('should display login form', () => {
    cy.get('[data-testid="login-form"]').should('exist')
    cy.get('[data-testid="username-input"]').should('exist')
    cy.get('[data-testid="password-input"]').should('exist')
    cy.get('[data-testid="login-button"]').should('exist')
  })
  
  it('should login successfully', () => {
    cy.get('[data-testid="username-input"]').type('testuser')
    cy.get('[data-testid="password-input"]').type('password123')
    cy.get('[data-testid="login-button"]').click()
    
    cy.url().should('include', '/dashboard')
    cy.get('[data-testid="welcome-message"]').should('contain', 'Welcome')
  })
})

部署与持续集成

9.1 Docker部署配置

# Dockerfile
FROM node:16-alpine

WORKDIR /app

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

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "serve"]

9.2 CI/CD配置

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm run test
    
    - name: Run lint
      run: npm run lint
    
    - name: Build
      run: npm run build
    
    - name: Upload coverage
      uses: codecov/codecov-action@v2

总结

通过本文的详细介绍,我们构建了一个完整的基于Vue3、TypeScript和Vite的企业级前端项目架构。该架构具备以下特点:

  1. 现代化技术栈:充分利用Vue3的Composition API、TypeScript的类型安全和Vite的构建优势
  2. 可维护性:采用组件化、模块化的架构设计,便于代码维护和扩展
  3. 工程化实践:包含完整的构建优化、测试策略和部署流程
  4. 企业级特性:支持权限控制、状态管理、API封装等企业级应用需求

这个架构设计不仅满足了当前项目的需求,也为未来的功能扩展和团队协作提供了良好的基础。通过合理的架构设计和最佳实践,我们可以构建出高性能、高可用、易维护的企业级前端应用。

在实际项目中,还需要根据具体业务需求进行调整和优化,但这个基础架构为项目的成功奠定了坚实的基础。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000