在經過兩年多的線上沉淀后,將監控代碼重新用 TypeScript 編寫,刪除冗余邏輯,正式開源。
  根據[shin-monitor](https://github.com/pwstrick/shin-monitor)的目錄結構可知,源碼集中在[src](https://github.com/pwstrick/shin-monitor/tree/main/src)目錄中。關于監控系統的迭代過程,可以參考[專欄](http://www.hmoore.net/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),歡迎閱讀。

推薦一款前端監控腳本:[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年
- 2024年
- 日志
- 2020