什么是代理模式?
代理模式(Proxy Pattern)是一種結構型設計模式,它通過創建代理對象來控制對原始對象的訪問。
這種模式在前端開發中廣泛應用,特別是在需要控制對象訪問、添加額外邏輯或優化性能的場景中。
??核心思想??:在客戶端代碼和真實對象之間添加中間層,這個中間層(代理)可以:
- 控制對原始對象的訪問權限
- 添加預處理/后處理邏輯
- 實現延遲加載
- 緩存昂貴操作的結果
- 記錄日志或監控行為
代理模式實現方式
1. 虛擬代理(延遲加載)
// 原始圖片加載器
class ImageLoader {constructor(url) {this.url = url;}load() {console.log(`Loading image from ${this.url}`);return `<img src="${this.url}" />`;}
}// 虛擬代理 - 延遲加載
class ImageProxy {constructor(url) {this.url = url;this.imageLoader = null; // 延遲初始化}load() {if (!this.imageLoader) {this.imageLoader = new ImageLoader(this.url);// 添加占位邏輯const placeholder = document.createElement('div');placeholder.style.width = '300px';placeholder.style.height = '200px';placeholder.style.background = '#eee';document.body.appendChild(placeholder);// 延遲實際加載setTimeout(() => {document.body.removeChild(placeholder);document.body.innerHTML += this.imageLoader.load();}, 2000);}return 'Image loading initiated...';}
}// 使用代理
const image = new ImageProxy('https://example.com/large-image.jpg');
console.log(image.load()); // 立即返回占位符,2秒后加載真實圖片
2. 緩存代理(記憶函數)
// 原始計算函數
function fibonacci(n) {if (n <= 1) return n;return fibonacci(n - 1) + fibonacci(n - 2);
}// 緩存代理
function createCachedProxy(fn) {const cache = new Map();return function(n) {if (cache.has(n)) {console.log(`Cache hit for n=${n}`);return cache.get(n);}const result = fn(n);cache.set(n, result);console.log(`Calculated for n=${n}`);return result;};
}// 使用代理
const cachedFib = createCachedProxy(fibonacci);console.log(cachedFib(35)); // 長時間計算
console.log(cachedFib(35)); // 立即返回緩存結果
3. 保護代理(訪問控制)
// 原始用戶服務
class UserService {constructor() {this.users = new Map([[1, { id: 1, name: 'Admin', role: 'admin' }]]);}deleteUser(id) {this.users.delete(id);return `User ${id} deleted`;}
}// 保護代理
class UserServiceProxy {constructor(user) {this.userService = new UserService();this.currentUser = user;}deleteUser(id) {if (this.currentUser.role !== 'admin') {throw new Error('Permission denied');}return this.userService.deleteUser(id);}
}// 使用代理
const adminProxy = new UserServiceProxy({ role: 'admin' });
console.log(adminProxy.deleteUser(1)); // 成功const userProxy = new UserServiceProxy({ role: 'user' });
userProxy.deleteUser(1); // 拋出權限錯誤
4. ES6 Proxy實現
// 原始對象
const database = {users: {1: { name: 'Alice', email: 'alice@example.com' }},getEmail: function(userId) {return this.users[userId]?.email;}
};// 創建代理
const protectedDatabase = new Proxy(database, {get(target, prop) {// 權限驗證if (prop === 'users') {throw new Error('Direct access to users denied');}return target[prop];},set(target, prop, value) {// 寫操作限制if (prop === 'users') {throw new Error('User modification requires admin privileges');}target[prop] = value;return true;}
});console.log(protectedDatabase.getEmail(1)); // 正常訪問
protectedDatabase.users = {}; // 拋出錯誤
console.log(protectedDatabase.users); // 拋出錯誤
代理模式優缺點分析
優點:
- ??訪問控制??:實現精細的權限管理
// API請求代理示例
class ApiProxy {constructor(api) {this.api = api;}async request(endpoint) {if (isRateLimited(endpoint)) {throw new Error('API rate limit exceeded');}trackRequest(endpoint);return this.api.request(endpoint);}
}
- ??性能優化??:通過緩存和延遲加載提升性能
// 圖片懶加載代理
const lazyImage = new Proxy(new Image(), {set(target, prop, value) {if (prop === 'src') {// 延遲到元素可見時加載const observer = new IntersectionObserver((entries) => {entries.forEach(entry => {if (entry.isIntersecting) {target.src = value;observer.unobserve(target);}});});observer.observe(target);return true;}return Reflect.set(...arguments);}
});document.body.appendChild(lazyImage);
lazyImage.src = 'https://example.com/large-image.jpg'; // 實際加載延遲到圖片可見時
- ??職責分離??:保持核心邏輯的純凈性
// 原始表單驗證
class FormValidator {validateEmail(email) {return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);}
}// 驗證代理添加日志
class LoggingValidatorProxy {constructor(validator) {this.validator = validator;}validateEmail(email) {const result = this.validator.validateEmail(email);console.log(`Email validation result for ${email}: ${result}`);return result;}
}
缺點:
- ??復雜性增加??:可能引入額外抽象層
// 過度設計的代理示例(不推薦)
class OverEngineeredProxy {constructor(service) {this.service = service;this.logger = new Logger();this.cache = new Cache();this.validator = new Validator();}async getData() {this.logger.logStart();if (!this.validator.validate()) {throw new Error('Validation failed');}const data = await this.cache.get('data') || this.service.getData();this.logger.logEnd();return data;}
}
- ??性能損耗??:額外的代理調用開銷
// 性能敏感的原始類
class Vector {constructor(x, y) {this.x = x;this.y = y;}add(other) {return new Vector(this.x + other.x, this.y + other.y);}
}// 添加日志代理可能影響性能
const loggedVector = new Proxy(new Vector(1,2), {get(target, prop) {if (typeof target[prop] === 'function') {return function(...args) {console.log(`Calling ${prop} with`, args);return target[prop].apply(target, args);};}return target[prop];}
});// 在需要高性能計算的場景中,這種代理會產生明顯開銷
- ??調試困難??:調用堆棧變深
// 多層代理導致的調試問題
const original = { method() { console.log('Original method'); }
};const proxy1 = new Proxy(original, {get(target, prop) {console.log('Proxy1 handler');return target[prop];}
});const proxy2 = new Proxy(proxy1, {get(target, prop) {console.log('Proxy2 handler');return target[prop];}
});proxy2.method(); // 調用鏈:proxy2 -> proxy1 -> original
工程實踐建議
1. 表單驗證代理
// 原始表單對象
const form = {values: {},errors: {},submit() {console.log('Submitting:', this.values);}
};// 驗證代理
const validatedForm = new Proxy(form, {set(target, prop, value) {if (prop === 'values') {// 自動觸發驗證target.errors = validateForm(value);if (Object.keys(target.errors).length === 0) {target.submit();}}return Reflect.set(...arguments);}
});function validateForm(values) {const errors = {};if (!values.email?.includes('@')) errors.email = 'Invalid email';if (values.password?.length < 6) errors.password = 'Password too short';return errors;
}// 使用
validatedForm.values = { email: 'user@example.com', password: '12345'
}; // 自動觸發驗證并顯示錯誤
2. API請求代理
// 請求代理工廠
function createApiProxy(api, config = {}) {return new Proxy(api, {get(target, prop) {const originalMethod = target[prop];if (typeof originalMethod !== 'function') return originalMethod;return async function(...args) {// 請求攔截if (config.beforeRequest) {args = config.beforeRequest(...args) || args;}try {const response = await originalMethod.apply(target, args);// 響應攔截return config.afterResponse ? config.afterResponse(response) : response;} catch (error) {// 錯誤處理if (config.errorHandler) {return config.errorHandler(error);}throw error;}};}});
}// 使用示例
const rawApi = {async getUser(id) {const res = await fetch(`/api/users/${id}`);return res.json();}
};const enhancedApi = createApiProxy(rawApi, {beforeRequest: (id) => {console.log(`Requesting user ${id}`);return [id]; // 可以修改參數},afterResponse: (data) => {console.log('Received response');return { ...data, fullName: `${data.firstName} ${data.lastName}` };},errorHandler: (error) => {console.error('API Error:', error);return { error: true, message: error.message };}
});// 調用方式保持一致
enhancedApi.getUser(123).then(console.log);
注意事項
- ??接口一致性??:代理必須保持與原對象相同的接口
// 錯誤的代理實現(接口不一致)
class BadProxy {constructor(file) {this.file = file;}// 遺漏了原始對象的save方法read() {return this.file.read();}
}
- ??避免深層代理嵌套??
// 不推薦的深層嵌套
const proxy1 = new Proxy(obj, handler1);
const proxy2 = new Proxy(proxy1, handler2);
const proxy3 = new Proxy(proxy2, handler3);
// 應盡量保持代理層級扁平
- ??注意內存管理??
// 代理導致的閉包內存泄漏
function createLeakyProxy() {const hugeData = new Array(1000000).fill('data');return new Proxy({}, {get(target, prop) {// 無意中持有hugeData的引用return hugeData[prop];}});
}
- ??性能關鍵路徑慎用??
// 在動畫循環中避免使用復雜代理
function animate() {// 直接訪問對象屬性element.x += velocity.x;element.y += velocity.y;// 而不是:// proxiedElement.x += velocity.x;requestAnimationFrame(animate);
}
- ??與裝飾器模式區分??
// 裝飾器模式(增強功能)
function withLogging(fn) {return function(...args) {console.log('Calling function');return fn(...args);};
}// 代理模式(控制訪問)
const proxiedFn = new Proxy(fn, {apply(target, thisArg, args) {if (!validate(args)) throw new Error('Invalid arguments');return Reflect.apply(target, thisArg, args);}
});
代理模式是前端架構中的重要模式,適用于:
- 需要訪問控制的場景(權限驗證、流量控制)
- 性能優化需求(緩存、延遲加載)
- 增強監控能力(日志記錄、性能跟蹤)
- 實現智能引用(自動清理、加載)
在實際工程中建議:
- 優先使用ES6 Proxy實現簡單代理邏輯
- 對性能敏感模塊謹慎使用
- 保持代理接口與原始對象一致
- 使用TypeScript增強類型安全
- 配合工廠模式創建復雜代理
正確使用代理模式可以提升代碼的可維護性和擴展性,但需要警惕模式濫用帶來的復雜性。
建議結合具體需求場景,在代碼清晰度和功能需求之間找到平衡點。