基于H5请求劫持能力如何设计一款异常监听SDK?

前言

最近对于前端稳定性的方向研究较多,主要负责的项目基于ARMS进行系统监控,而接口稳定性是系统稳定性最核心的参考指标之一,那设计一款类似ARMS这样的监听SDK,核心思路是怎样的?

原子能力

想要做到系统透明,就需要在每一次Web request的时候,都在监听器中监听并记录到最终的结果。目前主流的请求方式是fetchxhr

fetchXMLHttpRequest (XHR) 都用于在浏览器中进行网络请求,但它们有一些重要的区别:

  1. 语法和使用方式:

    • fetch 使用的是基于 Promises 的语法,这使得代码更加简洁和易于读写。它可以很好地与 async/await 结合使用。
    • XMLHttpRequest 使用的是基于回调的方式,这可能会导致回调嵌套(回调地狱),从而使代码难以维护。
  2. 返回数据:

    • fetch 默认不会自动发送或接收 cookies(没有credentials的设置为"same-origin"或者"include"时是不会发送cookies的),需要手动配置 credentials 选项。
    • XMLHttpRequest 会自动发送所有的请求头信息,包括 cookies,除非明确设置不发送。
  3. 错误处理:

    • fetch 只有在网络错误导致请求失败时才会被拒绝,即只有在无法完成请求时(如网络断开),Promise 才会被拒绝。对于 HTTP 错误状态码(如 404 或 500),它不会被拒绝,需要手动检查响应的 ok 属性。
    • XMLHttpRequest 则是在状态改变时,通过回调函数进行错误处理,状态码错误也可以在处理函数中处理。
  4. 进度信息:

    • XMLHttpRequest 提供了一些事件(如 progress 事件),可以用于实时获取上传或下载的进度。
    • fetch API 本身不直接提供进度信息,但是可以通过 ReadableStream API 来实现。
  5. 跨域请求:

    • fetch 支持 CORS,并通过 Mode 来管理请求的跨域权限。
    • XMLHttpRequest 也支持 CORS 但可能需要配置复杂一些。
  6. 取消请求:

    • fetch 可以与 AbortController 搭配使用来取消请求。

回归正题,如何在xhr中监听到接口呢?答案是重写原生原型Api,你可以将这段代码放到项目中的最开始,然后观察控制台,如果你的请求器基于xhr实现,会发现所有的请求都被监听下来了。

js 复制代码
(function() {
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
    this._url = url; // 保存请求的URL
    return originalOpen.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function(body) {
    this.addEventListener('load', function() {
      if (this.status >= 200 && this.status < 300) {
        // 请求成功,上报成功信息
        reportRequest('success', this._url, this.status, this.responseText);
      } else {
        // 请求失败,上报失败信息
        reportRequest('failure', this._url, this.status, this.responseText);
      }
    });

    this.addEventListener('error', function() {
      // 请求错误,上报错误信息
      reportRequest('error', this._url, this.status, this.statusText);
    });

    return originalSend.apply(this, arguments);
  };

  function reportRequest(type, url, status, response) {
    // 这里进行上报,可以通过fetch或其他方式发送上报请求
    console.log(`Request ${type}: ${url}, Status: ${status}, Response: ${response}`);
    // 例子:fetch('/report', { method: 'POST', body: JSON.stringify({ type, url, status, response }) })
  }
})();

fetch如何实现监听呢?同样也是重写。

js 复制代码
(function() {
  const originalFetch = window.fetch;

  window.fetch = function() {
    const args = arguments;
    const url = args[0];

    return originalFetch.apply(this, arguments)
      .then(response => {
        if (response.ok) {
          // 请求成功,上报成功信息
          response.clone().text().then(responseText => {
            reportRequest('success', url, response.status, responseText);
          });
        } else {
          // 请求失败,上报失败信息
          response.clone().text().then(responseText => {
            reportRequest('failure', url, response.status, responseText);
          });
        }
        return response;
      })
      .catch(error => {
        // 请求错误,上报错误信息
        reportRequest('error', url, 0, error.message);
        throw error;
      });
  };

  function reportRequest(type, url, status, response) {
    // 这里进行上报,可以通过fetch或其他方式发送上报请求
    console.log(`Request ${type}: ${url}, Status: ${status}, Response: ${response}`);
    // 例子:fetch('/report', { method: 'POST', body: JSON.stringify({ type, url, status, response }) })
  }
})();

看到这里,其实实现类似ARMS这样的API请求,很简单,ARMS底层一定也是依赖类似这样的代码块来实现原子能力,在此基础上,设计了一套可插拔、模块化的系统监控架构,来进行系统全方位的监听。

实现ARMS SDK

那如果想实现一个简易版的ARMS SDK该怎么做?先看一段使用SDK的代码块。

js 复制代码
import TraceApiPlugin from '@ali/trace-plugin-api';
import TracePerfPlugin from '@ali/trace-plugin-perf';
import TracePvPlugin from '@ali/trace-plugin-pv';
import TraceResourceErrorPlugin from '@ali/trace-plugin-resource-error';
import TraceSdk from '@ali/trace-sdk';
import { isProd } from './constants';

const traceEnv = isProd ? 'prod' : 'test';

