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. 性能优先 - 按需加载、代码分割
相关推荐
夜焱辰6 小时前
浏览器端 Agent 的文件版本管理:不用 Git,基于 OPFS + SQLite 自己造了一个
前端·人工智能
梦想的颜色6 小时前
TypeScript 完全指南(下):从类型体操到生产级配置
前端·javascript·typescript
Hi~晴天大圣8 小时前
npm使用介绍
前端·npm·node.js
888CC++9 小时前
如何在 C 语言中进行程序调试?
前端·javascript·算法
喵个咪9 小时前
基于 Taro 的 Headless CMS 多端前端架构:技术解析与二次开发导引
前端·react.js·taro
狂炫冰美式9 小时前
你还在古法PPT吗,试试HTML呢?免费编辑导出工具给 xdm 放这了
前端·后端·github
万少10 小时前
未来组织的分水岭不是员工数量,而是人才密度
前端·后端·面试
任磊abc10 小时前
nextjs16配置eslint+prettier
前端·eslint·nextjs·prettier
x***r15110 小时前
Another-Redis-Desktop-Manager.1.3.7安装步骤详解(附Redis可视化连接与Key管理教程)
前端·bootstrap·html
Captaincc10 小时前
你真的知道自己把 AI 用在了哪里吗?这是 Vibe Usage 想回答的问题
前端·vibecoding