前言☀️
本文是妙用 Promise 实现 Deferred,并以此为基础实现对并发接口请求的合并,会在讲解思路的基础上手把手带大家实现这套代码。当然,在给大家看我的代码之前先给大家讲一下我为什么要去做合并接口调用的事情。
这篇文章希望大家可以看看结束语~拜托啦🌿
service层🌟
寒草最近换了组,用的一些技术栈也发生了改变,也是接触了grpc「此处开坑,以后会讲,但是在这个之前我会讲graphql
」,现在我能体会到最大的变化就是服务端提供的接口粒度更细了。于是我们通常会对服务端的接口进行二次封装,以提供给视图层更好的接口调用服务。
这个service层的封装让前端需要做更多的业务逻辑,并且有以下几种用途(可能不全):
- 接口合并,业务逻辑封装,为视图层提供更易用的服务
- 将服务端不合理的设计屏蔽(服务端设计不合理,数据结构不合理,字段名不合理都在service层屏蔽掉)
- 逻辑复用
接口粒度与一致性🌟
说了半天service层,那也只是铺垫,下面我继续说,这里我们期望的是我们通过一个标识去拿一个资源。
其实我们出于接口粒度与一致性考量,首先如果我们要支持处理批量和单个资源,我们可能要在service去暴露两套接口,并且这两套接口的粒度是不一样的,但是其实干的是一个事情,只不过一个是批量,一个是单个而已。
// 单个 export const getMeta = async (id) => { // xxx return meta; } // 批量 export const getMetas = async (ids) => { // xxxx return metas; }
而且其中批量还是包括单个的。那么有人说了,那我们就向视图暴露批量接口不就行了么?
首先是获取单个资源要 getMetas([id]) ,这样用,即套一个[], 而且有的接口提供了batch,有的接口没有提供batch在使用上是割裂的。
所以我们拒绝冗余割裂的service层服务,并不会给视图层提供Batch接口,在视图层如果要调用同一个服务以使用不同的id拿到不同的多个同类资源(比如获取列表),就需要这样写:
const userMetas = Promise.all(UserIds.map(userId => getUserMeta(id));
这样整体的使用和实现上是一致的,即用单个id去拿对应的资源或者处理一件事。粒度和一致性都是统一的,和谐的。视图层(1 to 1)的接口调用逻辑上也是合理的。
性能问题🌟
但是如果只提供single的调用,不去做任何中间处理的话,会存在很大的问题:
- 性能问题(可能同时发出大量的接口请求)
- 开发体验问题
这里我解释一下开发体验的问题,因为我们grpc的接口调用在控制台的network中不能看到接口信息,不利于我们调试,所以我们会在控制台对接口信息进行打印。如果我们一次调用几十个接口,直接打印好几页,根本没法去定位问题。
于是服务端这个时候会提供对应的batch接口,说白了就是提供了批量处理的接口。这个batch接口其实相当于:
前端调了一个batch接口,服务端接收后再去拆成若干个调用。所以其实节省的其实是接口重复接口调用在数据传输中冗余数据和创建连接的时间消耗(不知道说的对不对,这是我的理解)
但是我们之前说了,我们从接口粒度和一致性考量不会给视图层暴露batch接口,所以视图层还是会调用获取单个资源的接口,那么我们就需要通过某种手段在service层对视图层调用的接口请求进行合并。
解决方案🌟
我想现在问题背景已经描述好了,现在需要的就是出一个结局方案了,其实很好想,我们完全可以做一个中转,将同一个接口的请求信息合并之后通过batch方法进行调用,内容如图所示:
- 视图层还是一个id去获取一个资源
- service层做参数收集转发调用服务端batch接口
- 服务端对batch进行拆分去获取资源
那么整个的思路理清楚了,我们现在开始实现它✨~
正篇📖
Deferred方法实现📚
首先,我们需要一个延迟处理回调的方案,并可以传递一个参数,将batch方法的返回值返回给之前的接口调用,那么我们来实现一个Deferred,其实就相当于把Promise的resolve和reject方法提到了外层。
function Deferred() { if (typeof (Promise) != 'undefined' && Promise.defer) { return Promise.defer(); } else if(this && this instanceof Deferred){ this.resolve = null; this.reject = null; const _this = this; this.promise = new Promise((resolve, reject) => { _this.resolve = resolve; _this.reject = reject; }); Object.freeze(this); } else { throw new Error(); } }
如何使用deferred:
const deferred = new Deferred(); const promise = deferred.promise; promise.then(res => { // xxxx 事件A }) async function fn() { const arr = await xxx;// 事件B promise.resolve(arr); }
解读一下,就是相当于我们的事件A
要在事件B
之后进行, 那么我们就可以在事件B
结束之后,将 deferred.promise
进行 reslove
,并把结果返回给事件A
。
separate方法实现📚
下面,我们进入本篇文章的正题,将如何将请求合并,这里我提供了一个工具方法,为什么叫separate呢,这个词的意思是分割,因为我要用它将service调用的batch接口进行封装,使其在视图层用着还是一个id获取一个资源,但是调用服务的时候会合并为一个。所以它的含义其实是:batch接口的分割
那么,我把我要讲解的内容放在代码的注释里,方便大家一行一行的对照查看:
const separate = function (multipleApi, singleApi) { // 闭包,建立一个独立的作用域 let length = 0; let argsList = []; let deferred = new Deferred(); // 工具方法,用于在一次请求完成后对作用域变量初始化 function init() { length = 0; argsList = []; deferred = new Deferred(); } return (...args) => { // 收集参数 argsList.push(args); return new Promise((resolve, reject) => { // 记录发出请求个数 length++; // 记录当前请求是第几个,用作标识 let index = length; // 设置定时器 let timer = setTimeout(() => { // 清空定时器 clearTimeout(timer); // 如果在定时器的回调中index和length相等,表示并发的请求结束了 if (index == length) { // 有时候service不仅写了batchapi还写了single api,当只进行一次接口调用的时候调single api更高效 if (length === 1 && singleApi) { // 兼容single api singleApi(...args).then(res => { init(); resolve(res); }).catch(err => { reject(err); }) } else { // 并发的多个请求结束,则调用batch接口 multipleApi(argsList).then(resList => { // deferred进行resolve,通知前面调用的接口数据已经拿回来了,并把信息传递给它们 deferred.resolve(resList); deferred.promise.then(resList => { init(); resolve(resList[index - 1]) }); }).catch(err => { //如果batch接口报错,则reject deferred.reject(err); reject(err); }); } } else { // 前面的接口调用的回调在deferred.promise的then中处理 deferred.promise.then(resList => { resolve(resList[index - 1]); }).catch((err) => { // 处理错误 reject(err); }); } }, 0) }); }; }
整体的流程图大概是:
整体的执行顺序从上到下:
- 并发五个请求,同时进行信息收集
- 请求结束,调用batch接口
- 用batch接口的数据拆分并回填到之前五个请求的响应中
完整代码 + 示例 🌰
代码:
function Deferred() { if (typeof (Promise) != 'undefined' && Promise.defer) { return Promise.defer(); } else if(this && this instanceof Deferred){ this.resolve = null; this.reject = null; const _this = this; this.promise = new Promise((resolve, reject) => { _this.resolve = resolve; _this.reject = reject; }); Object.freeze(this); } else { throw new Error(); } } const separate = function (multipleApi, singleApi) { let length = 0; let argsList = []; let deferred = new Deferred(); function init() { length = 0; argsList = []; deferred = new Deferred(); } return (...args) => { argsList.push(args); return new Promise((resolve, reject) => { length++; let index = length; let timer = setTimeout(() => { clearTimeout(timer); if (index == length) { if (length === 1 && singleApi) { // 兼容single api singleApi(...args).then(res => { init(); resolve(res); }).catch(err => { reject(err); }) } else { multipleApi(argsList).then(resList => { deferred.resolve(resList); deferred.promise.then(resList => { init(); console.log(index - 1); resolve(resList[index - 1]) }); }).catch(err => { deferred.reject(err); reject(err); }); } } else { deferred.promise.then(resList => { console.log(index - 1); resolve(resList[index - 1]); }).catch((err) => { reject(err); }); } }, 0) }); }; }
示例:
async function multipleApi(arr) { console.log(arr) return arr; } async function singleApi(...arr) { console.log(arr) return arr; } const serviceApi = separate(multipleApi, singleApi); const aa = async function() { const m = await Promise.all([serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n')]); console.log('promise.all', m); const l = await Promise.all([serviceApi('a','b','c'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n')]); console.log('promise.all', l); const n = await serviceApi(9, 1, 3); console.log('single', n); } aa();
运行结果:
待办:
当然现在它还并不完善的,比如:
- 错误处理拆分
- 重复请求的合并(key重复的请求,不是同一个api的请求)
最近比较忙,但是也会加入后续的计划中来哈
代码改进
增加了对重复请求的合并(key相同)
function Deferred() { if (typeof (Promise) != 'undefined' && Promise.defer) { return Promise.defer(); } else if(this && this instanceof Deferred){ this.resolve = null; this.reject = null; const _this = this; this.promise = new Promise((resolve, reject) => { _this.resolve = resolve; _this.reject = reject; }); Object.freeze(this); } else { throw new Error(); } } const separate = function (multipleApi, singleApi) { let length = 0; let argsList = []; let deferred = new Deferred(); let paramsMap = new Map(); function init() { length = 0; argsList = []; deferred = new Deferred(); paramsMap = new Map(); } return (...args) => { return new Promise((resolve, reject) => { length++; let requestIndex, responseIndex; const paramsStr = JSON.stringify(args); const _mapIndex = paramsMap.get(paramsStr); if(Number.isFinite(_mapIndex)) { responseIndex = _mapIndex; } else { responseIndex = length; argsList.push(args); paramsMap.set(paramsStr, responseIndex); } requestIndex = length; let timer = setTimeout(() => { clearTimeout(timer); if (requestIndex == length) { if (length === 1 && singleApi) { // 兼容single api singleApi(...args).then(res => { init(); resolve(res); }).catch(err => { reject(err); }) } else if(paramsMap.size === 1 && singleApi) { singleApi(...args).then(res => { deferred.resolve([res]); deferred.promise.then(res => { init(); resolve(res) }); }).catch(err => { deferred.reject(err); reject(err); }) } else { multipleApi(argsList).then(resList => { deferred.resolve(resList); deferred.promise.then(resList => { init(); resolve(resList[responseIndex - 1]) }); }).catch(err => { deferred.reject(err); reject(err); }); } } else { deferred.promise.then(resList => { resolve(resList[responseIndex - 1]); }).catch((err) => { reject(err); }); } }, 0) }); }; } async function multipleApi(arr) { console.log('api params', arr); return arr; } async function singleApi(...arr) { console.log('api params single', arr); return arr; } const serviceApi = separate(multipleApi, singleApi); const aa = async function() { const m = await Promise.all([serviceApi('m', '0', 'n'), serviceApi('1'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n')]); console.log('M promise.all', m); // const l = await Promise.all([serviceApi('a','b','c'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n'), serviceApi('m', '0', 'n')]); // const n = await serviceApi(9, 1, 3); const n = await Promise.all([serviceApi('1'), serviceApi('1'), serviceApi('1')]); console.log('N promise.all', n); } aa(); // export default batchApi;