一、为什么要关注架构?
经历过多个项目的迭代后,我深刻体会到:好的架构能让需求开发效率提升 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 | 版本管理 |
六、总结
一个好的前端架构应该具备:
- 类型安全 - TypeScript 全覆盖
- 逻辑复用 - Composables 抽离公共逻辑
- 模块清晰 - 按功能分层,职责单一
- 规范约束 - ESLint + 提交规范
- 性能优先 - 按需加载、代码分割