基于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的原型函数中加入一些简单的代码,即可监听项目中的所有请求,这样的原子能力很优雅,同时也几乎支撑着前端所有主流的监控系统。

相关推荐
徐小夕2 小时前
我用 AI 撸了个开源"万能预览器":浏览器直接打开 Office、CAD 和 3D 模型
前端·vue.js·github
小码哥_常2 小时前
Flutter Android 延迟加载代码指南:提升应用性能的关键
前端
这是个栗子2 小时前
TypeScript(三)
前端·javascript·typescript·react
kvo7f2JTy2 小时前
基于机器学习算法的web入侵检测系统设计与实现
前端·算法·机器学习
北风toto2 小时前
前端CSS样式详细笔记
前端·css·笔记
nanfeiyan3 小时前
git commit
前端
KaneLogger3 小时前
如何把AI方面的先发优势转化为结构优势
人工智能·程序员·架构
前端精髓5 小时前
移除 Effect 依赖
前端·javascript·react.js
码云之上5 小时前
从一个截图函数到一个 npm 包——pdf-snapshot 的诞生记
前端·node.js·github
码事漫谈6 小时前
AI提效,到底能强到什么程度?
前端·后端