Vue 3 + TypeScript 企业级项目架构实战:从0到1打造可维护的前端工程体系

在2026年的今天,Vue 3 + TypeScript 已成为企业级前端开发的黄金组合。根据最新技术调研,超过 75% 的中大型前端项目已采用这一技术栈。然而,很多开发者在使用时仍停留在"能跑就行"的阶段,缺乏系统性的架构设计思维。

本文将从工程规范类型安全架构设计性能优化四个维度,分享一套经过多个真实项目验证的最佳实践方案。


一、项目初始化与工程配置

1.1 使用 Vite 创建项目

复制代码
# 推荐使用官方脚手架
npm create vue@latest my-enterprise-app

# 项目配置选项
✔ Project name: ... my-enterprise-app
✔ Add TypeScript? ... Yes
✔ Add JSX Support? ... No
✔ Add Vue Router? ... Yes
✔ Add Pinia? ... Yes
✔ Add Vitest? ... Yes
✔ Add ESLint? ... Yes
✔ Add Prettier? ... Yes

1.2 核心依赖安装

复制代码
# UI组件库(按需选择)
npm install element-plus @element-plus/icons-vue

# HTTP请求
npm install axios

# 工具库
npm install lodash-es dayjs

# 类型定义
npm install -D @types/lodash-es

1.3 TypeScript 配置优化

