从项目重构踩坑到规范开发 TypeScript 模块化实战:如何解决多人协作中的命名冲突与依赖混乱
说实话,我之前接手过一个老项目,那场面真的是让人头疼。项目跑不起来,改个文件能引发连锁反应,每天像在玩扫雷一样。后来我才明白,问题就出在没有规范的模块化开发上。今天想和大家聊聊,我是怎么从一个”混乱重构”的坑里爬出来,找到TypeScript模块化开发的正确姿势。
先说说那个”噩梦”项目
我之前负责的项目,代码量大概有十万行左右。刚开始接手的时候,我看到文件目录就懵了:
src/
├── utils/
│ ├── helper.ts
│ ├── helper2.ts
│ └── helper3.ts
├── components/
│ ├── User/
│ │ ├── index.ts
│ │ └── User.tsx
│ ├── Button/
│ │ ├── index.ts
│ │ └── Button.tsx
│ └── ...
├── services/
│ ├── api.ts
│ ├── user.ts
│ └── ...
└── types/
├── index.ts
└── user.ts
看起来挺规整对吧?但打开文件一看,全是问题:
// src/utils/helper.ts
export const format = (str: string) => {
return str.trim();
};
export const validate = (value: any) => {
return value !== null;
};
// src/utils/helper2.ts
export const format = (num: number) => {
return num.toFixed(2);
};
好家伙,两个文件都导出了format函数,参数类型还不一致。当我引入这两个文件的时候,TypeScript直接报错。
import { format } from '../utils/helper';
import { format } from '../utils/helper2'; // 命名冲突!
命名冲突的三大”坑”
坑一:同名导出满天飞
// src/services/userService.ts
export interface User {
id: number;
name: string;
}
export const getUser = async (id: number): Promise<User> => {
return fetch(`/api/user/${id}`).then(res => res.json());
};
// src/services/adminService.ts
export interface User { // 和上面重名!
id: number;
name: string;
role: string;
}
export const getUser = async (id: number): Promise<User> => {
return fetch(`/api/admin/${id}`).then(res => res.json());
};
这两个文件都在services目录下,看似互不相干,但只要有人一不小心同时引入,就会出大问题。
坑二: barrel 文件滥用
// src/index.ts - 这种文件看着方便,实际上是个灾难
export * from './components';
export * from './services';
export * from './utils';
export * from './types';
然后别人这样引入:
import { User, getUser } from '@/index'; // 谁知道User是哪个?
坑三:循环依赖
// src/services/userService.ts
import { Logger } from '../utils/logger';
export const getUser = async (id: number) => {
Logger.log('Fetching user...');
return fetch(`/api/user/${id}`).then(res => res.json());
};
// src/utils/logger.ts
import { getUser } from '../services/userService';
export const Logger = {
log: (msg: string) => {
console.log(`[${new Date()}] ${msg}`);
},
getUserInfo: async (id: number) => {
const user = await getUser(id);
return user;
}
};
看到没?互相引用,典型的循环依赖。TypeScript能编译过,但运行起来就炸了。
模块化开发的正确姿势
第一步:建立清晰的分层结构
src/
├── modules/
│ ├── user/
│ │ ├── types.ts
│ │ ├── service.ts
│ │ ├── api.ts
│ │ └── index.ts
│ ├── order/
│ │ ├── types.ts
│ │ ├── service.ts
│ │ └── index.ts
│ └── product/
│ ├── types.ts
│ ├── service.ts
│ └── index.ts
├── shared/
│ ├── utils/
│ │ ├── format.ts
│ │ ├── validate.ts
│ │ └── index.ts
│ └── hooks/
│ ├── useFetch.ts
│ └── index.ts
├── app.ts
└── main.ts
这样的结构一眼就能看出每个模块的职责边界。
第二步:用命名空间解决命名冲突
// src/modules/user/types.ts
export namespace UserTypes {
export interface User {
id: number;
name: string;
email: string;
}
export interface CreateUserDto {
name: string;
email: string;
}
}
// src/modules/admin/types.ts
export namespace AdminTypes {
export interface User { // 同名不冲突了!
id: number;
name: string;
role: string;
permissions: string[];
}
export interface Admin extends UserTypes.User {
department: string;
}
}
用的时候:
import { UserTypes } from '@/modules/user/types';
import { AdminTypes } from '@/modules/admin/types';
const user: UserTypes.User = { id: 1, name: '张三', email: 'zhangsan@example.com' };
const admin: AdminTypes.User = { id: 2, name: '李四', role: 'admin', permissions: ['read', 'write'] };
第三步:建立自己的命名规范
我们可以给不同类型的导出加上前缀:
// src/modules/user/service.ts
import { UserTypes } from './types';
// 接口用 I 开头
export interface IUserService {
getUser(id: number): Promise<UserTypes.User>;
createUser(data: UserTypes.CreateUserDto): Promise<UserTypes.User>;
}
// 具体实现类用 CamelCase
export class UserService implements IUserService {
async getUser(id: number): Promise<UserTypes.User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async createUser(data: UserTypes.CreateUserDto): Promise<UserTypes.User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
}
// 工厂函数用 create 开头
export function createUserService(): IUserService {
return new UserService();
}
// src/modules/order/service.ts
import { OrderTypes } from './types';
export interface IOrderService {
getOrder(id: string): Promise<OrderTypes.Order>;
}
export class OrderService implements IOrderService {
// ...
}
这样,即使两个模块都有UserService和OrderService,也不会冲突。
第四步:用 Barrel 文件做精细控制
不要滥用export *,应该显式导出:
// src/modules/user/index.ts - 只导出需要的
export { UserTypes } from './types';
export type { IUserService } from './service';
export { UserService, createUserService } from './service';
// src/modules/order/index.ts
export { OrderTypes } from './types';
export type { IOrderService } from './service';
export { OrderService } from './service';
用的时候:
// 明确的导入路径,不会有歧义
import { UserTypes, createUserService } from '@/modules/user';
import { OrderService } from '@/modules/order';
第五步:处理跨模块依赖
// src/modules/user/api.ts
export const userApi = {
get: (id: number) => `/api/users/${id}`,
list: () => '/api/users',
create: () => '/api/users'
};
// src/modules/order/api.ts
export const orderApi = {
get: (id: string) => `/api/orders/${id}`,
list: (userId: number) => `/api/orders?userId=${userId}`,
create: () => '/api/orders'
};
// src/shared/api/client.ts
import { userApi } from '@/modules/user/api';
import { orderApi } from '@/modules/order/api';
export class ApiClient {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
async post<T>(url: string, data: any): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
}
export const apiClient = new ApiClient();
第六步:用路径别名让导入更清晰
在tsconfig.json中配置:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@modules/*": ["src/modules/*"],
"@shared/*": ["src/shared/*"],
"@utils/*": ["src/shared/utils/*"]
}
}
}
这样导入就变成了:
import { UserTypes, createUserService } from '@modules/user';
import { apiClient } from '@shared/api/client';
import { formatDate } from '@utils/format';
比import ... from '../../../modules/user'清爽多了。
实际项目中的完整示例
让我给你展示一个完整的多模块协作场景:
// src/modules/user/types.ts
export namespace UserModule {
export enum UserStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
BANNED = 'banned'
}
export interface User {
id: string;
username: string;
email: string;
status: UserStatus;
createdAt: Date;
updatedAt: Date;
}
export type CreateUserInput = Omit<User, 'id' | 'status' | 'createdAt' | 'updatedAt'>;
export type UpdateUserInput = Partial<CreateUserInput>;
}
// src/modules/user/service.ts
import { UserModule } from './types';
import { apiClient } from '@shared/api/client';
export interface IUserRepository {
findById(id: string): Promise<UserModule.User | null>;
findAll(): Promise<UserModule.User[]>;
create(data: UserModule.CreateUserInput): Promise<UserModule.User>;
update(id: string, data: UserModule.UpdateUserInput): Promise<UserModule.User>;
delete(id: string): Promise<void>;
}
export class UserRepository implements IUserRepository {
async findById(id: string): Promise<UserModule.User | null> {
const url = UserModule.ApiEndpoints.getUser(id);
const user = await apiClient.get<UserModule.User>(url);
return user;
}
async findAll(): Promise<UserModule.User[]> {
const url = UserModule.ApiEndpoints.listUsers();
return apiClient.get<UserModule.User[]>(url);
}
async create(data: UserModule.CreateUserInput): Promise<UserModule.User> {
const url = UserModule.ApiEndpoints.createUser();
return apiClient.post<UserModule.User>(url, data);
}
async update(id: string, data: UserModule.UpdateUserInput): Promise<UserModule.User> {
const url = UserModule.ApiEndpoints.updateUser(id);
return apiClient.put<UserModule.User>(url, data);
}
async delete(id: string): Promise<void> {
const url = UserModule.ApiEndpoints.deleteUser(id);
await apiClient.delete(url);
}
}
// src/modules/user/api.ts
export namespace UserModule {
export const ApiEndpoints = {
getUser: (id: string) => `/api/v1/users/${id}`,
listUsers: () => '/api/v1/users',
createUser: () => '/api/v1/users',
updateUser: (id: string) => `/api/v1/users/${id}`,
deleteUser: (id: string) => `/api/v1/users/${id}`
};
}
// src/modules/order/types.ts
export namespace OrderModule {
export enum OrderStatus {
PENDING = 'pending',
PAID = 'paid',
SHIPPED = 'shipped',
DELIVERED = 'delivered',
CANCELLED = 'cancelled'
}
export interface Order {
id: string;
userId: string;
totalAmount: number;
status: OrderStatus;
createdAt: Date;
}
export interface OrderItem {
orderId: string;
productId: string;
quantity: number;
price: number;
}
}
// src/modules/order/service.ts
import { OrderModule } from './types';
import { apiClient } from '@shared/api/client';
export class OrderService {
constructor(
private apiBase: string = '/api/v1/orders'
) {}
async createOrder(userId: string, items: OrderModule.OrderItem[]): Promise<OrderModule.Order> {
return apiClient.post<OrderModule.Order>(`${this.apiBase}`, {
userId,
items
});
}
async getOrderByUser(userId: string): Promise<OrderModule.Order[]> {
return apiClient.get<OrderModule.Order[]>(`${this.apiBase}?userId=${userId}`);
}
}
// src/modules/product/types.ts
export namespace ProductModule {
export interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
}
}
// src/modules/product/service.ts
import { ProductModule } from './types';
import { apiClient } from '@shared/api/client';
export class ProductService {
async getById(id: string): Promise<ProductModule.Product> {
return apiClient.get<ProductModule.Product>(`/api/v1/products/${id}`);
}
async listByCategory(category: string): Promise<ProductModule.Product[]> {
return apiClient.get<ProductModule.Product[]>(`/api/v1/products?category=${category}`);
}
}
// src/modules/user/index.ts
export { UserModule } from './types';
export type { IUserRepository } from './service';
export { UserRepository } from './service';
export { ApiEndpoints } from './api';
// src/modules/order/index.ts
export { OrderModule } from './types';
export { OrderService } from './service';
// src/modules/product/index.ts
export { ProductModule } from './types';
export { ProductService } from './service';
多人协作的最佳实践
1. 代码审查 Checklist
每次提交前,检查这些点:
- [ ] 是否有重复的命名?
- [ ] 模块边界是否清晰?
- [ ] 是否避免了循环依赖?
- [ ] Barrel 文件是否只导出必要的内容?
- [ ] 路径别名使用是否规范?
2. 建立模块注册表
// src/modules/index.ts
import { UserModule, UserRepository } from './user';
import { OrderModule, OrderService } from './order';
import { ProductModule, ProductService } from './product';
export const Modules = {
User: {
types: UserModule,
repository: UserRepository
},
Order: {
types: OrderModule,
service: OrderService
},
Product: {
types: ProductModule,
service: ProductService
}
};
3. 版本化 API 路径
// src/shared/constants.ts
export const API_VERSION = 'v1';
export const API_BASE_URL = `https://api.example.com/${API_VERSION}`;
4. 错误处理统一化
// src/shared/errors/index.ts
export class AppModuleError extends Error {
constructor(
public module: string,
public code: string,
message: string
) {
super(message);
this.name = 'AppModuleError';
}
}
export class UserNotFoundError extends AppModuleError {
constructor(userId: string) {
super('user', 'USER_NOT_FOUND', `User ${userId} not found`);
}
}
export class OrderValidationError extends AppModuleError {
constructor(message: string) {
super('order', 'ORDER_VALIDATION_ERROR', message);
}
}
总结
从我踩过的坑里总结出来的经验就是:模块化不是为了分文件而分文件,而是为了让每个人都能清楚地知道自己在哪一层、该做什么。
规范命名、清晰的模块边界、避免循环依赖、合理使用路径别名——这些看似繁琐的规则,实际上是在保护你和你团队的未来。下次重构的时候,不妨先花点时间梳理一下模块结构,事半功倍。
记住,好的代码结构就像一个好的房间布局——每个东西都有它该在的位置,你找起来方便,别人住进来也不懵。
