小程序數據埋點實踐之曝光量

什麼是數據埋點

所謂數據埋點就是應用在規定流程中 對特定行爲或事件進行數據採集 。使用採集的數據作用戶分析和頁面分析,能夠得到應用的整體使用狀況,爲後續優化產品和運營提供數據支撐。常見數據埋點內容包括:訪問量、停留時長、曝光量、點擊量、跳出率等等。html

微信小程序也爲咱們提供了自定義分析統計,其中包括 API 上報(代碼埋點),填寫配置(無埋點,只需在公衆後臺配置)。而第三方統計平臺比較有名的就是阿拉丁統計,只需引入集成的 SDK,開發成本低,可以知足大部分的需求。小程序

數據埋點須要分析頁面流程,肯定埋點需求,選擇埋點方式。若是是代碼埋點,主要關注觸發時機、條件判斷、捕獲數據,其次要注意是否有遺漏的場景沒有作到埋點。代碼埋點雖然成本較大(侵入代碼),可是精準度較高,可以很好的知足埋點需求。後端

什麼是曝光量

曝光量顧名思義是 指定元素出如今可觀察視圖內的次數 ,也能夠理解爲展現量。微信小程序

一般咱們會使用 點擊量 / 曝光量 得出 點擊率 ,做爲衡量一個內容是否受用戶喜好的指標之一。好比,曝光 100 次只有 10 人點擊,和曝光 100 次 有 100 我的點擊,很明顯後者更受用戶喜好。利用這些數據參考,能夠推薦更多用戶喜好的內容,以此來留住用戶。api

交叉觀察者

IntersectionObserver 接口,提供了一種異步觀察 目標元素與其祖先元素或頂級文檔視窗(viewport)交叉狀態 的方法,祖先元素與視窗(viewport)被稱爲根(root)。簡單來講就是,觀察的目標是否和祖先元素和視窗發生交叉,即進入或離開。數組

小程序從基礎庫 1.9.3 開始支持 wx.createIntersectionObserver 接口(組件內使用 this.createIntersectionObserver ),使用此接口可建立 IntersectionObserver對象 。對此接口不瞭解的能夠查看官方文檔緩存

基礎使用

// 建立實例
let ob = this.createIntersectionObserver()
// 相對於文檔視窗監聽
ob.relativeToViewport()
    .observe('.box', res => {
        // res.intersectionRatio 爲相交比例
        if (res.intersectionRatio > 0) {
            console.log('進入頁面')
        } else {
            console.log('離開頁面')
        }
    })

閾值

在建立實例時能夠傳入一些配置,其中 thresholds (閾值)是比較重要的一項配置,它能夠控制觸發回調的時機。 thresholds 是一個數字類型的數組,默認爲 [0] 。即相交比例爲 0 時觸發一次回調,下面咱們來設置閾值,看看會有什麼改變:服務器

// 建立實例
let ob = this.createIntersectionObserver({
    thresholds: [0, 0.5, 1]
})

從圖上能夠看到,元素在相交比例爲 00.51 都各自觸發了一次回調。在統計曝光量設置閾值很是有用,一般我會設置爲 1 ,表示元素要徹底展現在頁面上纔會進行記錄,這樣數據會更加真實準確。微信

收縮和擴展參照區域

除了閾值以外還有另外一項重要的設置,在使用 relativeTorelativeToViewport 規定參照區域時,咱們能夠傳入配置 margins 來收縮和擴展參照區域。 margins 包括 leftrighttopbottom 四個參數配置。併發

// 建立實例
let ob = this.createIntersectionObserver()
// 相對於文檔視窗監聽
ob.relativeToViewport({
        bottom: -330
    })
    .observe('.box', res => {
        // res.intersectionRatio 爲相交比例
        if (res.intersectionRatio > 0) {
            console.log('進入頁面')
        } else {
            console.log('離開頁面')
        }
    })

上面將參照區域底部收縮 330px,能夠理解爲總體的區域從底部開始被裁剪 330px,所以元素只有進入頁面上半區纔會觸發回調。

