1, Basic principles
● 1. Using the uniqueness of xpath, bind the management element and add events to send data management
● 2. The background management system establishes a function of visually selecting management elements and saving the configuration
● 3. The front end obtains the dotting configuration according to the page URL for initialization (binding events through xpath)
The basic process is shown in the figure:
2, Method of sending management data from the front end
The front end has several schemes to send dotting data
1. Traditional ajax request
Using traditional ajax request to send data, the disadvantage is that it is easy to block the request and is not friendly to users
Moreover, it has great disadvantages. When users close the page, the request will be truncated, that is, the sending will be terminated. It is not applicable to record the browsing time
axios.post(url, data); // Take Axios as an example
2. Dynamic picture
We can delay the unloading by creating a picture element in the beforeunload event handler and setting its src attribute to ensure the sending of data. Because most browsers delay the unloading to ensure the loading of pictures, the data can be sent in the unloading event.
const sendLog = (url, data) => { let img = document.createElement('img'); const params = []; Object.keys(data).forEach((key) => { params.push(`${key}=${encodeURIComponent(data[key])}`); }); img.onload = () => img = null; img.src = `${url}?${params.join('&')}`; };
3. sendBeacon
In order to solve the above problems, there is navigator Sendbeacon method, which is used to send requests, can ensure the effective delivery of data without blocking the unloading or loading of pages, and the coding is simpler than the above methods.
export const sendBeacon = (url, analyticsData) => { const apiUrl = config.apiRoot + url let data = getParams(analyticsData) // Compatible with browsers that do not support sendBeacon if (!navigator.sendBeacon) { const client = new XMLHttpRequest() // The third parameter indicates synchronous transmission client.open('POST', apiUrl, false) client.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') client.send(data) return } const formData = new FormData() Object.keys(analyticsData).forEach((key) => { let value = analyticsData[key] if (typeof value !== 'string') { // formData can only append string or Blob value = JSON.stringify(value) } formData.append(key, value) }) navigator.sendBeacon(apiUrl, formData) }
Finally, we use dynamic pictures because Alibaba cloud provides Alibaba cloud - collect logs through WebTracking Deal with a large number of data collection without causing server pressure on the website itself
3, Build SDK
Use webpack to build a project, package the js file package of a single sdk, and introduce the sdk into the front end (this part will not be repeated, and you can search webpack related materials if you are interested) Write a simple JS SDK with webpack
● visual selection of xpath reference plug-ins
Main functions of SDK:
- Expose the initialization method and the dotting method (to support manual dotting)
- Add the function of selecting xpath and expose it to the background management system
- Read the management configuration list according to the link URL
- Initialize the binding event function
- Enter the page and record a dot
- Record browsing duration
- SDK and parent iframe communication function (to transfer data to the background management system)
Example of recording tour duration:
// Statistical duration const viewTime = (data) => { let startTime = new Date().getTime() // Browse start time let endTime = null // Browse end time // Page unload trigger window.addEventListener('unload', () => { endTime = new Date().getTime() let params = { viewTime: (endTime - startTime) / 1000, eventType: 'view', accessId: ACCESS_ID } params = Object.assign(params, data) sendLog(params) }, false) }
// Select xpath to communicate across domains and pages import Postmate from 'postmate' import Inspector from '../plugins/inspect' // Select the xpath node plug-in let childIframe = null const myInspect = new Inspector() const getXpathForm = function (options) { myInspect.setOptions(options, (data) => { let params = { xpath: data, route: window.QWK_ANALYSIS_SDK_OPTIONS?.route || '' } childIframe.emit('send-data-event', params) }) } export default { // Communicate with parent iframe initMessage () { // Enable selected node debugging in development mode if (process.env.BUILD_ENV === 'dev') { document.querySelector('#selected').onclick = () => { myInspect.setOptions({ deactivate: true }, (data) => { console.log(data) }) } } const handshake = new Postmate.Model({ // iframe parent gets xpath getXpath: (options) => { getXpathForm(options) }, // Remove selection deactivate: () => { myInspect.deactivate() } }) // When parent <-> child handshake is complete, events may be emitted to the parent handshake.then((child) => { childIframe = child }) } }
// Export SDK // main.js entry file import { init } from './lib/init' import action from './lib/action' import selectXpath from './lib/select-xpath' // import { documentReady } from './plugins/common' // Initialize cross domain communication of selected xpath selectXpath.initMessage() // documentReady(() => { // //Initialize // // init().then(res => res) // }) // Export SDK module export { init, action }
4, Background management system building visual selection xpath
Step 1: introduce SDK into third-party websites
Write an xpath function in the sdk and expose it to the background management system
● visual selection of xpath reference plug-ins
Step 2: build a management system
Build an iframe to load the website, as shown in the figure:
Here we need to call the method of selecting the website xpath function in the SDK, which must communicate with the website in the loaded iframe
Because it is a third-party website loaded by iframe, there will be cross domain problems, so we need a plug-in to realize this function, post
GitHub link
<template> <div class="iframe-box" ref="content"> </div> </template> <script> import Postmate from 'postmate' export default { name: 'WebIframe', props: { src: { type: String, default: '' }, options: { type: Object, default: () => ({}) } }, data () { return { $child: null } }, mounted () { this.$nextTick(() => this.initMessage()) }, methods: { initMessage () { let handshake = new Postmate({ container: this.$refs.content, url: this.src, name: 'web-iframe-name', classListArray: ['iframe-class-content'], }) handshake.then((child) => { this.$child = child child.on('send-data-event', (data) => { this.$emit('selected', data) }) }) }, // Get select xpath getXpath () { let options = { clipboard: false, // Auto copy deactivate: true, // Destroy after selection ...this.options } try { this.$child.call('getXpath', options) } catch (e) { this.$errMsg('load SDK Failed, please try reloading the website~It is also possible that the current website does not introduce user behavior tracking SDK,Please contact relevant personnel to add') } }, // Remove selected layer remove () { this.$child && this.$child.call('deactivate') } } } </script
Select the node result, and the effect is shown in the figure below:
5, Problems encountered and Solutions
1. Communication problem of loading third-party website by background management system iframe
Because the third-party website is loaded through iframe for visual management, the parent iframe needs to communicate with the third-party website, but there will be cross domain problems. There are many solutions to cross domain problems. Here, the third-party plug-in post based on postmessage is used to solve them
2. Dynamic routing problem
Encounter pages such as article details, because there will be many links to article details https://www.baidu.com/article... Like this, the following detail/id is followed by many IDs. Such a page cannot be configured in every article. In this way, dynamic routing configuration needs to be done. Unified dynamic parameters are identified with other characters to load the configuration.
Scheme:
① Add a dynamic route ID in the configuration and read the database through the back end to match the dynamic route (the back end needs to do a lot of matching)
② Pure front-end operation. The front-end sdk and the background management system transfer dynamic routing to each other
After discussion, we choose the second one. The dynamic route is sent in when the front end initializes the SDK. At this time, when the dynamic route is received in the SDK, the configuration is loaded according to this route, and the SDK is passed to the background to select the xpath configuration for visualization. The configuration should also be saved through this route
At this time, the two sides can correspond one by one. We define such a route https://www.baidu.com/article... {id}, where {id} is a dynamic parameter
3. The dynamic node cannot be bound to the event problem
Because the dom of a dynamic node is not generated when the page is loaded, the initialization debinding event cannot find the DOM node, so the management of the node is invalid. In order to solve this problem, we can find this node through the global click event, use the click of document to find this dynamic node, and then get the target of the current click, which is equal to the node found by xpath, indicating that the currently clicked node is the node to which xpath needs to bind the event. At this time, we can send the corresponding data
let { data } = await getConfig(route) let eventList = data.filter((m) => m.eventType !== 'visible') let viewList = data.filter((m) => m.eventType === 'visible') let dynamicList = [] // Dynamically generated nodes // Click event or other events eventList.forEach((item) => { const node = item.xpath && document.evaluate(item.xpath, document).iterateNext() if (!node) { // The node cannot be found, indicating that it may be a dynamically generated node dynamicList.push(item) return } node.addEventListener(item.eventType || 'click', () => { action.track(item) }) }) // Find dynamically generated nodes by clicking open of document let dynamicClickList = dynamicList.filter((m) => m.eventType === 'click') if (dynamicClickList && dynamicClickList.length) { document.onclick = (event) => { const target = event.target || window.event.target const parentNode = target.parentNode for (let item of dynamicClickList) { // Save the found nodes first item.node = document.evaluate(item.xpath, document).iterateNext() || item.node } let xpathItem = dynamicClickList.find((m) => { return m.xpath && (target === m.node || parentNode === m.node) }) // Find the node and send the dot xpathItem && delete xpathItem.node && action.track(xpathItem) } }
4. Target enters visual area
Usage scenario: some elements of horizontal scrolling switching need to be managed when the target enters the visible area of the user, so there is such a demand
IntersectionObserver reference document link
let observer = null // visible area let isTrackList = [] // Already ordered if ('IntersectionObserver' in window) { // Create a listening node visual area observer = new IntersectionObserver(entries => { const image = entries[0] // Enter visual area if (image.isIntersecting) { // Management configuration of the current visual area let current = viewList.find((m) => m.xpath && image.target === document.evaluate(m.xpath, document).iterateNext()) // Already ordered let trackEd = isTrackList.find((m) => m.id === current.id) // No more points that have been hit if (current && !trackEd) { isTrackList.push(current) action.track(current) } } }) } viewList.forEach((item) => { const node = item.xpath && document.evaluate(item.xpath, document).iterateNext() if (!node) { return } // Listening node observer.observe(node) })