模块化开发

模块化是现代前端开发的基石,它帮助我们将代码组织成可维护、可复用的单元。本章将介绍前端模块化开发的最佳实践。

模块化原则

模块化目标

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: '文档文件'
};

模块API设计

导出设计

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 };

命名导出vs默认导出

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('网络错误');
        });
    });
});

东巴文小贴士

📦 模块化最佳实践

  1. 小而专注:每个模块只做一件事
  2. 明确接口:清晰的公共API
  3. 最小依赖:减少不必要的依赖
  4. 文档完善:说明模块用途和用法

🔄 循环依赖

避免循环依赖的方法:

  • 重构模块结构
  • 使用依赖注入
  • 提取共享模块
  • 使用事件通信

下一步

下一章将探讨 组件化思想,学习前端组件化开发的设计理念。