進入正題

通過以上一些介紹,相信你們對交叉觀察者的好處和使用都瞭解的差很少。接下來進入正題 ~

背景

這次我作的項目是資訊類目的小程序,主要用於發佈和轉載一些學術文章。對於這種資訊的項目,須要經過數據埋點來收集用戶的閱讀習慣,以此來爲用戶推薦文章。

埋點方面用微信後臺提供的自定義分析以文章爲單位進行收集,而咱們本身後臺會以用戶爲單位進行收集。前者得出總體用戶閱讀偏好和文章熱度,後者主要精確到用戶,分析用戶單位的閱讀偏好。

改造組件

在分析頁面佈局和pm的商討後,多處須要統計曝光量的文章區域展現都大體相同,恰好也在封裝的列表組件裏。因而將收集曝光量的邏輯都交由組件內部處理。

組件改造:

  1. 定義 isObserver 屬性,該屬性由外部傳入的布爾值控制是否收集曝光量
  2. 監聽傳入的 list ,爲每一個元素綁定交叉觀察者

如下部分代碼省略,只展現主要邏輯:

<block wx:for="{{list}}" wx:key="id">
    <view class="artic-item artic-item-{{index}}" data-id="{{item.id}}" data-index="{{index}}">
    </view>
</block>
const app = getApp()
Component({
    data: {
        currentLen: 0
    }
    properties: {
        list: {
            type: Array,
            value: []
        },
        isObserver: {
            type: Boolean,
            value: false
        }
    },
    observers: {
        list(list) {
            if (this.data.isObserver === false) {
                return
            }
            if (list.length) {
                // currentLen 記錄當前列表的長度
                // 用於計算監聽元素的索引,對已經監聽過的元素再也不重複監聽
                let currentLen = this.data.currentLen
                for (let i = 0; i < list.length - currentLen; i++) {
                    let ob = this.createIntersectionObserver({
                        thresholds: [1]
                    })
                    ob.relativeToViewport()
                        .observe('.artic-item-' + (currentLen + i), res => {
                            // 獲取元素的dataset
                            let {
                                id,
                                index
                            } = res.dataset
                            if (res.intersectionRatio === 1) {
                                // 此處收集曝光量,內部處理邏輯會在下面說起
                                this.sendExsureId(id)
                                // 元素出現後取消觀察者監聽,避免重複觸發
                                ob.disconnect()
                            }
                        })
                }
            }
            this.data.currentLen = list.length
        }
    }
})

發現🐛

理想狀況應該是切換到第二個分類打印3個文章,但因爲組件開始記錄第一個分類列表的 currentLen ,在切換到第二個分類時, currentLen 沒有被清除,致使循環長度錯誤。

解決:首先記錄列表第一項的 id ,當監聽列表變化,用新列表的第一項 id 做與之比較。若不相等,則表示列表被從新賦值,此時將 currentLen 置爲0。

Component({
    data: {
        flagId: 0,
        currentLen: 0
    }
    properties: {
        list: {
            type: Array,
            value: []
        },
        isObserver: {
            type: Boolean,
            value: false
        }
    },
    observers: {
        list(list) {
            if (this.data.isObserver === false) {
                return
            }
            if (list.length) {
                // 比較id
                if (this.data.flagId != list[0].id) {
                    this.data.currentLen = 0
                }
                let currentLen = this.data.currentLen
                for (let i = 0; i < list.length - currentLen; i++) {
                    let ob = this.createIntersectionObserver({
                        thresholds: [1]
                    })
                    ob.relativeToViewport()
                        .observe('.artic-item-' + (currentLen + i), res => {
                            let {
                                id,
                                index
                            } = res.dataset
                            if (res.intersectionRatio === 1) {
                                this.sendExsureId(id)
                                ob.disconnect()
                            }
                        })
                }
            }
            // 設置列表第一項id
            this.data.flagId = list[0] ? list[0].id : 0
            this.data.currentLen = list.length
        }
    }
})

組件優化

