- Published on
代理模式详解
- Authors
- Name
- 青雲
在软件设计中,代理模式(Proxy Pattern)是一种结构型设计模式。它为其他对象提供一种代理,并由代理对象控制对原对象的访问。代理模式常用于延迟对象的创建、控制对对象的访问以及提高系统的安全性。
为什么需要代理模式?
直接购买商品的流程可能涉及到找到卖家、沟通、支付、确认收货等复杂的步骤。这里,如果我们把每个买家和卖家看成是一个个对象,那么直接的交易就意味着对象间直接的交互。
现实生活中,我们现在很少会直接和卖家进行沟通和交易,而是通过购物平台(淘宝、拼多多、京东、抖音等)来完成这一系列操作。在这个场景中,购物平台就扮演了一个"代理"的角色。我们作为买家,通过代理(购物平台)来间接与卖家进行交云。
代理的作用
- 隐藏真实交易双方的细节:作为买家,我们不需要知道卖家的具体信息,也不需要与卖家直接沟通。所有这些都通过购物平台这个代理来完成。
- 增加安全性:购物平台会对交易双方进行验证,确保交易的安全性。
- 提供附加服务:除了基本的买卖功能,购物平台还会提供物流跟踪、售后服务、评价系统等额外的服务,这些是直接交易很难提供的。
- 减少直接交互带来的复杂性:通过代理,复杂的交易流程被简化,用户体验因此得到提升。
在软件开发中,我们有时需要在不改变现有代码的前提下,增加某些功能或逻辑。代理模式通过引入代理对象,巧妙地控制对原始对象的访问,使我们可以在不影响原始对象设计的情况下,轻松扩展其功能。
例如,在一个庞大的系统中,我们可能需要延迟某些非常耗时的对象的初始化,或者需要对某些关键操作进行日志记录和权限控制。通过代理模式,我们可以有效地解决这些问题。
基本概念
代理模式包括以下几个主要部分:
- 代理接口(Proxy Interface): 定义了代理类和原始类共有的方法,使得代理类可以用于替代原始类。
- 原始类(Real Subject): 实现了代理接口的具体类,是被代理的对象。
- 代理类(Proxy Class): 实现了代理接口,包含了对原始类对象的引用,并在调用方法时对其进行一些控制。
实现示例
我们将通过一个具体示例来展示代理模式的实现过程。假设我们有一个视频播放系统,在系统中我们可以播放大量的视频文件。由于初始化视频对象可能非常耗时,我们可以通过代理模式来实现视频对象的延迟加载。
定义代理接口和原始类
// 代理接口,定义统一的方法
interface Video {
play(): void;
}
// 具体原始类,表示实际的视频对象
class RealVideo implements Video {
private fileName: string;
constructor(fileName: string) {
this.fileName = fileName;
this.loadVideoFromDisk();
}
private loadVideoFromDisk(): void {
console.log(`Loading video: ${this.fileName}`);
}
play(): void {
console.log(`Playing video: ${this.fileName}`);
}
}
定义代理类
// 代理类,用于控制对实际视频对象的访问
class ProxyVideo implements Video {
private realVideo: RealVideo;
private fileName: string;
constructor(fileName: string) {
this.fileName = fileName;
}
play(): void {
if (!this.realVideo) {
this.realVideo = new RealVideo(this.fileName); // 延迟初始化
}
this.realVideo.play();
}
}
使用代理类播放视频
// 使用代理类来播放视频
const video = new ProxyVideo('example_video.mp4');
// 第一次播放,进行初始化加载
video.play();
// 第二次播放,不需要初始化
video.play();
Loading video: example_video.mp4
Playing video: example_video.mp4
Playing video: example_video.mp4
应用场景
代理模式适用于以下场景:
- 延迟加载:当某些对象初始化非常耗时时,可以通过代理模式实现延迟加载,只有在需要时才进行初始化。
- 访问控制:通过代理模式控制对原始对象的访问,添加权限验证等逻辑。
- 日志记录:在代理类中添加日志记录功能,记录对原始对象的访问情况。
- 远程代理:在分布式系统中,代理对象充当实际远程对象的本地代表。
前端开发中的应用
事件代理(Event Delegation)
在处理大量类似的事件监听器(比如列表中的每个项的点击事件)时,为每个元素单独添加事件监听器会影响性能和资源消耗。事件代理利用了事件冒泡的机制,只在父级元素上设置一个监听器来管理所有子元素的事件。这样,不仅减少了监听器的数量,还提高了程序的效率。
document.getElementById('list-container').addEventListener('click', function(e) {
if(e.target && e.target.nodeName == 'LI') {
console.log(e.target.id + ' was clicked');
}
});
虚拟代理(Virtual Proxy)
虚拟代理可以用来控制对一些开销较大的操作的访问,例如延迟加载(Lazy Loading)图片或其他资源。直到这些资源真正需要显示在屏幕上时才加载它们,减少初始加载时间,提升用户体验。
class ImageProxy {
private imageElement: HTMLImageElement;
private realImage: HTMLImageElement;
constructor(imageElement: HTMLImageElement, url: string) {
this.imageElement = imageElement;
this.realImage = new Image();
this.realImage.onload = () => {
this.imageElement.src = this.realImage.src;
};
this.realImage.src = url;
}
}
// 使用虚拟代理懒加载图片
const imgElement = document.getElementById('myImage') as HTMLImageElement;
new ImageProxy(imgElement, 'https://example.com/image.jpg');
保护代理(Protection Proxy)
在前端开发中,使用保护代理可以控制对某些敏感信息的访问。例如,只有在用户完成身份验证后才允许访问某个 API 或获取某些数据。
function fetchData(api) {
// 简化示例:检查是否有访问权限
if (!sessionStorage.getItem('authenticated')) {
console.log('Access denied. User not authenticated.');
return;
}
// 假装发起API请求
console.log('Fetching data from', api);
}
const proxyFetchData = new Proxy(fetchData, {
apply(target, thisArg, argumentsList) {
if (sessionStorage.getItem('userRole') !== 'admin') {
console.log('Access denied. Admin role required.');
return;
}
return Reflect.apply(...arguments);
}
});
缓存代理(Caching Proxy)
缓存代理可以用于缓存重复请求的结果,以避免不必要的计算或网络请求,优化性能。特别是在处理大量数据和高频访问的场景中非常有用。
const expensiveFunctionProxy = new Proxy(expensiveFunction, {
cache: new Map(),
apply(target, thisArg, argumentsList) {
const cacheKey = argumentsList.toString();
if (!this.cache.has(cacheKey)) {
const result = Reflect.apply(target, thisArg, argumentsList);
this.cache.set(cacheKey, result);
}
return this.cache.get(cacheKey);
}
});
开源库中的典型应用
Vue.js 数据响应机制
Vue.js 数据响应机制中广泛使用代理模式。在 Vue 3 中,使用了 ES6 的 Proxy
来监听数据对象的变化,从而实现响应式。当数据变化时,Proxy
能够拦截这些变化,并通知视图进行更新。
const data = { title: 'Hello, Vue 3' };
const proxyData = new Proxy(data, {
get(target, property) {
console.log(`Getting ${property}: ${target[property]}`);
return Reflect.get(...arguments);
},
set(target, property, value) {
console.log(`Setting ${property}: ${value}`);
return Reflect.set(...arguments);
}
});
proxyData.title; // 访问属性时,`get`捕获器被触发
proxyData.title = 'Hello, Proxy'; // 修改属性时,`set`捕获器被触发
Webpack 的 HMR
Webpack 的 HMR 功能通过代理模式实现模块热替换。当开发者对代码进行更改时,HMR 代理能够拦截这些更改并通知浏览器替换旧模块,从而无需完全刷新页面就可以更新应用。这大大提高了开发效率,特别是对于大型应用,避免了频繁的全页面刷新。
// Webpack 配置文件中的 HMR 配置
module.exports = {
// 其他配置项...
devServer: {
hot: true, // 开启 HMR
},
plugins: [
new webpack.HotModuleReplacementPlugin(), // 注册 HMR 插件
],
};
// 示例模块代码
if (module.hot) {
// 模块自身更新
module.hot.accept('./module-to-watch.js', () => {
console.log('Module updated!');
});
// 模块销毁时清理
module.hot.dispose(() => {
console.log('Module disposed!');
});
}
- 热替换插件:
HotModuleReplacementPlugin
插件使得 Webpack 能够支持 HMR。 - 模块更新与替换:通过
module.hot.accept
和module.hot.dispose
,模块能够在更新时执行相关逻辑,避免整个页面刷新。
Redux 中间件
Redux 库在处理异步操作和日志记录等功能时广泛使用代理模式,尤其是通过中间件机制。中间件本质上就是一个代理,它可以拦截、修改甚至拒绝 Redux 动作,实现了对数据流的高度控制。
const loggerMiddleware = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
Fetch API 拦截器
虽然 Fetch API 原生并不支持拦截器,但可以通过代理模式实现。这样的应用允许开发者拦截和修改 HTTP 请求与响应,是实现例如添加通用请求头、响应处理和错误处理的一个有效手段。
const fetchProxy = new Proxy(fetch, {
apply(target, thisArg, argumentsList) {
console.log(`Fetching: ${argumentsList[0]}`);
return Reflect.apply(...arguments).then(response => {
// Do something with the response
return response;
});
}
});
MobX 中的状态管理
MobX 是另一个利用代理模式实现响应式编程的库。通过使用 @observable
装饰器或 makeObservable
方法,MobX 能够将普通的 JavaScript 对象转换为可观察的对象。当可观察对象的状态发生变化时,依赖这些状态的计算值或反应会自动更新。
import { makeAutoObservable } from "mobx";
class Todo {
id = Math.random();
title = "";
finished = false;
constructor(title) {
makeAutoObservable(this);
this.title = title;
}
toggle() {
this.finished = !this.finished;
}
}
优缺点
优点
- 控制对象访问:通过代理对象,可以控制对原始对象的访问,实现权限控制、延迟加载等功能。
- 增强对象功能:在不改变原始对象的前提下,通过代理对象增强其功能,如添加日志记录、性能监控等。
- 优化性能:通过延迟加载和减少对象创建,提高系统性能。
缺点
- 增加代码复杂性:引入代理对象后,增加了类的数量和代码的复杂性。
- 潜在的性能开销:代理对象通过引用原始对象进行操作,可能导致一些额外的性能开销。
总结
代理模式是一种功能强大的设计模式,能够提供对对象访问的精细控制并附加额外的操作,适用于多种场景,如性能优化、安全控制及系统资源管理等方面。在实现时,应当权衡其带来的好处与潜在的复杂性,以确保设计的高效与可维护。