1. Basic Principles
● 1. Use the uniqueness of xpath to bind the dot element to add events to send data dots
● 2. The background management system builds a function to visually select dotted elements and save the configuration
● 3. The front end obtains the dot configuration according to the page URL for initialization (bind events through xpath)
The basic process is shown in the figure:
2. The way of sending dot data from the front end
There are several schemes for the front end to send dotted data
1. Traditional ajax request
Using traditional ajax requests to send data, the disadvantage is that it is easy to block requests and is not user-friendly
And the disadvantage is very big. When the user closes the page, the request will be cut off, 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 pictures
We can delay unloading to ensure data sending by creating an image element in the beforeunload event handler and setting its src attribute, because most browsers delay unloading to ensure image loading, so data can be in Sent in the uninstall 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 problem, there is the navigator.sendBeacon method. Using this method to send a request can ensure that the data is effectively delivered without blocking the unloading or loading of the page, and the coding is simpler than the above method.
export const sendBeacon = (url, analyticsData) => {
const apiUrl = config.apiRoot + url
let data = getParams(analyticsData)
// 兼容不支持sendBeacon的浏览器
if (!navigator.sendBeacon) {
const client = new XMLHttpRequest()
// 第三个参数表示同步发送
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只能append string 或 Blob
value = JSON.stringify(value)
}
formData.append(key, value)
})
navigator.sendBeacon(apiUrl, formData)
}
Finally, we used the dynamic image method, because the provided by Alibaba Cloud Alibaba Cloud-collection-collecting logs through WebTracking with a large amount of data collection without causing pressure on the website itself.
3. Build SDK
Use webpack to build a project, package a single sdk js file package, and import the sdk in the front end (this part will not be repeated, you can search for webpack related information if you are interested) Write a simple JS SDK with
● Visual selection xpath-reference plugin
The main functions of the SDK:
- Expose the initialization method, as well as the management method (in order to support manual management)
- Add the function of selecting xpath and expose it to the background management system
- Read the dot configuration list according to the link URL
- Initialize the binding event function
- Enter the page and record a click
- Record browsing time
- SDK and parent iframe communication function (in order to pass data to the background management system)
Example of recording tour duration:
// 统计时长
const viewTime = (data) => {
let startTime = new Date().getTime() // 浏览开始时间
let endTime = null // 浏览结束时间
// 页面卸载触发
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)
}
// 选取xpath跨域跨页面通信
import Postmate from 'postmate'
import Inspector from '../plugins/inspect' // 选取xpath节点插件
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 {
// 和父级iframe通信
initMessage () {
// 开发模式下启用选节点调试
if (process.env.BUILD_ENV === 'dev') {
document.querySelector('#selected').onclick = () => {
myInspect.setOptions({
deactivate: true
}, (data) => {
console.log(data)
})
}
}
const handshake = new Postmate.Model({
// iframe父级获取xpath
getXpath: (options) => {
getXpathForm(options)
},
// 移除选取
deactivate: () => {
myInspect.deactivate()
}
})
// When parent <-> child handshake is complete, events may be emitted to the parent
handshake.then((child) => {
childIframe = child
})
}
}
// 导出SDK
// main.js入口文件
import { init } from './lib/init'
import action from './lib/action'
import selectXpath from './lib/select-xpath'
// import { documentReady } from './plugins/common'
// 初始化选取xpath的跨域通信
selectXpath.initMessage()
// documentReady(() => {
// // 初始化
// // init().then(res => res)
// })
// 导出SDK模块
export {
init,
action
}
Fourth, the background management system to build a visual selection xpath
Step 1: Introduce SDK to third-party website
Write a selection xpath function in the sdk and expose it to the background management system call
● Visual selection xpath-reference plugin
Step 2: Build the management system
Build an iframe that loads the website, as shown:
We need to call the method of selecting the website xpath function in the SDK here, 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 plugin to achieve this function postmate
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)
})
})
},
// 获取选取xpath
getXpath () {
let options = {
clipboard: false, // 是否自动复制
deactivate: true, // 选择之后销毁
...this.options
}
try {
this.$child.call('getXpath', options)
} catch (e) {
this.$errMsg('加载SDK失败,请重新加载网站试试~也可能当前网站未引入用户行为轨迹跟踪SDK,请联系相关人员添加')
}
},
// 移除选取弹层
remove () {
this.$child && this.$child.call('deactivate')
}
}
}
</script
Select the node result, the effect is as follows:
5. Problems encountered and solutions
1. The background management system iframe loads the third-party website communication problem
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, a third-party based on postmessage is used. Plugin postmate to solve
2. Dynamic routing problem
When encountering pages such as article details, because the article details will have many links https://www.baidu.com/article/detail/24343 like this, the detail/id behind is followed by many ids, such pages It is impossible to configure every article, so you need to do dynamic routing configuration unified dynamic parameters and use other character identifiers to load the configuration.
plan:
1. Add a dynamic routing identifier to the configuration, and read the database through the backend to match dynamic routing (requires the backend to do a lot of matching)
②, pure front-end operation, the front-end sdk and the back-end management system transmit dynamic routes to each other
After discussion, we chose the second one. The dynamic route is passed in when the front-end initializes the sdk. At this time, the incoming dynamic route is received in the sdk, and the configuration is loaded according to this route, and the SDK passes it to the background to visually select the xpath configuration. , but also through this route to save the configuration
At this time, the two sides can be matched one by one. We define such a route https://www.baidu.com/article/detail/ {id}, where {id} is a dynamic parameter
3. Dynamic nodes cannot be bound to events
Since the DOM is not generated when the dynamic node is loaded, the DOM node cannot be found when the event is initialized to bind, so the management of the node is invalid. In order to solve this problem, we can use the global click event to find this node, use the click of the document to find this dynamic node, and then get the currently clicked target to be equal to the node found by xpath, indicating that the currently clicked node is the xpath that needs to be bound The node of the event, you can send the corresponding data at this time
let { data } = await getConfig(route)
let eventList = data.filter((m) => m.eventType !== 'visible')
let viewList = data.filter((m) => m.eventType === 'visible')
let dynamicList = [] // 动态生成的节点
// 点击事件或者其他事件
eventList.forEach((item) => {
const node = item.xpath && document.evaluate(item.xpath, document).iterateNext()
if (!node) {
// 找不到节点,说明有可能是动态生成的节点
dynamicList.push(item)
return
}
node.addEventListener(item.eventType || 'click', () => {
action.track(item)
})
})
// 通过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) {
// 先把查找到的节点给存下来
item.node = document.evaluate(item.xpath, document).iterateNext() || item.node
}
let xpathItem = dynamicClickList.find((m) => {
return m.xpath && (target === m.node || parentNode === m.node)
})
// 查找到节点,发送打点
xpathItem && delete xpathItem.node && action.track(xpathItem)
}
}
4. The target enters the visible area
Usage scenario: Some horizontal scrolling switching elements need to be clicked when the target enters the user-visible area, so there is such a demand
IntersectionObserver reference documentation link
let observer = null // 可视区域
let isTrackList = [] // 已经打点过的
if ('IntersectionObserver' in window) {
// 创建一个监听节点可视区域
observer = new IntersectionObserver(entries => {
const image = entries[0]
// 进入可视区域
if (image.isIntersecting) {
// 当前可视区域的打点配置
let current = viewList.find((m) => m.xpath && image.target === document.evaluate(m.xpath, document).iterateNext())
// 已经打点过的
let trackEd = isTrackList.find((m) => m.id === current.id)
// 已经打过的点不再打
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
}
// 监听节点
observer.observe(node)
})
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。