由於須要提早監聽文章的相交狀態,在 list 傳入時就開始循環 observe 。如今假設一個場景,在進入頁面時,已經爲一些文章註冊完成回調,但用戶並無看過這些文章就退出頁面。那是否是表示這些實例都沒有被 disconnect

解決:在 observe 時將每個觀察者實例存入數組,當組件銷燬時檢查數組中是否有觀察者實例,若是有,則調用這些實例的 disconnect

Component({
    data: {
        currentLen: 0,
        obItems: [] // 存放實例的數組
    },
    observers: {
        list(list) {
            if (this.data.isObserver === false) {
                return
            }
            if (list.length) {
                if (this.data.flagId != list[0].id) {
                    this.data.currentLen = 0
                    // 取消實例的監聽
                    this.removeObItems()
                }
                let currentLen = this.data.currentLen
                for (let i = 0; i < list.length - currentLen; i++) {
                    let ob = this.createIntersectionObserver({
                        thresholds: [1]
                    })
                    ob.relativeToViewport().observe('.artic-item-' + (currentLen + i), res => {
                        let {
                            index,
                            id
                        } = res.dataset
                        if (res.intersectionRatio === 1) {
                            this.sendExsureId(id)
                            ob.disconnect()
                            // 取消監聽後 將實例移出數組
                            this.data.obItems.shift()
                        }
                    })
                    // 將實例存入數組
                    this.data.obItems.push(ob)
                }
            } else {
                // 取消實例的監聽
                this.removeObItems()
            }
            this.data.flagId = list[0] ? list[0].id : 0
            this.data.currentLen = list.length
        }
    },
    lifetimes: {
        detached() {
            // 組件銷燬時 取消實例的監聽
            this.removeObItems()
        }
    },
    methods: {
        removeObItems() {
            if (this.data.obItems.length) {
                this.data.obItems.forEach(ob => {
                    ob.disconnect()
                })
            }
        }
    }
})

收集處理

如今組件可以收集到曝光文章的ID,剩下的就是日後臺發送數據。那麼問題來了,難道文章曝光一次就發起一次請求嗎?若是不怕和後端同事幹架的話,你能夠這麼作。要知道屢次發起請求,服務器🍐會很大。用戶量比較大後,對服務器可以承受的併發量會有很大的考驗。因此正確的作法應該是,把收集到的ID緩存起來,在達到必定數量的時候一塊兒發送過去。

接下來對收集的數據作些處理:

// 這個上面收集曝光量的函數
sendExsureId(id) {
    if (typeof app.globalData.exposureIds === 'undefined') {
        // exposureIds 是定義在全局用於存放曝光文章 ID 的數組
        app.globalData.exposureIds = []
    }
    app.globalData.exposureIds.push(id)
    // 當數組到達 50 個,開始上報數據
    if (app.globalData.exposureIds.length >= 50) {
        wx.$api.recordExposure({
            // 由於 ID 比較多,我和後端約定好使用逗號分隔
            ids: app.globalData.exposureIds.join(',')
        })
        // 上報後清空數組
        app.globalData.exposureIds = []
    }
}

看起來好像實現到這裏就大功告成,可是咱們還要考慮一種狀況。假如用戶只看了 40 個就退出小程序,而上報條件是達到 50 個纔會發送數據,那麼這部分有用的數據就會被丟失。由於小程序沒有回調可以監聽到小程序被銷燬,這裏只能使用小程序的 onHide 函數來作些事情。當小程序進入後臺時 onHide 函數就會被執行,此時能夠在函數裏上報數據。

App({
    onHide() {
        if (this.globalData.exposureIds.length) {
            wx.$api.recordExposure({
                ids: this.globalData.exposureIds.join(',')
            })
            this.globalData.exposureIds = []
        }
    }
})

寫在最後

說實話,在埋點這方面的知識不算很熟悉,業務場景也比較簡單。由於沒有大佬指導,也是看着需求往這方面去作,有哪裏錯誤或遺漏請指出。若是你有更好的方案或經驗,歡迎評論區交流💖~

相關文章
相關標籤/搜索