深色模式
做了五年前端,从写页面到写系统,从维护老项目到搭建新架构。今年带团队做了一个大型管理后台项目,沉淀了一些架构设计的思考。
架构的核心问题
前端架构要解决的三个核心问题:
1. 如何组织代码?(目录结构、模块划分)
2. 如何管理状态?(数据流、缓存策略)
3. 如何保证质量?(类型、测试、规范)目录结构设计
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── base/ # 基础组件(Button、Input)
│ └── business/ # 业务组件(UserSelect、OrderTable)
├── composables/ # 组合式函数(Vue 3)
│ ├── useAuth.ts # 认证
│ ├── usePagination.ts # 分页
│ └── useRequest.ts # 请求
├── layouts/ # 布局组件
├── router/ # 路由
│ ├── index.ts
│ └── guards.ts # 路由守卫
├── services/ # API 服务层
│ ├── user.ts
│ ├── order.ts
│ └── http.ts # Axios 实例
├── store/ # 状态管理
│ ├── modules/
│ └── index.ts
├── styles/ # 全局样式
│ ├── variables.scss
│ ├── mixins.scss
│ └── global.scss
├── types/ # TypeScript 类型
│ ├── api.ts # API 响应类型
│ ├── model.ts # 业务模型类型
│ └── store.ts # Store 类型
├── utils/ # 工具函数
│ ├── format.ts
│ ├── validate.ts
│ └── storage.ts
├── views/ # 页面
│ ├── dashboard/
│ ├── user/
│ └── order/
└── main.tsAPI 服务层设计
typescript
// services/http.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
const http = axios.create({
baseURL: process.env.VUE_APP_API_BASE,
timeout: 10000,
});
// 请求拦截:token
http.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截:统一错误处理
http.interceptors.response.use(
(response: AxiosResponse) => {
const { code, data, message } = response.data;
if (code === 0) return data;
// 业务错误
return Promise.reject(new Error(message || '请求失败'));
},
error => {
if (error.response?.status === 401) {
// token 过期,跳转登录
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default http;typescript
// services/user.ts
import http from './http';
import type { User, UserQuery, PaginatedResult } from '@/types/model';
export const userService = {
// 获取用户列表
getList(params: UserQuery): Promise<PaginatedResult<User>> {
return http.get('/users', { params });
},
// 获取用户详情
getById(id: number): Promise<User> {
return http.get(`/users/${id}`);
},
// 创建用户
create(data: Partial<User>): Promise<User> {
return http.post('/users', data);
},
// 更新用户
update(id: number, data: Partial<User>): Promise<User> {
return http.put(`/users/${id}`, data);
},
// 删除用户
delete(id: number): Promise<void> {
return http.delete(`/users/${id}`);
},
};状态管理策略
typescript
// 不是什么都要放 Vuex/Pinia
// 状态分层:
// 1. 组件内状态:useState / ref / reactive
// 只在组件内使用,不需要共享
// 例如:表单输入、弹窗开关
// 2. 全局业务状态:Pinia / Vuex
// 多个组件/页面共享
// 例如:用户信息、权限、全局配置
// 3. 服务端状态:缓存 + 请求
// 从 API 获取的数据
// 例如:列表数据、详情数据
// store/modules/auth.ts
import { defineStore } from 'pinia';
import { authService } from '@/services/auth';
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('token') || '',
user: null as User | null,
permissions: [] as string[],
}),
getters: {
isLoggedIn: (state) => !!state.token,
hasPermission: (state) => (perm: string) => state.permissions.includes(perm),
},
actions: {
async login(credentials: { username: string; password: string }) {
const { token, user, permissions } = await authService.login(credentials);
this.token = token;
this.user = user;
this.permissions = permissions;
localStorage.setItem('token', token);
},
logout() {
this.token = '';
this.user = null;
this.permissions = [];
localStorage.removeItem('token');
},
},
});权限设计
typescript
// utils/permission.ts
import { useAuthStore } from '@/store/modules/auth';
// 指令方式
export const vPermission = {
mounted(el: HTMLElement, binding: { value: string | string[] }) {
const authStore = useAuthStore();
const perms = Array.isArray(binding.value) ? binding.value : [binding.value];
const hasPerm = perms.some(p => authStore.hasPermission(p));
if (!hasPerm) {
el.parentNode?.removeChild(el);
}
},
};
// 组件方式
// <template>
// <permission :required="['user:create']">
// <button>新增用户</button>
// </permission>
// </template>路由设计
typescript
// router/index.ts
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/views/dashboard/Index.vue'),
meta: { title: '仪表盘', icon: 'dashboard' },
},
{
path: 'user',
name: 'User',
redirect: '/user/list',
meta: { title: '用户管理', icon: 'user', permission: 'user:view' },
children: [
{
path: 'list',
name: 'UserList',
component: () => import('@/views/user/List.vue'),
meta: { title: '用户列表' },
},
{
path: ':id',
name: 'UserDetail',
component: () => import('@/views/user/Detail.vue'),
meta: { title: '用户详情', hidden: true },
},
],
},
],
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/Index.vue'),
meta: { title: '登录', layout: 'blank' },
},
];错误边界处理
typescript
// utils/error-handler.ts
export function setupErrorHandler(app) {
// Vue 全局错误处理
app.config.errorHandler = (err, vm, info) => {
console.error('Vue 错误:', err);
// 上报到监控平台
reportError({
type: 'vue-error',
error: err.message,
stack: err.stack,
component: vm?.$options?.name,
info,
});
};
// JS 全局错误
window.addEventListener('error', (event) => {
reportError({
type: 'js-error',
error: event.message,
filename: event.filename,
line: event.lineno,
col: event.colno,
});
});
// Promise 未捕获错误
window.addEventListener('unhandledrejection', (event) => {
reportError({
type: 'promise-rejection',
error: event.reason?.message || String(event.reason),
});
});
}小结
- 架构的核心是让代码结构清晰、团队协作高效
- 分层设计:组件层、业务层、服务层、基础设施层
- API 服务层统一管理请求,类型定义保证接口契约
- 状态管理按范围分层,不是所有状态都需要全局管理
- 权限、路由、错误处理是企业级应用的基础设施,优先设计