背景
在现在的公司中,会存在多个环境,里面的角色,某些页面的数据都不相同,如果要使用其中某些环境的api就需要手动切换代理ip,在联调的过程中也需要用这种方式来请求对应后端同学的服务,主要有以下问题。
- 没切换一次ip,项目就需要重新启动
- 切换ip后,账号的token会保留,使用这个token会请求不到新ip的数据
- 后端同学还没有开发,前端先开始开发只能使用mock
- 想测试某个api却没有数据
设想
- 使用拦截器,通过插件控制代理无需重新加载页面
- 拦截器能够实现mock的功能,不需要侵入代码实现单机开发
- 存储历史数据或编辑数据,在没有数据时可以使用拦截器重现数据
拦截器原理
拦截器的原理很简单,原生fetch,接受两个参数,一个是请求路径resource,resource可以是url也可以是request对象,另外一个参数是config,可以配置method,header等请求参数。而拦截器则是通过替换window.fetch,在其外面再套一层处理起request以及response。
fetch拦截
替换原生fetch,对原生fetch进行包裹
function initFetchInterceptor() {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
const [resource, config] = args;
const response = await originalFetch(resource, config);
return response;
};
}
request拦截
request拦截比较简单,就是直接获取resource以及config进行处理,替换为自己想要的参数再发起请求
function initFetchInterceptor() {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
// resource => https://api.github.com?name=meadery
const [resource, config] = args;
// 把resouce替换
const _reource = 'https://api.github.com?name=meadery'
const response = await originalFetch(_reource, config);
return response;
};
}
response拦截
我们直接通过请求一个api看一下response返回以及我们最终拿到的数据
const btn = document.getElementById("btn");
function initFetchInterceptor() {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
const [resource, config] = args;
console.log("resource: ", resource);
console.log("config: ", config);
const response = await originalFetch(resource, config);
console.log("response: ", response);
return response;
};
}
initFetchInterceptor();
btn.addEventListener("click", async function () {
const data = await fetch("https://api.github.com", {
method: "GET",
});
console.log("data: ", data);
});
response
在network中则可以看到
如果要在控制台中看到network这样的数据,则要通过response.json获取
const btn = document.getElementById("btn");
function initFetchInterceptor() {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
const [resource, config] = args;
console.log("resource: ", resource);
console.log("config: ", config);
const response = await originalFetch(resource, config);
console.log("response: ", response);
return response;
};
}
initFetchInterceptor();
btn.addEventListener("click", async function () {
const response = await fetch("https://api.github.com", {
method: "GET",
});
response.json().then(data => {
console.log('data: ', data);
})
console.log("response: ", response);
});
替换json的内容
const btn = document.getElementById("btn");
function initFetchInterceptor() {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
const [resource, config] = args;
console.log("resource: ", resource);
console.log("config: ", config);
const response = await originalFetch(resource, config);
console.log("response: ", response);
const json = () => response
.clone()
.json()
.then(() => "hello interceptor");
response.json = json;
return response;
};
}
initFetchInterceptor();
btn.addEventListener("click", async function () {
const response = await fetch("https://api.github.com", {
method: "GET",
});
response.json().then((data) => {
console.log("data: ", data);
});
console.log("response: ", response);
});
到这里起来前端的拦截原来都清楚了,可以通过修改对应的request,config来控制请求参数,通过修改response来处理.
到这里,要实现对api请求的mock需要已经可以实现,但是用这样的方式,每次使用都需要修改代码,而单纯的mock使用其他一些拦截工具还是可以实现.
浏览器插件
知道了拦截原理,但是如果现在要通过请求拦截对某接口进行mock,还是要在项目文件上面进行编码,那么我们通过编写浏览器插件,把我们的拦截器的代码注入到指定页面那不就可以了吗?
浏览器插件开发
官网的文档肯定是最新,基础开发可以直接看文档。
拦截器操作流程
manifest.json
首先编辑浏览器插件的manifest.json配置基础的文件配置,我们选几个主要的来介绍它的配置内容。
{
"manifest_version": 3,
"name": "fetch_interceptor",
"description": "fetch_interceptor",
"version": "1.0",
"icons": {
"16": "images/16.png",
"32": "images/32.png",
"48": "images/48.png",
"128": "images/128.png"
},
"content_scripts": [
{
"js": ["scripts/content.js"],
"matches": ["<all_urls>"],
"run_at": "document_end"
}
],
"web_accessible_resources": [
{
"resources": ["resources/ajaxInterceptor.js"],
"matches": ["<all_urls>"]
}
],
"action": {
"default_icon": {
"16": "images/16.png",
"32": "images/32.png",
"48": "images/48.png",
"128": "images/128.png"
},
"default_popup": "popup/index.html"
},
"permissions": ["scripting", "activeTab", "tabs"]
}
content_scripts
content_scripts插件启用后,会注入到web页面中,我们可以通过配置run_at来选择注入到时机。matches则配置了<all_urls>声明匹配所有页面。
注意,如果这里想要直接把拦截的代码直接写到content_scripts里面行不通的,因为content_scripts的代理执行环境是独立的,它虽然能够操作web上面的dom,却不能直接改变window对象, 如下:
window.fetch = null;
代码不会生效, 还是能够正常请求。
而且,在实际的开发中,请求拦截我们需要配置开关和需要拦截的路径等等,我们还需要一个popup插件html来进行我们的配置操作。
action.default_popup
这个就是我们常见的浏览器插件右上角的视窗,实际上也是一个html文件
popup.html
添加一个按钮可以让我们给拦截器做开关
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>popup</title>
</head>
<body>
<button id="btn">开启</button>
<script src="./popup.js"></script>
</body>
</html>
popup.js
每一次点击,切换开关状态,并通过chrome api发送到content_scripts
const btn = document.getElementById("btn");
let enabled = false;
btn.addEventListener("click", () => {
enabled = !enabled;
btn.innerHTML = enabled ? '开启' : '关闭';
// 获取当前的tab便签,推送消息
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, {
type: "interceptor",
data: {
enabled,
},
});
}
});
});
content_scripts与web_accessible_resources
{
"content_scripts": [
{
// js路径
"js": ["scripts/content.js"],
// 匹配所有页面
"matches": ["<all_urls>"],
// 注入时间, document_end即在DOM已经已经完成加载,但脚本和图像等资源可能仍在加载
"run_at": "document_end"
}
],
"web_accessible_resources": [
{
"resources": ["resources/ajaxInterceptor.js"],
"matches": ["<all_urls>"]
}
],
}
content.js
注入拦截器代码
content_scripts虽然可以直接操作dom,但如果如果直接通过content_scirpts新增scripts标签注入到web中,但如果这样的话,chrome插件会提示安全相关的问题,而且如果代码量较大,直接编写script字符串页不符合实际。这里可以通过web_accessible_resources定义我们想要的代码文件,在执行时获取其链接再注入到页面中。
function initScript(url) {
const script = document.createElement("script");
script.src = url;
document.body.appendChild(script);
}
function initAjaxInterceptor() {
const url = chrome.runtime.getURL("resources/ajaxInterceptor.js");
initScript(url);
}
initAjaxInterceptor();
监听配置变化
content_scripts虽然有独立的执行环境,但它能够通过postMessage把消息推送到web
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
const { type, data} = request;
if (type === 'interceptor') {
postMessage(request, '*');
}
});
ajaxInterceptor.js
// 拦截器
function initFetchInterceptor() {
const { fetch: originalFetch } = window;
const fetch = async (...args) => {
const [resource, config] = args;
const response = await originalFetch(resource, config);
const json = () =>
response
.clone()
.json()
.then(() => "hello interceptor");
response.json = json;
return response;
};
return fetch;
}
const INTERCEPTOR_CONFIG = {
enabled: true,
originalFetch: window.fetch,
fetchInterceptor: initFetchInterceptor(),
};
// 监听配置是否开启
window.addEventListener("message", function (event) {
const eventData = event.data;
const { type, data } = eventData;
if (type !== "interceptor") return;
const { enabled = false } = data;
console.log('enabled: ---', enabled);
window.fetch = enabled
? INTERCEPTOR_CONFIG.fetchInterceptor
: INTERCEPTOR_CONFIG.originalFetch;
});
web
用vite创建一个react的项目,发起一个请求
async function fetchApi() {
const r = await fetch('https://api.github.com', {
method: 'GET',
});
r.json().then(data => {
console.log('data: ', data);
})
};
打开拦截
拦截成功
资料
Intercepting JavaScript Fetch API requests and responses - LogRocket Blog