const trace = TraceSdk({
  pid: 'relax-drink', // 必填 请在https://arms.alibaba-inc.com/arms/project/create 申请项目ID
  errorEnable: true, // 非必填 默认已经开启了js报错,若关闭则设置errorEnable:false
  env: traceEnv, // 可选 prod | pre | daily | string
  ignoreErrors: [/^Script error\.?$/], // 可选,用法参考"初始化配置说明"
  aplusUrl: 's-gm.mmstat.com', // 可选 默认s-gm.mmstat.com, 海外(新加坡)埋点配置 sg.mmstat.com
  plugins: [
    // 使用插件
    [TracePvPlugin, { autoPV: false }],
    [TraceApiPlugin, { sampling: 0.1 }], // 接口监控插件
    [TracePerfPlugin, { enableLCP: true }],
    [TraceResourceErrorPlugin], // 前端资源异常监控插件
  ],
  useSendBeacon: false,
});

// 执行
trace.install();

代码块中包含了ARMS的其他能力,包括js error性能资源的监控,我们本节只基于接口请求做示例。

我们整理下实现思路:

  1. 最终进行install进行监控器实例初始化;
  2. 将所有配置基于一个Object注入到SDK中;
  3. 配置中有plugins插件这样的概念,因此需要设计单独的插件类;

因此我们简易设计出一个TraceSDK的架子:

js 复制代码
function TraceSdk(config) {
  // 默认配置
  const defaultConfig = {
    errorEnable: true,
    env: 'prod',
    aplusUrl: 's-gm.mmstat.com',
    useSendBeacon: false,
    ignoreErrors: [],
    plugins: [],
  };

  // 合并用户提供的配置和默认配置
  const finalConfig = { ...defaultConfig, ...config };

  // 初始化插件
  function initializePlugins(plugins) {
    plugins.forEach(([PluginConstructor, options]) => {
      const plugin = new PluginConstructor(options);
      plugin.initialize();
    });
  }

  // 安装SDK
  function install() {
    console.log('Trace SDK 安装中...');
    setupErrorHandling();
    initializePlugins(finalConfig.plugins);
  }

  // 设置错误处理(简单实现)
  function setupErrorHandling() {
    if (finalConfig.errorEnable) {
      window.addEventListener('error', (event) => {
        const errorMessage = event.message;
        if (
          !finalConfig.ignoreErrors.some((regex) => regex.test(errorMessage))
        ) {
          console.error('捕获到错误:', errorMessage);
          // 可在此执行错误报告逻辑
        }
      });
    }
  }

  // 公开的API
  return {
    install,
  };
}

// 示例插件构造函数(模拟)

function TraceApiPlugin(options) {
  this.options = options || {};
  this.initialize = function () {
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function (method, url, async, user, pass) {
      this._url = url; // 保存请求的URL
      return originalOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function (body) {
      this.addEventListener('load', function () {
        if (this.status >= 200 && this.status < 300) {
          // 请求成功,上报成功信息
          reportRequest('success', this._url, this.status, this.responseText);
        } else {
          // 请求失败,上报失败信息
          reportRequest('failure', this._url, this.status, this.responseText);
        }
      });

      this.addEventListener('error', function () {
        // 请求错误,上报错误信息
        reportRequest('error', this._url, this.status, this.statusText);
      });

      return originalSend.apply(this, arguments);
    };

    function reportRequest(type, url, status, response) {
      // 这里进行上报,可以通过fetch或其他方式发送上报请求
      console.log(
        `Request ${type}: ${url}, Status: ${status}, Response: ${response}`,
      );
      // 例子:fetch('/report', { method: 'POST', body: JSON.stringify({ type, url, status, response }) })
    }
  };
}

如果想启用更多的能力,则ARMS内部肯定会有类似TraceJsErrorPluginTracePerfPlugin等,直接在初始化阶段注入到TraceSDK实例初始化即可。

结尾

本文我们了解了如何实现请求劫持,并且通过成熟的监控平台ARMS的组件实例方式,推测了ARMS是如何在是三方项目中实现监听,保障稳定性的。其次,我们还发现只需在xhrfetch的原型函数中加入一些简单的代码,即可监听项目中的所有请求,这样的原子能力很优雅,同时也几乎支撑着前端所有主流的监控系统。

相关推荐
杨荧11 分钟前
【开源免费】基于Vue和SpringBoot的网上商城系统(附论文)
前端·javascript·jvm·vue.js·spring boot·spring cloud·开源
__night_26 分钟前
kiran-qt5-integration
java·前端·qt
小李老笨了27 分钟前
React组件化开发
前端·javascript·react.js
BillKu37 分钟前
vue3+ts+element-plus 表单el-form取消回车默认提交
前端·javascript·vue.js
鸠摩智首席音效师1 小时前
如何修改 Angular 运行的主机和端口 ?
前端·javascript·angular.js
我明天再来学Web渗透1 小时前
【2024年-11月-30日-开源社区openEuler实践记录】dde_autotest_euler:守护桌面体验的智能测试利器
运维·开发语言·架构·开源·开源软件
找方案1 小时前
应急指挥系统总体架构方案
架构·应急指挥
yuanbenshidiaos1 小时前
QT--------------常用的界面组件使用
前端·数据库·qt
Rattenking1 小时前
【CSS】 ---- CSS 实现图片背景清除的滑动效果三种方法
前端·javascript·css
关山月2 小时前
掌握package.json:全面指南🚀
前端