前端通讯:ajax设计方案(八)--- 设计请求池,复用请求,让前端通讯快、更快、再快一点

直接进入主题,本篇文章有点长,包括从设计阶段,到摸索阶段,再到实现阶段,最后全面覆盖测试阶段(包括数据搜集清洗),还有与主流前端通讯框架进行对比PK阶段。html

首先介绍一下一些概念:前端

  1. 浏览器的并发能力:浏览器设计当初就定义了浏览器打开页面,同时发送http请求的瞬时数量。这样设计有不少缘由,同时保护浏览器和服务器。具体能够谷歌或者百度关键字:浏览器并发。node

  2. 浏览器针对服务器域名请求的并发限制数量:jquery

    

  3. 请求池:相似于数据库链接池同样,对数据库请求链接进行分配管理等等ios

  4. 复用请求:对于生命周期已经结束的请求不进行销毁,重复利用,减小从新向浏览器申请资源,减小通用库设计中的通用检查。git

 

引入新功能:链接池github

 

当初为啥要作这个功能:ajax

  上次迭代最后留了一个彩蛋,我在一次测试中,发送完成一个请求以后,将这个请求存到一个变量中,而后又尝试从新打开,发现居然能够发送出去。好奇心害死猫,我在浏览器中用console.time去计算执行这段js代码的时间,发现重复使用的请求使用时间大概是前者的一半,甚至更少。既然,他能够这么优秀,那我为啥不能欲罢不能呢。因此,这一步下去就是一波三折...chrome

 

摸索思路:数据库

 

 

按着当时的设计思路一步一步的来确认

   1. 首先确认了一个请求发送完了以后,能够再次打开而后发送。这样就保证了方向是对的,能够继续优化下去

   2. 其次,对于一个域名,浏览器的同时发送请求的数目就如上面图片上描述的,主流基本都是6个,因此请求池将基于浏览器最大并发作链接的配置,不过由于浏览器差别,会将链接池的链接数量作配置进行配置

   3. 将这个请求池塞满配置的请求将是第一任务,对于XMLHttpRequest这个对象,主流的想法都是作一个拷贝,当前浅拷贝是不行的,那就作了一次深度拷贝测试,塞满请求池。不过,该方案夭折了。

   4. 由于请求对象的原型是XMLHttpRequest,这个对象实现了浏览器的接口才能经过浏览器API发送http请求,可是深度拷贝的对象是Object,object不实现浏览器的接口,因此夭折了,没法直接拷贝,难道我要相似于发送6次空请求以后去搜集请求池链接吗?这个是绝对不可能饶恕的行为,因此作了下一步的摸索

   5. 若是XMLHttpRequest对象实现了浏览器的接口,那么咱们将深度拷贝的对象的原型的指向从新指向到XMLHttpRequest。这样是否是能够发送请求了。而后测试了,阿西吧,非法请求,并且对于XMLHttpRequest对象中的response、responseText等熟悉仍是只读了,从新拷贝赋值浏览器报错,非法,全都是非法。

   6. 气馁了好久,把大脑放空换个思路去解决这个问题。参考了其余相似请求池的设计思路,要么一开始就作初始化,将池子中的链接一次性生产出来,或者每次创建的链接消费完毕以后再纳入池子中,等待链接满了以后才启用链接池。

   7. 考虑到前端每一个页面,或者对于如今的每一个组件中能发送请求的数量并非很大,可能一个路由下就发送了4-5个请求就解决一个小业务需求,根本达不到启动请求池的基本配置。其次,经过浏览器的performance监控页面性能,发现初始化的时间就在几毫秒之间,相对于后期使用加速,这个时间的付出,对于后期的使用是很值得的预算。因此,选择了第一种方式,一次性初始化请求池,等待调用。下面缕清思惟,作了更完整和全面的设计。

 

请求池设计方案:

左边 -- 生成请求池部分 

  1. 加载ajax-js库的时候,经过判断请求池开关是否打开,打开的话就走初始化这一套流程。

  2. 建立基于全局配置的空请求-->copy所需参数-->建立空XMLHttpRequest对象-->合并copy参数到空对象上-->读取配置数量生成请求池

  3. 由于请求池是基于全局配置生成的请求,因此若是全局配置变更,将触发请求池的reload,从新生成。

 

中间 -- 经过请求池发送请求的生命周期

  1. 请求进来,首先判断开关是否打开,打开的话就先判断请求池中请求的数量是否还有。

  2. 对比参数,是否有额外预设参数的变动(除url、data、successEvent、eroorEvent外),若是有将在请求发送前进行设置。(例如:request的Header将在每次开启的时候所有从新置空)

  3. 在请求的生命周期结束以后,而后归还请求池。注意:请求的生命周期结束有不少情况,发送错误url、timeout超时等在浏览器层面就失败的,以及请求发送成功时候,非200、304等错误,好比4XX或者5XX系列错误的生命周期结束

  4. 最后判断是否有排队的请求,有的话取出再来,没有直接返回池子

 

