前言
最近对于前端稳定性的方向研究较多,主要负责的项目基于ARMS
进行系统监控,而接口稳定性
是系统稳定性最核心的参考指标之一,那设计一款类似ARMS
这样的监听SDK,核心思路是怎样的?
原子能力
想要做到系统透明,就需要在每一次Web request的时候,都在监听器中监听并记录到最终的结果。目前主流的请求方式是fetch
和xhr
。
fetch
和 XMLHttpRequest
(XHR) 都用于在浏览器中进行网络请求,但它们有一些重要的区别:
-
语法和使用方式:
fetch
使用的是基于 Promises 的语法,这使得代码更加简洁和易于读写。它可以很好地与async
/await
结合使用。XMLHttpRequest
使用的是基于回调的方式,这可能会导致回调嵌套(回调地狱),从而使代码难以维护。
-
返回数据:
fetch
默认不会自动发送或接收 cookies(没有credentials的设置为"same-origin"或者"include"时是不会发送cookies的),需要手动配置credentials
选项。XMLHttpRequest
会自动发送所有的请求头信息,包括 cookies,除非明确设置不发送。
-
错误处理:
fetch
只有在网络错误导致请求失败时才会被拒绝,即只有在无法完成请求时(如网络断开),Promise 才会被拒绝。对于 HTTP 错误状态码(如 404 或 500),它不会被拒绝,需要手动检查响应的ok
属性。XMLHttpRequest
则是在状态改变时,通过回调函数进行错误处理,状态码错误也可以在处理函数中处理。
-
进度信息:
XMLHttpRequest
提供了一些事件(如progress
事件),可以用于实时获取上传或下载的进度。fetch
API 本身不直接提供进度信息,但是可以通过ReadableStream
API 来实现。
-
跨域请求:
fetch
支持 CORS,并通过 Mode 来管理请求的跨域权限。XMLHttpRequest
也支持 CORS 但可能需要配置复杂一些。
-
取消请求:
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
、性能
、资源
的监控,我们本节只基于接口请求做示例。
我们整理下实现思路:
- 最终进行
install
进行监控器实例初始化; - 将所有配置基于一个
Object
注入到SDK
中; - 配置中有
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
内部肯定会有类似TraceJsErrorPlugin
、TracePerfPlugin
等,直接在初始化阶段注入到TraceSDK
实例初始化即可。
结尾
本文我们了解了如何实现请求劫持,并且通过成熟的监控平台ARMS
的组件实例方式,推测了ARMS
是如何在是三方项目中实现监听,保障稳定性的。其次,我们还发现只需在xhr
、fetch
的原型函数中加入一些简单的代码,即可监听项目中的所有请求,这样的原子能力很优雅,同时也几乎支撑着前端所有主流的监控系统。