https://zhuanlan.zhihu.com/p/27659302html
本文介紹了一種通用的前端埋點方案的設計和實現,具備適配項目普遍,易於使用,與業務邏輯解耦等優勢,已經在外賣商業平臺進行了一段時間的試用,並取得良好效果。前端
銷售CRM方向是外賣爲銷售人員提供各維度的工具和平臺,以幫助提升銷售人員工做的效率。在銷售CRM方向的PC端,一直沒有對用戶行爲的數據採集(即埋點數據採集),因此對於分析用戶行爲、觀察產品使用情況、制定產品策略等都缺少相關的數據支持。jquery
因此在今年3月份銷售CRM方向決定啓動PC端的各方向的埋點,包括智子、任務制、HES、商機等多個系統。PM整理的埋點個數達到了100多個。git
在埋點的後端方案採用DA的SAK。而在前端方向,這幾個系統有使用jquery+widget的老方案,也有基於Vue的技術棧實現。須要如何埋點?怎樣實現簡單高效的埋點?這是須要咱們解決的問題。github
業界的埋點方案主要分爲如下三類:後端
在當時排期緊湊,人力緊缺的狀況下,顯然不容許咱們去開發可視化埋點方案和無埋點方案,因此只能採起代碼埋點方案。瀏覽器
代碼埋點分爲 命令式埋點 與 聲明式埋點 。app
命令式埋點,顧名思義,開發者須要手動在須要埋點的節點處進行埋點。如點擊按鈕或連接後的回調函數、頁面ready時進行請求的發送。你們確定都很熟悉這樣的代碼:框架
// 頁面加載時發送埋點請求
$(document).ready(function(){
// ... 這裏存在一些業務邏輯
sendRequest(params);
});
// 按鈕點擊時發送埋點請求
$('button').click(function(){
// ... 這裏存在一些業務邏輯
sendRequest(params);
});
能夠很容易發現,這樣的作法頗有可能會將埋點代碼侵入業務代碼,這使總體業務代碼變得繁瑣,容易出錯,且後續代碼會越發膨脹,難以維護。因此,咱們須要讓埋點的代碼與具體的業務邏輯解耦,即 聲明式埋點 ,從而提升埋點的效率和代碼的可維護性。dom
理論上,聲明式埋點只須要關注兩個問題:
所以,能夠很快想出一個聲明式埋點的方法:
// key表示埋點的惟一標識;act表示埋點方式 <button data-stat="{key:'111', act: 'click'}">埋點</button>
那麼能夠去遍歷DOM樹,找到 [data-stat] 的節點,給這個button綁上click事件,把這些參數在回調函數中經過請求發出去。
在DOM節點(html)上聲明埋點,與業務邏輯(一般在Javascript文件中)就解耦了。調用也很方便。
看起來很美,但這樣就能解決問題了嗎?顯然是不夠的。還須要解決如下問題:
回顧一下,咱們須要解決的問題是:
咱們最終提出了一個基於Vue指令(Directive)和混合(Mixin)的解決方案:
因爲在埋點的需求中有部分項目使用了Vue做爲基礎框架,結合上面聲明式埋點的例子,很容易就聯想到 Vue自定義指令。Vue自定義指令提供了一種機制,將數據的變化映射爲 DOM 行爲。以 Vue 1.x 版本爲例,自定義指令提供了幾個鉤子函數:
- bind:只調用一次,在指令第一次綁定到元素上時調用。
- update: 在 bind 以後當即以初始值爲參數第一次調用,以後每當綁定值變化時調用,參數爲新值與舊值
- unbind:只調用一次,在指令從元素上解綁時調用
這樣的特性能夠很好的解決以上的一些問題。咱們只須要像這樣:
Vue.directive('stat', { bind: function () { // 準備工做 }, update: function (newValue, oldValue) { // 值更新時的工做 // 也會以初始值爲參數調用一次, 此時能夠根據傳值類型來進行相應埋點行爲的請求處理 }, unbind: function () { // 清理工做 } })
在一個Vue應用中,不須要再去遍歷DOM樹,由於在Vue應用中基本全部DOM操做都是使用數據的變動結合Vue的內置指令實現,Vue能夠感知到這些變動。在指令從元素上解綁時咱們也能夠去銷燬已經綁定的事件。
那麼接下來的問題是,還有一些項目基於 jquery + widget 的老方案實現,那麼在這些項目中的DOM操做是jquery甚至原生DOM API來實現,Vue的自定義指令就沒法工做。舉個例子:
<div id="container"> <button id="btn">click</button> </div> <script> new Vue({ el: '#container', directives: {stat} }) $('#btn').click(function() { $('#container').append('<button v-stat="{key: '3', act: 'click'}">click</button>') }) </script>
在上面例子中,雖然Vue已經掛載到 container 容器上,引入了自定義指令stat, #btn 這個按鈕點擊時插入了一段帶有指令v-stat的按鈕,由於Vue沒法感知這個DOM變動,因此該指令不能被解析。這樣的方式就會失效。
以前在外賣運營平臺方向有基於 jquery 的DOM劫持操做的實現,在全部DOM操做中加入埋點相關的邏輯;由於沒法保證全部的DOM操做都使用 jquery , 且不能保證全部埋點邏輯徹底一致,因此也沒法通用。
那麼,怎樣保證在任意庫,包括原生API的DOM操做下都感知到DOM的變動而且通知Vue從新解析指令呢?這裏就須要引入 MutationObserver。
MutationObserver是在DOM3標準中提出的標準API,提供讓開發者感知到在某一個DOM節點變動的能力。能夠監聽如下場景:
MutationObserver的瀏覽器支持狀況已經比較好了.
&lt;img src="https://pic4.zhimg.com/v2-1419467a7f6369f269fe3977f097bcc2_b.jpg" data-rawwidth="1490" data-rawheight="152" class="origin_image zh-lightbox-thumb" width="1490" data-original="https://pic4.zhimg.com/v2-1419467a7f6369f269fe3977f097bcc2_r.jpg"&gt;但爲了保證MutationObserver能夠在全部瀏覽器上正常工做,咱們仍然引入了這個API的polyfill,詳情可見這裏。
在此能力的前提下,咱們就能夠在任意的DOM操做下觸發Vue進行從新解析指令。
咱們將 MutationObserver 封裝進一個 Vue mixin , 非Vue應用的業務代碼只須要引入這個mixin,這樣也能夠很好地解耦。
詳細的實現原理能夠見如下僞代碼:
let observer; export default { ready() { // 開啓監聽 observer = new MutationObserver(mutations => { this.$compile(this.$el); }); observer.observe(this.$el, config); }, destroyed() { // 清理工做 observer.disconnect(); observer.takeRecords(); } }
關於MutationObserver的詳細介紹請見 標準文獻。
埋點庫另外一部分主要的邏輯是處理埋點行爲。
Ready事件的處理,在頁面根元素綁定指令後,在指令第一次update鉤子調用時便可認爲該元素ready, 直接發起請求埋點便可;
click事件的處理,在該節點上綁定click事件,在指令解綁時銷燬該事件。
區域展示埋點即:當區域爲可見狀態變動時進行埋點。
那麼,咱們一樣須要監聽節點的可見狀態變動。
理論上,DOM可見狀態的變動也在MutationObserver的監聽範圍內,最初的一種思路是:
let observer = new MutationObserver((mutations) => { if (mutations[0].oldValue.indexOf('display: none') > -1 && mutations[0].target.style.display !== 'none') { sendRequest(); } }) let config = { attributes: true, attributeOldValue: true, attributeFilter: ['style'] }; observer.observe(el, config);
可是這種思路很快被否決,由於很顯然,可見狀態還有多是被節點類名class控制的。而具體節點上的類名是沒法預期的,所以這種方案行不通。
最終咱們使用了開源庫 VisSense。VisSense提供了監聽可見狀態變動的能力,具體請見這裏,本文不進行詳細描述。
VisSense 實際使用了消息訂閱模式和setInterval來進行週期性的節點狀態檢查,感興趣的同窗能夠看看它的源碼。
因而在這裏咱們就能夠進行很方便的可見狀態監聽:
function handleShow(el) { var visMonitor = VisSense(el).monitor({ visible: function() { sendRequest(); } }); visMonitor.start(); }
眼球曝光埋點標識用戶是否「看到」了某個區域,那麼用前端的方式來解釋就是:
主要的實現思路就是監聽scroll事件,與當前節點的scrollTop進行對比。
因爲本次需求未涉及眼球曝光,本部分再也不贅述。
上面的聲明式埋點方案已經能夠解決大多數問題。
可是,不是100%的狀況都適用聲明式埋點,主要發生在 DOM操做不受開發者徹底控制 的狀況。
舉個例子,在使用百度地圖API時,在地圖上打一些POI點(markPoint), 或者一些蒙層(如Polygon), 再在點擊這些覆蓋物時埋點,因爲這些DOM操做是百度地圖API完成的,沒法預期插入了哪些DOM,天然就不能在這些DOM上插入指令。因此只能在調用API時進行命令式埋點。須要咱們也提供命令式埋點支持。
命令式埋點的大部分邏輯實際已經包含在指令中,因而咱們在指令中提供了這樣的接口方式:
export default { bind() {...}, update() {...}, unbind() {...}, sendStat(val) { // 命令式埋點接口 } }
引入此模塊後,便可以看成Vue指令使用,也能夠當作一個API來使用。
此外,埋點方案還提供了可配置能力,能夠設定測試環境仍是生產環境的規則(根據URL匹配),設定埋點請求的URL地址,是否開啓debug模式等。
在測試環境下,埋點請求的時機只會在瀏覽器中進行console.log並打印出觸發埋點的節點,不會實際發送請求,能夠支持測試環境下的正常開發,又能夠避免埋點出現髒數據。
new Vue({ el: '#app', // 根節點 directives: {stat}, mixins: [observerMixin] // 非Vue項目須要引入 })
而後在頁面相應節點進行聲明式埋點便可:
<div id="app" v-stat="{'act':'ready',' key':'samplepg'}"></div> // 頁面展示埋點
<button v-stat="{'act':'click', 'key':'samplebtn'}"></button> // 點擊統計埋點
<div id="container" v-stat="{'act':'show',' key':'samplepn'}"></div> // 區域展示埋點
這樣的埋點方式十分簡便快捷。
在實際業務開發過程當中,本埋點方案平滑適配了Vue項目和jquery等開發的一些老項目,能夠很好地和業務代碼解耦,只須要在須要埋點的DOM節點上進行聲明式埋點,開發簡單高效,在排期人力緊張的狀況下,很好地支持了100餘個埋點數據統計。
前端的數據採集和上報是構建數據平臺的重要環節,而前端如何進行埋點也是值得深究的。爲了快速知足業務的大量埋點需求,咱們使用了本文的埋點方案,並且已經大量在商業平臺部開發中使用,不管從FE同窗的開發反饋、實際產出數據的結果來看都達到咱們的預期,後續會繼續在一些業務上進行持續迭代和優化。