做者:Lilly Jiang, Aiqing Dongjavascript
通常來講,一個完整的埋點體系由如下三個部分構成:html
埋點上報是將應用層事件上傳至上層平臺的過程。比方說,在某購物網站上,用戶點擊了「收藏」按鈕,此時,一個點擊事件就生成了,這一事件會被上報至一個數據分析平臺。這樣,相關的數據分析師、產品經理、運營等同窗即可以在數據分析平臺,經過這些上報的事件數據分析,得出應用中能夠優化的方方面面。因而可知,埋點上報是每一個產品走向卓越的重要一環。前端
經過以上描述,咱們認識了埋點上報過程的兩大主角:應用與數據分析平臺。從前端技術的角度來講,咱們一般還須要第三個角色的助攻,那就是數據平臺 SDK. 這個 SDK 封裝了數據分析平臺的各類接口,暴露出簡單的方法讓咱們進行調用,實現簡易的埋點上傳。java
咱們能夠把應用層事件分爲兩大類:git
咱們爲這兩種事件分別開發了一套埋點上傳 SDK。下面,咱們就來詳細地講解一下這兩套 SDK 的技術知識。github
monitor-tracer
monitor-tracer
是一個用來監控頁面及組件可見時長和活躍時長的前端 SDK,同時也是 Monitor 前端埋點體系的一個核心組成部分。web
爲了更好地理解用戶對各個業務功能的使用情況,從而進行相應的產品優化和調整:typescript
對於通常的網頁應用,咱們須要對用戶在應用某個頁面的停留及活躍時長進行相應的統計;npm
對於大盤 / 看板 / dashboard 類型的頁面(以下圖所示),咱們但願在頁面維度的基礎上,更進一步地統計每一個組件對用戶的可見時長,從而對它們的排列順序和內容進行優化。編程
基於以上需求,咱們開發了 monitor-tracer
SDK, 旨在實現對 「頁面可見、活躍時長」 及 「組件可見時長」 的統計。
location.pathname
來做區分;其關係爲:
在咱們的設計中,衡量一個頁面的停留及活躍時長鬚要兩個重要的指標:
可見性 (visibility)
visible
- 頁面在當前瀏覽器的 viewport 中,且瀏覽器窗口未被最小化;invisible
- 頁面不在當前瀏覽器的 viewport 中,或因瀏覽器最小化致使其沒法被看到。活躍性 (activity)
active
- 用戶在網頁中有活動(例如鼠標、鍵盤活動及頁面滾動等);inactive
- 用戶在網頁中沒有任何活動。只要能獲取到這四種狀態發生的時間戳,就能夠按下圖所示方法,累加計算出頁面從載入到退出的可見和活躍時長:
Google 公司在 2018 年 7 月份提出了一套用來描述網頁生命週期的 Page Lifecycle API 規範,本 SDK 即是基於這套規範來監聽頁面可見性變化的。規範指出,一個網頁從載入到銷燬的過程當中,會經過瀏覽器的各類事件在如下六種生命週期狀態 (Lifecycle State) 之間相互轉化。
生命週期狀態 | 描述 |
---|---|
active |
網頁可見,且具備焦點 |
passive |
網頁可見,但處於失焦狀態 |
hidden |
網頁不可見,但未被瀏覽器凍結,通常由用戶切換到別的 tab 或最小化瀏覽器觸發 |
frozen |
網頁被瀏覽器凍結(一些後臺任務例如定時器、fetch 等被掛起以節約 CPU 資源) |
terminated |
網頁被瀏覽器卸載並從內存中清理。通常用戶主動將網頁關閉時觸發此狀態 |
discarded |
網頁被瀏覽器強制清理。通常由系統資源嚴重不足引發 |
生命週期狀態之間的轉化關係以下圖所示:
由以上信息,咱們能夠得出頁面生命週期狀態和頁面可見狀態之間的映射關係:
生命週期狀態 | 可見狀態 |
---|---|
active passive |
visible |
hidden terminated frozen discarded |
invisible |
所以,咱們只須要監聽頁面生命週期的變化並記錄其時間,就能夠相應獲取頁面可見性的統計數據。
在制定 Page Lifecycle 規範的同時,Google Chrome 團隊也開發了 PageLifecycle.js
SDK, 實現了這套規範中描述的生命週期狀態的監聽,併兼容了 IE 9 以上的全部瀏覽器。出於易用性及穩定性的考慮,咱們決定使用這個 SDK 來進行生命週期的監聽。因爲 PageLifecycle.js
自己使用 JavaScript 編寫,咱們爲其添加了類型定義並封裝爲了兼容 TypeScript 的 @byted-cg/page-lifecycle-typed
SDK. PageLifecycle.js
的使用方法以下:
import lifecycleInstance, {
StateChangeEvent,
} from "@byted-cg/page-lifecycle-typed";
lifecycleInstance.addEventListener("statechange", (event: StateChangeEvent) => {
switch (event.newState) {
case "active":
case "passive":
// page visible, do something
break;
case "hidden":
case "terminated":
case "frozen":
// page invisible, do something else
break;
}
});
複製代碼
經過 PageLifecycle.js
, 咱們即可以監聽 statechange
事件來在頁面生命週期發生變化時得到通知,並在生命週期狀態爲 active
和 passive
時標記頁面爲 visible
狀態,在生命週期狀態爲其餘幾個時標記頁面爲 invisible
狀態,更新最後一次可見的時間戳,並累加頁面可見時間。
PageLifecycle.js
的缺陷對於咱們的需求來講,PageLifecycle.js
自己存在以下兩個缺陷。咱們也針對這兩個缺陷作了一些改進。
沒法監控到單頁應用 (SPA) 中頁面的變化
在單頁應用中,頁面一般依靠 history 或 hash 路由的變化來切換,頁面自己並不會從新載入,所以 PageLifecycle.js
沒法感知頁面的切換。爲了應對這種狀況,咱們在 monitor-tracer 中手動添加了針對路由變化事件(popstate
replacestate
等)的監聽。若是發現頁面的路由發生了變化,會認爲當前頁面進入了 terminated
生命週期,從而進行相應的處理。這裏對路由變化進行監聽的邏輯複用了咱們以前開發的 @byted-cg/puzzle-router SDK, 有興趣的同窗能夠參考。
沒法捕獲 discarded
生命週期
discarded
生命週期發生在網頁被瀏覽器強制清除時。此時網頁已經被銷燬並從內存中清理,沒法向外傳遞任何事件,所以 PageLifecycle.js
也就沒法推送 discarded
事件。這種狀況一旦發生,就會形成被清除的網頁統計數據的丟失。爲了應對此場景,monitor-tracer
會在頁面進入 invisible
狀態時,將現有的頁面時長統計數據存儲使用 JSON.stringify
序列化並儲存在 localStorage
當中。若是頁面恢復 visible
狀態,則會把 localStorage
中的數據清空;而若是頁面被清除,則會在下一次進入頁面時先將 localStorage
中存儲的上一個頁面的數據經過事件推送出去。這樣就最大程度地保證了頁面即便被強制清除,其數據也能被送出而不至丟失。
相較於頁面可見性,頁面活躍性的判斷要更加直截了當一些,直接經過如下方法判斷頁面狀態爲 active
仍是 inactive
便可。
active
判斷標準經過監聽一系列的瀏覽器事件,咱們就能夠判斷用戶是否在當前頁面上有活動。monitor-tracer
會監聽如下的六種事件:
事件 | 描述 |
---|---|
keydown |
用戶敲擊鍵盤時觸發 |
mousedown |
用戶點擊鼠標按鍵時觸發 |
mouseover |
用戶移動鼠標指針時觸發 |
touchstart |
用戶手指接觸觸摸屏時觸發(僅限觸屏設備) |
touchend |
用戶手指離開觸摸屏時觸發(僅限觸屏設備) |
scroll |
用戶滾動頁面時觸發 |
一旦監聽到以上事件,monitor-tracer
就會將頁面標記爲 active
狀態,並記錄當前時間戳,累加活躍時長。
inactive
判斷標準如下兩種狀況下,頁面將會被標記爲 inactive
狀態:
invisible
. 由於若是頁面對用戶不可見,那麼它必定是不活躍的。頁面被標記爲 inactive
後,monitor-tracer
會記錄當前時間戳並累加活躍時長。
統計組件級別活躍時長鬚要兩個條件,一是拿到全部須要統計的 DOM 元素,二是對這些 DOM 元素按照必定的標準進行監控。
獲取 DOM 元素要求咱們對整個 DOM 結構的改變進行監控。monitor-tracer
使用了 MutationObserver
API, DOM 的任何變更,好比節點的增減、屬性的變更、文本內容的變更,均可以經過這個 API 獲得通知。
概念上,它很接近事件,能夠理解爲 DOM 發生變更就會觸發 MutationObserver
事件。可是,它與事件有一個本質不一樣:事件是同步觸發,也就是說,DOM 的變更馬上會觸發相應的事件,而 MutationObserver
則是異步觸發,DOM 的變更並不會立刻觸發,而是要等到當前全部 DOM 操做都結束才觸發。
這樣的設計是爲了應對 DOM 變更頻繁的特色,節省性能。舉例來講,若是文檔中連續插入 1000 個 <p></p>
標籤,就會連續觸發 1000 個插入事件,執行每一個事件的回調函數,這極可能形成瀏覽器的卡頓。而 MutationObserver
徹底不一樣,只在 1000 個標籤都插入結束後纔會觸發,並且只觸發一次。
MutationObserver
API 的用法以下:
const observer = new MutationObserver(function (mutations, observer) {
mutations.forEach(function (mutation) {
console.log(mutation.target); // target: 發生變更的 DOM 節點
});
});
observer.observe(document.documentElement, {
childList: true, //子節點的變更(指新增,刪除或者更改)
attributes: true, // 屬性的變更
characterData: true, // 節點內容或節點文本的變更
subtree: true, // 表示是否將該觀察器應用於該節點的全部後代節點
attributeOldValue: false, // 表示觀察 attributes 變更時,是否須要記錄變更前的屬性值
characterDataOldValue: false, // 表示觀察 characterData 變更時,是否須要記錄變更前的值。
attributeFilter: false, // 表示須要觀察的特定屬性,好比['class','src']
});
observer.disconnect(); // 用來中止觀察。調用該方法後,DOM 再發生變更則不會觸發觀察器
複製代碼
爲了在衆多 DOM 元素中找到須要監聽的元素,咱們須要一個方法來標記這些元素。monitor-tracer
SDK 規定,一個組件若是須要統計活躍時長,則須要爲其添加一個 monitor-pv
或 data-monitor-pv
屬性。在使用 MutationObserver
掃描 DOM 變化時,monitor-tracer
會將有這兩個屬性的 DOM 元素收集到一個數組裏,以供監聽。例以下面兩個組件:
<div monitor-pv='{ "event": "component_one_pv", "params": { ... } }'>
Component One
</div>
<div>Component Two</div>
複製代碼
Component One 由於添加了 monitor-pv
屬性,會被記錄並統計可見時長。而 Component Two 則不會。
babel-plugin-tracer
插件若是須要監控的組件是經過一些組件庫(例如 Ant Design 或 ByDesign)編寫的,那麼爲其添加 monitor-pv
或 data-monitor-pv
這樣的自定義屬性可能會被組件自身過濾從而不會出如今最終生成的 DOM 元素上,致使組件的監控不生效。爲了解決相似的問題,咱們開發了 @byted-cg/babel-plugin-tracer
babel 插件。此插件會在編譯過程當中尋找添加了 monitor-pv
屬性的組件,並在其外層包裹一個自定義的 <monitor></monitor>
標籤。例如:
import { Card } from 'antd';
const Component = () => {
return <Card monitor-pv={{ event: "component_one_pv", params: { ... } }}>HAHA</Card>
}
複製代碼
若是不添加插件,那麼最終生成的 DOM 爲:
<div class="ant-card">
<!-- ... Ant Design Card 組件 -->
</div>
複製代碼
可見 monitor-pv
屬性通過組件過濾後消失了。
而安裝 babel 插件後,最終編譯生成的 DOM 結構爲:
<monitor is="custom" data-monitor-pv='{ "event": "component_one_pv", "params": { ... } }' >
<div class="ant-card">
<!-- ... Ant Design Card 組件 -->
</div>
</monitor>
複製代碼
monitor-pv
屬性獲得了保留,而且插件自動爲其添加了 data-
前綴,以應對 React 16 以前版本僅支持 data-
開頭的自定義屬性的問題;同時將傳入的對象使用 JSON.stringify
轉換成了 DOM 元素 attribute 惟一支持的 string
類型。
因爲自定義標籤沒有任何樣式,因此包裹該標籤也不會影響到原有組件的樣式。monitor-tracer
SDK 在掃描 DOM 元素後,會同時收集全部 <monitor></monitor>
標籤中的元素的信息,並對其包裹的元素進行監控。
對組件可見性的判斷可分爲三個維度:
IntersectionObserver
API 判斷;display
visibility
及 opacity
樣式屬性判斷;這裏咱們使用了 IntersectionObserver
API. 該 API 提供了一種異步檢測目標元素與祖先元素或 viewport 相交狀況變化的方法。
過去,相交檢測一般要用到事件監聽,而且須要頻繁調用 Element.getBoundingClientRect
方法以獲取相關元素的邊界信息。事件監聽和調用 Element.getBoundingClientRect
都是在主線程上運行,所以頻繁觸發、調用可能會形成性能問題。這種檢測方法極其怪異且不優雅。
而 IntersectionObserver
API 會註冊一個回調函數,每當被監視的元素進入或者退出另一個元素時(或者 viewport),或者兩個元素的相交部分大小發生變化時,該回調方法會被觸發執行。這樣,咱們網站的主線程不須要再爲了監聽元素相交而辛苦勞做,瀏覽器會自行優化元素相交管理。
IntersectionObserver
API 的用法以下:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(function (entry) {
/** entry.boundingClientRect // 目標元素的矩形區域的信息 entry.intersectionRatio // 目標元素的可見比例,即 intersectionRect 佔 boundingClientRect 的比例,徹底可見時爲 1,徹底不可見時小於等於 0 entry.intersectionRect // 目標元素與視口(或根元素)的交叉區域的信息 entry.isIntersecting // 標示元素是已轉換爲相交狀態 (true) 仍是已脫離相交狀態 (false) entry.rootBounds // 根元素的矩形區域的信息, getBoundingClientRect 方法的返回值,若是沒有根元素(即直接相對於視口滾動),則返回 null entry.target // 被觀察的目標元素,是一個 DOM 節點對象 entry.time // 可見性發生變化的時間,是一個高精度時間戳,單位爲毫秒 **/
});
},
{
threshold: [0, 0.25, 0.5, 0.75, 1], //該屬性決定了何時觸發回調函數。它是一個數組,每一個成員都是一個門檻值,默認爲 [0],即交叉比例 (intersectionRatio) 達到 0 時觸發回調函數
}
);
observer.observe(document.getElementById("img")); // 開始監聽一個目標元素
observer.disconnect(); // 中止所有監聽工做
複製代碼
若是一個組件和 viewport 的相交比例小於某個值(默認爲 0.25),那麼這個組件就會被標記爲 invisible
. 反之,若是比例大於某個值(默認爲 0.75),那麼這個組件就會被標記爲 visible
。
若是元素的 CSS 樣式設爲了 visibility: hidden
或 opacity: 0
,那麼即便其與 viewport 的相交比例爲 1,對用戶來講也是不可見的。所以咱們須要額外判斷目標元素的 CSS 屬性是否爲可見。
若是一個組件的樣式被設置爲了如下之一,那麼它就會被標記爲 invisible
.
visibility: hidden
display: none
opacity: 0
當頁面不可見時,全部組件天然都不可見,所以在頁面爲 invisible
的狀態下,monitor-tracer
會將需監控的全部組件的狀態也標記爲 invisible
。
monitor
monitor
SDK 的定位數據平臺 SDK 的單一的埋點上報方式,沒法知足咱們開發中對 clean code 的極致追求
數據平臺的 SDK 每每只提供了上報埋點的函數式方法,雖然能夠知足咱們的平常開發需求,可是並不能解決咱們在寫埋點代碼時的兩大痛點:
咱們但願埋點代碼能夠輕易地添加、修改與刪除,而且對業務代碼沒有影響。所以,咱們基於 TypeScript 開發對框架無感的 monitor
SDK. 它支持逐個上傳多個埋點,而且接受返回埋點的函數,將其返回值上報;它提供了三種方式注入埋點,覆蓋了全部場景,將埋點與業務代碼徹底分離。
能夠看出,monitor
既是一個數據處理器,又是一個方法庫。更直觀一些,使用 monitor
後,咱們的應用在上報埋點時的流程以下:
埋點由應用層發送給 monitor
後,monitor
首先會對數據進行處理,再調用數據平臺 SDK, 將埋點事件上報給數據平臺。
在對 monitor
有了初步瞭解後,這篇文章將主要講解 monitor
是如何經過如下三種埋點注入的方式,解耦業務邏輯與埋點邏輯的。
下面咱們來看一下 monitor
和 monitor-tracer
SDK 具體的技術設計及實現方法。
monitor
提供了類指令方式注入埋點。例如,下段代碼用 monitor-click
指令注入了埋點。在此按鈕被點擊 (click) 時,monitor-click
所對應的值,即一個事件,就會被上報。
// 指令式埋點示例
<Button
monitor-click={JSON.stringify({
type: "func_operation",
params: { value: 3 },
})}
>
Click Me
</Button>
複製代碼
這是如何實現的呢?爲何僅僅給組件加了一個 monitor-click
屬性,monitor
就會在這個按鈕被點擊時上報埋點了呢?
其實,monitor
SDK 在初始化時,會給當前的 document
對象加上一系列 Event Listeners, 監聽 hover
click
input
focus
等事件。當監聽器被觸發時,monitor
會從觸發事件的 target
對象開始,逐級向上遍歷,查看當前元素是否有對應此事件的指令,若是有,則上報此事件,直至遇到一個沒有事件指令的元素節點。如下示意圖展現了類指令式埋點的上報流程:
以以下代碼爲例,當光標 hover 到 Button
時,document
對象上所安裝的監聽 hover
事件的函數便會執行。這個函數首先在 event.target
即 Button
上查找是否有與 hover
事件相關的指令(即屬性)。Button
有 monitor-hover
這個指令,此時函數便上傳此指令所對應的事件,即 { type: 'func_operation', params: { value: 1 }}
。
接下來,函數向上一層,到了 Button
的父元素,即 div
, 重複上述過程,它找到了 data-monitor-hover
這個指令,便一樣地上報了對應的埋點事件。而到了 section
這一層,雖然其有 data-monitor-click
指令,但此指令並不對 hover
事件進行響應,所以,這個逐級上報埋點的過程結束了。
// 指令式埋點實現逐級上報
<section data-monitor-click={JSON.stringify({ type: 'func_operation', params: { value: 3 }, })} >
<div data-monitor-hover={JSON.stringify({ type: 'func_operation', params: { value: 2 }, })} >
<Button monitor-hover={JSON.stringify({ type: 'func_operation', params: { value: 1 }, })} >
Click Me
</Button>
</div>
</section>
複製代碼
類指令式埋點注入適合簡單的埋點上報,清晰的與業務代碼實現了分離。可是若是咱們須要在上報事件前,對所上報的數據進行處理,那麼這種方式就沒法知足了。而且,並非全部的場景均可以被 DOM 事件所覆蓋。若是我想在用戶在搜索框輸入某個值時,上報埋點,那麼我就須要對用戶輸入的值進行分析,而不能在 input
事件每次觸發時都上報埋點。
裝飾器本質上是一個高階函數。它接受一個函數,返回另外一個被修飾的函數。所以,咱們很天然地想到用裝飾器將埋點邏輯注入到業務函數,既實現了埋點與業務代碼的分離,又可以適應於複雜的埋點場景。
下面的代碼使用了 @monitorBefore
修飾器。@monitorBefore
所接收的函數的返回值,便是要上報的事件。在 handleSearch
函數被調用的時候,monitor
會首先上報埋點事件,而後再執行 handleSearch
函數的邏輯。
// @monitorBefore 使用示例
@monitorBefore((value: string) => ({
type: 'func_operation',
params: { keyword: value },
}))
handleSearch() {
console.log(
'[Decorators Demo]: this should happen AFTER a monitor event is sent.',
);
}
return (
<AutoComplete onSearch={handleSearch} />
)
複製代碼
@readonly
理解裝飾器原理裝飾器是如何實現將埋點邏輯和業務邏輯相整合的呢?在咱們詳細解讀 @monitorBefore
以前,讓咱們先從一個經常使用的裝飾器 @readonly
講起吧。
裝飾器應用於一個類的單個成員上,包括類的屬性、方法、getters 和 setters. 在被調用時,裝飾器函數會接收 3 個參數:
target
- 裝飾器所在的類name
- 被裝飾的函數的名字descriptor
- 被裝飾的函數的屬性描述符// @readonly裝飾器的代碼實現
readonly = (target, name, descriptor) => {
console.log(descriptor);
descriptor.writable = false;
return descriptor;
};
複製代碼
上述代碼經過 console.log
輸出的結果爲:
以咱們常見的 @readonly
爲例,它的實現方法如上。經過在上述代碼中 log 出來 descriptor
, 咱們得知 descriptor
的屬性分別爲:
writable
- 被裝飾的函數是否能被賦值運算符改變;enumerable
- 被裝飾的函數是否出如今對象的枚舉屬性中;configurable
- 被裝飾的函數的描述符是否可以被改變,是否可以從對象上被刪除;value
- 被裝飾的函數的值,即其對應的函數對象。可見,@readonly
裝飾器將 descriptor
的 writable
屬性設置爲 false
, 並返回這個 descriptor
, 便成功將其裝飾的類成員設置爲只讀態。
咱們以以下方式使用 @readonly
裝飾器:
class Example {
@readonly
a = 10;
@readonly
b() {}
}
複製代碼
@monitorBefore
的實現@monitorBefore
裝飾器要比 @readonly
複雜一些,它是如何將埋點邏輯與業務邏輯融合,生成一個新的函數的呢?
首先,咱們來看看下面這段代碼。monitorBefore
接收一個埋點事件 event
做爲參數,並返回了一個函數。返回的函數的參數與上面講過的 @readonly
所接收的參數一致。
// monitorBefore 函數源代碼
monitorBefore = (event: MonitorEvent) => {
return (target: object, name: string, descriptor: object) =>
this.defineDecorator(event, descriptor, this.before);
};
複製代碼
// @monitorBefore 使用方式
@monitorBefore((value: string) => ({
type: 'func_operation',
params: { keyword: value },
}))
handleSearch() {
console.log(
'[Decorators Demo]: this should happen AFTER a monitor event is sent.',
);
}
return (
<AutoComplete onSearch={handleSearch} />
)
複製代碼
在編譯時,@monitorBefore
接收了一個 event
參數,並返回了以下函數:
f = (target: object, name: string, descriptor: object) =>
this.defineDecorator(event, descriptor, this.before);
複製代碼
然後,編譯器又調用函數 f
, 把當前的類、被裝飾的函數名稱與其屬性描述符傳給f
. 函數 f
返回的 this.defineDecorator(event, descriptor, this.before)
會被解析爲一個新的 descriptor
對象,它的 value
會在運行時被調用,也就是說會在 onSearch
被觸發時所調用。
如今,讓咱們詳細解讀 defineDecorator
函數是如何改變生成一個新的 descriptor
的吧。
// defineDecorator 函數源代碼
before = (event: MonitorEvent, fn: () => any) => {
const that = this;
return function (this: any) {
const _event = that.evalEvent(event)(...arguments);
that.sendEvent(_event);
return fn.apply(this, arguments);
};
};
defineDecorator = ( event: MonitorEvent, descriptor: any, decorator: (event: MonitorEvent, fn: () => any) => any ) => {
if (isFunction(event) || isObject(event) || isArray(event)) {
const wrapperFn = decorator(event, descriptor.value);
function composedFn(this: any) {
return wrapperFn.apply(this, arguments);
}
set(descriptor, "value", composedFn);
return descriptor;
} else {
console.error(
`[Monitor SDK @${decorator}] the event argument be an object, an array or a function.`
);
}
};
monitorBefore = (event: MonitorEvent) => {
return (target: object, name: string, descriptor: object) =>
this.defineDecorator(event, descriptor, this.before);
};
複製代碼
defineDecorator
函數接收三個參數:
event
- 須要上報的埋點;descriptor
- 被裝飾的函數的屬性描述符;decorator
- 一個高階函數。它接收一個埋點事件與一個回調,返回一個函數,用來進行埋點上報,並然後執行回調。decorator
首先返回了一個函數 wrapperFn
. 在被調用時,wrapperFn
會先上報埋點,而後執行 descriptor.value
的邏輯,即被裝飾的函數。
在 defineDecorator
的 composedFn
中, 咱們用 wrapperFn.apply(this, arguments)
將調用被裝飾的函數時傳入的參數透傳給 wrapperFn
。
最後,咱們將 composedFn
設爲 descriptor.value
, 這樣,咱們就成功生成了一個融合了埋點邏輯與業務邏輯的新函數。
裝飾器埋點注入方式十分整潔,可以清晰地與業務代碼區分。不管是增添、修改仍是刪除埋點,都無需顧慮會對業務代碼形成改動。可是其侷限性也是顯而易見的,裝飾器只能用於類組件,如今咱們經常使用的函數式組件是沒法使用裝飾器的。
爲了可以在函數式組件中,實現裝飾器埋點帶來的功能,咱們還支持了埋點鉤子 useMonitor
. 與裝飾器的原理相同,useMonitor
接收一個埋點函數,一個業務函數,返回一個新的函數將兩者融合,既實現了代碼層面上的清晰分離,又覆蓋了全場景的埋點注入。
// useMonitor 源代碼
useMonitor = (fn: () => any, event: MonitorEvent) => {
if (!event) return fn;
const that = this;
return function (this: any) {
const _event = that.evalEvent(event)(...arguments);
that.sendEvent(_event);
return fn.apply(this, arguments);
};
};
複製代碼
useMonitor
的實現較爲簡單,只是一個高階函數,不像裝飾器須要語法解析。它返回了一個函數,在被調用時會先上傳埋點事件,在執行業務邏輯。其使用方式以下:
// useMonitor 使用示例
const Example = (props: object) => {
const handleChange = useMonitor(
// 業務邏輯
(value: string) => {
console.log("The user entered", value);
},
// 埋點邏輯
(value: string) => {
return {
type: "func_operation",
params: { value },
};
}
);
return <Search onSearch={handleChange} />;
};
複製代碼
上述三種埋點方式,覆蓋了全部使用場景。不論你是用 React, Vue, 仍是原生 JavaScript, 不論你是使用類組件,仍是函數式組件,不論你的埋點是否須要複雜的前置邏輯,monitor
SDK 都提供了適合你的場景的使用方式。
PageLifecycle.js
wolfy87-eventemitter
MutationObserver
APIIntersectionObserver
APImonitor
SDK - [Lilly Jiang]monitor-tracer
SDK