前端设计模式
大约 19 分钟约 5795 字
前端设计模式
简介
前端设计模式是将经典的 GoF 设计模式应用于 JavaScript/TypeScript 前端开发的实践总结。在 React、Vue、Angular 等现代框架中,观察者模式、策略模式、代理模式、装饰器模式等被广泛使用。理解这些模式不仅能帮助我们更好地使用框架,还能在日常开发中编写出更优雅、可维护的代码。本文将系统讲解前端开发中最常用的设计模式,并提供 TypeScript 实现代码。
特点
观察者模式(Observer)
EventEmitter 实现
/**
* 观察者模式 — 定义对象间一对多的依赖关系
* 当一个对象状态改变时,所有依赖它的对象都会收到通知
*
* 前端应用场景:
* - Vue 的响应式系统(依赖收集 + 派发更新)
* - React 的状态管理触发重新渲染
* - DOM 事件系统(addEventListener)
* - Redux 的 subscribe 机制
* - 组件间通信(EventBus)
*/
// --- 手动实现 EventEmitter ---
class EventEmitter<T extends Record<string, any[]>> {
private listeners: Map<keyof T, Set<(...args: any[]) => void>> = new Map();
/**
* 订阅事件
*/
on<K extends keyof T>(event: K, callback: (...args: T[K]) => void): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
// 返回取消订阅函数
return () => this.off(event, callback);
}
/**
* 取消订阅
*/
off<K extends keyof T>(event: K, callback: (...args: T[K]) => void): void {
this.listeners.get(event)?.delete(callback);
}
/**
* 触发事件
*/
emit<K extends keyof T>(event: K, ...args: T[K]): void {
this.listeners.get(event)?.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Event handler error for "${String(event)}":`, error);
}
});
}
/**
* 只监听一次
*/
once<K extends keyof T>(event: K, callback: (...args: T[K]) => void): () => void {
const wrapper = (...args: T[K]) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
/**
* 移除所有监听器
*/
removeAllListeners(event?: keyof T): void {
if (event) {
this.listeners.delete(event);
} else {
this.listeners.clear();
}
}
}
// --- 使用示例 ---
// 定义事件类型(类型安全)
interface AppEvents {
'user:login': [userId: string, timestamp: Date];
'user:logout': [reason: string];
'cart:update': [items: CartItem[], total: number];
'notification': [message: string, type: 'info' | 'warning' | 'error'];
}
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
// 创建类型安全的事件总线
const eventBus = new EventEmitter<AppEvents>();
// 订阅事件(自动推断参数类型)
const unsubscribe = eventBus.on('user:login', (userId, timestamp) => {
console.log(`用户 ${userId} 登录于 ${timestamp.toISOString()}`);
});
eventBus.on('cart:update', (items, total) => {
console.log(`购物车更新: ${items.length} 件商品,总计 ${total}`);
});
// 触发事件
eventBus.emit('user:login', 'user-123', new Date());
eventBus.emit('cart:update', [{ id: '1', name: 'Book', price: 29.9, quantity: 2 }], 59.8);
// 取消订阅
unsubscribe();React 中的观察者模式应用
/**
* React 状态管理 — 基于观察者模式的自定义 Store
* 类似 Zustand 的简化实现
*/
class SimpleStore<T> {
private state: T;
private listeners: Set<(state: T) => void> = new Set();
constructor(initialState: T) {
this.state = initialState;
}
getState(): T {
return this.state;
}
setState(partial: Partial<T> | ((prev: T) => Partial<T>)): void {
const newState = typeof partial === 'function'
? { ...this.state, ...partial(this.state) }
: { ...this.state, ...partial };
this.state = newState;
this.listeners.forEach(listener => listener(newState));
}
subscribe(listener: (state: T) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}
// 使用示例
interface AppState {
user: { id: string; name: string } | null;
theme: 'light' | 'dark';
notifications: string[];
}
const appStore = new Store<AppState>({
user: null,
theme: 'light',
notifications: [],
});
// 在 React 组件中使用
function useStore<T>(store: Store<T>): T {
const [state, setState] = React.useState(store.getState());
React.useEffect(() => {
return store.subscribe(setState);
}, [store]);
return state;
}
// 组件
function UserProfile() {
const state = useStore(appStore);
if (!state.user) return <div>未登录</div>;
return (
<div>
<p>欢迎, {state.user.name}</p>
<button onClick={() => appStore.setState({ user: null })}>
退出
</button>
</div>
);
}策略模式(Strategy)
表单验证策略
/**
* 策略模式 — 定义一系列算法,将每个算法封装起来,使它们可以互换
*
* 前端应用场景:
* - 表单验证(不同字段不同验证规则)
* - 排序算法切换
* - 支付方式选择
* - 主题切换
*/
// --- 验证策略定义 ---
type ValidationResult = { valid: boolean; message?: string };
interface ValidationStrategy {
validate(value: any): ValidationResult;
}
// 具体策略:必填校验
class RequiredStrategy implements ValidationStrategy {
constructor(private message: string = '此字段为必填项') {}
validate(value: any): ValidationResult {
const valid = value !== null && value !== undefined && String(value).trim() !== '';
return { valid, message: valid ? undefined : this.message };
}
}
// 具体策略:最小长度
class MinLengthStrategy implements ValidationStrategy {
constructor(private minLength: number, private message?: string) {}
validate(value: any): ValidationResult {
const valid = String(value || '').length >= this.minLength;
return {
valid,
message: valid ? undefined : (this.message || `最少 ${this.minLength} 个字符`)
};
}
}
// 具体策略:邮箱格式
class EmailStrategy implements ValidationStrategy {
private emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
validate(value: any): ValidationResult {
const valid = this.emailRegex.test(String(value || ''));
return { valid, message: valid ? undefined : '请输入有效的邮箱地址' };
}
}
// 具体策略:手机号格式
class PhoneStrategy implements ValidationStrategy {
private phoneRegex = /^1[3-9]\d{9}$/;
validate(value: any): ValidationResult {
const valid = this.phoneRegex.test(String(value || ''));
return { valid, message: valid ? undefined : '请输入有效的手机号码' };
}
}
// 具体策略:自定义正则
class PatternStrategy implements ValidationStrategy {
constructor(private pattern: RegExp, private message: string) {}
validate(value: any): ValidationResult {
const valid = this.pattern.test(String(value || ''));
return { valid, message: valid ? undefined : this.message };
}
}
// --- 策略上下文:表单验证器 ---
class FormValidator {
private fieldRules: Map<string, ValidationStrategy[]> = new Map();
// 为字段添加验证规则(可链式调用)
addRule(fieldName: string, ...strategies: ValidationStrategy[]): FormValidator {
const existing = this.fieldRules.get(fieldName) || [];
this.fieldRules.set(fieldName, [...existing, ...strategies]);
return this;
}
// 验证整个表单
validate(formData: Record<string, any>): { valid: boolean; errors: Record<string, string[]> } {
const errors: Record<string, string[]> = {};
let allValid = true;
for (const [fieldName, strategies] of this.fieldRules) {
const value = formData[fieldName];
const fieldErrors: string[] = [];
for (const strategy of strategies) {
const result = strategy.validate(value);
if (!result.valid) {
fieldErrors.push(result.message || '验证失败');
allValid = false;
}
}
if (fieldErrors.length > 0) {
errors[fieldName] = fieldErrors;
}
}
return { valid: allValid, errors };
}
}
// --- 使用示例 ---
const validator = new FormValidator();
validator
.addRule('username', new RequiredStrategy(), new MinLengthStrategy(3, '用户名至少 3 个字符'))
.addRule('email', new RequiredStrategy(), new EmailStrategy())
.addRule('phone', new RequiredStrategy(), new PhoneStrategy())
.addRule('password', new RequiredStrategy(), new MinLengthStrategy(8, '密码至少 8 位'));
// 验证表单
const result = validator.validate({
username: 'ab', // 太短
email: 'invalid', // 格式错误
phone: '1234567', // 格式错误
password: '1234567', // 太短
});
console.log(result);
// { valid: false, errors: { username: [...], email: [...], phone: [...], password: [...] } }支付策略
/**
* 策略模式 — 支付方式选择
*/
interface PaymentStrategy {
pay(amount: number): Promise<{ success: boolean; transactionId: string }>;
}
class AlipayStrategy implements PaymentStrategy {
async pay(amount: number) {
console.log(`支付宝支付: ¥${amount}`);
// 调用支付宝 SDK
return { success: true, transactionId: `ALI_${Date.now()}` };
}
}
class WechatPayStrategy implements PaymentStrategy {
async pay(amount: number) {
console.log(`微信支付: ¥${amount}`);
// 调用微信支付 SDK
return { success: true, transactionId: `WX_${Date.now()}` };
}
}
class CreditCardStrategy implements PaymentStrategy {
constructor(private cardNumber: string, private cvv: string) {}
async pay(amount: number) {
console.log(`信用卡支付: ¥${amount}`);
return { success: true, transactionId: `CC_${Date.now()}` };
}
}
// 支付上下文
class PaymentContext {
private strategy: PaymentStrategy;
constructor(strategy: PaymentStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
async checkout(amount: number) {
return this.strategy.pay(amount);
}
}
// 使用
const payment = new PaymentContext(new AlipayStrategy());
await payment.checkout(99.9);
// 切换支付方式
payment.setStrategy(new WechatPayStrategy());
await payment.checkout(199.9);装饰器模式(Decorator)
高阶组件(HOC)与装饰器
/**
* 装饰器模式 — 动态地给对象添加额外职责
*
* 前端应用场景:
* - React HOC(高阶组件)
* - TypeScript 装饰器(类/方法/属性)
* - 中间件模式(Redux/Express)
* - 功能增强(日志、权限、缓存)
*/
// --- 方法装饰器:性能测量 ---
function measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
console.log(`${propertyKey} 执行耗时: ${duration.toFixed(2)}ms`);
return result;
};
return descriptor;
}
// --- 方法装饰器:防抖 ---
function debounce(delay: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let timeoutId: ReturnType<typeof setTimeout>;
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => originalMethod.apply(this, args), delay);
};
return descriptor;
};
}
// --- 方法装饰器:错误处理 ---
function catchError(defaultValue?: any) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
console.error(`${propertyKey} 执行失败:`, error);
return defaultValue;
}
};
return descriptor;
};
}
// --- 使用装饰器的服务类 ---
class DataService {
@measure
@catchError([])
async fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
return response.json();
}
@measure
@catchError(null)
async fetchUserById(id: string): Promise<User | null> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
@debounce(300)
@catchError([])
async searchUsers(keyword: string): Promise<User[]> {
const response = await fetch(`/api/users/search?q=${keyword}`);
return response.json();
}
}
interface User {
id: string;
name: string;
email: string;
}
// --- React HOC 装饰器模式 ---
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
): React.FC<P & { loading?: boolean }> {
return function WithLoadingComponent({ loading, ...props }) {
if (loading) {
return <div className="loading-spinner">加载中...</div>;
}
return <WrappedComponent {...(props as P)} />;
};
}
function withErrorBoundary<P extends object>(
WrappedComponent: React.ComponentType<P>
): React.FC<P> {
return class WithErrorBoundary extends React.Component<P, { hasError: boolean; error?: Error }> {
state = { hasError: false, error: undefined };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <div className="error">出错了: {this.state.error?.message}</div>;
}
return <WrappedComponent {...this.props} />;
}
};
}
// 组合使用
// const EnhancedComponent = withErrorBoundary(withLoading(UserList));代理模式(Proxy)
Vue3 响应式原理
/**
* 代理模式 — 为其他对象提供代理以控制对这个对象的访问
*
* 前端应用场景:
* - Vue3 的 Proxy 响应式系统
* - API 请求代理(缓存、重试)
* - 图片懒加载代理
* - 访问控制代理
*/
// --- 简化版 Vue3 响应式系统 ---
type EffectFn = () => void;
type Deps = Set<EffectFn>;
type DepsMap = Map<string, Deps>;
let activeEffect: EffectFn | null = null;
function effect(fn: EffectFn) {
activeEffect = fn;
fn(); // 立即执行一次,收集依赖
activeEffect = null;
}
function reactive<T extends object>(target: T): T {
const depsMap: DepsMap = new Map();
const handler: ProxyHandler<T> = {
get(obj, key: string) {
// 依赖收集
if (activeEffect) {
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
return Reflect.get(obj, key);
},
set(obj, key: string, value) {
const result = Reflect.set(obj, key, value);
// 派发更新
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effectFn => effectFn());
}
return result;
}
};
return new Proxy(target, handler);
}
// --- 使用示例 ---
const state = reactive({
count: 0,
name: 'World',
});
effect(() => {
console.log(`Hello ${state.name}! Count: ${state.count}`);
// 输出: Hello World! Count: 0
});
state.count++;
// 自动触发 effect,输出: Hello World! Count: 1
state.name = 'Vue';
// 自动触发 effect,输出: Hello Vue! Count: 1API 代理 — 缓存代理
/**
* API 缓存代理 — 自动缓存 GET 请求结果
*/
class ApiCacheProxy {
private cache: Map<string, { data: any; timestamp: number }> = new Map();
private ttl: number;
constructor(private apiClient: ApiClient, ttlMs: number = 60000) {
this.ttl = ttlMs;
}
async get<T>(url: string): Promise<T> {
const cached = this.cache.get(url);
const now = Date.now();
// 缓存命中且未过期
if (cached && now - cached.timestamp < this.ttl) {
console.log(`缓存命中: ${url}`);
return cached.data as T;
}
// 缓存未命中,请求 API
console.log(`请求 API: ${url}`);
const data = await this.apiClient.get<T>(url);
this.cache.set(url, { data, timestamp: now });
return data;
}
// POST/PUT/DELETE 不缓存,但清除相关缓存
async post<T>(url: string, body: any): Promise<T> {
this.invalidateCache(url);
return this.apiClient.post<T>(url, body);
}
async put<T>(url: string, body: any): Promise<T> {
this.invalidateCache(url);
return this.apiClient.put<T>(url, body);
}
async delete(url: string): Promise<void> {
this.invalidateCache(url);
await this.apiClient.delete(url);
}
// 清除相关缓存
private invalidateCache(url: string): void {
const basePath = url.split('/').slice(0, -1).join('/');
for (const key of this.cache.keys()) {
if (key.startsWith(basePath)) {
this.cache.delete(key);
}
}
}
}
interface ApiClient {
get<T>(url: string): Promise<T>;
post<T>(url: string, body: any): Promise<T>;
put<T>(url: string, body: any): Promise<T>;
delete(url: string): Promise<void>;
}工厂模式(Factory)
组件工厂
/**
* 工厂模式 — 将对象的创建过程封装起来
*
* 前端应用场景:
* - UI 组件动态创建
* - 图表类型工厂
* - 表单字段工厂
* - 消息提示工厂(Toast/Notification)
*/
// --- 表单字段工厂 ---
type FieldType = 'text' | 'email' | 'password' | 'number' | 'select' | 'checkbox' | 'textarea';
interface FieldConfig {
type: FieldType;
name: string;
label: string;
placeholder?: string;
required?: boolean;
options?: { label: string; value: string }[];
defaultValue?: any;
}
// React 组件工厂
class FormFieldFactory {
private static componentMap: Map<FieldType, React.FC<FieldConfig>> = new Map();
static register(type: FieldType, component: React.FC<FieldConfig>): void {
this.componentMap.set(type, component);
}
static create(config: FieldConfig): React.ReactElement | null {
const Component = this.componentMap.get(config.type);
if (!Component) {
console.warn(`未注册的字段类型: ${config.type}`);
return null;
}
return React.createElement(Component, config);
}
static createForm(configs: FieldConfig[]): React.ReactElement[] {
return configs
.map(config => this.create(config))
.filter((el): el is React.ReactElement => el !== null);
}
}
// 注册组件
FormFieldFactory.register('text', ({ name, label, placeholder }) => (
<div className="form-field">
<label>{label}</label>
<input type="text" name={name} placeholder={placeholder} />
</div>
));
FormFieldFactory.register('email', ({ name, label, placeholder }) => (
<div className="form-field">
<label>{label}</label>
<input type="email" name={name} placeholder={placeholder} />
</div>
));
FormFieldFactory.register('select', ({ name, label, options }) => (
<div className="form-field">
<label>{label}</label>
<select name={name}>
{options?.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
</div>
));
// 使用工厂创建表单
const formConfig: FieldConfig[] = [
{ type: 'text', name: 'name', label: '姓名', required: true },
{ type: 'email', name: 'email', label: '邮箱', required: true },
{ type: 'select', name: 'role', label: '角色', options: [
{ label: '管理员', value: 'admin' },
{ label: '用户', value: 'user' },
]},
];
function DynamicForm() {
return <form>{FormFieldFactory.createForm(formConfig)}</form>;
}Toast 消息工厂
/**
* Toast 消息工厂 — 统一的消息提示创建
*/
type ToastType = 'success' | 'error' | 'warning' | 'info';
interface ToastConfig {
type: ToastType;
message: string;
duration?: number;
closable?: boolean;
}
class ToastFactory {
private static container: HTMLElement | null = null;
private static getContainer(): HTMLElement {
if (!this.container) {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
}
return this.container;
}
private static iconMap: Record<ToastType, string> = {
success: '\u2713',
error: '\u2717',
warning: '\u26A0',
info: '\u2139',
};
private static colorMap: Record<ToastType, string> = {
success: '#52c41a',
error: '#ff4d4f',
warning: '#faad14',
info: '#1890ff',
};
static show(config: ToastConfig): () => void {
const container = this.getContainer();
const toast = document.createElement('div');
const color = this.colorMap[config.type];
const icon = this.iconMap[config.type];
toast.className = 'toast-item';
toast.style.borderLeft = `4px solid ${color}`;
toast.innerHTML = `
<span class="toast-icon" style="color:${color}">${icon}</span>
<span class="toast-message">${config.message}</span>
${config.closable !== false ? '<button class="toast-close">×</button>' : ''}
`;
container.appendChild(toast);
// 关闭函数
const close = () => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
};
// 关闭按钮
toast.querySelector('.toast-close')?.addEventListener('click', close);
// 自动关闭
if (config.duration !== 0) {
setTimeout(close, config.duration || 3000);
}
return close;
}
static success(message: string, duration?: number) {
return this.show({ type: 'success', message, duration });
}
static error(message: string, duration?: number) {
return this.show({ type: 'error', message, duration: duration || 5000 });
}
static warning(message: string, duration?: number) {
return this.show({ type: 'warning', message, duration });
}
static info(message: string, duration?: number) {
return this.show({ type: 'info', message, duration });
}
}
// 使用
ToastFactory.success('保存成功!');
ToastFactory.error('网络请求失败', 5000);
ToastFactory.warning('请注意,数据可能不完整');单例模式(Singleton)
全局状态管理
/**
* 单例模式 — 保证一个类只有一个实例
*
* 前端应用场景:
* - 全局状态 Store
* - 配置管理
* - WebSocket 连接管理
* - 日志服务
*/
// --- 线程安全的单例(ES Module 天然单例)---
class ConfigManager {
private static instance: ConfigManager | null = null;
private config: Map<string, any> = new Map();
private constructor() {
// 私有构造函数防止外部 new
}
static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
get<T>(key: string, defaultValue?: T): T | undefined {
return this.config.get(key) ?? defaultValue;
}
set(key: string, value: any): void {
this.config.set(key, value);
}
loadFromEnv(env: Record<string, string | undefined>): void {
Object.entries(env).forEach(([key, value]) => {
if (value !== undefined) {
this.config.set(key, value);
}
});
}
}
// --- WebSocket 单例管理 ---
class WebSocketManager {
private static instance: WebSocketManager | null = null;
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private listeners: Map<string, Set<(data: any) => void>> = new Map();
private constructor(private url: string) {}
static getInstance(url?: string): WebSocketManager {
if (!WebSocketManager.instance && url) {
WebSocketManager.instance = new WebSocketManager(url);
}
return WebSocketManager.instance!;
}
connect(): void {
if (this.ws?.readyState === WebSocket.OPEN) return;
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket 已连接');
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const type = data.type || 'default';
this.listeners.get(type)?.forEach(cb => cb(data.payload));
};
this.ws.onclose = () => {
console.log('WebSocket 已断开');
this.attemptReconnect();
};
}
on(type: string, callback: (data: any) => void): () => void {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(callback);
return () => this.listeners.get(type)?.delete(callback);
}
send(type: string, payload: any): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type, payload }));
}
}
private attemptReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('WebSocket 重连失败');
return;
}
const delay = Math.pow(2, this.reconnectAttempts) * 1000;
this.reconnectAttempts++;
setTimeout(() => {
console.log(`尝试第 ${this.reconnectAttempts} 次重连...`);
this.connect();
}, delay);
}
}适配器模式(Adapter)
API 适配层
/**
* 适配器模式 — 将一个类的接口转换为客户期望的另一个接口
*
* 前端应用场景:
* - API 响应格式适配
* - 第三方库接口统一
* - 旧接口兼容新代码
*/
// --- API 适配器 ---
// 后端返回的原始数据格式
interface ApiUser {
user_id: string;
user_name: string;
email_address: string;
avatar_url: string | null;
created_at: string;
is_active: number; // 0 或 1
}
// 前端期望的数据格式
interface AppUser {
id: string;
name: string;
email: string;
avatar: string;
createdAt: Date;
active: boolean;
}
// 适配器
class UserAdapter {
static toAppUser(apiUser: ApiUser): AppUser {
return {
id: apiUser.user_id,
name: apiUser.user_name,
email: apiUser.email_address,
avatar: apiUser.avatar_url || '/default-avatar.png',
createdAt: new Date(apiUser.created_at),
active: apiUser.is_active === 1,
};
}
static toApiUser(appUser: Partial<AppUser>): Partial<ApiUser> {
const result: Partial<ApiUser> = {};
if (appUser.id !== undefined) result.user_id = appUser.id;
if (appUser.name !== undefined) result.user_name = appUser.name;
if (appUser.email !== undefined) result.email_address = appUser.email;
if (appUser.avatar !== undefined) result.avatar_url = appUser.avatar;
if (appUser.active !== undefined) result.is_active = appUser.active ? 1 : 0;
return result;
}
}
// 在 API 层统一使用适配器
class UserRepository {
async getUser(id: string): Promise<AppUser> {
const response = await fetch(`/api/users/${id}`);
const apiUser: ApiUser = await response.json();
return UserAdapter.toAppUser(apiUser);
}
async updateUser(user: Partial<AppUser>): Promise<AppUser> {
const apiData = UserAdapter.toApiUser(user);
const response = await fetch(`/api/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const apiUser: ApiUser = await response.json();
return UserAdapter.toAppUser(apiUser);
}
}外观模式(Facade)
SDK 封装
/**
* 外观模式 — 为子系统中的一组接口提供统一的高层接口
*
* 前端应用场景:
* - SDK 封装(简化复杂的 API 调用)
* - 组件库的统一入口
* - 工具函数库
*/
// --- 第三方地图 SDK 外观封装 ---
class MapFacade {
private map: any;
private markers: Map<string, any> = new Map();
private geocoder: any;
constructor(containerId: string) {
// 隐藏复杂的初始化逻辑
const container = document.getElementById(containerId)!;
this.map = new ThirdPartyMap(container, {
center: { lat: 39.9042, lng: 116.4074 },
zoom: 12,
style: 'normal',
});
this.geocoder = new ThirdPartyGeocoder();
}
// 简化的添加标记方法
addMarker(id: string, lat: number, lng: number, title: string): void {
const marker = this.map.addMarker({
position: { lat, lng },
title,
animation: 'drop',
});
this.markers.set(id, marker);
}
// 简化的地址搜索
async searchAddress(address: string): Promise<{ lat: number; lng: number; formatted: string }[]> {
const results = await this.geocoder.geocode(address);
return results.map((r: any) => ({
lat: r.geometry.location.lat(),
lng: r.geometry.location.lng(),
formatted: r.formatted_address,
}));
}
// 简化的导航
navigateTo(markerId: string): void {
const marker = this.markers.get(markerId);
if (marker) {
this.map.panTo(marker.getPosition());
this.map.setZoom(16);
}
}
// 简化的清除标记
clearMarkers(): void {
this.markers.forEach(marker => marker.remove());
this.markers.clear();
}
destroy(): void {
this.map.destroy();
this.markers.clear();
}
}
// 使用外观后的简洁 API
const map = new MapFacade('map-container');
map.addMarker('office', 39.9042, 116.4074, '公司');
map.addMarker('home', 39.9142, 116.4174, '家');
map.navigateTo('office');命令模式(Command)
撤销/重做实现
/**
* 命令模式 — 将请求封装为对象,支持撤销和重做
*
* 前端应用场景:
* - 文本编辑器的撤销/重做
* - 画布操作的撤销/重做
* - 表单操作历史
*/
interface Command {
execute(): void;
undo(): void;
describe(): string;
}
class CommandManager {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
private maxHistory = 50;
execute(command: Command): void {
command.execute();
this.undoStack.push(command);
this.redoStack = []; // 新命令清空 redo 栈
if (this.undoStack.length > this.maxHistory) {
this.undoStack.shift();
}
}
undo(): boolean {
const command = this.undoStack.pop();
if (!command) return false;
command.undo();
this.redoStack.push(command);
return true;
}
redo(): boolean {
const command = this.redoStack.pop();
if (!command) return false;
command.execute();
this.undoStack.push(command);
return true;
}
canUndo(): boolean { return this.undoStack.length > 0; }
canRedo(): boolean { return this.redoStack.length > 0; }
getHistory(): string[] {
return this.undoStack.map(cmd => cmd.describe());
}
}
// --- 画布操作命令 ---
interface CanvasState {
elements: any[];
}
class CanvasEditor {
state: CanvasState = { elements: [] };
commandManager = new CommandManager();
addElement(element: any): void {
this.commandManager.execute({
execute: () => this.state.elements.push(element),
undo: () => this.state.elements.pop(),
describe: () => `添加元素: ${element.type}`,
});
}
removeElement(index: number): void {
const removed = this.state.elements[index];
this.commandManager.execute({
execute: () => this.state.elements.splice(index, 1),
undo: () => this.state.elements.splice(index, 0, removed),
describe: () => `删除元素: ${removed.type}`,
});
}
moveElement(index: number, newX: number, newY: number): void {
const element = this.state.elements[index];
const oldX = element.x;
const oldY = element.y;
this.commandManager.execute({
execute: () => { element.x = newX; element.y = newY; },
undo: () => { element.x = oldX; element.y = oldY; },
describe: () => `移动元素到 (${newX}, ${newY})`,
});
}
undo() { return this.commandManager.undo(); }
redo() { return this.commandManager.redo(); }
}中介者模式(Mediator)
事件总线
/**
* 中介者模式 — 用一个中介对象封装一系列对象的交互
*
* 前端应用场景:
* - 组件间通信(EventBus)
* - 表单联动
* - 模块间解耦
*/
// --- 跨组件通信中介者 ---
class ComponentMediator {
private channels: Map<string, Map<string, (...args: any[]) => void>> = new Map();
/**
* 注册组件到频道
*/
register(channel: string, componentId: string, handler: (...args: any[]) => void): void {
if (!this.channels.has(channel)) {
this.channels.set(channel, new Map());
}
this.channels.get(channel)!.set(componentId, handler);
}
/**
* 从频道注销组件
*/
unregister(channel: string, componentId: string): void {
this.channels.get(channel)?.delete(componentId);
}
/**
* 在频道上广播消息
*/
broadcast(channel: string, senderId: string, data: any): void {
const subscribers = this.channels.get(channel);
if (!subscribers) return;
subscribers.forEach((handler, id) => {
if (id !== senderId) { // 不通知发送者自己
handler(data);
}
});
}
}
// 使用示例:表单联动
const mediator = new ComponentMediator();
// 省份选择器 — 选择省份后广播
mediator.register('region', 'province-select', (data) => {
// 收到其他组件的消息
});
// 城市选择器 — 监听省份变化
mediator.register('region', 'city-select', (data) => {
if (data.type === 'province-changed') {
// 根据省份加载城市列表
console.log(`加载 ${data.province} 的城市列表`);
}
});
// 区县选择器 — 监听城市变化
mediator.register('region', 'district-select', (data) => {
if (data.type === 'city-changed') {
console.log(`加载 ${data.city} 的区县列表`);
}
});
// 广播省份变化
mediator.broadcast('region', 'province-select', {
type: 'province-changed',
province: '上海',
});模块模式(Module)
模块化封装
/**
* 模块模式 — 通过闭包实现封装和信息隐藏
* TypeScript/ES Module 天然支持模块模式
*/
// --- 工具模块 ---
const MathUtils = (() => {
// 私有变量和函数(不导出)
const PI = Math.PI;
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
// 公共 API
return {
toRadians(degrees: number): number {
return degrees * (PI / 180);
},
toDegrees(radians: number): number {
return radians * (180 / PI);
},
distance(x1: number, y1: number, x2: number, y2: number): number {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
},
lerp(start: number, end: number, t: number): number {
return start + (end - start) * clamp(t, 0, 1);
},
randomInRange(min: number, max: number): number {
return Math.random() * (max - min) + min;
},
};
})();
// 使用
const angle = MathUtils.toRadians(90);
const dist = MathUtils.distance(0, 0, 3, 4);优点
缺点
性能注意事项
总结
前端设计模式是经典 GoF 模式在 JavaScript/TypeScript 环境下的实践。观察者模式是所有响应式框架的基础;策略模式让表单验证和算法切换更灵活;装饰器模式是 React HOC 和 TypeScript 装饰器的理论基础;代理模式是 Vue3 响应式的核心;命令模式支持撤销重做。正确运用这些模式可以显著提升前端代码的质量和可维护性。
关键知识点
- 观察者模式是 Vue 响应式和 React 状态管理的基础
- 策略模式通过组合替代继承,适合表单验证等场景
- 装饰器模式在 TypeScript 装饰器和 React HOC 中广泛使用
- Vue3 使用 Proxy 实现响应式,替代了 Vue2 的 Object.defineProperty
- 工厂模式适合动态创建 UI 组件
- 单例模式适合全局配置和连接管理
- 命令模式支持撤销/重做操作
- 中介者模式解耦组件间的直接依赖
常见误区
- 误区:每个功能都要用设计模式
纠正:只在确实需要解决特定问题时使用模式 - 误区:TypeScript 装饰器和 GoF 装饰器完全相同
纠正:TS 装饰器是语言特性,GoF 装饰器是设计思想,概念相关但实现不同 - 误区:单例模式是全局变量的替代
纠正:单例提供受控的访问和初始化,比全局变量更安全 - 误区:观察者模式就是 addEventListener
纠正:DOM 事件是观察者的一种实现,但观察者模式的应用更广泛
进阶路线
- 初级:掌握观察者、策略、工厂等基础模式
- 中级:理解装饰器、代理、命令模式,在项目中实践
- 高级:组合多种模式解决复杂问题(如状态管理库的设计)
- 专家级:设计前端框架/库,将模式融入 API 设计
适用场景
- 中大型前端项目的架构设计
- 需要高可维护性的企业级应用
- 组件库和工具库开发
- 状态管理方案设计
- 复杂交互功能(拖拽、画布、编辑器)
落地建议
- 团队统一学习 5-6 个最常用的模式
- 在 Code Review 中识别可以用模式优化的代码
- 建立项目级的设计模式使用规范
- 不强制使用模式,以解决问题为导向
- 重构时考虑引入合适的模式
排错清单
复盘问题
- Vue3 为什么从 Object.defineProperty 切换到 Proxy?
- React Hooks 是否替代了 HOC(装饰器模式)?
- 如何在 TypeScript 中实现类型安全的事件总线?
- 命令模式如何支持持久化(保存到 localStorage)?
- 中介者模式和观察者模式有什么区别?
- 如何避免工厂模式中的 switch-case 反模式?
- 在微前端架构中,如何使用适配器模式统一不同子应用的 API?
- 如何测试使用了设计模式的代码?
