前端工具集(7) -- 原生js实现并扩展jquery的ajax功能

之前在做手机端页面的时候,为了减少体积,舍弃了jquery和zepto, 自己用原生写了一个ajax库,不仅可以实现jquery库的ajax功能,而且还进行了扩展,主要有以下功能:

  1. 支持ajax跨域请求
  2. 支持jsonp跨域请求
  3. 支持异步加载js文件
  4. 支持deferred用法或者直接传入回调参数的用法
  5. 支持同步调用
  6. 支持ajax请求失败的加载重试次数设置
  7. 兼容ie6-ie9 的异步同域请求,因为有兼容 window.ActiveXObject 对象

不支持的功能:

  1. xhr对象的上传和下载,这个是库没有,因为做这个库的初衷就是更好的加载js资源或者进行接口请求操作。 如果是要专门针对ajax上传和下载文件的,那么应该要重新封装一个库。
  2. 不支持ie8,ie9的跨域请求,因为没有封装XDomainRequest对象,可以参考这一篇: ie8,ie9 使用 XDomainRequest 进行跨域,至于ie8以下的异步跨域请求,那就不能用ajax的方式了,得用其他的方式,比如如果是get的话,那么就可以用jsonp来处理,可以参照这个:前端工具集(1) -- jsonp原生实现
  3. ie 10 开始支持xhr对象,不过没有withCredentials这个属性,所以如果是ie10的话,并且需要支持cookie跨域的话,那么就需要将cookie的内容读取出来,然后放到参数里面,最后由服务端来进行读取。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/**
* Ajax 请求库
*/
(function (_window) {
_window.globalEval = _window.globalEval || eval;

var Deferred = function () {
var self, successCbs, failCbs, alwaysCbs, status,
result, createDeferredFn, createInvokeFn;

self = this;
successCbs = [];
failCbs = [];
alwaysCbs = [];
status = 'waiting';
result = [];

createDeferredFn = function (invokeStatus, cbs) {
return function (fn) {
if (invokeStatus.indexOf(status) > -1) {
fn.apply(self, result);
} else {
cbs.push(fn);
}
return this;
};
};

createInvokeFn = function (newStatus, cbs) {
return function () {
var args = arguments;
status = newStatus;
cbs.concat(alwaysCbs).forEach(function(cb){
cb.apply(self, args);
result = args;
});
};
};

return {
done: createDeferredFn(['resolved'], successCbs),
fail: createDeferredFn(['rejected'], failCbs),
always: createDeferredFn(['resolved', 'rejected'], alwaysCbs),
resolve: createInvokeFn('resolved', successCbs),
reject: createInvokeFn('rejected', failCbs)
};
};

_window.ajax = {
/**
* 发送 jsonp 请求
* @param url
* @param {Object|Function} [data]
* @param {Function} [successCb]
* @param {Function} [failCb]
* @param {Function} [alwaysCb]
*/
jsonp: function (url, data, successCb, failCb, alwaysCb) {
var callbackName, script, defer;

defer = Deferred();

if (typeof url === 'object') {
var tmp = url;
url = tmp.url;
data = tmp.data;
successCb = tmp.success;
failCb = tmp.fail;
alwaysCb = tmp.always;

} else if (typeof data === 'function') {
successCb = data;
data = {};
}

data.callback = 'jsonp' + new Date().getTime();
callbackName = data.callback;

if (data) {
url = url + '?' + toUrlParam(data);
}
script = document.createElement("script");
script.id = callbackName;

function clean() {
alwaysCb && alwaysCb();
window[callbackName] = null;
script.parentNode.removeChild(script);
}

window[callbackName] = function (resp) {
// TODO 实现 jsonp 的 responseText
// defer.responseText =
successCb && successCb(resp);
defer.resolve(resp);
clean();
console.debug('jsonp end:', resp);
};

script.onerror = function (e) {
failCb && failCb(e);
defer.reject(e);
clean();
console.debug('jsonp error:', e);
};

console.debug('jsonp start:', url);
script.src = url;
document.body.appendChild(script);
return defer;
},

/**
* 发送 ajax 请求,用法与 jqeruy 类似
* @param opts
*/
ajax: function (opts) {
var dataType, async, fakeXhr, type;

opts = opts || {};
opts.dataType = opts.dataType ? opts.dataType.toLowerCase() : '';
opts.async = typeof opts.async === 'undefined' ? true : opts.async;
opts.type = opts.type ? opts.type.toLowerCase() : 'get';

dataType = opts.dataType;
async = opts.async;
type = opts.type;

if (async && type === 'get' && dataType === 'jsonp') {
fakeXhr = ajax.jsonp.call(ajax, opts);
} else {
fakeXhr = ajax.xhrRequest.call(ajax, opts);
}
return fakeXhr;
},

requestRepeat: function (opts) {
opts.defer = Deferred();
opts.countTime = 0;
opts.maxTime = opts.maxTime || 3;
opts.request = ajax.ajax;
return function () {
return ajax.ajax(opts);
}
},

xhrRequest: function (opts) {
var self, url, data, type, dataType, async, resp, defer, xhr, maxTime, withCredentials, timeout;

defer = opts.defer || Deferred();
maxTime = opts.maxTime || 3;
opts.countTime = typeof opts.countTime === 'undefined' ? maxTime : ++opts.countTime;

self = this;
opts = opts || {};
url = opts.url;
data = opts.data;
async = opts.async;
dataType = opts.dataType;
type = opts.type;
timeout = opts.timeout;
var _timer = null;
// 是否允许跨域的时候,带上cookie
// 如果是同步请求的话,必须要为false,不然会报错
withCredentials = async ? !!opts.withCredentials : false;

if ('data' in opts && typeof data !== 'string') {
data = toUrlParam(data);
if (type === 'get') url += '?' + data;
}
xhr = window.ActiveXObject ? createActiveXHR() : createStandardXHR(withCredentials);
xhr.open(type, url, async);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
defer.responseText = xhr.responseText;
clearTimeout(_timer)
switch (xhr.status) {
case 200:
resp = self.decodeResponse(xhr.responseText, dataType);
opts.success && opts.success(resp);
defer.resolve(resp);
break;
default :
if (opts.countTime < maxTime) {
opts.request(opts);
} else {
opts.fail && opts.fail(xhr);
defer.reject(xhr);
}
break;
}
}
};
if (data) {
if (type === 'post') {
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
}
xhr.send(data);
console.debug('ajax start:', url, data);
} else {
xhr.send();
console.debug('ajax start:', url);
}


if (!async) {
defer.responseText = xhr.responseText;
}else if(timeout) {
_timer = setTimeout(function () {
xhr.abort();
opts.fail && opts.fail(xhr);
defer.reject(xhr)
}, timeout)
}

return defer;
},

decodeResponse: function (responseText, dataType) {
switch (dataType) {
default :
case 'html':
return responseText;
case 'json':
return JSON.parse(responseText);
case 'script':
globalEval(responseText);
return '';
}
},

"getJSON": function (url, data, successCb, failCb, alwaysCb) {
if (typeof url === 'object') {
var tmp = url;
url = tmp.url;
data = tmp.data;
successCb = tmp.success;
failCb = tmp.fail;
alwaysCb = tmp.always;

} else if (typeof data === 'function') {
successCb = data;
data = {};
}

this.ajax({
url: url,
data: data,
success: successCb,
fail: failCb,
always: alwaysCb
});
}
};

