在我们日常开发中,经常需要做一些请求拦截的事情。例如拦截 js 文件、css、图片以及接口请求数据,用来做一些 mock 以及异常监控的事情。这在前端也并不新鲜。主要原理都是通过 mock api 实现一些劫持的动作。但是实际在实现的时候还是存在一些细节的点。

本文系统的整理了前端请求拦截的所有方法,以备各位不时之需。
一、传统请求拦截:xmlhttprequest、fetch
日常工作中,我们最常见的前端请求就是 Ajax 请求,包括 XMLHttpRequest
和 fetch
。
这两类请求的拦截我们在日常的开发中应该经常会用到,例如接口 mock、接口信息查看,请求日志上报等。拦截的方法也很常规,无非就是重写 XMLHttpRequest 和 fetch 方法。
我们废话不多说,直接上代码。
1. 拦截 XMLHttpRequest
js
const interceptXHR = (onIntercept: (info) => void) => {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
// 用来关联request和response
let id = 0;
XMLHttpRequest.prototype.open = function (...args) {
this._url = args[1];
this._method = args[0];
this._headers = {};
return originalOpen.apply(this, args);
};
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
this._headers[header] = value;
return originalSetRequestHeader.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (...args) {
// 创建请求信息对象
const requestInfo = {
data: {
method: this._method,
url: this._url,
headers: this._headers,
body: args[0],
type: "xhr",
}
id: id++,
type: 'request'
}
onIntercept(requestInfo)
// 监听响应
this.addEventListener("readystatechange", function () {
if (this.readyState === 4) {
// 拦截响应头
const allResHeaders = this.getAllResponseHeaders();
// 合并请求和响应信息
const responseInfo = {
data: {
status: this.status,
statusText: this.statusText,
responseHeaders: allResHeaders,
response: this.responseText,
},
id: requestInfo.id,
type: 'response'
}
// 调用回调函数
requestInfo(completeInfo);
}
});
return originalSend.apply(this, args);
};
};
// 调用拦截方法
interceptXHR((info) => {
console.log(`请求信息: ${info.type}`, info);
});
上述代码不仅可以拦截请求的 url、method、body,还能收集所有设置的请求头,并在响应返回时打印所有响应头。
拦截 xhr 的核心点是通过重写 open、setRequestHeader 和 send 方法来拦截请求相关的信息,通过监听 readystatechange 来拦截响应。
需要注意的是,这里的 open 方法是不支持异步的,否则后面的 send 和 setRequestHeader 调用会抛出异常。
如果你想要实现一个异步的 open 方法,请确保 send 和 setRequestHeader 的调用在 open 方法之后执行。
2. 拦截 fetch
js
const interceptFetch = (onIntercept: (requestInfo) => void) => {
const originalFetch = window.fetch;
let id = 0
window.fetch = function(input, init = {}) {
// 获取请求信息
const url = typeof input === 'string' ? input : input.url;
const method = init.method || 'GET';
// 获取请求头
let headers = {};
if (init.headers) {
if (init.headers instanceof Headers) {
init.headers.forEach((value, key) => {
headers[key] = value;
});
} else {
headers = init.headers;
}
} else if (input instanceof Request) {
input.headers.forEach((value, key) => {
headers[key] = value;
});
}
// 创建请求信息对象
const requestInfo = {
data: {
method,
url,
headers,
body: init.body,
},
id: id++
type: 'request'
}
onIntercept(requestInfo)
// 调用原始fetch并拦截响应
return originalFetch.apply(this, arguments).then(response => {
// 克隆response以便读取body
const responseClone = response.clone();
// 获取响应头
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
// 读取响应体
return responseClone.text().then(responseText => {
// 合并请求和响应信息
const completeInfo = {
data: {
status: response.status,
statusText: response.statusText,
responseHeaders,
response: responseText
},
id: id++,
type: 'response'
}
// 调用回调函数
onIntercept(completeInfo);
// 返回原始response
return response;
});
});
};
};
fetch 的拦截同样可以获取和打印请求头、请求体,以及响应头信息,方便做更细致的监控和调试。
相对于XHR的拦截,fetch则简单很多。需要注意的是:
- fetch方法支持多个参数,存在不同的定义(重载),重写的时候需要把这几种重载方式都兼容好。
- 劫持response的时候通常是通过response clone的方法实现,避免同一个response被多次使用引发bug。当然这部分也可以重写response的text或者json方法实现。
js
// 调用拦截方法
interceptFetch((info) => {
console.log("Fetch 信息:", info);
});
这样,所有通过 XHR 和 fetch 发起的请求都会被我们"截获",可以做日志、埋点等操作。
但是mock还不行。我们只需要在上述方法稍加修改就可以实现mock能力。
如何实现接口mock
我们只需要稍微修改下interceptFetch和interceptXHR方法,让它们支持返回mock数据。
1. 支持mock的interceptXHR
js
const interceptXHR= (onIntercept: (requestInfo) => any) => {
// ..以上代码不变
// 调用回调函数,获取mock数据
const mockData = onIntercept(requestInfo);
// 如果有mock数据,直接返回mock响应
if (mockData !== undefined) {
// 模拟异步响应
setTimeout(() => {
// 设置响应状态
Object.defineProperty(this, 'status', { value: mockData.status || 200 });
Object.defineProperty(this, 'statusText', { value: mockData.statusText || 'OK' });
Object.defineProperty(this, 'responseText', { value: mockData.response || '' });
Object.defineProperty(this, 'readyState', { value: 4 });
// 触发readystatechange事件
this.dispatchEvent(new Event('readystatechange'));
}, 0);
return;
}
//... 以下代码不变
};
2. 支持 mock 的 interceptFetch
js
const interceptFetch = (onIntercept: (requestInfo) => any) => {
// ..以上代码不变
// 调用回调函数,获取mock数据
const mockData = onIntercept(requestInfo)
// 如果有mock数据,返回mock响应
if (mockData !== undefined) {
const mockResponse = new Response(mockData.response || '', {
status: mockData.status || 200,
statusText: mockData.statusText || 'OK',
headers: mockData.headers || {}
});
return Promise.resolve(mockResponse);
}
//... 以下代码不变
};
划重点:
- xhr的拦截的方式是通过dispatch readystatechange、load事件实现
- fetch是通过直接返回一个新创建的response实现
3. 使用示例
js
// 使用支持mock的拦截方法
interceptXHR((info) => {
// 根据请求信息决定是否返回mock数据
if (info.url.includes("/api/user")) {
return {
status: 200,
statusText: "OK",
response: JSON.stringify({ id: 1, name: "Mock User" }),
};
}
// 返回undefined表示不mock,走正常流程
});
interceptFetch((info) => {
if (info.url.includes("/api/posts")) {
return {
status: 200,
statusText: "OK",
response: JSON.stringify([{ id: 1, title: "Mock Post" }]),
headers: { "Content-Type": "application/json" },
};
}
});
此外,不管是fetch还是xhr,都可以通过Proxy api实现mock,原理上大差不差,这里就不提供的代码了。
二、拦截图片、CSS、JS 等资源请求
除了 Ajax 请求,页面还会加载各种静态资源,比如图片、样式、脚本等。这些资源的拦截就相对复杂一点。但是本质上也是通过重写相应的方法来实现。
1. 拦截图片请求
js
Object.defineProperty(HTMLImageElement.prototype, "src", {
set: function (value) {
console.log("图片资源请求:", value);
// 可以在这里做拦截或替换
return value;
},
});
2. 拦截 JS 脚本请求
js
Object.defineProperty(HTMLScriptElement.prototype, "src", {
set: function (value) {
console.log("JS资源请求:", value);
return value;
},
});
3. 拦截 CSS 样式请求
js
Object.defineProperty(HTMLLinkElement.prototype, "href", {
set: function (value) {
console.log("CSS资源请求:", value);
return value;
},
});
通过这种方式,页面上所有通过 DOM 加载的图片、脚本、样式资源都能被我们感知和处理。
三、Service Worker:全方位网络请求拦截
此外,针对高版本的浏览器,我们还可以通过service worker的方式来实现拦截。
这种方式是基于浏览器自身的api,不需要mock任何方法,功能更强大,体验更好。可以拦截网页发出的所有请求。
唯一的问题就是兼容性不好,ios似乎目前还不支持。
具体可以参考 Can I use Service Worker 获取最新的兼容性信息。
1. 注册 Service Worker
js
// main.js
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
2. 在 Service Worker 中拦截请求
js
// sw.js
self.addEventListener("fetch", function (event) {
console.log("Service Worker 拦截请求:", event.request.url);
// 可以自定义响应
// event.respondWith(fetch(event.request));
});
以上就是前端请求拦截的全部姿势。
如果有更好的方法,也欢迎各位评论、指正。