對於很多前端同學來說,「埋點」常常是一個不願面對卻又無法逃避的話題。為什麼這麼說呢,相信很多前端同學都深有體會:首先埋點這個事基本是前端「獨享」的,伺服器端基本不太涉及;其次新增埋點,往往看起來很簡單但實際做起來很麻煩,很多時候為了獲取一些埋點需要的資訊甚至要對已經寫好的程式碼進行傷筋動骨的修改。
雖然前端埋點費時費力,做起來沒什麼成就感,但是埋點作為收集線上業務資料(使用者購買行為、活動轉化等)的重要途徑,為產品策略調整提供了重要資料支撐,特別是在像618、雙11等大促活動中,埋點資料採集對於促銷活動的策略制定、及時調整及最終收益效果的驗證都至關重要,因此又是一件研發同學必須要認真對待的事情。本文結合多年來各平臺專案實踐經驗,總結了埋點類需求的開發實戰經驗及技巧,希望通過本文的分享能讓更多讀者在開發中儘量少走彎路,準確高效完成埋點開發任務,保證業務在大促及常態運營中的穩定資料支撐。
言歸正傳,對於各種型別的埋點來說,曝光埋點往往最為複雜、需要用到的技術也最全面、如果實現方式不合理可能造成的影響也最大,因此本文將重點介紹曝光埋點尤其是長列表(或捲動檢視)內元素曝光埋點的實現思路及避坑技巧;
長列表(或捲動檢視)中元素的曝光埋點,關鍵是如何監聽子元素的「曝光」事件。「曝光」即元素進入到了螢幕的可見區域,也就是能被使用者看到了,這是人類的直觀視覺感受,那麼如何用程式碼的方式來判定呢?目前大概有這麼三種方法:1.根據介面下發分頁資料估算可見元素;2.監聽捲動檢視的捲動事件,實時計算元素相對位置;3. 利用瀏覽器(或其他平臺如小程式、Taro)標準API監聽元素與可見區域的相交變化。下面分別介紹一下這三種方法的具體原理、適用範圍及優缺點。
實現思路:長列表的資料往往通過分頁介面進行載入,可以利用這一特性,以單頁資料返回的維度粗略估算元素的可見性,具體說就是以每一次的介面返回的資料當做當前可見的元素的列表;
優點:
缺點:
由於缺點很明顯,誤差太大,現在很少有人這麼來實現曝光埋點,但是在很多精度要求不高的場景或者年代很久的程式碼中還能看到這種實現方式
實現思路:監聽長列表(或捲動檢視容器)的捲動事件,通過平臺UI基礎介面(如瀏覽器DOM介面getBoundingClientRect)實時獲取元素座標(包括位置和大小資訊等),並計算同可視區域的相對狀態(是否有重疊)來判定元素是否「可見」;
優點:
缺點:
這種方式雖然計算量大、邏輯複雜、效能較差(當然也可以進行一些效能上的優化,代價是程式碼複雜度變的更高,不利於後續更新維護),但是計算結果是準確的,在沒有出現方法三中的Web端標準介面(2016)之前,在計算精度要求嚴格的場景下,這視乎是唯一的選擇;
實現思路:Intersection Observer API做為一個專門用於監聽頁面元素相交變化的Web標準API介面,在2016年首先在Chrone瀏覽器中提供,並在隨後的幾年內得到了各主流瀏覽器的支援;利用該介面提供的非同步查詢元素相對於其他元素或視窗位置的能力,可以高效的對頁面內元素的相交(可見性)變化進行監聽;
優點:
缺點:
基於以上,這種方式是目前最推薦的一種實現元素曝光監聽的方式,具體怎麼用呢,下面對H5、小程式、Taro的使用場景分別來介紹一下。
簡單來說,利用Intersection Observer API來進行檢視元素的可見性觀察主要分成這麼幾個步驟:建立觀察者、對目標元素新增觀察、處理觀察結果(回撥)、停止觀察;
首先我們需要建立一個觀察者IntersectionObserver
,用於監聽目標元素相對於根檢視(可以是父檢視或當前視窗)的相交狀態變化情況(即元素的「可見」狀態)。具體建立方式是利用Web標準API:IntersectionObserver
構造方法,具體程式碼如下:
let observer = new IntersectionObserver(callback, options);
其中callback
就是當監聽到元素位置變化時觸發的回撥方法,具體定義及用法會在第三步處理觀察結果中具體介紹;
options
是客製化觀察模式的引數,引數定義如下
interface IntersectionObserverInit {
root?: Element | Document | null;
rootMargin?: string;
threshold?: number | number[];
}
callback
);可以是單一數值(number)也可以是一組數值;例如當設定為0.25時,只有當相交達到0.25時(增大到0.25或減小到0.25都會觸發)才會觸發回撥;如果是一組數值的話,相交比例達到其中任意值時也都會觸發回撥;(備註:除此外,元素首次新增觀察時也會觸發一次回撥,不論是否達到閾值)例如上圖中的threshold設定狀態,每當元素滑動到虛線位置與父檢視邊界相交時就會觸發回撥
有了觀察者後,就可以對目標元素進行觀察了,具體程式碼如下:
let target = document.querySelector('#listItem');
observer.observe(target);
需要注意新增觀察的時機,要保證在目標元素建立好以後再新增觀察;如果是動態建立的元素(例如分頁載入資料),需要在每次建立完元素後再次對新增的元素新增觀察;
當被觀察的目標元素與參照檢視(root)相交的比例達到設定的閾值時,就會觸發我們註冊的回撥方法(callback
),回撥方法的定義如下:
interface IntersectionObserverCallback {
(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void;
}
可以看到該回撥方法內可以接收到兩個引數:
entries
:IntersectionObserverEntry的陣列,是相交狀態發生變化的元素的集合,每個IntersectionObserverEntry物件內有6個屬性:
observer:當前觀察者;
有了這些資訊,就可以輕鬆監測目標元素的可見狀態變化,方面進行後續的埋點上報、資料記錄、延遲載入等各種處理;
註冊的回撥函數將會在主執行緒中被執行,所以該函數執行速度要儘可能的快。如果有一些耗時的操作需要執行,建議使用 Window.requestIdleCallback() 方法。
如果需要停止觀察,可以在合適的時間解除對某個元素的觀察或終止對所有目標元素的觀察;
// 停止觀察某個目標元素
observer.unobserve(target)
// 終止對所有目標元素可見性變化的觀察,關閉監視器
observer.disconnect()
需要注意的是在Intersection Observer API 的V2版本新增了一個 isVisible
屬性(新版Chrome瀏覽器已經支援,Safari等其他瀏覽器內不支援),用來標識元素是否「可見」(因為即使元素在可視區域內,也有肯能因為被其他元素遮擋、樣式屬性hiden等影響導致元素不能被看到);官方說明中,為了保證效能,這個欄位的值不一定是準確的,除非特殊場景,不建議使用這個欄位,大部分場景isIntersecting
就夠了;
感興趣的可以檢視檔案說明:Intersection Observer V2 Explained
瀏覽器 | 平臺 | 支援的版本 | 釋出時間 |
---|---|---|---|
Chrome | PC | 51 | 2016-05-25 |
Chrome Android | Android | 51 | 2016-06-08 |
WebView Android | Android | 51 | 2016-06-08 |
Safari | macOS | 12.1 | 2019-03-25 |
Safari on iOS | iOS | 12.2 | 2019-03-25 |
Edge | PC | 15 | 2017-04-05 |
Firefox | PC | 55 | 2017-08-08 |
Firefox for Android | Android | 55 | 2017-08-08 |
Opera | PC | 38 | 2016-06-08 |
Opera Android | Android | 41 | 2016-10-25 |
可以根據具體使用場景(支援的瀏覽器版本情況)來決定是否直接使用標準API還是需要新增polyfill或其他方式來相容低版本瀏覽器;
同Web端介面類似,微信小程式提供了對應的小程式版本API介面,功能同web端的Intersection Observer API類似,使用方式也基本相同,只是部分細節存在差異;具體步驟:
通過微信小程式框架提供的IntersectionObserver wx.createIntersectionObserver(Object component, Object options)
方法,可以方便的建立觀察者;
Page({
data: {
appear: false
},
onLoad() {
this._observer = wx.createIntersectionObserver(this,{
thresholds: [0.2, 0.5]
})
//其他處理...
},
onUnload() {
if (this._observer) this._observer.disconnect()
}
})
類比web端的IntersectionObserver構造方法,不同的是小程式裡這一步不需要設定回撥方法,回撥方法放到後面新增觀察的時候註冊;
入參說明:
component
一般需要傳當前頁面或元件範例;options
可定義觸發閾值、是否同時觀測多個目標節點等資訊
不同於web端的建立時指定,小程式端提供了兩個單獨介面用於指定參照節點(參照區域)
IntersectionObserver IntersectionObserver.relativeTo(string selector, Object margins)
:使用選擇器指定一個節點,作為參照區域之一,即以該節點的佈局區域作為參照區域。IntersectionObserver IntersectionObserver.relativeToViewport(Object margins)
: 指定頁面顯示區域作為參照區域之一範例:
this._observer = this._observer.relativeTo('.scroll-view')
同樣可以通過margins
來對參照區域進行擴充套件(或收縮);如果有多個參照節點,則會取它們佈局區域的 交集
作為參照區域。
通過前兩步建立好觀察者,設定好相關引數(觸發閾值、是否多目標等)並指定參照區域後,就可以對目標元素進行觀察了。這裡通過選擇器的方式(web端是元素範例)來指定目標元素,同時這裡需要指定相交狀態變化的回撥方法:IntersectionObserver.observe(string targetSelector, function callback)
範例:
this._observer.observe('.ball', (res) => {
console.log(res);
this.setData({
appear: res.intersectionRatio > 0
})
})
當元素相對於參照區域的相交情況發生變化(同web端一致,觸發時機由第一步建立觀察者時設定的thresholds閾值決定)就會觸發相應的回撥方法。回撥方法內接受的引數同web端基本一致,但也存在差異:
IntersectionObserver.disconnect()
停止監聽。回撥函數將不再觸發
if (this._observer) this._observer.disconnect()
注意:在元件內,如果在attached元件生命週期函數內新增內部子元素的相交變化觀察可能無法監聽成功,原因是此時元件佈局還未完成,元件內節點未完成建立;
Taro內也提供了對應的IntersectionObserver的API,其API的定義及使用方式基本是同微信小程式端對齊的;Taro本身是支援H5、小程式等多端的,其IntersectionObserver介面內部對H5、微信小程式、京東小程式等各平臺進行了對齊抹平,具體來說在H5端是按照微信小程式端的格式進行的封裝,其內部實現是呼叫的Web端的Intersection Observer API,在小程式端由於標準對齊,基本上就是橋接對應平臺小程式原生的介面;
由於介面定義及使用方式同微信小程式對齊,這裡就不再贅述Taro端的具體使用方式,需要說明的是由於Taro框架的特殊性(相比小程式原生方式多了一層),在用Taro進行小程式端滑動曝光監聽開發時,有幾個容易出錯或需要特殊處理的點:
由於Taro執行時機制,在Taro元件的資料更新方法(例如setState)執行後立刻新增監聽可能會不生效,原因是對應的由資料驅動的小程式元素範例此時還未完成建立或掛載,需要新增延遲或在Taro.nextTick回撥內執行(Taro最新版本已經預設將observe方法新增到Taro.nextTick內執行);如果遇到新增監聽不生效的情況,可以嘗試這個方法;
Taro.nextTick(() => {
//將監聽新增時機(延遲作到下一個時間片再執行),解決監聽新增時元素尚未建立導致的監聽無效問題
expoObjserver.relativeTo('.clpScrollView').observe('.coupon-item', result => {
console.log(result.intersectionRatio);
//...
});
});
在建立observer時需要傳入小程式的頁面或者元件範例,而在Taro元件或頁面內直接使用this獲取的是Taro層的頁面或元件的範例,兩者是不同的;
那麼如何獲取小程式層的元件範例呢:
Taro.getCurrentInstance().page
獲取小程式頁面的範例;props.$scope
獲取到小程式的自定義元件物件範例如果建立及設定正確,隨著列表的滑動或其他元素的位置變化,對應的回撥方法應該會被觸發,在回撥方法內我們需要接收回撥的入引數並進行處理(例如上報相關業務資訊)。根據Taro檔案定義,回撥方法的入參是ObserveCallbackResult
型別:
interface ObserveCallbackResult {
/** 目標邊界 */
boundingClientRect: BoundingClientRectResult
/** 相交比例 */
intersectionRatio: number
/** 相交區域的邊界 */
intersectionRect: IntersectionRectResult
/** 參照區域的邊界 */
relativeRect: RelativeRectResult
/** 相交檢測時的時間戳 */
time: number
}
可以看到其中只有intersectionRatio、time等位置相交的相關資訊,但是卻沒有自定義資料欄位(作為對比微信小程式提供的回撥方法內除了這些還包括節點 ID、節點自定義資料屬性dataset等資訊) ,那麼在Taro內如何獲取目標元素上的其他資料資訊呢?
實際偵錯發現,雖然檔案中沒有id、dataset,但是實際返回值內是有這倆欄位的,哈哈,看到這裡是不是覺得沒啥問題了,以為只是Taro檔案跟我們開了個小小的玩笑;先別高興的太早,雖然實際返回值裡有dataset欄位,但是不幸的是dataset欄位始終是空的,實際返回結果(result)範例如下:
{
"id": "_n_82",
"dataset": {},
"time": 1687763057268,
"boundingClientRect": {
"x": 89.109375,
"y": 19,
...
},
"intersectionRatio": 1,
"intersectionRect": {
"x": 89.109375,
"y": 19,
...
},
"relativeRect": {
"x": 82.109375,
...
}
}
what?why?看到這裡估計大家有想砸鍵盤的衝動,先彆著急,我們先來分析一下為什麼dataset
是空呢?
這是由於dataset
是小程式的特殊的模版屬性,主要作用是可以在事件回撥的 event
物件中獲取到 dataset
相關資料,Taro對於這些能力是部分支援的,Taro通過在邏輯層的模擬已經支援在事件回撥物件中通過 event.target.dataset
或 event.currentTarget.dataset
獲取到。但是由於是在邏輯層模擬實現的,並沒有真正在模板設定這個屬性。所以在小程式中有一些 API(如:createIntersectionObserver)獲取到頁面的節點的時候,是獲取不到dataset
的。
上一點所說的,Taro 對於小程式 dataset 的模擬是在小程式的邏輯層實現的。並沒有真正在模板設定這個屬性。
但在小程式中有一些 API(如:createIntersectionObserver)獲取到頁面的節點的時候,由於節點上實際沒有對應的屬性而獲取不到。--來自Taro官方檔案: Taro-React-dataset
既然在回撥傳參中直接取值是空,那我們該怎麼獲取元素上的自定義資料呢?
官方給出的解決方案是使用taro-plugin-inject
外掛,向子元素內注入一些通用屬性;實際驗證發現,利用外掛插入後回撥的dataset中確實能看到有對應的屬性,但是該方法插入的屬性只能是統一的固定值,無法根據實際資料動態設定屬性值,因此該方案不能滿足訴求。
//專案 config/index.js 中的 plugins 設定:
plugins: [
[
'@tarojs/plugin-inject',
{
components: {
View: {
'data-index': "'dataIndex'",
'data-info': "'dateInfo'"
},
},
},
]
],
//實際返回值
{
"id": "_n_82",
"dataset": {
"index": "dataIndex",
"info": "dateInfo"
},
"time": 1687766275879,
...
}
根據Taro官方檔案關於React框架使用差異的描述(Taro-React-生命週期觸發機制),Taro3在小程式邏輯層上實現了一份遵循Web標準 BOM 和 DOM API。通過這些API可以獲取對應的虛擬DOM節點(TaroElement物件),既然是邏輯層實現的,那麼節點上應該也能看到對應的dataset
資訊。
Taro DOM Reference檔案內的TaroElement欄位說明也證實了這一點。
那麼具體如何實現呢?回撥引數中雖然沒有我們想要的自定義資料欄位,但是可以拿到節點id資訊,可以通過Taro提供的document.getElementById();
API利用節點id獲取對應的Taro虛擬DOM節點,從該節點上拿到我們需要的dataset
資訊,程式碼如下:
Taro.nextTick(() => {
//將監聽新增時機(延遲作到下一個時間片再執行),解決監聽新增時元素尚未建立導致的監聽無效問題
expoObjserver.relativeTo('.clpScrollView').observe('.coupon-item', result => {
if (!result?.id) return;
// !!!獲取虛擬DOM節點
const tarTaroElement = document.getElementById(result?.id);
const dataInfo = tarTaroElement?.dataset; //拿到dataset資訊
console.log(tarTaroElement);
console.log(dataInfo);
//...
});
});
至此,我們就拿到了想要的自定義資料(業務資料),後續就是根據述求隨意的使用這些資料了,Taro內列表滑動元素曝光埋點搞定~
總結一下,通過上面幾種常見方案和各平臺內的具體實現的介紹,長列表滑動元素的曝光監聽問題應該不再是難事,搞定了滑動元素曝光監聽,基於此之上的曝光埋點或者其他高階玩法(如長列表優化-資源惰性載入、無限迴圈捲動等)後續我們都可以從容應對。
作者:京東零售 丁鑫
來源:京東雲開發者社群