模块化是现代前端开发的基石,它帮助我们将代码组织成可维护、可复用的单元。本章将介绍前端模块化开发的最佳实践。
const modularPrinciples = {
encapsulation: {
description: '封装',
goal: '隐藏内部实现,暴露公共接口'
},
reusability: {
description: '复用性',
goal: '模块可在不同项目中复用'
},
maintainability: {
description: '可维护性',
goal: '修改一个模块不影响其他模块'
},
testability: {
description: '可测试性',
goal: '模块可独立测试'
},
dependency: {
description: '依赖管理',
goal: '明确模块间的依赖关系'
}
};
const designPrinciples = {
singleResponsibility: {
name: '单一职责',
description: '一个模块只做一件事',
example: '用户模块只处理用户相关逻辑'
},
highCohesion: {
name: '高内聚',
description: '模块内部元素紧密相关',
example: '表单验证模块包含所有验证规则'
},
lowCoupling: {
name: '低耦合',
description: '模块之间依赖最小化',
example: '通过接口通信,而非直接访问'
},
interfaceSegregation: {
name: '接口隔离',
description: '不应强迫依赖不使用的接口',
example: '拆分大接口为多个小接口'
}
};
project/
├── src/
│ ├── modules/
│ │ ├── user/
│ │ │ ├── index.js
│ │ │ ├── UserService.js
│ │ │ ├── UserRepository.js
│ │ │ └── __tests__/
│ │ │ └── UserService.test.js
│ │ ├── product/
│ │ │ ├── index.js
│ │ │ ├── ProductService.js
│ │ │ └── components/
│ │ │ ├── ProductCard.js
│ │ │ └── ProductList.js
│ │ └── shared/
│ │ ├── utils/
│ │ │ ├── format.js
│ │ │ └── validate.js
│ │ └── constants/
│ │ └── index.js
│ ├── components/
│ │ ├── Button/
│ │ │ ├── index.js
│ │ │ ├── Button.js
│ │ │ └── Button.module.css
│ │ └── index.js
│ ├── services/
│ │ ├── api.js
│ │ └── storage.js
│ └── index.js
├── tests/
├── package.json
└── README.md
const moduleStructure = {
index: '模块入口文件,导出公共API',
main: '主要逻辑文件',
types: '类型定义文件(TypeScript)',
constants: '常量定义',
utils: '工具函数',
components: '子组件',
hooks: '自定义Hooks(React)',
tests: '测试文件',
styles: '样式文件',
docs: '文档文件'
};
const userService = {
async getUser(id) { /* ... */ },
async createUser(data) { /* ... */ },
async updateUser(id, data) { /* ... */ },
async deleteUser(id) { /* ... */ }
};
const UserConstants = {
STATUS_ACTIVE: 'active',
STATUS_INACTIVE: 'inactive',
ROLES: ['admin', 'user', 'guest']
};
export { userService, UserConstants };
export default userService;
export function formatDate(date) { /* ... */ }
export function formatCurrency(amount) { /* ... */ }
export function formatAddress(address) { /* ... */ }
export const API_BASE_URL = 'https://api.example.com';
export const API_TIMEOUT = 5000;
export { formatDate, formatCurrency, formatAddress, API_BASE_URL, API_TIMEOUT };
const exportGuidelines = {
namedExport: {
use: '工具函数、常量、多个相关功能',
pros: ['可以部分导入', '名称一致', 'IDE支持好'],
example: "import { formatDate, formatCurrency } from './utils'"
},
defaultExport: {
use: '模块主要功能、类、React组件',
pros: ['导入时可自定义名称', '表达模块主功能'],
example: "import UserService from './UserService'"
},
recommendation: '一个模块最多一个默认导出,配合命名导出'
};
class UserService {
constructor({ apiClient, logger, config }) {
this.api = apiClient;
this.logger = logger;
this.config = config;
}
async getUser(id) {
this.logger.info(`获取用户: ${id}`);
return this.api.get(`/users/${id}`);
}
}
const container = {
services: new Map(),
factories: new Map(),
register(name, factory) {
this.factories.set(name, factory);
},
get(name) {
if (this.services.has(name)) {
return this.services.get(name);
}
const factory = this.factories.get(name);
if (factory) {
const service = factory(this);
this.services.set(name, service);
return service;
}
throw new Error(`服务 ${name} 未注册`);
}
};
container.register('apiClient', () => new ApiClient());
container.register('logger', () => new Logger());
container.register('config', () => ({ baseUrl: '/api' }));
container.register('userService', (c) => new UserService({
apiClient: c.get('apiClient'),
logger: c.get('logger'),
config: c.get('config')
}));
const userService = container.get('userService');
class DataRepository {
constructor(dataSource) {
this.dataSource = dataSource;
}
async find(id) {
return this.dataSource.find(id);
}
async save(entity) {
return this.dataSource.save(entity);
}
}
class ApiDataSource {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async find(id) {
const response = await fetch(`${this.baseUrl}/${id}`);
return response.json();
}
async save(entity) {
const response = await fetch(this.baseUrl, {
method: 'POST',
body: JSON.stringify(entity)
});
return response.json();
}
}
class LocalStorageDataSource {
constructor(key) {
this.key = key;
}
async find(id) {
const data = JSON.parse(localStorage.getItem(this.key) || '[]');
return data.find(item => item.id === id);
}
async save(entity) {
const data = JSON.parse(localStorage.getItem(this.key) || '[]');
const index = data.findIndex(item => item.id === entity.id);
if (index >= 0) {
data[index] = entity;
} else {
data.push(entity);
}
localStorage.setItem(this.key, JSON.stringify(data));
return entity;
}
}
const apiRepo = new DataRepository(new ApiDataSource('/api/users'));
const localRepo = new DataRepository(new LocalStorageDataSource('users'));
class EventBus {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return () => this.off(event, callback);
}
off(event, callback) {
if (!this.listeners.has(event)) return;
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
emit(event, data) {
if (!this.listeners.has(event)) return;
this.listeners.get(event).forEach(callback => {
callback(data);
});
}
once(event, callback) {
const wrapper = (data) => {
callback(data);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
export const eventBus = new EventBus();
class Store {
constructor(reducer, initialState) {
this.reducer = reducer;
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
dispatch(action) {
this.state = this.reducer(this.state, action);
this.listeners.forEach(listener => listener(this.state));
return action;
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
export const store = new Store(counterReducer, initialState);
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { UserService } from './UserService';
import { ApiClient } from './ApiClient';
describe('UserService', () => {
let userService;
let mockApiClient;
beforeEach(() => {
mockApiClient = {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn()
};
userService = new UserService({ apiClient: mockApiClient });
});
afterEach(() => {
vi.clearAllMocks();
});
describe('getUser', () => {
it('应该调用API获取用户', async () => {
const mockUser = { id: 1, name: '张三' };
mockApiClient.get.mockResolvedValue(mockUser);
const result = await userService.getUser(1);
expect(mockApiClient.get).toHaveBeenCalledWith('/users/1');
expect(result).toEqual(mockUser);
});
it('应该处理API错误', async () => {
mockApiClient.get.mockRejectedValue(new Error('网络错误'));
await expect(userService.getUser(1)).rejects.toThrow('网络错误');
});
});
});
📦 模块化最佳实践
- 小而专注:每个模块只做一件事
- 明确接口:清晰的公共API
- 最小依赖:减少不必要的依赖
- 文档完善:说明模块用途和用法
🔄 循环依赖
避免循环依赖的方法:
- 重构模块结构
- 使用依赖注入
- 提取共享模块
- 使用事件通信
下一章将探讨 组件化思想,学习前端组件化开发的设计理念。