基础知识

前端开发中的六种经典设计模式

深入理解观察者模式、单例模式、工厂模式、策略模式、装饰者模式和代理模式的实现与应用

2023年9月4日18 分钟1297 阅读
JavaScriptTypeScript设计模式架构
前端开发中的六种经典设计模式

前端开发中的六种经典设计模式

设计模式是软件开发中经过验证的解决方案模板,能够帮助我们编写更加可维护、可扩展的代码。本文将深入介绍前端开发中最常用的六种设计模式:观察者模式单例模式工厂模式策略模式装饰者模式代理模式

设计模式概览

模式核心思想典型应用场景
观察者模式一对多依赖,状态变化自动通知事件总线、状态管理
单例模式全局唯一实例缓存管理、配置中心
工厂模式统一接口创建对象组件工厂、服务创建
策略模式算法封装可互换表单验证、支付方式
装饰者模式动态扩展功能请求拦截、日志增强
代理模式控制对象访问延迟加载、权限控制

一、观察者模式 (Observer)

模式定义

"

观察者模式定义了对象之间一对多的依赖关系。当一个对象的状态发生变化时,所有依赖于它的对象都会收到通知并自动更新。

核心实现

typescript
// EventBus.ts - 事件总线实现
class EventBus {
  private events = new Map<string, Function[]>();

  // 订阅事件
  on(eventName: string, callback: Function) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, []);
    }
    this.events.get(eventName)!.push(callback);
  }

  // 触发事件
  emit(eventName: string, ...args: any[]) {
    if (this.events.has(eventName)) {
      this.events.get(eventName)?.forEach(cb => cb(...args));
    } else {
      console.error(`事件 ${eventName} 不存在`);
    }
  }

  // 取消订阅
  off(eventName: string, callback?: Function) {
    if (!callback) {
      this.events.delete(eventName);
    } else {
      const callbacks = this.events.get(eventName)!.filter(cb => cb !== callback);
      this.events.set(eventName, callbacks);
    }
  }

  // 单次订阅
  once(eventName: string, callback: Function) {
    const wrapper = (...args: any[]) => {
      callback(...args);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
  }
}

export default EventBus;

React 组件中的应用

tsx
import { useEffect, useState } from 'react';
import EventBus from './EventBus';

// 创建全局事件总线实例
const bus = new EventBus();

// 组件A:订阅用户登录事件
function HeaderComponent() {
  const [user, setUser] = useState<{ name: string } | null>(null);

  useEffect(() => {
    const handleLogin = (userData: { name: string }) => {
      console.log('用户登录,更新头部信息', userData);
      setUser(userData);
    };

    bus.on('user-login', handleLogin);

    // 清理订阅
    return () => bus.off('user-login', handleLogin);
  }, []);

  return (
    <header>
      {user ? `欢迎,${user.name}` : '请登录'}
    </header>
  );
}

// 组件B:触发登录事件
function LoginButton() {
  const handleLogin = () => {
    // 模拟登录成功
    bus.emit('user-login', { name: 'Alice', id: 123 });
  };

  return <button onClick={handleLogin}>登录</button>;
}

应用场景

  • 跨组件通信:非父子组件间的消息传递
  • 全局状态同步:用户登录状态、主题切换
  • 解耦模块:降低组件间的直接依赖

二、单例模式 (Singleton)

模式定义

"

单例模式确保一个类只有一个实例,并提供一个全局访问点。

核心实现

typescript
// CacheManager.ts - 缓存管理器
class CacheManager {
  private static instance: CacheManager;
  private cache = new Map<string, any>();
  private expireTime = new Map<string, number>();

  // 私有构造函数,禁止外部实例化
  private constructor() {}

  // 获取唯一实例
  static getInstance(): CacheManager {
    if (!CacheManager.instance) {
      CacheManager.instance = new CacheManager();
    }
    return CacheManager.instance;
  }

  // 设置缓存(支持过期时间)
  set(key: string, value: any, ttl?: number) {
    this.cache.set(key, value);
    if (ttl) {
      this.expireTime.set(key, Date.now() + ttl);
    }
  }

  // 获取缓存
  get<T>(key: string): T | null {
    // 检查是否过期
    const expireAt = this.expireTime.get(key);
    if (expireAt && Date.now() > expireAt) {
      this.delete(key);
      return null;
    }
    return this.cache.get(key) ?? null;
  }

  // 删除缓存
  delete(key: string) {
    this.cache.delete(key);
    this.expireTime.delete(key);
  }