function createStandardXHR(withCredentials) {
try {
var xhrTmp = new window.XMLHttpRequest();
xhrTmp.withCredentials = withCredentials;
return xhrTmp;
} catch (e) {
}
}

function createActiveXHR() {
try {
return new window.ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
}
}

function toUrlParam(obj) {
var arr = [];
for (var key in obj) {
arr.push(key + '=' + encodeURIComponent(obj[key]));
}
return arr.join('&');
}
})(window);

用法的话:

  1. jsonp 用法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    window.ajax.jsonp(
    'http://xxx.com/phone/premium',
    {q: "111"}
    ).done(function (resp) {
    console.log("done 1");
    }).done(function (resp) {
    console.log("done 2");
    }).done(function (resp) {
    console.log("done 3");
    }).fail(function () {
    console.log("fail 1");
    }).fail(function () {
    console.log("fail 2");
    }).fail(function () {
    console.log("fail 3");
    }).always(function () {
    console.log("always 1");
    }).always(function () {
    console.log("always 2");
    });
    1
  2. 多次数的 ajax 请求, 如果失败的话,会重复请求三次
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var request = ajax.requestRepeat({
    url: "http://xxx.com/phone/premium1",
    dataType: 'json',
    type: 'get',
    maxTime: 3
    })();
    request.done(function (res) {
    console.log("done")
    }).fail(function () {
    console.log("fail")
    })
    1
  1. 普通的 ajax 请求:
    1
    2
    3
    4
    5
    6
    7
    var request = ajax.ajax({
    url: "http://xxx.com/phone/premium",
    }).done(function (res) {
    console.log("done")
    }).fail(function () {
    console.log("fail")
    })

ps: 如果真的要在ie6,ie7 下使用的话,那么有一个函数会报错,就是 Array.prototype.forEach,所以这边要加上这个polyfill方法: 文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {

Array.prototype.forEach = function(callback, thisArg) {

var T, k;

if (this == null) {
throw new TypeError(' this is null or not defined');
}

// 1. Let O be the result of calling toObject() passing the
// |this| value as the argument.
var O = Object(this);

// 2. Let lenValue be the result of calling the Get() internal
// method of O with the argument "length".
// 3. Let len be toUint32(lenValue).
var len = O.length >>> 0;

// 4. If isCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}

// 5. If thisArg was supplied, let T be thisArg; else let
// T be undefined.
if (arguments.length > 1) {
T = thisArg;
}

// 6. Let k be 0
k = 0;

// 7. Repeat, while k < len
while (k < len) {

var kValue;

// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty
// internal method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {

// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];

// ii. Call the Call internal method of callback with T as
// the this value and argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
}

这样就没问题了。 基本上这个库可以满足正常的接口请求的方法。