好久不见,怎么这么久没更新了呢?
Emm…最近绩效评估季,绩效总结、360 评估,要写的东西比较多嚯,耽搁了一段时间
废话不多说,迎来 JavaScript 设计模式第三篇:代理模式 ~

代理模式概念
代理模式给某一个对象提供一个代理对象或者占位符,并由代理对象控制原对象的引用,也可以理解为对外暴露的接口并不是原对象。通俗地讲,生活中也有比较常见的代理模式:中介、寄卖、经纪人等等。而这种模式存在的意义在于当访问者与被访问者不方便直接访问/接触的情况下,提供一个替身来处理事务流程,实际访问的是替身,替身将事务做了一些处理/过滤之后,再转交给本体对象以减轻本体对象的负担。
最简代理模式实现
由简入繁
上面了解了代理模式的相关概念,接下来我们用一个最简代理模式的例子实现一下代理模式,从代码中感受代理模式的流程
Talk is Cheap. Show me the code!

- client 向服务端发送一个请求
- proxy 代理请求转发给服务端
- 服务端处理请求
const Request = function () {};
const client = { requestTo: (server) => { const req = new Request(); server.receiveRequest(req); }, };
const server = { handleRequest: (request) => { console.log('receive request: ', request); }, };
const proxy = { receiveRequest: (request) => { console.log('proxy request: ', request); server.handleRequest(request); }, };
client.requestTo(proxy);
|
保护代理
保护代理,顾名思义是为了保护本体
基于权限控制对资源的访问
下面用一个场景和例子来实际感受一下,基于上面最简代理模式进行扩展,我们可以使用保护代理实现,过滤未通过身份校验的请求、监听服务端 ready 才发送请求等操作,保护实体服务端不被非法请求攻击和降低服务端负担。
const proxy = { receiveRequest: (request) => { const pass = validatePassport(request); if (pass) { server.listenReady(() => { console.log('proxy request: ', request); server.handleRequest(request); }); } }, };
|
虚拟代理
虚拟代理作为创建开销大的对象的代表,协助控制创建开销大的资源,直到真正需要一个对象的时候再去创建它,由虚拟代理来扮演对象的替身,对象创建后,再将资源直接委托给实体对象
下面将会实现一个虚拟代理实现图片预加载的例子,从代码和实际场景中感受虚拟代理的作用。
- 实体图片对象挂载在 body 中
- 由于加载图片耗时较高,开销较大,加载图片资源时
- 将实体图片对象设置为 loading 状态
- 使用替身对象执行图片资源加载
- 监听替身对象资源加载完成,将资源替换给实体对象
const img = (() => { const imgNode = document.createElement('img'); document.body.appendChild(img); return { setSrc: (src) => { imgNode.src = src; }, setLoading: () => { imgNode.src = 'loading.gif'; }, }; })();
const proxyImg = (() => { const tempImg = new Image(); tempImg.onload = function () { img.setSrc(this.src); }; return { setSrc: (src) => { img.setLoading(); tempImg.src = src; }, }; })();
proxyImg.setSrc('file.jpg');
|
代理模式的应用
看完保护代理和虚拟代理之后,下面来看看代理模式在前端中的一些具体应用
请求优化(埋点、错误聚合上报)
前段时间有幸受邀参加了 ByteTech 字节青训营的评委,主要参加评审的是前端监控系统主题项目。前端监控就会涉及一些错误等信息的上报,部分项目只实现了最简的 HTTP 请求上报。

而有部分项目对这块内容做了以下优化,是一个比较贴切的代理模式实践场景:
- Navigator.sendBeacon
- 数据聚合上报(未使用代理模式优化版本为,每次 report 都使用请求上报)
下面简单实现一下两种上报的示意代码
const events = []; const TIMER = 10000; let timer = null;
const init = () => { timer = setInterval(() => { const evts = events.splice(0, events.length); navigator.sendBeacon('/path', { events: evts }); }, TIMER); };
const destroyed = () => { clearInterval(timer); };
const report = (eventName, data) => { events.push({ eventName, data, }); };
|
const events = []; const LIMIT = 10;
const reportRequest = () => { const evts = events.splice(0, LIMIT); navigator.sendBeacon('/path', { events: evts }); };
const report = (eventName, data) => { events.push({ eventName, data, }); if (events.length >= LIMIT) { reportRequest(); } };
|
数据缓存代理