右边 -- 请求排队系统

  1. 对于请求若是超出请求池数量,请求池确定没法承载,就算能承载,其实浏览器也是将这些超出并发的请求进行排队,等待一波结束以后再发送。因此有个排队的系统很重要的啦,来管理请求。

  2. 对于超出容量请求,首先排队。而后,在请求生命结束以后,将会去检查排队系统是否还有排队请求。其次,若是有就进行取出,再走请求的生命周期,若是每页,那就没请求啦

 

代码片断展现:

1. 加载类库时判断开关,初始化代码

 1   var outputObj = function () {
 2     //虽然在IE六、7上能够支持,可是最好升级你的浏览器,毕竟xp已经淘汰,面向将来吧,骚年,和我一块儿努力吧!!
 3     if (tool.getIEVersion() < 7) {
 4       //实在不想说:升级你的浏览器吧
 5       throw new Error("Sorry,please update your browser.(IE8+)");
 6     }
 7 
 8     // 是否开启链接池
 9     if (initParam.pool.isOpen) {
10       tool.createPool()
11     }
12 
13     return tempObj;
14   };
View Code

 2. 建立方法

1     // 建立请求池中连接
2     createPool: function () {
3       // IE 系列不支持发送请求传'',因此默认/
4       tempObj.common({url: '/'}, true)
5       tool.deepCloneXhr(selfData.xhr, initParam.pool.requestNumber)
6     },
View Code

3. 拷贝通用参数存入请求池

 1     // 拷贝xhr参数
 2     deepCloneXhr: function (data, requestNum) {
 3       var mapping = {
 4         currentUrl: true,
 5         onerror: true,
 6         onload: true,
 7         onreadystatechange: true,
 8         ontimeout: true,
 9         timeout: true,               // IE系列只有open链接以后才支持覆盖
10         withCredentials: true,
11         xhr_ie8: true
12       }
13       var temp = {}
14 
15       for (var key in data) {
16         if (mapping[key]) {
17           if (!isNaN(tool.getIEVersion()) && key !== 'timeout') {
18             temp[key] = data[key]
19           } else {
20             var newKey = '_' + key
21             temp[newKey] = data[key]
22           }
23         }
24       }
25 
26       for (var i = 0; i < requestNum; i++) {
27         var nullRequest = tool.createXhrObject()
28         tool.MergeObject(nullRequest, temp)
29         selfData.requestPool.push(nullRequest)
30       }
31     },
View Code

4. 使用加速,判断是否开启(只举例post)

 1     //异步post请求
 2     post: function (url, data, successEvent, errorEvent, timeoutEvent) {
 3       var ajaxParam = {
 4         type: "post",
 5         url: url,
 6         data: data,
 7         contentType: '',
 8         successEvent: successEvent,
 9         errorEvent: errorEvent,
10         timeoutEvent: timeoutEvent
11       };
12 
13       if (initParam.pool.isOpen) {
14         tool.useRequestPool(ajaxParam)
15       } else {
16         tempObj.common(ajaxParam);
17       }
18     },
View Code

5. 使用请求池连接,设置基本配置(url,datda,successEvent)

 1     // 请求池申请请求使用
 2     useRequestPool: function (param) {
 3       // 判断请求池中是否有可用请求
 4       if (selfData.requestPool.length !== 0) {
 5         var temp = selfData.requestPool.shift(), sendData = '', tempHeader = {}
 6         // 赋值操做,将数据捆绑到原型上
 7         temp.callback_success = param.successEvent
 8         temp.callback_error = param.errorEvent
 9         temp.callback_timeout = param.timeoutEvent
10         temp.data = param.data
11 
12         // 处理参数
13         switch (param.contentType) {
14           case '':
15             tool.each(tool.MergeObject(param.data, initParam.publicData), function (item, index) {
16               sendData += (index + "=" + item + "&")
17             });
18             sendData = sendData.slice(0, -1);
19             break
20           case 'json':
21             sendData = JSON.stringify(tool.MergeObject(param.data, initParam.publicData))
22             break
23           case 'form':
24             if (!tool.isEmptyObject(initParam.publicData)) {
25               tool.each(initParam.publicData, function (item, index) {
26                 param.data.append(index, item)
27               })
28             }
29             sendData = param.data
30             break
31         }
32 
33         //判断请求类型
34         if (param.type === 'get') {
35           temp.open(param.type, tool.checkRealUrl(param.url, temp) + (sendData === '' ? '' : ('?' + sendData)))
36         } else {
37           temp.open(param.type, tool.checkRealUrl(param.url, temp))
38         }
39 
40         param.responseType ? (temp.responseType = param.responseType) : null
41 
42         if (!isNaN(tool.getIEVersion())) {
43           temp.timeout = temp._timeout
44         }
45 
46         switch (param.contentType) {
47           case '':
48             tempHeader['Content-Type'] = 'application/x-www-form-urlencoded'
49             break
50           case 'json':
51             tempHeader['Content-Type'] = 'application/json'
52             break
53         }
54 
55         //设置http协议的头部
56         tool.each(tool.MergeObject(tempHeader, initParam.requestHeader), function (item, index) {
57           temp.setRequestHeader(index, item)
58         });
59 
60         //发送请求
61         temp.send(param.type === 'get' ? '' : sendData);
62       } else {
63         // 没有请求,加载到待发送队列中
64         selfData.queuePool.push(param)
65       }
66     },
View Code

