打造自己的请求拦截器

469 阅读5分钟

背景

在现在的公司中,会存在多个环境,里面的角色,某些页面的数据都不相同,如果要使用其中某些环境的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