- 通过 ProxyStorage 代理缓存中间件,实现支持设置缓存过期时间
- 代理一层,便于切换缓存中间件,增加可维护性
const Storage = { set(key, value, maxAge) { localStorage.setItem( key, JSON.stringify({ maxAge, value, time: Date.now(), }) ); }, get(key) { const v = localStorage.getItem(key); if (!v) { return null; } const { maxAge, value, time } = JSON.parse(v); const now = Date.now(); if (now < time + maxAge * 1000) { return value; } else { localStorage.removeItem(key); return null; } }, has(key) { const v = localStorage.getItem(key); if (!v) { return false; } const { maxAge, time } = JSON.parse(v); const now = Date.now(); if (now < time + maxAge * 1000) { return true; } else { localStorage.removeItem(key); return false; } }, };
|
请求函数的封装
通过代理模式封装请求函数,可以实现以下功能:
- 植入通用参数、通用请求头
- 全局请求埋点上报
- 全局异常状态码处理器
- 全局请求错误、异常上报和处理

const SUCCESS_STATUS_CODE = 200, FAIL_STATUS_CODE = 400; const isValidHttpStatus = (statusCode) => statusCode >= SUCCESS_STATUS_CODE && statusCode < FAIL_STATUS_CODE;
const ErrorCode = { NotLogin: 2022, }; const ErrorHandler = { [ErrorCode.NotLogin]: redirectToLoginPage, };
const request = async (reqParams) => { const { headers, method, data, params, url } = reqParams; const requestObj = { url: url + `?${qs.stringify({ ...commonParams, ...params })}`, headers: { ...commonHeaders, ...headers }, data, method, start: Date.now(), }; try { reportEvent(AJAX_START, requestObj); const res = await ajax(requestObj); requestObj.end = Date.now(); requestObj.response = res; reportEvent(AJAX_END, requestObj);
const { statusCode, data: resData } = res; const { errorCode } = resData;
if (!isValidHttpStatus(statusCode)) { reportEvent(AJAX_ERROR, requestObj); throw resData; } else if (errorCode) { reportEvent(AJAX_WARNING, requestObj); if (ErrorHandler(errorCode)) { ErrorHandler(errorCode)(); } else { throw resData; } } else { return resData; } } catch (error) { requestObj.error = error; reportEvent(AJAX_ERROR, requestObj); throw error; } };
|
Vue 中的代理模式
将数据、方法、计算属性等代理到组件实例上
let vm = new Vue({ data: { msg: 'hello', vue: 'vue', }, computed: { helloVue() { return this.msg + ' ' + this.vue; }, }, mounted() { console.log(this.helloVue); }, });
|
Koa 中的代理模式
context 上下文代理封装在 request 和 response 里的属性
app.use((context) => { console.log(context.request.url); console.log(context.url); console.log(context.response.body); console.log(context.body); });
|
其他代理模式
除了本文提到的代理模式应用,还有其他非常多的变体和应用
这里简要列举和介绍一下,就不一一详细展开说明了
- 防火墙代理:控制网络资源访问,保护主体不让”坏人“接近
- 远程代理(科学上网):为一个对象在不同的地址空间提供局部代表,比如大家的”科学上网“
- 保护代理:用于对象应该有不同的访问权限的情况
- 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加的操作,比如计算一个对象被引用的次数(可能用于 GC 的引用计数

小结
代理模式有着许多的小分类,前端开发工作中常用的有虚拟代理、保护代理和缓存代理等。其实读到这里,大家也能感受到,日常开发工作中常做的一个动作 —— ”封装“ ,其实就是代理模式的运用 ~

设计模式系列文章推荐
掘金:前端 LeBron
知乎:前端 LeBron
持续分享技术博文,关注微信公众号 👇🏻