6. 在默认周期中回收连接(onreadystatechange和onload)

 1       //xmlhttprequest每次变化一个状态所监控的事件(可拓展)
 2       xhr.onreadystatechange = function () {
 3         switch (this.readyState) {
 4           case 1://打开
 5             //do something
 6             break;
 7           case 2://获取header
 8             //do something
 9             break;
10           case 3://请求
11             //do something
12             break;
13           case 4://完成
14             //在ie8下面,无xhr的onload事件,只能放在此到处理回调结果
15             if (this.xhr_ie8) {
16               if (this.status === 200 || this.status === 304) {
17                 if (this.responseType == "json") {
18                   this.callback_success ?
19                     this.callback_success(ajaxSetting.transformResponse(JSON.parse(this.responseText))) :
20                     ajaxSetting.successEvent(ajaxSetting.transformResponse(JSON.parse(this.responseText)))
21                 } else {
22                   this.callback_success ?
23                     this.callback_success(ajaxSetting.transformResponse(this.responseText)) :
24                     ajaxSetting.successEvent(ajaxSetting.transformResponse(this.responseText))
25                 }
26               } else {
27                 // 请求错误搜集
28                 tool.uploadAjaxError({
29                   type: 'request',
30                   errInfo: JSON.stringify(this.data ? this.data : ajaxSetting.data),
31                   errUrl: this.currentUrl,
32                   errLine: this.status,
33                   Browser: navigator.userAgent
34                 })
35               }
36               // 针对IE8 请求池处理
37               if (ajaxSetting.pool.isOpen) {
38                 tool.responseOver(this)
39               }
40             } else {
41               if (this.status === 0) {
42                 // 发送不存在请求,将不会走onload,直接这里就挂了,请求归还请求池
43                 if (ajaxSetting.pool.isOpen) {
44                   tool.responseOver(this)
45                 }
46               }
47             }
48             break;
49         }
50         ;
51       };
View Code
 1       //onload事件(IE8下没有该事件)
 2       xhr.onload = function (e) {
 3         if (this.readyState === 4 && (this.status == 200 || this.status == 304)) {
 4           /*
 5           *  ie浏览器全系列不支持responseType='json'和response取值,因此在ie下使用JSON.parse进行转换
 6           * */
 7           if (!isNaN(tool.getIEVersion())) {
 8             if (this.responseType === 'json') {
 9               this.callback_success ?
10                 this.callback_success(ajaxSetting.transformResponse(JSON.parse(this.responseText))) :
11                 ajaxSetting.successEvent(ajaxSetting.transformResponse(JSON.parse(this.responseText)));
12             } else {
13               this.callback_success ?
14                 this.callback_success(ajaxSetting.transformResponse(this.responseText)) :
15                 ajaxSetting.successEvent(ajaxSetting.transformResponse(this.responseText));
16             }
17           } else {
18             this.callback_success ?
19               this.callback_success(ajaxSetting.transformResponse(this.response)) :
20               ajaxSetting.successEvent(ajaxSetting.transformResponse(this.response));
21           }
22         } else {
23           /*
24            *  这边为了兼容IE八、9的问题,以及请求完成而形成的其余错误,好比404等
25            *   若是跨域请求在IE八、9下跨域失败不走onerror方法
26            *       其余支持了Level 2 的版本 直接走onerror
27            * */
28           this.callback_error ?
29             this.callback_error(e.currentTarget.status, e.currentTarget.statusText) :
30             ajaxSetting.errorEvent(e.currentTarget.status, e.currentTarget.statusText);
31 
32           // 请求错误搜集
33           tool.uploadAjaxError({
34             type: 'request',
35             errInfo: JSON.stringify(this.data ? this.data : ajaxSetting.data),
36             errUrl: this.currentUrl,
37             errLine: this.status,
38             Browser: navigator.userAgent
39           })
40         }
41 
42         // 生命周期结束以后返回数据池,不绑定状态(是否为成功或失败状态)
43         if (ajaxSetting.pool.isOpen) {
44           tool.responseOver(this)
45         }
46       };
View Code

