如何实现一个通用ajax拦截器
本文由 小茗同学 发表于 2024-05-15 浏览(203)
最后修改 2024-05-15 标签:

前言

在各类ajax框架基础之上实现拦截非常简单,比如jQuery的ajax就内置了ajaxStart事件,但是如何实现一个通用的、不借助任何框架的拦截器呢?

原生的ajax主要由XMLHttpRequestfetch来实现的(过时的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事件;