  // 清空所有缓存
  clear() {
    this.cache.clear();
    this.expireTime.clear();
  }

  // 获取缓存大小
  get size(): number {
    return this.cache.size;
  }
}

export default CacheManager;

使用示例

typescript
// 任何地方获取的都是同一个实例
const cache1 = CacheManager.getInstance();
const cache2 = CacheManager.getInstance();

console.log(cache1 === cache2); // true

// 设置缓存
cache1.set('user', { name: 'Alice' });
cache1.set('token', 'abc123', 3600000); // 1小时过期

// 获取缓存
console.log(cache2.get('user')); // { name: 'Alice' }

应用场景

  • 全局配置管理:应用配置、环境变量
  • 缓存服务:HTTP 请求缓存、数据缓存
  • 日志服务:统一的日志收集器
  • WebSocket 连接:确保只有一个连接实例

三、工厂模式 (Factory)

模式定义

"

工厂模式通过提供一个创建对象的接口来创建对象,而无需指定具体的类。可以根据输入参数决定创建哪种类型的对象。

核心实现

typescript
// 定义产品接口
interface Component {
  render(): JSX.Element;
  validate?(): boolean;
}

// 具体产品:文本输入框
class TextInput implements Component {
  constructor(
    private label: string,
    private placeholder: string
  ) {}

  render() {
    return (
      <div className="form-field">
        <label>{this.label}</label>
        <input type="text" placeholder={this.placeholder} />
      </div>
    );
  }

  validate() {
    return true;
  }
}

// 具体产品:下拉选择框
class SelectInput implements Component {
  constructor(
    private label: string,
    private options: { value: string; label: string }[]
  ) {}

  render() {
    return (
      <div className="form-field">
        <label>{this.label}</label>
        <select>
          {this.options.map(opt => (
            <option key={opt.value} value={opt.value}>
              {opt.label}
            </option>
          ))}
        </select>
      </div>
    );
  }
}

// 具体产品:日期选择器
class DatePicker implements Component {
  constructor(private label: string) {}

  render() {
    return (
      <div className="form-field">
        <label>{this.label}</label>
        <input type="date" />
      </div>
    );
  }
}

// 表单字段配置类型
interface FieldConfig {
  type: 'text' | 'select' | 'date';
  label: string;
  placeholder?: string;
  options?: { value: string; label: string }[];
}

// 工厂类
class FormFieldFactory {
  static create(config: FieldConfig): Component {
    switch (config.type) {
      case 'text':
        return new TextInput(config.label, config.placeholder || '');
      case 'select':
        return new SelectInput(config.label, config.options || []);
      case 'date':
        return new DatePicker(config.label);
      default:
        throw new Error(`未知字段类型: ${config.type}`);
    }
  }

  // 批量创建
  static createMany(configs: FieldConfig[]): Component[] {
    return configs.map(config => this.create(config));
  }
}

export { FormFieldFactory, type FieldConfig };

使用示例

tsx
// 动态生成表单
function DynamicForm() {
  const fieldConfigs: FieldConfig[] = [
    { type: 'text', label: '姓名', placeholder: '请输入姓名' },
    { type: 'select', label: '性别', options: [
      { value: 'male', label: '男' },
      { value: 'female', label: '女' }
    ]},
    { type: 'date', label: '出生日期' }
  ];

  const fields = FormFieldFactory.createMany(fieldConfigs);

  return (
    <form>
      {fields.map((field, index) => (
        <div key={index}>{field.render()}</div>
      ))}
    </form>
  );
}

应用场景

  • 动态表单生成:根据配置创建不同类型的表单字段
  • 图表组件:根据数据类型创建不同类型的图表
  • 消息通知:根据类型创建不同样式的通知组件

四、策略模式 (Strategy)

模式定义

"

策略模式定义了一系列算法,并将每一个算法封装起来,让它们可以互换。让算法的变化独立于使用算法的客户。

核心实现

typescript
// 策略接口
interface ValidationStrategy {
  validate(value: string): boolean;
  message: string;
}

// 具体策略:必填验证
class RequiredStrategy implements ValidationStrategy {
  message = '此字段为必填项';

  validate(value: string): boolean {
    return value.trim().length > 0;
  }
}

// 具体策略:邮箱验证
class EmailStrategy implements ValidationStrategy {
  message = '请输入有效的邮箱地址';

  validate(value: string): boolean {
    const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
    return emailRegex.test(value);
  }
}