7. 回收方法

1     // 请求周期结束操做
2     responseOver: function (xhr) {
3       selfData.requestPool.push(xhr)
4       if (selfData.queuePool.length > 0) {
5         var tempData = selfData.queuePool.shift()
6         tool.useRequestPool(tempData)
7       }
8     }
View Code

 

如下为测试结果展现(测试覆盖面:针对主流浏览器测试自身不开请求池、开启请求池、与主流框架axios、jquery的ajax对比,对比精度连续发送十、100、1000、5000次请求):

 

chrome(测试单位:微秒):

 

 

 

 

 

 

firefox(测试单位:毫秒)  PS:在5000的请求测试下,除了开启请求池能够正常进行,其余方式全都浏览器崩溃

 

 

 

 

 

safari(测试单位:微秒):

 

 

 

 

 

opera(测试单位:微秒)

 

 

 

 

 

edge(测试单位:毫秒)

 

 

 

 

 

IE11(测试单位:毫秒)

 

 

 

 

IE9(测试单位:毫秒)

 

 

 

 

 

IE8(测试单位:毫秒)   PS:在IE8下面没有找到axios ie8下运行方案,jquery就不作测试,就作自身对比测试

 

 

 

 

 

 

 首先陈述几个问题:

  1. 测试单位取值

    使用浏览器的console.time方法去取值,在不一样浏览器得到的参数不同

  2. 为何不一样意取值

    由于ajax-js在开启请求池的用时中达到了0.00X等级,也就是微秒的级别,在折线图中没法展现,因此增大单位,好看出趋势

 

 

从全部清洗数据得到的统计图中总结如下:

  1. jquery.ajax速度和性能最慢

  2. ajax-js 开启请求池情况下,性能最好,最快

  3. ajax-js类库不开启请求池,相对来讲,比jquery中的ajax性能高,可是,不得不认可,axios比ajax-js优秀。

  4. ajax-js类库开启请求池的情况下,针对于自身,性能提升至少一倍以上,比axios还快,快不少

 

 

数据搜集在:https://github.com/GerryIsWarrior/ajax/tree/master/ajax-dataCenter/collect

清洗完数据造成的报表:https://github.com/GerryIsWarrior/ajax/tree/master/ajax-dataCenter/collect

数据清洗的工具:https://github.com/GerryIsWarrior/ajax/tree/master/ajax-dataCenter/tool

清洗完成造成的数据报表能够直接打开,能够更清晰的看数据

如需测试,能够去github下载该项目,在ajax-interface文件下,写了一个express的node服务器,能够 npm i 初始化以后,而后 npm run start 启动。而后在ajax-testing目录下有个html文件,提供了测试案例和一些简单demo。

这次对外暴露的方法都开启了请求池加速(开启全局加速配置),除了common方法,预留一个未加速通用方法,防止有特殊需求。并且,本次请求池只是针对一个域名的加速,二期,将会增长针对不一样域名加速的请求池。

 

github地址:https://github.com/GerryIsWarrior/ajax   对你有帮助或启发,点个小星星,支持继续研究下去

 

此次的迭代一波三折,中间有不少次走到一半就持续不下去了,可是由于刚开始测试的基础方向是对的,因此不想放弃,而后都是理顺思路一步一步的去走,看本身到底错在什么地方,这个方向为何不能走。还有为了此次测试,作了大量的数据搜集,从mac系统和window不停切换,为主流浏览器作兼容测试和主流框架对比测试数据搜集。搜集完成以后,耐心的作数据清洗,将混杂的数据,进行分类概括,而后找对应报表,最后将数据加载到报表上,作更直观的展现。

 

过程是艰难的,结果是值得兴奋的。正视本身的不足,axios是一个优秀的框架,速度和性能绝对是很好的。在没有作请求池优化以前,本身写的库达不到那个程度,不过在开启了请求池以后,在性能和速度方面已经超过了axios,jquery就不谈了。在研究前端通讯的过程当中收获了不少不少,从基础到设计,从底层到优化,没有什么是解决不了的问题。认清本身,作更好的本身,没有什么是不可能的。

从0-1是艰难的,从99-100更是难上加难。可是,只要去作,终是离那个方向更近一点,前端共勉!!

 

下一步迭代:更完备的文档和更完备的测试

相关文章
相关标签/搜索