复制代码
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,           // 开启严格模式
    "noUnusedLocals": true,   // 检查未使用变量
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "paths": {                // 路径别名
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@views/*": ["./src/views/*"],
      "@api/*": ["./src/api/*"],
      "@stores/*": ["./src/stores/*"],
      "@utils/*": ["./src/utils/*"],
      "@types/*": ["./src/types/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "exclude": ["node_modules", "dist"]
}

二、企业级项目目录结构

复制代码
src/
├── api/                    # API接口层
│   ├── modules/           # 按业务模块划分
│   │   ├── user.ts
│   │   ├── order.ts
│   │   └── product.ts
│   ├── index.ts           # 统一导出
│   └── types.ts           # API类型定义
├── assets/                # 静态资源
│   ├── images/
│   ├── styles/
│   │   ├── variables.scss # 全局变量
│   │   ├── mixins.scss    # SCSS混入
│   │   └── global.scss    # 全局样式
│   └── icons/             # SVG图标
├── components/            # 公共组件
│   ├── base/              # 基础组件
│   │   ├── BaseButton.vue
│   │   ├── BaseTable.vue
│   │   └── BaseModal.vue
│   ├── business/          # 业务组件
│   └── layout/            # 布局组件
├── composables/           # 组合式函数
│   ├── useLoading.ts
│   ├── usePagination.ts
│   └── usePermission.ts
├── config/                # 配置文件
│   ├── env.ts             # 环境变量
│   └── constants.ts       # 常量定义
├── directives/            # 自定义指令
│   ├── permission.ts
│   └── loading.ts
├── hooks/                 # 业务Hooks
├── layouts/               # 布局文件
│   ├── DefaultLayout.vue
│   └── AdminLayout.vue
├── router/                # 路由配置
│   ├── index.ts
│   ├── routes.ts          # 路由表
│   └── guards.ts          # 路由守卫
├── stores/                # Pinia状态管理
│   ├── modules/
│   │   ├── user.ts
│   │   └── app.ts
│   └── index.ts
├── types/                 # TypeScript类型定义
│   ├── api.d.ts
│   ├── common.d.ts
│   └── vue-shim.d.ts
├── utils/                 # 工具函数
│   ├── request.ts         # Axios封装
│   ├── storage.ts         # 本地存储
│   └── validate.ts        # 验证工具
├── views/                 # 页面组件
│   ├── login/
│   ├── dashboard/
│   └── system/
├── App.vue
└── main.ts

三、核心模块最佳实践

3.1 Axios 封装与类型安全

复制代码
// src/utils/request.ts
import axios, { 
  type AxiosInstance, 
  type AxiosRequestConfig,
  type AxiosResponse 
} from 'axios'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'

// 响应数据类型
export interface ResponseData<T = any> {
  code: number
  data: T
  message: string
}

class Request {
  private instance: AxiosInstance

  constructor(baseConfig: AxiosRequestConfig) {
    this.instance = axios.create(baseConfig)
    this.setupInterceptors()
  }

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

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse<ResponseData>) => {
        const { code, data, message } = response.data
        
        if (code === 200) {
          return data
        }
        
        ElMessage.error(message || '请求失败')
        return Promise.reject(new Error(message))
      },
      (error) => {
        ElMessage.error(error.message || '网络错误')
        return Promise.reject(error)
      }
    )
  }

  public request<T>(config: AxiosRequestConfig): Promise<T> {
    return this.instance.request(config)
  }

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

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

export const request = new Request({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

3.2 API 模块化设计

复制代码
// src/api/modules/user.ts
import { request } from '@/utils/request'
import type { UserInfo, LoginParams, LoginResult } from '@/types/api'

export const userApi = {
  // 用户登录
  login: (params: LoginParams) => 
    request.post<LoginResult>('/auth/login', params),
  
  // 获取用户信息
  getUserInfo: () => 
    request.get<UserInfo>('/user/info'),
  
  // 更新用户信息
  updateUserInfo: (data: Partial<UserInfo>) => 
    request.put<UserInfo>('/user/info', data),
  
  // 退出登录
  logout: () => 
    request.post('/auth/logout')
}

3.3 Pinia 状态管理

复制代码
// src/stores/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { userApi } from '@/api/modules/user'
import type { UserInfo } from '@/types/api'

export const useUserStore = defineStore('user', () => {
  // State
  const token = ref<string>(localStorage.getItem('token') || '')
  const userInfo = ref<UserInfo | null>(null)

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => userInfo.value?.name || '访客')
  const userRoles = computed(() => userInfo.value?.roles || [])

  // Actions
  async function login(params: { username: string; password: string }) {
    const result = await userApi.login(params)
    token.value = result.token
    localStorage.setItem('token', result.token)
    await fetchUserInfo()
  }

  async function fetchUserInfo() {
    userInfo.value = await userApi.getUserInfo()
  }

  async function logout() {
    await userApi.logout()
    token.value = ''
    userInfo.value = null
    localStorage.removeItem('token')
  }

  return {
    token,
    userInfo,
    isLoggedIn,
    userName,
    userRoles,
    login,
    fetchUserInfo,
    logout
  }
})

3.4 组合式函数(Composables)

复制代码
// src/composables/usePagination.ts
import { ref, reactive } from 'vue'

export interface PaginationConfig {
  page: number
  pageSize: number
  total: number
}

export function usePagination(initialConfig: Partial<PaginationConfig> = {}) {
  const pagination = reactive<PaginationConfig>({
    page: 1,
    pageSize: 10,
    total: 0,
    ...initialConfig
  })

  const currentPage = computed({
    get: () => pagination.page,
    set: (val) => { pagination.page = val }
  })

  function reset() {
    pagination.page = 1
    pagination.total = 0
  }

  function setTotal(total: number) {
    pagination.total = total
  }

  return {
    pagination,
    currentPage,
    reset,
    setTotal
  }
}

// src/composables/useLoading.ts
import { ref } from 'vue'

export function useLoading(initialValue = false) {
  const loading = ref(initialValue)

  const startLoading = () => { loading.value = true }
  const stopLoading = () => { loading.value = false }
  const withLoading = async <T>(fn: () => Promise<T>): Promise<T> => {
    startLoading()
    try {
      return await fn()
    } finally {
      stopLoading()
    }
  }

  return {
    loading,
    startLoading,
    stopLoading,
    withLoading
  }
}

四、组件开发规范

4.1 使用 <script setup> 语法糖

复制代码
<!-- src/components/base/BaseTable.vue -->
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import type { PropType } from 'vue'

// 类型定义
export interface TableColumn {
  prop: string
  label: string
  width?: number
  sortable?: boolean
}

export interface TableData {
  [key: string]: any
}

// Props 定义(推荐方式)
const props = defineProps<{
  columns: TableColumn[]
  data: TableData[]
  loading?: boolean
  pagination?: {
    page: number
    pageSize: number
    total: number
  }
}>()

// Emits 定义
const emit = defineEmits<{
  (e: 'page-change', page: number): void
  (e: 'size-change', size: number): void
  (e: 'sort-change', { prop, order }: { prop: string; order: string }): void
}>()

// 计算属性
const tableHeight = computed(() => {
  return props.data.length > 10 ? '500px' : 'auto'
})

// 事件处理
const handlePageChange = (page: number) => {
  emit('page-change', page)
}
</script>

<template>
  <el-table
    :data="data"
    :loading="loading"
    :height="tableHeight"
    v-bind="$attrs"
  >
    <el-table-column
      v-for="col in columns"
      :key="col.prop"
      :prop="col.prop"
      :label="col.label"
      :width="col.width"
      :sortable="col.sortable"
    />
  </el-table>
  
  <el-pagination
    v-if="pagination"
    :current-page="pagination.page"
    :page-size="pagination.pageSize"
    :total="pagination.total"
    @current-change="handlePageChange"
  />
</template>

4.2 组件类型导出

复制代码
// src/components/base/index.ts
export { default as BaseButton } from './BaseButton.vue'
export { default as BaseTable } from './BaseTable.vue'
export { default as BaseModal } from './BaseModal.vue'

// 导出类型
export type { TableColumn, TableData } from './BaseTable.vue'

五、路由架构设计

复制代码
// src/router/routes.ts
import type { RouteRecordRaw } from 'vue-router'

export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login/index.vue'),
    meta: { title: '登录', hidden: true }
  },
  {
    path: '/404',
    name: '404',
    component: () => import('@/views/error/404.vue'),
    meta: { title: '404', hidden: true }
  }
]

export const asyncRoutes: RouteRecordRaw[] = [
  {
    path: '/',
    component: () => import('@/layouts/DefaultLayout.vue'),
    children: [
      {
        path: '',
        name: 'Dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        meta: { title: '首页', icon: 'dashboard' }
      },
      {
        path: 'system',
        name: 'System',
        meta: { title: '系统管理', icon: 'setting' },
        children: [
          {
            path: 'user',
            name: 'UserManage',
            component: () => import('@/views/system/user/index.vue'),
            meta: { title: '用户管理', permission: ['user:list'] }
          }
        ]
      }
    ]
  }
]

// src/router/guards.ts
import type { Router } from 'vue-router'
import { useUserStore } from '@/stores/user'

export function setupRouterGuards(router: Router) {
  router.beforeEach(async (to, from, next) => {
    const userStore = useUserStore()
    
    // 设置页面标题
    document.title = to.meta.title 
      ? `${to.meta.title} - 管理系统` 
      : '管理系统'

    // 登录验证
    if (to.path !== '/login' && !userStore.isLoggedIn) {
      next(`/login?redirect=${to.path}`)
      return
    }

    // 权限验证
    if (to.meta.permission) {
      const hasPermission = to.meta.permission.some((p: string) =>
        userStore.userRoles.includes(p)
      )
      if (!hasPermission) {
        next('/403')
        return
      }
    }

    next()
  })
}

六、性能优化策略

6.1 组件懒加载

复制代码
// 路由懒加载(默认支持)
const UserManage = () => import('@/views/system/user/index.vue')

// 组件异步加载
const HeavyComponent = defineAsyncComponent(() => 
  import('@/components/business/HeavyComponent.vue')
)

6.2 列表虚拟滚动

复制代码
<template>
  <el-table-v2
    :columns="columns"
    :data="largeData"
    :width="800"
    :height="600"
  />
</template>

6.3 计算属性缓存

复制代码
// 利用 computed 的缓存特性
const filteredList = computed(() => {
  return props.list.filter(item => item.status === 'active')
})

七、代码规范与质量保障

7.1 ESLint + Prettier 配置

复制代码
// .eslintrc.cjs
module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  rules: {
    'vue/multi-word-component-names': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/explicit-function-return-type': 'warn'
  }
}

7.2 Git Hooks 配置

复制代码
// package.json
{
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix",
    "format": "prettier --write ."
  },
  "devDependencies": {
    "husky": "^8.0.0",
    "lint-staged": "^15.0.0"
  },
  "lint-staged": {
    "*.{vue,js,ts,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

八、总结与建议

📋 核心要点回顾

维度 最佳实践
构建工具 Vite 5.x(极速热更新)
状态管理 Pinia(完整TS支持)
代码风格 <script setup> + Composition API
类型安全 严格模式 + 完整类型定义
代码规范 ESLint + Prettier + Husky
API设计 模块化 + 统一响应类型

🚀 进阶建议

  1. 渐进式迁移:老项目可逐步引入 TypeScript,从工具函数开始
  2. 类型优先:先定义接口类型,再实现业务逻辑
  3. 组件文档:使用 Storybook 或 VitePress 建立组件文档
  4. 单元测试:核心业务逻辑编写 Vitest 测试用例
  5. 性能监控:接入前端监控平台(如 Sentry)
相关推荐
CappuccinoRose2 小时前
CSS 语法学习文档(十五)
前端·学习·重构·渲染·浏览器
Marshall1512 小时前
DC-SDK 实战指南:基于 Cesium 的三维数字孪生大屏开发 前言 在当今数字孪生、智慧城市等领域的开发中,三维地图可视化已经成为核心需求。
前端
少云清2 小时前
【UI自动化测试】5_web自动化测试 _元素操作和元素信息获取
前端·web自动化测试
lyyl啊辉3 小时前
2. Vue数据双向绑定
前端·vue.js
CappuccinoRose4 小时前
CSS 语法学习文档(十七)
前端·css·学习·布局·houdini·瀑布流布局·csspaintingapi
keyborad pianist4 小时前
Web开发 Day1
开发语言·前端·css·vue.js·前端框架
Never_Satisfied4 小时前
在HTML & CSS中,可能导致父元素无法根据子元素的尺寸自动调整大小的情况
前端·css·html
We་ct5 小时前
LeetCode 101. 对称二叉树:两种解法(递归+迭代)详解
前端·算法·leetcode·链表·typescript
lyyl啊辉5 小时前
6. Vue开源三方UI组件库
vue.js