// 具体策略:手机号验证
class PhoneStrategy implements ValidationStrategy {
  message = '请输入有效的手机号码';

  validate(value: string): boolean {
    const phoneRegex = /^1[3-9]d{9}$/;
    return phoneRegex.test(value);
  }
}

// 具体策略:密码强度验证
class PasswordStrategy implements ValidationStrategy {
  message = '密码需包含大小写字母和数字,至少8位';

  validate(value: string): boolean {
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d).{8,}$/;
    return passwordRegex.test(value);
  }
}

// 具体策略:身份证验证
class IdCardStrategy implements ValidationStrategy {
  message = '请输入有效的身份证号码';

  validate(value: string): boolean {
    const idCardRegex = /(^d{15}$)|(^d{18}$)|(^d{17}(d|X|x)$)/;
    return idCardRegex.test(value);
  }
}

// 验证器上下文
class Validator {
  private strategies: ValidationStrategy[] = [];

  // 添加验证策略
  addStrategy(strategy: ValidationStrategy): this {
    this.strategies.push(strategy);
    return this; // 支持链式调用
  }

  // 执行验证
  validate(value: string): { valid: boolean; errors: string[] } {
    const errors: string[] = [];

    for (const strategy of this.strategies) {
      if (!strategy.validate(value)) {
        errors.push(strategy.message);
      }
    }

    return {
      valid: errors.length === 0,
      errors
    };
  }
}

// 策略工厂
const ValidationStrategies = {
  required: () => new RequiredStrategy(),
  email: () => new EmailStrategy(),
  phone: () => new PhoneStrategy(),
  password: () => new PasswordStrategy(),
  idCard: () => new IdCardStrategy()
};

export { Validator, ValidationStrategies };

使用示例

typescript
// 创建验证器并组合策略
const emailValidator = new Validator()
  .addStrategy(ValidationStrategies.required())
  .addStrategy(ValidationStrategies.email());

const passwordValidator = new Validator()
  .addStrategy(ValidationStrategies.required())
  .addStrategy(ValidationStrategies.password());

// 执行验证
console.log(emailValidator.validate(''));
// { valid: false, errors: ['此字段为必填项', '请输入有效的邮箱地址'] }

console.log(emailValidator.validate('test@example.com'));
// { valid: true, errors: [] }

console.log(passwordValidator.validate('weak'));
// { valid: false, errors: ['密码需包含大小写字母和数字,至少8位'] }

console.log(passwordValidator.validate('Strong123'));
// { valid: true, errors: [] }

应用场景

  • 表单验证:不同字段使用不同的验证规则
  • 支付方式:微信、支付宝、银行卡等不同支付策略
  • 排序算法:根据数据特点选择不同排序策略
  • 价格计算:会员折扣、满减优惠等不同计价策略

五、装饰者模式 (Decorator)

模式定义

"

装饰者模式在不修改对象结构的情况下,动态地为对象扩展功能。

核心实现

typescript
// 基础请求接口
interface HttpService {
  request<T>(url: string, options?: RequestInit): Promise<T>;
}

// 基础请求类
class BaseHttpService implements HttpService {
  async request<T>(url: string, options?: RequestInit): Promise<T> {
    const response = await fetch(url, options);
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json();
  }
}

// 装饰器基类
abstract class HttpServiceDecorator implements HttpService {
  constructor(protected service: HttpService) {}

  abstract request<T>(url: string, options?: RequestInit): Promise<T>;
}

// 装饰器:缓存功能
class CacheDecorator extends HttpServiceDecorator {
  private cache = new Map<string, { data: any; timestamp: number }>();
  private ttl: number;

  constructor(service: HttpService, ttl = 60000) {
    super(service);
    this.ttl = ttl;
  }

  async request<T>(url: string, options?: RequestInit): Promise<T> {
    const cacheKey = `${options?.method || 'GET'}-${url}`;

    // 检查缓存
    const cached = this.cache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      console.log(`[Cache] 命中缓存: ${url}`);
      return cached.data;
    }

    // 请求并缓存
    const data = await this.service.request<T>(url, options);
    this.cache.set(cacheKey, { data, timestamp: Date.now() });
    console.log(`[Cache] 已缓存: ${url}`);
    return data;
  }
}

// 装饰器:重试功能
class RetryDecorator extends HttpServiceDecorator {
  private maxRetries: number;
  private delay: number;

  constructor(service: HttpService, maxRetries = 3, delay = 1000) {
    super(service);
    this.maxRetries = maxRetries;
    this.delay = delay;
  }

