聊聊前端请求拦截那些事

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

本文系统的整理了前端请求拦截的所有方法,以备各位不时之需。

一、传统请求拦截:xmlhttprequest、fetch

日常工作中,我们最常见的前端请求就是 Ajax 请求,包括 XMLHttpRequestfetch

这两类请求的拦截我们在日常的开发中应该经常会用到,例如接口 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));
});

以上就是前端请求拦截的全部姿势。

如果有更好的方法,也欢迎各位评论、指正。

相关推荐
brzhang23 分钟前
我十几个项目都是这套Flutter 快速开发框架,今天开源了,从此你只用关心业务了
前端·后端·架构
qziovv33 分钟前
控制Vue对话框显示隐藏
javascript·vue.js·elementui
BLACK5951 小时前
Nuxt3中PC端与移动端适配的三种方式(含Nuxt官网同款实现方式)
前端·vue.js·nuxt.js
小宁爱Python1 小时前
TypeScript 泛型详解:从基础到实战应用
前端·javascript·typescript
鱼樱前端1 小时前
一文讲解时下比较火的Rust语言之-rust开发环境搭建
前端·javascript
Moment2 小时前
Next.js 15.4 正式发布:Turbopack 全面稳定,预热 Next.js 16 😍😍😍
前端·javascript·node.js
虚!!!看代码2 小时前
uni-app 跳转页面传参
前端·vue.js·uni-app
脑袋大大的2 小时前
UniApp 自定义导航栏:解决安全区域适配问题的完整实践
前端·javascript·安全·uni-app·uniapp·app开发·uniappx
这辈子谁会真的心疼你2 小时前
pdf格式怎么提取其中一部分张页?
前端·pdf
人工智能训练师3 小时前
Tailwind CSS中设定宽度和高度的方法
前端·css·css3