说实话,刚接手那个几千个文件的老项目时,我差点没哭出来。
那时候的代码库就像个没有规划的老城区——路窄、房歪、电线乱拉,想找找某个功能在哪,比在大海里捞针还难。更可怕的是,每当想改个功能,就会有一堆奇怪的红线报错从角落冒出来,仿佛在嘲笑你的不自量力。
但经过半年的重构,我们硬是把这块烂摊子收拾得井井有条。今天就想跟你聊聊,我是怎么做的,以及为什么这些方法真的有效。
一、先认清问题:为什么依赖会乱成一锅粥?
很多团队刚开始用TypeScript时,都觉得”反正有类型检查,怕什么”。结果呢?代码量上去了,复杂度也上去了,但维护成本反而更高。
我见过最常见的几种”病态”现象:
1. 循环依赖 A模块导入B,B模块又导入A。这在JavaScript里可能还能跑,但在TypeScript里,编译时就会崩溃,或者运行时出现undefined。
2. 过度耦合 一个UI组件直接导入数据库模块,或者一个工具函数依赖整个业务逻辑层。这种写法在小型项目里还行,一旦项目变大,改一处动全身。
3. 接口不一致 前端定义了一个User类型,后端又定义了一个UserDTO类型,两者长得差不多但字段对不上。结果前端传来数据,后端接收时各种类型错误。
4. 类型定义散落各处 有的类型在utils里,有的在models里,还有的直接写在组件里面。找起来费时,改起来更费时。
这些问题如果不解决,项目迟早会变成一座纸牌屋,轻轻一碰就塌。
二、建立清晰的模块边界
2.1 分层架构是基础
我们重构的第一步,就是给项目画了一张”地图”。不是那种复杂的架构图,而是一个简单清晰的分层结构:
src/
├── api/ # API调用层,只负责和网络通信
├── models/ # 数据模型层,纯TypeScript类型定义
├── services/ # 业务逻辑层,处理核心业务
├── features/ # 功能模块层,按业务领域划分
├── components/ # UI组件层
├── hooks/ # React Hooks
├── utils/ # 纯工具函数
├── constants/ # 常量定义
└── types/ # 全局类型定义
关键原则是:每一层只能依赖它下面的层,不能向上依赖。
比如,components可以导入services,但services绝对不能导入components。这样做的目的是让业务逻辑和UI展示彻底解耦。
2.2 用 barrel file 简化导入
以前我们的导入语句长得像这样:
import { User } from '../../models/user';
import { fetchUser } from '../../services/userService';
import { formatDate } from '../../utils/dateFormatter';
这太乱了。我们统一用 barrel file( barrel file 就是一个导出多个模块的索引文件)来管理:
// src/models/index.ts
export { User, type CreateUserInput } from './user';
export { Product, type CreateProductInput } from './product';
export { Order, type CreateOrderInput } from './order';
// src/services/index.ts
export { UserService } from './userService';
export { ProductService } from './productService';
这样导入就简洁多了:
import { User, UserService } from '@/models';
import { formatDate } from '@/utils';
我们用 @/ 作为 src 目录的别名,在 tsconfig.json 里配置一下就行:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
2.3 严格的导入规则
我们在项目里加了 ESLint 规则,强制禁止跨层导入:
// eslint.config.js
export default [
{
rules: {
// 禁止components导入services以外的层
'import/no-import-module-exports': 'error',
// 禁止循环依赖
'import/no-cycle': ['error', { maxDepth: 2 }],
// 禁止从内部导入非导出内容
'import/no-internal-modules': ['error', {
allow: ['@/models/*', '@/utils/*']
}]
}
}
]
这些规则一开始会让很多人抱怨,但坚持两周后,大家就习惯了,代码质量明显提升。
三、统一接口定义,解决兼容性问题
接口不兼容是大型项目最常见的痛点之一。我们采用的是 “契约优先” 的策略。
3.1 建立全局类型定义
所有跨模块使用的类型,统一放在 src/types/ 目录下:
// src/types/common.ts
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp: number;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
}
export interface ValidationError {
field: string;
message: string;
}
export interface ApiError extends Error {
code: string;
details?: ValidationError[];
}
// src/types/user.ts
export interface User {
id: string;
username: string;
email: string;
role: UserRole;
createdAt: string; // ISO 8601 格式
updatedAt: string;
}
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator'
}
export type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
export type UpdateUserInput = Partial<CreateUserInput>;
// src/types/product.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
categoryId: string;
stock: number;
images: string[];
createdAt: string;
updatedAt: string;
}
export type CreateProductInput = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;
3.2 API 层使用统一响应类型
我们封装了一个统一的请求工具,所有 API 调用都返回标准格式:
// src/api/client.ts
import type { ApiResponse, PaginatedResponse } from '@/types';
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async post<T>(endpoint: string, data: unknown): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async getPaginated<T>(endpoint: string): Promise<PaginatedResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
export const apiClient = new ApiClient('https://api.example.com');
这样,所有 API 调用都有统一的类型,前端不需要关心后端的响应格式变化。
3.3 使用 Zod 进行运行时验证
类型定义只是编译时的保障,运行时数据还是要验证。我们引入了 Zod 库:
// src/validations/user.ts
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string().uuid(),
username: z.string().min(3).max(20),
email: z.string().email(),
role: z.enum(['admin', 'user', 'moderator']),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export const CreateUserInputSchema = UserSchema.omit({
id: true,
createdAt: true,
updatedAt: true
}).extend({
password: z.string().min(8),
});
export type User = z.infer<typeof UserSchema>;
export type CreateUserInput = z.infer<typeof CreateUserInputSchema>;
在 services 层使用验证:
// src/services/userService.ts
import { apiClient } from '@/api/client';
import { UserSchema, CreateUserInputSchema } from '@/validations/user';
import type { User, CreateUserInput } from '@/types/user';
export class UserService {
async getUser(id: string): Promise<User> {
const response = await apiClient.get<User>(`/users/${id}`);
// 运行时验证,确保数据符合预期
return UserSchema.parse(response.data);
}
async createUser(input: CreateUserInput): Promise<User> {
// 验证输入
const validatedInput = CreateUserInputSchema.parse(input);
const response = await apiClient.post<User>('/users', validatedInput);
return UserSchema.parse(response.data);
}
async listUsers(page = 1, pageSize = 20): Promise<User[]> {
const response = await apiClient.getPaginated<User>('/users');
return UserSchema.array().parse(response.data);
}
}
这样,即使后端返回的数据格式有问题,也能在第一时间被发现,而不是等到运行时才报错。
四、功能模块化的最佳实践
4.1 按业务领域划分模块
我们采用了 feature-based 的组织方式,每个业务功能一个目录:
src/features/
├── auth/
│ ├── components/ # 登录表单、注册表单等
│ ├── hooks/ # useAuth, useLogin 等
│ ├── services/ # 认证相关服务
│ ├── types/ # 认证相关类型
│ ├── validations/ # 表单验证
│ └── index.ts # 导出公共API
├── products/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ ├── types/
│ ├── validations/
│ └── index.ts
└── orders/
├── components/
├── hooks/
├── services/
├── types/
├── validations/
└── index.ts
每个 feature 目录内部是一个完整的模块,对外只通过 index.ts 暴露必要的 API:
// src/features/auth/index.ts
export { AuthProvider, type AuthContextType } from './components/AuthProvider';
export { useAuth } from './hooks/useAuth';
export { LoginForm } from './components/LoginForm';
export { RegisterForm } from './components/RegisterForm';
export type { LoginInput, RegisterInput } from './validations/auth';
4.2 使用依赖注入避免循环依赖
循环依赖最头疼。我们的解决方案是依赖注入:
// 不好的做法 - 循环依赖
// src/features/auth/services/authService.ts
import { UserService } from '@/services/userService'; // 循环!
// 好的做法 - 依赖注入
// src/features/auth/services/authService.ts
import type { UserService } from '@/services/userService';
export class AuthService {
constructor(private userService: UserService) {}
async login(username: string, password: string) {
// 使用注入的 userService
const user = await this.userService.getUserByUsername(username);
// ...
}
}
在应用启动时组装依赖:
// src/app.ts
import { UserService } from '@/services/userService';
import { AuthService } from '@/features/auth/services/authService';
import { ProductService } from '@/features/products/services/productService';
// 创建服务实例
const userService = new UserService();
const authService = new AuthService(userService);
const productService = new ProductService(userService);
// 传入组件或 context
export const appContext = {
auth: authService,
user: userService,
product: productService,
};
4.3 类型安全的事件系统
对于模块间的通信,我们避免了使用全局事件总线,而是使用类型安全的事件系统:
// src/events/types.ts
export type AppEvents = {
'user:login': { user: User };
'user:logout': void;
'product:create': { product: Product };
'product:update': { id: string; data: Partial<Product> };
'order:create': { order: Order };
};
// src/events/emitter.ts
import type { AppEvents } from './types';
type Listener<T> = (data: T) => void;
class EventEmitter<Events extends Record<string, unknown>> {
private listeners = new Map<keyof Events, Set<Listener<any>>>();
on<K extends keyof Events>(event: K, listener: Listener<Events[K]>) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
return () => this.off(event, listener);
}
off<K extends keyof Events>(event: K, listener: Listener<Events[K]>) {
this.listeners.get(event)?.delete(listener);
}
emit<K extends keyof Events>(event: K, data: Events[K]) {
this.listeners.get(event)?.forEach(listener => listener(data));
}
}
export const eventEmitter = new EventEmitter<AppEvents>();
使用方式:
// 订阅事件
eventEmitter.on('user:login', (data) => {
console.log('User logged in:', data.user);
});
// 触发事件
eventEmitter.emit('user:login', { user: currentUser });
这样,事件名称和数据结构都有类型检查,不会出现拼写错误或数据结构不匹配的问题。
五、工具函数和常量的组织
5.1 工具函数按用途分组
// src/utils/date.ts
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
export function formatDateTime(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleString('zh-CN');
}
export function isToday(date: Date | string): boolean {
const d = typeof date === 'string' ? new Date(date) : date;
const today = new Date();
return d.toDateString() === today.toDateString();
}
// src/utils/string.ts
export function truncate(str: string, length: number): string {
return str.length > length ? str.slice(0, length) + '...' : str;
}
export function camelToSnake(str: string): string {
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}
export function generateId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
5.2 常量集中管理
// src/constants/api.ts
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.example.com';
export const API_TIMEOUT = 10000;
export const RETRY_COUNT = 3;
// src/constants/app.ts
export const APP_NAME = 'MyApp';
export const APP_VERSION = '1.0.0';
export const SUPPORTED_LANGUAGES = ['zh-CN', 'en-US'] as const;
// src/constants/routes.ts
export const ROUTES = {
HOME: '/',
LOGIN: '/login',
REGISTER: '/register',
PRODUCTS: '/products',
ORDERS: '/orders',
PROFILE: '/profile',
} as const;
// src/constants/storage.ts
export const STORAGE_KEYS = {
TOKEN: 'auth_token',
USER: 'user_info',
PREFS: 'user_preferences',
} as const;
六、实际案例:重构一个电商模块
让我用一个具体的例子,展示如何从零开始设计一个模块。
6.1 需求分析
我们需要实现商品管理功能,包括:
- 商品列表(分页、筛选、排序)
- 商品详情
- 创建/更新商品
- 删除商品
- 商品分类管理
6.2 类型定义
”`typescript
// src/features/products/types/product.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
originalPrice?: number;
categoryId: string;
categoryName?: string; // 前端展示用
stock: number;
images: string[];
specs: Record
export enum ProductStatus { DRAFT = ‘draft’, ACTIVE = ‘active’, INACTIVE = ‘inactive’, SOLD_OUT = ‘sold_out’, }
export type CreateProductInput = Omit