  async request<T>(url: string, options?: RequestInit): Promise<T> {
    let lastError: Error | null = null;

    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        return await this.service.request<T>(url, options);
      } catch (error) {
        lastError = error as Error;
        console.log(`[Retry] 第 ${attempt} 次请求失败,${
          attempt < this.maxRetries ? `${this.delay}ms 后重试` : '已达最大重试次数'
        }`);

        if (attempt < this.maxRetries) {
          await new Promise(resolve => setTimeout(resolve, this.delay));
        }
      }
    }

    throw lastError;
  }
}

// 装饰器:日志功能
class LoggingDecorator extends HttpServiceDecorator {
  async request<T>(url: string, options?: RequestInit): Promise<T> {
    const startTime = performance.now();
    console.log(`[Log] 开始请求: ${options?.method || 'GET'} ${url}`);

    try {
      const data = await this.service.request<T>(url, options);
      const duration = (performance.now() - startTime).toFixed(2);
      console.log(`[Log] 请求成功: ${url} (${duration}ms)`);
      return data;
    } catch (error) {
      const duration = (performance.now() - startTime).toFixed(2);
      console.error(`[Log] 请求失败: ${url} (${duration}ms)`, error);
      throw error;
    }
  }
}

// 装饰器:Token 认证
class AuthDecorator extends HttpServiceDecorator {
  constructor(
    service: HttpService,
    private getToken: () => string | null
  ) {
    super(service);
  }

  async request<T>(url: string, options?: RequestInit): Promise<T> {
    const token = this.getToken();
    const headers = new Headers(options?.headers);

    if (token) {
      headers.set('Authorization', `Bearer ${token}`);
    }

    return this.service.request<T>(url, {
      ...options,
      headers
    });
  }
}

export {
  BaseHttpService,
  CacheDecorator,
  RetryDecorator,
  LoggingDecorator,
  AuthDecorator
};

使用示例

typescript
// 组合多个装饰器
const getToken = () => localStorage.getItem('token');

// 基础服务 -> 认证 -> 重试 -> 缓存 -> 日志
const httpService = new LoggingDecorator(
  new CacheDecorator(
    new RetryDecorator(
      new AuthDecorator(
        new BaseHttpService(),
        getToken
      ),
      3,  // 最大重试3次
      1000 // 重试间隔1秒
    ),
    60000 // 缓存60秒
  )
);

// 使用增强后的服务
const users = await httpService.request<User[]>('/api/users');

// 控制台输出:
// [Log] 开始请求: GET /api/users
// [Cache] 已缓存: /api/users
// [Log] 请求成功: /api/users (125.32ms)

应用场景

  • HTTP 请求增强:添加缓存、重试、日志、认证
  • 组件功能扩展:为组件添加 loading、错误边界
  • 数据处理管道:数据格式化、验证、转换

六、代理模式 (Proxy)

模式定义

"

代理模式为其他对象提供一个代理对象以控制对该对象的访问。适用于延迟加载、权限控制、缓存等场景。

核心实现

typescript
// 图片加载代理 - 实现懒加载
interface ImageLoader {
  load(url: string): Promise<HTMLImageElement>;
}

// 真实图片加载器
class RealImageLoader implements ImageLoader {
  async load(url: string): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = url;
    });
  }
}

// 代理图片加载器 - 添加占位图和懒加载
class LazyImageProxy implements ImageLoader {
  private realLoader: RealImageLoader;
  private loadedImages = new Map<string, HTMLImageElement>();
  private placeholderUrl: string;

  constructor(placeholderUrl = '/placeholder.svg') {
    this.realLoader = new RealImageLoader();
    this.placeholderUrl = placeholderUrl;
  }

  async load(url: string): Promise<HTMLImageElement> {
    // 检查是否已加载
    if (this.loadedImages.has(url)) {
      console.log(`[Proxy] 使用已加载图片: ${url}`);
      return this.loadedImages.get(url)!;
    }

    // 先返回占位图
    console.log(`[Proxy] 显示占位图,开始加载: ${url}`);

    // 异步加载真实图片
    const img = await this.realLoader.load(url);
    this.loadedImages.set(url, img);
    console.log(`[Proxy] 图片加载完成: ${url}`);

    return img;
  }
}

// API 访问代理 - 实现权限控制
interface ApiService {
  getData(endpoint: string): Promise<any>;
  postData(endpoint: string, data: any): Promise<any>;
  deleteData(endpoint: string): Promise<any>;
}

