Vue3 + TypeScript 项目架构设计:从 0 搭建企业级前端工程

一、为什么要关注架构?

经历过多个项目的迭代后,我深刻体会到:好的架构能让需求开发效率提升 3 倍,而糟糕的架构会让简单的需求变得异常复杂。

本文将分享一套经过生产环境验证的 Vue3 + TS 项目架构,适合中大型团队协作。

二、项目目录结构

复制代码
src/
├── api/                 # 接口管理
│   ├── modules/         # 按业务模块划分
│   ├── interceptors.ts  # 请求/响应拦截器
│   └── request.ts       # axios 封装
├── components/          # 公共组件
│   ├── business/        # 业务组件
│   └── common/          # 通用组件(Button、Modal等)
├── composables/         # 组合式函数(复用逻辑)
├── directives/          # 自定义指令
├── hooks/               # 与 Vue 无关的工具函数
├── layouts/             # 布局组件
├── router/              # 路由配置
│   ├── index.ts
│   └── routes.ts        # 路由表
├── stores/              # Pinia 状态管理
│   ├── modules/
│   └── index.ts
├── styles/              # 全局样式
│   ├── variables.scss   # SCSS 变量
│   └── mixins.scss      # 混入
├── types/               # 全局类型定义
├── utils/               # 工具函数
├── views/               # 页面视图
└── App.vue

三、核心模块设计

3.1 API 层封装:类型安全的请求

复制代码
// api/request.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

interface ResponseData<T = unknown> {
  code: number;
  data: T;
  message: string;
}

class HttpClient {
  private instance: AxiosInstance;

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

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

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response) => {
        const { data } = response;
        if (data.code !== 200) {
          // 统一错误处理
          handleError(data);
          return Promise.reject(data);
        }
        return data.data;
      },
      (error) => {
        handleNetworkError(error);
        return Promise.reject(error);
      }
    );
  }

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

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

export const http = new HttpClient({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000
});

3.2 类型安全的 API 定义

复制代码
// api/modules/user.ts
import { http } from '../request';

export interface UserInfo {
  id: number;
  username: string;
  avatar: string;
  email: string;
}

export interface LoginParams {
  username: string;
  password: string;
}

export const userApi = {
  login: (params: LoginParams) => 
    http.post<string>('/auth/login', params),
    
  getUserInfo: () => 
    http.get<UserInfo>('/user/info'),
    
  updateProfile: (data: Partial<UserInfo>) => 
    http.post<void>('/user/update', data)
};

3.3 组合式函数:逻辑复用

复制代码
// composables/useTable.ts
import { ref, computed } from 'vue';

interface UseTableOptions<T, Q> {
  fetchFn: (params: Q) => Promise<{ list: T[]; total: number }>;
  initialQuery?: Q;
}

export function useTable<T, Q extends Record<string, unknown>>(options: UseTableOptions<T, Q>) {
  const { fetchFn, initialQuery = {} as Q } = options;
  
  const loading = ref(false);
  const data = ref<T[]>([]);
  const total = ref(0);
  const query = ref<Q>({ ...initialQuery, page: 1, pageSize: 10 });
  
  const pagination = computed(() => ({
    current: query.value.page as number,
    pageSize: query.value.pageSize as number,
    total: total.value
  }));

  const fetchData = async () => {
    loading.value = true;
    try {
      const res = await fetchFn(query.value);
      data.value = res.list;
      total.value = res.total;
    } finally {
      loading.value = false;
    }
  };

  const onPageChange = (page: number) => {
    query.value.page = page;
    fetchData();
  };

  // 初始化加载
  fetchData();

  return {
    loading,
    data,
    pagination,
    query,
    fetchData,
    onPageChange
  };
}

// 使用示例
const { loading, data, pagination, onPageChange } = useTable({
  fetchFn: userApi.getList,
  initialQuery: { keyword: '' }
});

3.4 Pinia Store 最佳实践

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

export const useUserStore = defineStore('user', () => {
  // State
  const userInfo = ref<UserInfo | null>(null);
  const token = ref(localStorage.getItem('token') || '');
  
  // Getters
  const isLoggedIn = computed(() => !!token.value);
  const username = computed(() => userInfo.value?.username || '');
  
  // Actions
  const login = async (loginForm: { username: string; password: string }) => {
    const res = await userApi.login(loginForm);
    token.value = res;
    localStorage.setItem('token', res);
    await fetchUserInfo();
  };

  const fetchUserInfo = async () => {
    userInfo.value = await userApi.getUserInfo();
  };

  const logout = () => {
    token.value = '';
    userInfo.value = null;
    localStorage.removeItem('token');
  };

  return {
    userInfo,
    token,
    isLoggedIn,
    username,
    login,
    logout,
    fetchUserInfo
  };
});

四、代码规范配置

4.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',
    'plugin:prettier/recommended'
  ],
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    sourceType: 'module'
  },
  rules: {
    'vue/multi-word-component-names': 'off',
    '@typescript-eslint/no-explicit-any': 'warn'
  }
};

4.2 Git 提交规范

使用 husky + lint-staged + commitlint

复制代码
// commitlint.config.cjs
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']]
  }
};

提交格式:

复制代码
feat: 添加用户登录功能
fix: 修复表格分页 BUG
docs: 更新 README

五、工程化工具链

工具 用途
Vite 构建工具(比 Webpack 快 10 倍)
Vitest 单元测试
Cypress E2E 测试
Storybook 组件文档
Changesets 版本管理

六、总结

一个好的前端架构应该具备:

  1. 类型安全 - TypeScript 全覆盖
  2. 逻辑复用 - Composables 抽离公共逻辑
  3. 模块清晰 - 按功能分层,职责单一
  4. 规范约束 - ESLint + 提交规范
  5. 性能优先 - 按需加载、代码分割
相关推荐
happymaker06263 小时前
vue的声明周期、钩子函数、工程化开发
前端·javascript·vue.js
小跘an吻纸3 小时前
Vue 3 Composition API实战
typescript·前端框架·vue·响应式
Irene19916 小时前
ElementPlus 与成熟后台框架对比:vue-element-plus-admin、vue-pure-admin等
前端·ui·框架·vue3
尘中客10 小时前
放弃 Echarts?前端直接渲染后端高精度 SVG 矢量图流的踩坑记录
前端·javascript·echarts·前端开发·svg矢量图·echarts避坑
FreeBuf_10 小时前
Chrome 0Day漏洞遭野外利用
前端·chrome
小彭努力中10 小时前
199.Vue3 + OpenLayers 实现:点击 / 拖动地图播放音频
前端·vue.js·音视频·openlayers·animate
2501_9160074710 小时前
网站爬虫原理,基于浏览器点击行为还原可接口请求
前端·javascript·爬虫·ios·小程序·uni-app·iphone
前端大波11 小时前
Sentry 每日错误巡检自动化:设计思路与上手实战
前端·自动化·sentry
Highcharts.js12 小时前
适合报表系统的可视化图表|Highcharts支持直接导出PNG和PDF
javascript·数据库·react.js·pdf