I. Background introduction

In e-commerce products (you can open your Taobao, Tmall and JINGdong apps), users’ behaviors and interaction habits can be reverse-introduced through data burying on the exposure of goods, so as to optimize recommendation and search algorithms and interaction, and of course, the ultimate goal is to increase users’ purchasing power.

Exposure: When the product appears in front of the user, in the browser window, it is called exposure

The two most straightforward ways

  • 1, the scroll event monitoring, through [Element getBoundingClientRect () – Web API interface reference | MDN] calculated a certain commodity and the position of the window
  • 2, the page runs a timer, timing calculation of a commodity and window position

Either approach works, but the getBoundingClientRect API causes page backflow and can cause performance problems if used improperly.

Based on this, the browser specially built for us a [Intersection computes the Observer API – Web API | MDN], get rid of performance related details, let developers only care about the business logic




Intersection Observer API

[Intersection computes the dot exposure scheme based on Observer API – Web API interface reference | MDN]

This API are relatively new, as for the compatibility problems [the w3c IntersectionObserver/polyfill] solution are available, and in essence is calculated with getBoundingClientRect location, specific how to implement can go to see the source code


Third, the idea of burying data

  1. new IntersectionObserver() Instantiate a global_observer, each commodity DOM adds itself to_observer(this will be implemented using Vue instructions)
  2. When an item DOM enters the window, gather information about the item and store it in a global arraydotArr, and then unobserve the DOM of the commodity
  3. fromdotArrIt is relatively simple to take data in
  • Run timer, check every N seconds, if dotArr has data, report directly;
  • If in N seconds,dotArrIs greater than a certain amount of datamaxNumDon’t wait for the timer
  • Dotting is not difficult, difficult is not to leak and do not repeat the report of data, the user leaves the page in front of the boundary data processing
      • Browser environment:dotArrSave a copy of it at the same timelocalStorageIf the user is really in the interval of N seconds, and the data is not enough to report the maximum amountmaxNumLeave the page, then this batch of data will wait for the user next time to enter the page, directly fromlocalStorageOf course, if the user never visits the page again or clears the browser cache, a little data loss is acceptable.
      • Client webview environment: register webView closed life hook events (need client support), all before leaving


    Four, code implementation

    1. Encapsulate Exposure class

    // polyfill
    import 'intersection-observer';
    // Self-encapsulation data reporting method, actually is the network request
    import { DotData } from './DotData'
    
    // The throttling time can be set to a larger value. The default is 100ms
    IntersectionObserver.prototype['THROTTLE_TIMEOUT'] = 300;
    
    export default class Exposure {
      dotDataArr: Array<string>;
      maxNum: number;
      // _observer is a collection of observers
      _observer;
      _timer: number;
    
      constructor(maxNum = 20) {
        // The data that is currently collected and not reported is the data of the DOM node that has entered the window
        this.dotDataArr = [];
        this.maxNum = maxNum;
        this._timer = 0;
        // The Exposure class is instantiated globally only once, and the init method is executed only once
        this.init();
      }
    
      init() {
        const self = this;
        // Init will only be executed once, so leave the boundary handlers here
        // Type up the remaining data in localStorage
        this.dotFromLocalStorage();
        // Register the client webView's close life hook event
        this.beforeLeaveWebview();
    
        this._observer = new IntersectionObserver(function (entries, observer) {
          entries.forEach(entry= > {
            // This logic is triggered every time an item enters the window
            if (entry.isIntersecting) {
              // Clear the current timer
              clearTimeout(self._timer);
              
          
    const ctm = entry.target.attributes['data-dot'].value; // Add the collected data to the data array to be reported self.dotDataArr.push(ctm); // Unobserve the DOM of the item after collecting data for the item self._observer.unobserve(entry.target); // If the number of points exceeds a certain threshold, the number of points will be deleted if (self.dotDataArr.length >= self.maxNum) { self.dot(); } else { self.storeIntoLocalstorage(self.dotDataArr); if (self.dotDataArr.length > 0) { // As long as there is a new CTM coming in then if there is no automatic 2 seconds later self._timer = window.setTimeout(function () { self.dot(); }, 2000)}}}})}, {root: null.rootMargin: "0px".threshold: 0.5 // This threshold can be a little lower }); } // Each commodity executes the add method through a globally unique instance of Exposure, adding itself to the observer add(entry) { this._observer && this._observer.observe(entry.el) } dot() { // Delete this batch of CTMS const dotDataArr = this.dotDataArr.splice(0.this.maxNum); DotData(dotDataArr); // Update localStorage as well this.storeIntoLocalstorage(this.dotDataArr); } storeIntoLocalstorage(dotDataArr) { / /... Put it in localStorage, what is the format of the string you want to define } dotFromLocalStorage() { const ctmsStr = window.localStorage.getItem('dotDataArr'); if (ctmsStr) { / /... If you have data, report it } } beforeLeaveWebview() { let win: any = window; // Specify the hook implementation with the client injectEvent("webviewWillDisappear", () = > {if (this.dotDataArr.length > 0) { DotData(this.dotDataArr); }}}})Copy the code

    2. Vue instantiation + encapsulation instruction

    [Custom directive — vue.js]

    // entry JS file main.js
    // Introduce Exposure class
    Exp is the globally unique instance
    const exp = new Exposure();
    
    // VUE encapsulates an instruction, and every item that uses this instruction automatically adds itself to the observer
    Vue.directive('exp-dot', {
        bind(el, binding, vnode) {
            exp.add({el: el, val: binding.value})
        }
    })Copy the code

    3. Use of commodities

    Use instructions for each item as you loop

    :data-dot=”item.dotData” is the data we want to collect

    <div 
        v-exp-dot 
        v-for="item in list" 
        :key="item.id" 
        class="" 
        :data-dot="item.dotData"
    >
      // ... 
    </div>Copy the code

    In the initial scenario, we intended to manually trigger a batch of observations at a time, such as a pull-up load generating a batch of new items, a manual interactive load of new items, and so on, directly manipulating the Observer instance to observe the new batch of items

    Later, I found that the coupling with business logic is too heavy, and all the goods on a page are not necessarily obediently rendered by an array cycle in order, there may also be a variety of resource bits, there are a variety of reasons, it is better to directly let each commodity triggered by itself to be observed


    Five or more

    Thank you for your patience to see here, hope to gain!

    I like to keep records during the learning process and share my accumulation and thinking on the road of front-end. I hope to communicate and make progress with you. For more articles, please refer to [amandakelake’s Github blog].


    reference

    [exposure statistical test based on IntersectionObserver | xgfe]

    [Beforeunload Drop Cause Analysis and Solutions]