在经过两年多的线上沉淀后,将监控代码重新用 TypeScript 编写,删除冗余逻辑,正式开源。
  根据[shin-monitor](https://github.com/pwstrick/shin-monitor)的目录结构可知,源码集中在[src](https://github.com/pwstrick/shin-monitor/tree/main/src)目录中。关于监控系统的迭代过程,可以参考[专栏](https://www.kancloud.cn/pwstrick/fe-questions/2363166)。
## 一、入口
  入口文件是 index.ts,旁边的 utils.ts 是一个工具库。
  在 index.ts 中,将会引入 lib 目录中的 error、action 和 performance 三个文件。
**1)defaults**
  声明 defaults 变量,配置了各个参数的默认属性,各个参数的[使用指南](https://github.com/pwstrick/shin-monitor#rocket-%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)可以查看注释、readme 或[demo](https://github.com/pwstrick/shin-monitor/tree/main/demo)目录中的文件。
~~~
const defaults: TypeShinParams = {
src: '//127.0.0.1:3000/ma.gif', // 采集监控数据的后台接收地址
psrc: '//127.0.0.1:3000/pe.gif', // 采集性能参数的后台接收地址
pkey: '', // 性能监控的项目key
subdir: '', // 一个项目下的子目录
rate: 5, // 随机采样率,用于性能搜集,范围是 1~10,10 表示百分百发送
version: '', // 版本,便于追查出错源
record: {
isOpen: true, // 是否开启录像
src: '//cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js' // 录像地址
},
error: {
isFilterErrorFunc: null, // 需要过滤的代码错误
isFilterPromiseFunc: null, // 需要过滤的Promise错误
},
console: {
isOpen: true, // 默认是开启,在本地调试时,可以将其关闭
isFilterLogFunc: null, // 过滤要打印的内容
},
crash: {
isOpen: true, // 是否监控页面奔溃,默认开启
validateFunc: null, // 自定义页面白屏的判断条件,返回值包括 {success: true, prompt:'提示'}
},
event: {
isFilterClickFunc: null, // 在点击事件中需要过滤的元素
},
ajax: {
isFilterSendFunc: null // 在发送监控日志时需要过滤的通信
},
identity: {
value: '', // 自定义的身份信息字段
getFunc: null, // 自定义的身份信息获取函数
},
};
~~~
**2)setParams()**
   setParams() 函数中,会初始化引入的 3 个类,然后开始监控页面错误、计算性能参数、监控用户行为。
~~~
function setParams(params: TypeShinParams): TypeShinParams {
if (!params) {
return null;
}
const combination = defaults;
// 只重置 params 中的参数
for(const key in params) {
combination[key] = params[key];
}
// 埋入自定义的身份信息
const { getFunc } = combination.identity;
getFunc && getFunc(combination);
// 监控页面错误
const error = new ErrorMonitor(combination);
error.registerErrorEvent(); // 注册 error 事件
error.registerUnhandledrejectionEvent(); // 注册 unhandledrejection 事件
error.registerLoadEvent(); // 注册 load 事件
error.recordPage();
shin.reactError = error.reactError.bind(error); // 对外提供 React 的错误处理
shin.vueError = error.vueError.bind(error); // 对外提供 Vue 的错误处理
// 启动性能监控
const pe = new PerformanceMonitor(combination);
pe.observerLCP(); // 监控 LCP
pe.observerFID(); // 监控 FID
pe.registerLoadAndHideEvent(); // 注册 load 和页面隐藏事件
// 为原生对象注入自定义行为
const action = new ActionMonitor(combination);
action.injectConsole(); // 监控打印
action.injectRouter(); // 监听路由
action.injectEvent(); // 监听事件
action.injectAjax(); // 监听Ajax
return combination;
}
~~~
  函数中做了大量初始化工作,若不需要某些监控行为,可自行删除。
## 二、lib 目录
  在[lib](https://github.com/pwstrick/shin-monitor/tree/main/src/lib)目录中,存放着整个监控系统的核心逻辑。
**1)Http**
  Http 的主要工作是通信,也就是将搜集起来的监控日志或性能参数,统一发送到后台。
  并且在 Http 中,还会根据算法生成身份标识字符串,以及做最后的参数组装工作。
  监控日志原先采用的发送方式是 Image,目的是跨域,但是发送的数据量有限,像 Ajax 通信,如果需要记录响应,那么长度就会不够。
  因此后期就改成了 fetch() 函数,默认只会上传 8000 长度的数据。
