虽然jquery的较新的api已经很好用了, 但是在实际工作还是有做二次封装的必要,好处有:1,二次封装后的API更加简洁,更符合个人的使用习惯;2,可以对ajax操作做一些统一处理,比如追加随机数或其它参数。同时在工作中,我们还会发现,有一些ajax请求的数据,对实时性要求不高,即使我们把第一次请求到的这些数据缓存起来,然后当相同请求再次发起时直接拿之前缓存的数据返回也不会对相关功能有影响,通过这种手工的缓存控制,减少了ajax请求,多多少少也能帮助我们提高网页的性能。本文介绍我自己关于这两方面问题的做法,欢迎交流和指正。
代码下载
(注:因为用到了ajax,所以不能在file协议下运行,必须运行在http下)1. 封装jquery的ajax
其实这个部分的相关内容在之前的一篇博客引入过,只不过那里面只是引用,没有详细说明,另外ajax缓存代理组件的实现也是基于这个二次封装之后的ajax组件的,所以有必要在这里对它详细说明一下,虽然实现并不复杂(详见注释说明):
//根据关键的几个参数统一创建ajax对象
function create(_url,_method,_data,_async,_dataType) {
//添加随机数
if (_url.indexOf('?') > -1) {
_url = _url + '&rnd=' + Math.random();
} else {
_url = _url + '?rnd=' + Math.random();
}
//为请求<a href="https://www.jb51.cc/tag/tianjia/" target="_blank" class="keywords">添加</a>ajax标识,方便<a href="https://www.jb51.cc/tag/houtai/" target="_blank" class="keywords">后台</a>区分ajax和非ajax请求
_url += '&_ajax=1';
//返回jquery创建的ajax对象,以便外部拿到这个对象以后可以通过
//.done .fail .always来<a href="https://www.jb51.cc/tag/tianjia/" target="_blank" class="keywords">添加</a>回调
//这么做是为了保留jquery ajax中好用的部分
return $.ajax({
url: _url,dataType: _dataType,async: _async,method: _method,data: _data
});
}
//ajax就是本组件全局唯一的实例,它的实例方法通过后面的循环代码添加
//methods对象配置ajax各个实例方法的参数:
//name: 方法名称
//method: http请求方法,get or post
//async: 发送请求时是否异步
//dataType: 返回的数据类型,html or json
var ajax = {},methods = [
{
name: 'html',method: 'get',async: true,dataType: 'html'
},{
name: 'get',dataType: 'json'
},{
name: 'post',method: 'post',{
name: 'syncGet',async: false,{
name: 'syncPost',dataType: 'json'
}
];
//由于二次封装需要对外提供的每个实例方法创建ajax的逻辑是相同的
//所以通过这种方式统一定义各个实例方法
//关键代码为下面代码中的那个立即调用的函数
//它返回了一个新的闭包函数作为实例方法
for (var i = 0,l = methods.length; i < l; i++) {
ajax[methods[i].name] = (function (i) {
return function () {
/**
- 每个实例方法接收三个参数
- 第一个表示要请求的地址
- 第二个表示要提交到后台的数据,是一个object对象,如{param1: 'value1'}
- 第三个表示后台返回的数据类型,最最常用的就是html or json,绝大部分情况下这个参数不用传,会使用methods里面定义的dataType
*/
var _url = arguments[0],_data = arguments[1],_dataType = arguments[2] || methods[i].dataType;
return create(_url,methods[i].method,methods[i].async,_dataType);
}
})(i);
}
return ajax;
});
使用方式:
//以GET方式请求html内容
Ajax.html('html/demo',{
param1: 'value1',param2: 'value2'
}).done(function(response){
//请求成功的回调
}).fail(function(){
//请求失败的回调
}).always(function(){
//请求完成的回调
});
//以GET方式请求json数据
Ajax.get('api/demo',param2: 'value2'
}).done(function(response){
//请求成功的回调
}).fail(function(){
//请求失败的回调
}).always(function(){
//请求完成的回调
});
//以POST方式请求json数据
Ajax.post('api/demo',param2: 'value2'
}).done(function(response){
//请求成功的回调
}).fail(function(){
//请求失败的回调
}).always(function(){
//请求完成的回调
});
//以GET方式发送同步请求,获取json数据
Ajax.syncGet('api/demo',param2: 'value2'
}).done(function(response){
//请求成功的回调
}).fail(function(){
//请求失败的回调
}).always(function(){
//请求完成的回调
});
//以POST方式发送同步请求,获取json数据
Ajax.syncPost('api/demo',param2: 'value2'
}).done(function(response){
//请求成功的回调
}).fail(function(){
//请求失败的回调
}).always(function(){
//请求完成的回调
});
});
由于这个组件的每个实例方法返回的对象就是$.ajax创建的对象,所以我们完全可以照常使用.done .fail .always来添加回调,就跟直接用$.ajax没有任何区别。为什么API要设计成html,get,post,syncGet,syncPost这几个方法,而且连dataType基本都是固定的?
那是因为在项目中,我们完全可以约定在异步请求的时候只能用html或json这两种dataType,其它dataType不允许,现在的web项目这两种方式已经完全够用了,至少我没有碰到过非得使用别的dataType不可的情况;而且在实际工作当中html,syncPost这几个方法几乎能够涵盖我们需要的所有异步请求的场景,每当我们要用ajax的时候,无非考虑的就是get还是post,同步还是异步,请求的是html还是json这三个问题,通过它们就能把每个问题都解决了。当然jsonp,rest API这两种情况就另说了,这个组件不是为它们服务的,这也是它的局限性,它还是倾向于在传统的web项目中使用。
2. ajax缓存代理
要实现一个简单的ajax缓存代理组件,首先要清楚这个缓存代理的作用,在本文开篇说到过缓存代理的应用场景:当使用缓存代理第一个发起某个请求时,在请求成功后将数据缓存下来,然后当再次发起相同请求时直接返回之前缓存的数据,缓存代理的作用是控制何时发送请求去后台加载数据,何时不发送请求直接从缓存中读取之前加载的数据。为了实现一个简单的缓存代理,有三个问题要解决:
1)代理对象必须与被代理的对象有相同的API 拿前面的Ajax组件来说,它提供有html,syncPost方法,那么它的代理对象也必须同时具有这些方法,而且调用方式,传入参数都必须完全一致,只有这样,当我们在使用代理对象的时候,就跟在使用原组件对象没有区别。而且在缓存代理内部,在某些条件下是需要调用原组件对象发送ajax请求的,如果接口不同,调用方式不同,参数不同,如何能保证内部能够正确调用原组件对象呢?这个条件还有一个好处,就是当我们下次不想使用代理对象的时候,能够以最小的代价将代理对象替换为原组件对象。 这一点其实是设计模式中代理模式的基本要求。
2)缓存数据存储时的缓存索引问题 也就是说我们以什么样的索引才能保证同一个请求的数据在缓存之后,下次查找时还能根据请求信息查找到呢?ajax缓存有别于其它缓存的地方在于它请求的地址可能包含可变的参数值,同一个地址如果后面的参数不同,那么对应的请求结果也就不一定相同,所以简单起见,可以考虑把请求地址跟请求参数统一作为缓存索引,这样就能对缓存进行简单管理。同时考虑到其它可变性,还应有其它的一些要求,详见后面组件实现中的注释说明。
3)缓存有效时间 虽然要实现的缓存代理很简单,但是这个问题一定是要考虑的,每个缓存代理实例,能够缓存数据的有效时间不一定相同,有的可能只缓存几分钟,有的可能缓存几十分钟,当缓存时间失效时,缓存代理就得删除原来的缓存,然后重新去加载数据才行。
综合这些问题,基于第一部分的Ajax组件,最终实现的缓存代理组件AjaxCache的代码如下(有注释详解):
var Ajax = require('mod/ajax');
//缓存列表
var cache = {};
/**
- 生成缓存索引:
- 由于索引是根据url和data生成的(data是一个对象,存放Ajax要提交到后台的数据)
- 所以要想同一个url,同样的data能够有效地使用缓存,
- 切勿在url和data中包含每次可变的参数值,如随机数等
- 比如有一个请求:
- url: aaa/bbb/cccc?r=0.312738
- data: {name: 'json'}
- 其中url后面的r是一个随机数,每次外部发起这个请求时,r的值都会变化
- 由于r每次都不同,最终会导致缓存索引不相同,结果缓存就无法命中
- 注:随机数可放置在原始的Ajax组件内
- 还有:如果是同一个接口,最好在同一个页面内,统一url的路径类型,要么都是相对路径,要么都是绝对路径
- 否则也会导致缓存无法有效管理
*/
function generateCacheKey(url,data) {
return url + $.param(data);
}
return function (opts) {
opts = opts || {};
var cacheInterval = opts.cacheInterval || (1000 * 60 * 60);//缓存有效时间,默认60分钟
var proxy = {};
for (var i in Ajax) {
if (Object.prototype.hasOwnProperty.call(Ajax,i)) {
//在proxy对象上定义Ajax组件每一个实例<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>的代理
//注意这个立即<a href="https://www.jb51.cc/tag/diaoyong/" target="_blank" class="keywords">调用</a>的<a href="https://www.jb51.cc/tag/hanshu/" target="_blank" class="keywords">函数</a>表达式
//它返回了一个闭包<a href="https://www.jb51.cc/tag/hanshu/" target="_blank" class="keywords">函数</a>就是最终的代理<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>
proxy[i] = (function (i) {
return function () {
var _url = arguments[0],cacheKey = generateCacheKey(_url,_data),cacheItem = cache[cacheKey],isCacheValid = false;
if (cacheItem) {
var curTime = +new Date();
if (curTime - cacheItem.cacheStartTime <= cacheInterval) {
//如果请求时间跟缓存开始时间的间隔在缓存有效时间范围内,就表示缓存是有效的
isCacheValid = true;
} else {
//否则就把缓存清掉
delete cache[cacheKey];
}
}
if (isCacheValid) {
//模拟一个异步任务来返回已经缓存的数据
//通过$defer延迟对象,可以保证这个模拟任务返回的对象跟原始Ajax组件<a href="https://www.jb51.cc/tag/diaoyongfanhui/" target="_blank" class="keywords">调用返回</a>的对象有相同的API
//这是代理的关键:代理对象与被代理的对象应该具有相同API
//只有这样当我们取消代理的时候,不会对那些用了代理的组件进行<a href="https://www.jb51.cc/tag/xiugai/" target="_blank" class="keywords">修改</a>
var $defer = $.Deferred();
setTimeout(function () {
$defer.resolve(cacheItem.res);
},10);
return $.when($defer);
}
//缓存失效或者没有缓存的时候<a href="https://www.jb51.cc/tag/diaoyong/" target="_blank" class="keywords">调用</a>原始的Ajax组件的同名<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" class="keywords">方法</a>去<a href="https://www.jb51.cc/tag/houtai/" target="_blank" class="keywords">后台</a>请求数据
return Ajax[i].apply(Ajax,arguments).done(function (res) {
//在请求成功之后将结果缓存,并记录当前时间作为缓存的开始时间
cache[cacheKey] = {
res: res,cacheStartTime: +new Date()
}
});
}
})(i);
}
}
return proxy;
};
});
<div class="cnblogs_code">在第一部分和本部分的实现中,最关键的都是那个立即调用的函数表达式,没有它返回的闭包,代码就会有问题,这也是闭包在循环中应用的经典问题。