好久不见,怎么这么久没更新了呢?

Emm…最近绩效评估季,绩效总结、360 评估,要写的东西比较多嚯,耽搁了一段时间

废话不多说,迎来 JavaScript 设计模式第三篇:代理模式 ~

美好的一天从学习开始 - 做个爱学习的 doge_学习_doge_装逼_萌萌哒表情

代理模式概念

代理模式给某一个对象提供一个代理对象或者占位符,并由代理对象控制原对象的引用,也可以理解为对外暴露的接口并不是原对象。通俗地讲,生活中也有比较常见的代理模式:中介、寄卖、经纪人等等。而这种模式存在的意义在于当访问者与被访问者不方便直接访问/接触的情况下,提供一个替身来处理事务流程,实际访问的是替身,替身将事务做了一些处理/过滤之后,再转交给本体对象以减轻本体对象的负担。

最简代理模式实现

由简入繁

上面了解了代理模式的相关概念,接下来我们用一个最简代理模式的例子实现一下代理模式,从代码中感受代理模式的流程

Talk is Cheap. Show me the code!

image-20220919195415997

  • 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);
/**
* proxy request: Request {}
* receive request: Request {}
*/

保护代理

保护代理,顾名思义是为了保护本体

基于权限控制对资源的访问

下面用一个场景和例子来实际感受一下,基于上面最简代理模式进行扩展,我们可以使用保护代理实现,过滤未通过身份校验的请求、监听服务端 ready 才发送请求等操作,保护实体服务端不被非法请求攻击和降低服务端负担。

const proxy = {
receiveRequest: (request) => {
// 校验身份
const pass = validatePassport(request);
if (pass) {
// 监听服务端 ready 后代理请求
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 {
// 代理开始将实体对象设置为loading状态,使用替身对象开始加载图片资源
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(() => {
// 定时使用 sendBeacon 上报
const evts = events.splice(0, events.length);
navigator.sendBeacon('/path', { events: evts });
}, TIMER);
};

const destroyed = () => {
// 销毁时清除定时器
clearInterval(timer);
};

const report = (eventName, data) => {
// sdk 上报工具函数,聚合事件
events.push({
eventName,
data,
});
};
  • 定量分组
const events = [];
const LIMIT = 10;

const reportRequest = () => {
// 定量分组使用 sendBeacon 上报
const evts = events.splice(0, LIMIT);
navigator.sendBeacon('/path', { events: evts });
};

const report = (eventName, data) => {
// sdk 上报工具函数,聚合事件
events.push({
eventName,
data,
});
if (events.length >= LIMIT) {
reportRequest();
}
};

数据缓存代理

  • 之前看到过一道面试题:前端怎么实现缓存到期自动删除缓存?

    • 如果正向地去思考,缓存到期了,程序都关闭了,怎么删?

    • 换个角度:不在过期时立即 set ,get 时才需要判断缓存是否过期

      在 get 时判断下是否过期,过期了再删除不就得了 ~

小猫咪觉得很赞(猫猫点赞表情包)_点赞_很赞_小猫咪表情

  • 通过 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 的引用计数

拿捏了_拿捏表情

小结

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

刻苦学习了一天,好充实、好幸福 - 做个爱学习的 doge_学习_doge_装逼_萌萌哒表情

设计模式系列文章推荐

掘金:前端 LeBron

知乎:前端 LeBron

持续分享技术博文,关注微信公众号 👇🏻

img