~~~
public send(data: TypeSendParams, callback?: ParamsCallback): void {
// var ts = new Date().getTime().toString();
// var img = new Image(0, 0);
// img.src = shin.param.src + "?m=" + _paramify(data) + "&ts=" + ts;
const m = this.paramify(data);
// 大于8000的长度,就不在上报,废弃掉
if (m.length >= 8000) {
return;
}
const body: TypeSendBody = { m };
callback && callback(data, body); // 自定义的参数处理回调
// 如果修改headers,就会多一次OPTIONS预检请求
fetch(this.params.src, {
method: "POST",
// headers: {
// 'Content-Type': 'application/json',
// },
body: JSON.stringify(body)
});
}
~~~
  而性能参数的发送采用了[sendBeacon()](https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon)方法,在页面关闭时也能上报,这是普通的请求所不具备的特性。
  它能将少量数据异步 POST 到后台,并且支持跨域,而少量是指多少并没有特别指明,由浏览器控制,网上查到的资料说一般在 64KB 左右。
~~~
public sendPerformance(data: TypeCaculateTiming): void {
// 如果传了数据就使用该数据,否则读取性能参数,并格式化为字符串
var str = this.paramifyPerformance(data);
var rate = randomNum(10, 1); // 选取1~10之间的整数
if (this.params.rate >= rate && this.params.pkey) {
navigator.sendBeacon(this.params.psrc, str);
}
}
~~~
**2)Error**
  在 Error 中,会注册 window 的 error 事件,用于监控脚本或资源错误,在脚本错误中,会提示行号和列号。
  不过资源错误是看不到具体的错误原因的,只会给个结果,出现了错误,连错误状态码也没有。
