前言
在各类ajax
框架基础之上实现拦截非常简单,比如jQuery的ajax就内置了ajaxStart
事件,但是如何实现一个通用的、不借助任何框架的拦截器呢?
原生的ajax主要由XMLHttpRequest
和fetch
来实现的(过时的ActiveXObject
先不考虑),要实现通用ajax的拦截,必须在这些原生方法上面下手脚。
本文所说的拦截器仅仅指的是插入自己的业务代码,并不能改变ajax
的结果,所以说成ajax
事件可能会更好理解。本文以捕获ajax
记录为例来实现一个简易ajax拦截器。
fetch的拦截
fetch的拦截非常简单。由于fetch
只是一个普通的方法,调用后返回一个promise
,并不能用来做构造函数,也就不用关心原型之类的事情,所以fetch的拦截极其简单,6行代码实现fetch的拦截:
// 覆盖原生的 fetch 方法
const tempFetchName = '__rawFetch__';
if (!window[tempFetchName]) {
window[tempFetchName] = window.fetch;
window.fetch = function(url, options) {
ajaxInterceptor(url, options);
return window[tempFetchName](url, options);
};
}
XMLHttpRequest的拦截
XMLHttpRequest
是一个构造函数,要拿到url信息只需在其原型的open
方法上面做手脚即可,open的标准语法为xhr.open(method, url, async, user, password))
:
// 覆盖 xhr 原生的 open 方法
const tempXhrOpenName = '__rawOpen__';
if (window.XMLHttpRequest) {
var prototype = window.XMLHttpRequest.prototype;
// 某些第三方库比较暴力导致 prototype.open 丢失,所以这里需要特殊判断一下
if (!prototype[tempXhrOpenName] && prototype.open) {
prototype[tempXhrOpenName] = prototype.open;
prototype.open = function(method, url, async, user, password) {
// xhr 的 options 暂时只能捕获到 method
ajaxInterceptor(url, {method: method});
prototype[tempXhrOpenName].call(this, method, url, async, user, password);
};
}
}
这里补充一点,网上可能有些其它框架也会重写XMLHttpRequest甚至丢失部分原型,比如一个名叫 page.js 的进度插件就有类似如下代码:
var _XMLHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = function(flags) {
var req;
req = new _XMLHttpRequest(flags);
monitorXHR(req);
return req;
};
虽然它复制了部分prototype
上的方法到新的对象上来,但是open方法就没有复制,所以导致没被重写后的XMLHttpRequest
原型上没有open这个方法,只能说这类实现不够完整。
如果要实现完整的XMLHttpRequest
重写以及拦截,可以参考mock.js
模块中 xhr.js 里面的具体实现。
完整代码
这里我要实现这样一个功能,无论何时都能拿到页面已经调用过的ajax记录,有新的ajax调用时能够收到通知,综合上述代码简单实现如下:
/**
* ajax暴力拦截器
*/
;(function(window) {
var ajaxInterceptor = {
_history: [],
_events: {},
// 获取当前页面已经发生过的所有ajax记录
getAll: function() {
return this._history;
},
// 请求一个新的URL
request: function(url, options) {
url = parseURL(joinURL(url));
this._history.push({url: url, options: options || {}});
this.emit('request', url, options || {});
},
// 绑定一个新事件,目前仅支持 'request'
on: function(type, fn) {
this._events[type] = this._events[type] || [];
this._events[type].push(fn);
},
// 取消事件
off: function(type, fn) {
if(fn === undefined) this._events[type] = [];
else {
var callbacks = this._events[type] || [];
var idx = callbacks.indexOf(fn);
if(idx >= 0) {
callbacks.splice(idx, 1);
}
}
},
// 触发事件
emit: function(type) {
var params = [].slice.call(arguments, 1);
(this._events[type] || []).forEach(function(fn) {
fn.apply(this, params);
});
}
};
// 覆盖原生的 fetch 方法
const tempFetchName = '__rawFetch__';
if (!window[tempFetchName]) {
window[tempFetchName] = window.fetch;
window.fetch = function(url, options) {
ajaxInterceptor.request(url, options);
return window[tempFetchName](url, options);
};
}
// 覆盖 xhr 原生的 open 方法
const tempXhrOpenName = '__rawOpen__';
if (window.XMLHttpRequest) {
var prototype = window.XMLHttpRequest.prototype;
// 某些第三方库比较暴力导致 prototype.open 丢失,所以这里需要特殊判断一下
if (!prototype[tempXhrOpenName] && prototype.open) {
prototype[tempXhrOpenName] = prototype.open;
prototype.open = function(method, url, async, user, password) {
// xhr 的 options 暂时只能捕获到 method
ajaxInterceptor.request(url, {method: method});
prototype[tempXhrOpenName].call(this, method, url, async, user, password);
};
}
}
window.ajaxInterceptor = ajaxInterceptor;
})(window);
使用:
ajaxInterceptor.getAll()
可以拿到所有的ajax历史记录;ajaxInterceptor.on('request', (url, options) => {})
捕获ajax事件;