// 真实 API 服务
class RealApiService implements ApiService {
  async getData(endpoint: string) {
    const response = await fetch(endpoint);
    return response.json();
  }

  async postData(endpoint: string, data: any) {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    return response.json();
  }

  async deleteData(endpoint: string) {
    const response = await fetch(endpoint, { method: 'DELETE' });
    return response.json();
  }
}

// 权限代理
type UserRole = 'admin' | 'editor' | 'viewer';

class AuthorizedApiProxy implements ApiService {
  private realService: RealApiService;
  private userRole: UserRole;

  // 权限配置
  private permissions: Record<string, UserRole[]> = {
    getData: ['admin', 'editor', 'viewer'],
    postData: ['admin', 'editor'],
    deleteData: ['admin']
  };

  constructor(userRole: UserRole) {
    this.realService = new RealApiService();
    this.userRole = userRole;
  }

  private checkPermission(action: string): boolean {
    const allowedRoles = this.permissions[action] || [];
    return allowedRoles.includes(this.userRole);
  }

  async getData(endpoint: string) {
    if (!this.checkPermission('getData')) {
      throw new Error('权限不足:无法读取数据');
    }
    return this.realService.getData(endpoint);
  }

  async postData(endpoint: string, data: any) {
    if (!this.checkPermission('postData')) {
      throw new Error('权限不足:无法创建数据');
    }
    return this.realService.postData(endpoint, data);
  }

  async deleteData(endpoint: string) {
    if (!this.checkPermission('deleteData')) {
      throw new Error('权限不足:无法删除数据');
    }
    return this.realService.deleteData(endpoint);
  }
}

export { LazyImageProxy, AuthorizedApiProxy, type UserRole };

使用示例

typescript
// 图片懒加载
const imageProxy = new LazyImageProxy('/loading.gif');

// 在视口内时才加载
const observer = new IntersectionObserver(async (entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      const img = entry.target as HTMLImageElement;
      const realImg = await imageProxy.load(img.dataset.src!);
      img.src = realImg.src;
      observer.unobserve(img);
    }
  }
});

// 权限控制
const viewerApi = new AuthorizedApiProxy('viewer');
const adminApi = new AuthorizedApiProxy('admin');

// viewer 只能读取
await viewerApi.getData('/api/posts');     // 成功
await viewerApi.postData('/api/posts', {}); // 抛出错误:权限不足

// admin 可以执行所有操作
await adminApi.getData('/api/posts');      // 成功
await adminApi.postData('/api/posts', {}); // 成功
await adminApi.deleteData('/api/posts/1'); // 成功

应用场景

  • 延迟加载:图片懒加载、组件按需加载
  • 权限控制:API 访问控制、功能权限
  • 缓存代理:缓存计算结果、API 响应
  • 日志代理:记录对象的访问和操作

七、核心原则

在实际项目中应用设计模式时,需要遵循以下原则:

1. 避免过度设计

typescript
// 不好的做法:简单场景使用复杂模式
class SimpleDataFactory {
  static create(type: string) {
    if (type === 'user') return { name: '', age: 0 };
  }
}

// 更好的做法:直接使用简单对象
const createUser = () => ({ name: '', age: 0 });

2. 关注代码可读性

设计模式是为了提升代码的可维护性,而非炫技。如果模式的引入让代码更难理解,那就不应该使用。

3. 结合框架特性

typescript
// React Hooks 可以替代部分传统模式

// 观察者模式 -> useContext + useState
const UserContext = createContext<User | null>(null);

function useUser() {
  const context = useContext(UserContext);
  if (!context) throw new Error('useUser must be used within UserProvider');
  return context;
}

// 单例模式 -> 模块级变量
// cache.ts
const cache = new Map();
export const getCache = () => cache;

设计模式选择指南

需要解耦组件通信? → 观察者模式
需要全局唯一实例? → 单例模式
需要动态创建对象? → 工厂模式
需要可切换的算法? → 策略模式
需要动态扩展功能? → 装饰者模式
需要控制对象访问? → 代理模式

总结

"

设计模式是前端开发中的重要工具,但不是万能解药。选择合适的模式需要根据具体场景权衡利弊。记住:简单的代码往往是最好的代码,只有当复杂性确实存在时,才引入设计模式来管理这种复杂性。

掌握这六种设计模式,能够帮助你编写出更加模块化、可测试、可维护的前端代码。在实际项目中,这些模式往往可以组合使用,发挥更大的威力。

文章标签

# JavaScript# TypeScript# 设计模式# 架构
返回首页