~~~
window.addEventListener('error', (event: ErrorEvent): void => {
const errorTarget = event.target as (Window | TypeEventTarget);
// 过滤掉与业务无关或无意义的错误
if (isFilterErrorFunc && isFilterErrorFunc(event)) {
return;
}
// 过滤 target 为 window 的异常
if (
errorTarget !== window
&& (errorTarget as TypeEventTarget).nodeName
&& CONSTANT.LOAD_ERROR_TYPE[(errorTarget as TypeEventTarget).nodeName.toUpperCase()]
) {
this.handleError(this.formatLoadError(errorTarget as TypeEventTarget));
} else {
// 过滤无效错误
event.message && this.handleError(
this.formatRuntimerError(
event.message,
event.filename,
event.lineno,
event.colno,
// event.error,
),
);
}
}, true); // 捕获
~~~
  还会注册 window 的 unhandledrejection 事件,用于监控未处理的 Promise 错误,当 Promise 被 reject 且没有 reject 处理器时触发。
  在 unhandledrejection 事件中,对于响应信息,其实是做了些扩展的,参考《[SDK中的 unhandledrejection 事件](https://www.cnblogs.com/strick/p/14574492.html)》。
~~~
window.addEventListener('unhandledrejection',(event: PromiseRejectionEvent): void => {
// 处理响应数据,只抽取重要信息
const { response } = event.reason;
// 若无响应,则不监控
if (!response || !response.request) {
return;
}
const desc: TypeAjaxDesc = response.request.ajax;
desc.status = event.reason.status || response.status;
// 过滤掉与业务无关或无意义的错误
if(isFilterPromiseFunc && isFilterPromiseFunc(desc)) {
return;
}
this.handleError({
type: CONSTANT.ERROR_PROMISE,
desc,
// stack: event.reason && (event.reason.stack || "no stack")
});
}, true);
~~~
  这 2 个错误的使用,都在[demo/error.html](https://github.com/pwstrick/shin-monitor/blob/main/demo/error.html)中有所记录,另一个重要的错误是白屏。
  在白屏时,还会上报录像内容,白屏的迭代过程可以参考[此处](https://www.cnblogs.com/strick/p/14986378.html)。
  对 body 的子元素做深度优先搜索,若已找到一个有高度的元素、或若元素隐藏、或元素有高度并且不是 body 元素,则结束搜索。
  为了便于定位白屏原因,在白屏时,还会记录些元素信息,例如元素类型、样式、高度等。
~~~
private isWhiteScreen(): TypeWhiteScreen {
const visibles = [];
const nodes = []; //遍历到的节点的关键信息,用于查明白屏原因
// 深度优先遍历子元素
const dfs = (node: HTMLElement): void => {
const tagName = node.tagName.toLowerCase();
const rect = node.getBoundingClientRect();
// 选取节点的属性作记录
const attrs: TypeWhiteHTMLNode = {
id: node.id,
tag: tagName,
className: node.className,
display: node.style.display,
height: rect.height
};
const src = (node as HTMLImageElement).src;
if(src) {
attrs.src = src; // 记录图像的地址
}
const href =(node as HTMLAnchorElement).href;
if(href) {
attrs.href = href; // 记录链接的地址
}
nodes.push(attrs);
// 若已找到一个有高度的元素,则结束搜索
if(visibles.length > 0) return;
// 若元素隐藏,则结束搜索
if (node.style.display === 'none') return;
// 若元素有高度并且不是 body 元素,则结束搜索
if(rect.height > 0 && tagName !== 'body') {
visibles.push(node);
return;
}
node.children && [].slice.call(node.children).forEach((child: HTMLElement): void => {
const tagName = child.tagName.toLowerCase();
// 过滤脚本和样式元素
if(tagName === 'script' || tagName === 'link') return;
dfs(child);
});
};
dfs(document.body);
return {
visibles: visibles,
nodes: nodes
};
}
~~~
  监控白屏的时机,是在 load 事件中,延迟 1 秒触发。
  原先是在 DOMContentLoaded 事件内触发,经测试发现,当因为脚本错误出现白屏时,两个事件的触发时机会很接近。
  在线上监控时发现会有一些误报,HTML是有内容的,那很可能是 DOMContentLoaded 触发时,页面内容还没渲染好。
  对于热门的 React 和 Vue 库,声明了两个方法:reactError() 和 vueError(),将这两个方法分别应用于项目中,就能监控框架错误了。
  React 需要在项目中创建一个 ErrorBoundary 类,在类中调用 reactError() 方法。
  如果 Vue 是被模块化引入的,那么就得在模块的某个位置调用该方法,因为此时 Vue 不会绑定到 window 中,即不是全局变量。
**3)Action**
  在 Action 中会监控打印、路由、点击事件和 Ajax 通信。这 4 种行为都会对原生对象进行注入,它们的使用也都可以在[demo](https://github.com/pwstrick/shin-monitor/tree/main/demo)目录中找到。
  以路由为例,不仅要监听 popstate 事件,还要重写 pushState 和 replaceState。
~~~
public injectRouter(): void {
/**
* 全局监听跳转
* 点击后退、前进按钮或者调用 history.back()、history.forward()、history.go() 方法才会触发 popstate 事件
* 点击 <a href=/xx/yy#anchor>hash</a> 按钮也会触发 popstate 事件
*/
const _onPopState = window.onpopstate;
window.onpopstate = (args: PopStateEvent): void => {
this.sendRouterInfo();
_onPopState && _onPopState.apply(this, args);
};
/**
* 监听 pushState() 和 replaceState() 两个方法
*/
const bindEventListener = (type: string): TypeStateEvent => {
const historyEvent: TypeStateEvent = history[type];
return (...args): void => {
// 触发 history 的原始事件,apply 的第一个参数若不是 history,就会报错
const newEvent = historyEvent.apply(history, args);
this.sendRouterInfo();
return newEvent;
};
};
history.pushState = bindEventListener('pushState');
history.replaceState = bindEventListener('replaceState');
}
~~~
**4)Performance**
  Performance 主要是对性能参数的搜集,大部分的性能参数是通过 performance.getEntriesByType('navigation')\[0\] 或 performance.timing 获取的。
  performance.timing 已被废弃,尽量不要使用,此处只是为了兼容。Performance 的迭代过程可以参考[此处](https://www.cnblogs.com/strick/p/14578711.html)。
  参数的发送时机有两者,第一种是 window.load 事件中,第二种是页面隐藏的事件中。
  LCP、FID、FP 等参数可通过浏览器提供的对象获取。
~~~
public observerLCP(): void {
const lcpType = 'largest-contentful-paint';
const isSupport = this.checkSupportPerformanceObserver(lcpType);
// 浏览器兼容判断
if(!isSupport) {
return;
}
const po = new PerformanceObserver((entryList): void=> {
const entries = entryList.getEntries();
const lastEntry = (entries as any)[entries.length - 1] as TypePerformanceEntry;
this.lcp = {
time: rounded(lastEntry.renderTime || lastEntry.loadTime), // 时间取整
url: lastEntry.url, // 资源地址
element: lastEntry.element ? removeQuote(lastEntry.element.outerHTML) : '' // 参照的元素
};
});
// buffered 为 true 表示调用 observe() 之前的也算进来
po.observe({ type: lcpType, buffered: true } as any);
// po.observe({ entryTypes: [lcpType] });
/**
* 当有按键或点击(包括滚动)时,就停止 LCP 的采样
* once 参数是指事件被调用一次后就会被移除
*/
['keydown', 'click'].forEach((type): void => {
window.addEventListener(type, (): void => {
// 断开此观察者的连接
po.disconnect();
}, { once: true, capture: true });
});
}
~~~
  FMP 需要自行计算,才能得到,我采用了一套比较简单的规则。
* 首先,通过 MutationObserver 监听每一次页面整体的 DOM 变化,触发 MutationObserver 的回调。
* 然后在回调中,为每个 HTML 元素(不包括忽略的元素)打上标记,记录元素是在哪一次回调中增加的,并且用数组记录每一次的回调时间。
* 接着在触发 load 事件时,先过滤掉首屏外和没有高度的元素,以及元素列表之间有包括关系的祖先元素,再计算各次变化时剩余元素的总分。
* 最后在得到分数最大值后,从这些元素中挑选出最长的耗时,作为 FMP。
  为了能更好的描述出首屏的时间,将 LCP 和 FMP 两个时间做比较,取最长的那个时间。
*****
> 原文出处:
[博客园-从零开始搞系列](https://www.cnblogs.com/strick/category/1928903.html)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020