上一篇文章《巧用域名發散,緩解單廣告位並發請求限制》中提到了我已經將廣告的數據請求寫成了單廣告位請求。既然數據請求都已經是單廣告位的了,那么曝光統計也理所應當是單廣告位的。
pv是什么?
我們找一下百度百科的解釋: 就是頁面瀏覽量(page view),通常是衡量一個網絡新聞頻道或網站甚至一條網絡新聞的主要指標。網頁瀏覽數是評價網站流量最常用的指標之一,簡稱為PV。
曝光是什么,區別在哪?
簡單來說,就是用戶在瀏覽器中看到了我們關注的東西后,給后台的一個反饋。之所以把pv和曝光放在同一個專題下講,是因為二者統計的都是瀏覽量。不同的是,pv關注點在頁面,統計的的是頁面的瀏覽量;而曝光關注的是我頁面中內容是否被用戶統計到,換個說法是面向DOM結構的瀏覽量統計。
原來是什么樣的,我想要的是什么樣的?
原來的曝光統計就是很普通的加載后發出一個請求。雖然每個廣告位都有自己的曝光,但是這種曝光僅僅是能統計到今天投放的廣告的量,這樣這要從投放端看看投放了啥,再乘以頁面pv,就相等了。沒有太大實際意義,還白白的占用的帶寬。我想要的是統計用戶看到的廣告是哪些,由於用戶不一定會看完整個頁面,這樣每個廣告位的瀏覽量一定是小於等於pv。這樣我們就可以統計到,哪些廣告位被砍的多,哪些廣告位被看到的次數少,甚至哪些廣告位從未被看到過。更進一步來說曝光統計還可以進一步對頁面設計以及改版有一定的知道意義,我們常說沒有數據的優化就是空談。對於大多數網站的頁面往往都是在摸索中前進,我們並不知道什么樣的設計更能夠受到用戶的青睞,一次改版中有受歡迎的部分,也有不受歡迎的部分。
那么我們是如何知道大多數用戶的想法呢?就是反饋。調查問卷、隨機訪問可以嗎?當然這樣做會得到一定的反饋,同時也消耗了大量的人力物力。從另一個角度來考慮,這種方式會帶來類似“薛定諤的貓”的效果,我們主動的問卷活動,或者隨機訪問,本身就會干擾反饋的結果。比如我問別人,“我長得帥嗎?”對方礙於面子,或者時間趕不耐煩的敷衍或是為了要活動獎品,草草的全都打鈎。這種反饋往往都不真實,比如上學的時候的教師滿意度調查,大家是不是都寫得滿意。
比較好的方式是讓用戶不知道自己正被調查,通過觀察用戶在瀏覽頁面時的行為來統計判斷用戶的感受,嘴上往往會說謊,但身體卻很誠實。用戶在看網頁時的交互行為往往是個人感受最客觀的表達。
前端技術實現思路
首先是要分析哪些用戶行為是我們應該且可收集的,比如頁面pv、曝光、點擊等是比較公認的用戶行為,他們之間也應該符合漏斗轉化模型。再有一些公司還會針對鼠標滑過,懸停時間等做一些等交互行為甚至行為細節(比如頁面渲染完成后的曝光時間、划過次數、懸停時長)做一些統計。我們今天就簡單說一下頁面pv、曝光、點擊中的曝光。
頁面pv,往往是通過js發出一個請求,這個請求最簡單的方法就是利用<img>標簽的src屬性發起get請求。
點擊就要分兩種情況:本身有跳轉和本身無跳轉。如果本身有跳轉,一般的方式都是統計鏈接+redirect原目標鏈接的方式。如果是本身無跳轉,就要通過js綁定click事件,在事件回調函數中利用pv的方法發出一個個體請求,所以我們一本會把發pv的方法封裝到公用模塊。
曝光是我想重點說的。曝光,顧名思義就是讓用戶看到才算,換個說法就比較直接了,就是相關的DOM出現在瀏覽器的可視區。大多數的用戶端網頁都是縱向滾動條的形式,判斷是否出現在可視區,也變成了dom在文檔中位置和頁面滾動條位置的比較。
DOM位置 <= (滾動條位置 + 可視區高度)
當然我在這個需求的不斷優化過程中經歷了幾個階段:
階段一:監聽滾動事件和遍歷DOM
這種思路主要來自於懶加載的實現,對頁面中的相關dom添加已定義的屬性,每次出發頁面的滾動事件,就遍歷帶有自定義屬性的DOM,
1 <div id="ad1" ad-pv="曝光請求地址"> 2 .......一大堆 3 </div>
並比對DOM和滾動條的位置,如果DOM位置小於等於滾動條+可視區高度,我們就認為“曝光了”,這是我們就對其利用pv的公共函數發出請求,后台接受請求,並計入統計數據庫。對於發出過曝光請求的DOM,我們應該做出標記,以便下一次出發頁面滾動時過濾掉。比如將ad-pv的屬性值賦值為空(ad-pv="")。下一次不處理為空的DOM。
階段二:減少頻繁的調用
在上面這種方法下,就是監聽滾動條太頻繁,性能損耗太大,而且在異步的回調中涉及好多的DOM狀態修改。一次鼠標滾輪會出發好多次的scroll事件回調。其實我只想要最后一次回調,該怎么做呢? 答案很簡單——“減少函數被調用的次數”。具體“函數節流”或者“防反跳”的實現方法,網上能搜到很多,我在項目中直接使用的underscore中的debounce,至於為什么不用throttle,好多的文章都說“scroll 時更新樣式,如隨動效果用throttle”,但是我不是想在scroll時更新樣式,而是停止滾動時時最終回調一次,debounce更符合我的需求。
我將處理判斷位置並發曝光請求的這種事兒,都用debounce封裝了一層,await 設為500ms。在scroll里面調用debouncePv()
1 $(window).on("scroll",function(){ 2 debouncePv(); 3 });
這樣每一次鼠標滾輪,都會在停止500ms后觸發僅一次的判斷邏輯。
階段三:減少DOM遍歷的注冊機制與引用計數控制狀態
每次處理函數中連理DOM的好處是實時獲取還有那些沒曝光的DOM,但是也帶來了一些問題,比如DOM上存儲曝光請求地址本就顯得不合理;遍歷DOM樹的耗時時相比於滾輪的頻率也不小,如果不加debounce,還真說不好下一次的回調和當前的DOM遍歷那個先結束。我們借鑒目前好多MV*框架中用數據結構來模擬一層DOM的思想。我們用一個對象數組來存儲所有需要曝光的DOM的文檔位置,並且保證按位置從小到大排序。每次觸發,我們的滾動條位置只需要和數組中每一項的文檔位置比較並對“符合位置區間”發出曝光請求,直到“不符合位置”的停止,這樣的好處有三個:
1、遍歷數組比遍歷DOM要快、也不用在DOM上加過多的標記;
2、不用每次所有的DOM比較一遍,可以盡早停止;
3、對於“曝光”過的對象,可以從數組中刪除,下一次處理函數直接從上一次停止的對象(文檔位置對應的數據)開始比較。
我們應該如何生成這個數組呢?我不建議一開始的時候只遍歷一遍DOM,如果我的DOM是js異步加載渲染的或者頁面用了類似bigPipe的技術,就不適合了。我的廣告代碼要使用整個站點所有的頁面,就要支持動態添加。所以我在外層暴露的接口是用於添加曝光對象的。這樣隨時向數組中可以添加。每次添加后對數組按照文檔位置排序一次。保證事件處理的時候用的是一個有序的數組。我管這種動態添加的方式叫注冊機制。
目前我對外暴露錄得接口只有添加接口。但是我畢竟綁定了一個事件,監聽事件會帶來一定的性能損耗。我不能總是在監聽吧。理想的方式是,有曝光對象就監聽,沒有曝光對象就解綁事件。還好我的數組是“注冊”進來的,我可以在注冊時增加引用計數,發曝光請求的時候減少引用計數。這樣我就可以在注冊(0->1)或是每次處理結束的時候判斷是綁或不綁事件。這樣我對外的接口還是只有注冊用的函數。

這里介紹一個小技巧。解綁的時候,萬一頁面本身就對scroll綁定了事件,我這一解綁不就把頁面的事件給解除了嗎?好在jquery提供了一個事件命名空間的概念。我也是只綁定和解除自己空間下的scroll。我綁定的空間名:ads-lazy-pv
1 lazyPvListener: function(){ 2 var self = this; 3 $(window).on("scroll.ads-lazy-pv",function(){ 4 self.debouncePv() 5 }); 6 }, 7 lazyPvOff: function(){ 8 var self = this; 9 $(window).off("scroll.ads-lazy-pv"); 10 }
總結:
這樣一個曝光的邏輯就開發完了,如果僅僅完成“任務”,其實並不難,但優化卻占用了我很大的精力。這個曝光看起來沒用太多高深的技術,但開發起來卻很是用心。尤其是想廣告代碼這種跑在所有頁面上的代碼,更要考慮到很多復雜的情況,稍有紕漏將是公司財產的損失,身為一個開發人員,每一步都要膽大心細。
