代理模式 (Proxy Pattern)又称委托模式,它为目标对象创造了一个代理对象,以控制对目标对象的访问。
代理模式把代理对象插入到访问者和目标对象之间,从而为访问者对目标对象的访问引入一定的间接性。正是这种间接性,给了代理对象很多操作空间,比如在调用目标对象前和调用后进行一些预操作和后操作,从而实现新的功能或者扩展目标的功能。
一、代理模式例子:
明星总是有个助理,或者说经纪人,如果某导演来请这个明星演出,或者某个品牌来找明星做广告,需要经纪人帮明星做接洽工作。而且经纪人也起到过滤的作用,毕竟明星也不是什么电影和广告都会接。
打官司是件非常麻烦的事,包括查找法律条文、起草法律文书、法庭辩论、签署法律文件、申请法院执行等等流程。此时,当事人就可聘请代理律师来完成整个打官司的所有事务。当事人只需与代理律师签订全权委托协议,那么整个打官司的过程,当事人都可以不用出现。法院的一些复杂事务都可以通过代理律师来完成,而法院需要当事人完成某些工作的时候,比如出庭,代理律师才会通知当事人,并为当事人出谋划策。
在类似的场景中,有以下特点:
- 导演/法院(访问者)对明星/当事人(目标)的访问都是通过经纪人/律师(代理)来完成;
- 经纪人/律师(代理)对访问有过滤的功能;
二、代码实现
//目标对象target: 明星 const SuperStar = { name: '小鲜肉', scheduleFlag: false, scheduleFlagActually: false, playAdvertisement: function (advertisement) { console.log(this.name + '做任务: ' + advertisement) } } //代理对象proxy: 经纪人 const proxyAssistant = { name: '经纪人张某', scheduleTimeByPromise: function () { return new Promise((resolve, reject) => { console.log("schedule time") setTimeout(() => { console.log("小鲜肉空出时间了"); resolve(); }, 2000) }) }, scheduleTimeByProxy: function (advertisement) { const schedule = new Proxy(SuperStar, { // handler回调,prop为发生变化的属性,val为发生变化的值 // 有值发生变化后才执行回调,此处setTimeout先执行 // 只要对象属性发生变化就会监听执行回调 set(obj, prop, val) { console.log('proxy values: ', prop, val) if (prop !== 'scheduleFlag') { return; } if (obj.scheduleFlag === false && val === true) { obj.scheduleFlag = true; obj.playAdvertisement(advertisement); } } }); setTimeout(() => { console.log("小鲜肉空出时间了"); schedule.scheduleFlag = true; }, 2000) }, scheduleTimeByDefineProperty: function (advertisement) { // 只能监听scheduleFlag变化,还要借用变量 Object.defineProperty(SuperStar, 'scheduleFlag', { get() { return SuperStar.scheduleFlagActually }, set(val) { if (SuperStar.scheduleFlagActually === false && val === true) { SuperStar.scheduleFlagActually = true; SuperStar.playAdvertisement(advertisement) } } }); setTimeout(() => { console.log("小鲜肉空出时间了"); SuperStar.scheduleFlag = true; }, 2000) }, playAdvertisement: function (condition, advertisement) { if (condition > 100000) { console.log('接受任务: ' + advertisement); //SuperStar.playAdvertisement(advertisement); // proxyAssistant.scheduleTimeByPromise() // .then(()=> { // SuperStar.playAdvertisement(advertisement) // }); proxyAssistant.scheduleTimeByProxy(advertisement); } else { console.log('不满足条件,不做') } } } //访问者visitor proxyAssistant.playAdvertisement(100, "拍广告"); proxyAssistant.playAdvertisement(1000000, "拍广告");
三、代理模式在实战中的应用
-
拦截器
拦截器的思想在实战中应用非常多,比如我们在项目中经常使用 Axios 的实例来进行 HTTP 的请求,使用拦截器 interceptor
可以提前对 request
请求和 response
返回进行一些预处理,比如:
request
请求头的设置,和 Cookie 信息的设置;- 权限信息的预处理,常见的比如验权操作或者 Token 验证;
- 数据格式的格式化,比如对组件绑定的
Date
类型的数据在请求前进行一些格式约定好的序列化操作; - 空字段的格式预处理,根据后端进行一些过滤操作;
response
的一些通用报错处理,比如使用 Message 控件抛出错误;
除了 HTTP 相关的拦截器之外,还有 vue-router、react-router 路由跳转的拦截器,可以进行一些路由跳转的预处理等操作。
拦截器看起来似乎和装饰者模式很像,但是要注意装饰者模式和代理模式的区别,代理模式控制访问者对目标对象的访问,而装饰者模式只给目标对象添加功能,原有功能不变且可直接使用。Axios 拦截器是可以取消请求的,vue-router 路由拦截器也可以进行路由截停和重定向等等复杂操作,这些场景下,无疑是代理模式,因为这里的拦截器控制了对目标对象的访问,如果没有进行访问控制而只进行消息预处理和后处理,那么则可以当作是装饰者模式。
-
前端框架的数据响应式化
现在的很多前端框架或者状态管理框架都使用上面介绍的 Object.defineProperty
和 Proxy
来实现数据的响应式化,比如 Vue、Mobx、AvalonJS 等,Vue 2.x 与 AvalonJS 使用前者,而 Vue 3.x 与 Mobx 5.x 使用后者。
Vue 2.x 中通过 Object.defineProperty
来劫持各个属性的 setter/getter
,在数据变动时,通过发布-订阅模式发布消息给订阅者,触发相应的监听回调,从而实现数据的响应式化,也就是数据到视图的双向绑定。
为什么 Vue 2.x 到 3.x 要从 Object.defineProperty
改用 Proxy
呢,是因为前者的一些局限性,导致的以下缺陷:
- 无法监听利用索引直接设置数组的一个项,例如:
vm.items[indexOfItem] = newValue
; - 无法监听数组的长度的修改,例如:
vm.items.length = newLength
; - 无法监听 ES6 的
Set
、WeakSet
、Map
、WeakMap
的变化; - 无法监听
Class
类型的数据; - 无法监听对象属性的新加或者删除;
除此之外还有性能上的差异,基于这些原因,Vue 3.x 改用 Proxy
来实现数据监听了。当然缺点就是对 IE 用户的不友好,兼容性敏感的场景需要做一些取舍。
-
正向代理与反向代理
反向代理对应的是正向代理(Forward Proxy),他们的区别是:
- 正向代理: 一般的访问流程是客户端直接向目标服务器发送请求并获取内容,使用正向代理后,客户端改为向代理服务器发送请求,并指定目标服务器(原始服务器),然后由代理服务器和原始服务器通信,转交请求并获得的内容,再返回给客户端。正向代理隐藏了真实的客户端,为客户端收发请求,使真实客户端对服务器不可见;
- 反向代理: 与一般访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后将请求转发给内部网络上真正进行处理的服务器,得到的结果返回给客户端。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。
反向代理一般在处理跨域请求的时候比较常用,属于服务端开发人员的日常操作了,另外在缓存服务器、负载均衡服务器等等场景也是使用到代理模式的思想。