前端SDK
系列文章:
本文是前端监控SDK
系列的第三篇,主要介绍用户访问页面时,最常见的几个指标:
- pv
- uv
- 曝光
PV
pv
上报通常有以下几种场景:
- 刷新页面
- 从
A
页面跳转到B
页面 - 页面前进、后退:如点击浏览器的前进、后退按钮,或者调用
history.go(-1), history.go(1)
非 spa
应用在跳转后都会刷新页面,监听 window
的 load
事件即可
window.addEventListener('load',() => {
const href = window.location.href
report({
eventType: 'pv',
action: 'reload',
from: href,
to: href
})
})
在 spa
应用中,通过以下俩种方法来触发跳转:
hash
模式:更改页面hash
值history
模式:调用pushState
和replaceState
更改页面url
但是,这俩种方法都不会刷新页面
对于 hash
模式,可以通过监听 hashchange
事件来进行上报:
listen(window, 'hashchange', (ev: HashChangeEvent) => {
// 在 hashchange之前触发过 popstate 事件,此时在 hashchange 事件中不用再次上报
if (popstateFired) {
popstateFired = false
} else {
const { oldURL, newURL } = ev
report({
eventType: 'pv',
action: 'navigation',
from: oldURL,
to: newURL
})
from = newURL
}
})
对于 history
模式,可以重写 pushState
和 replaceState
函数,在重写函数中上报页面信息:
let from = getLocationHref()
let popstateFired = false
const historyReplacer = (origin: Function) => {
return function (this: Window['history'], state: object, unused: string, url?: string) {
popstateFired = false
const result = origin.call(this, state, unused, url)
const to = getLocationHref()
report({
eventType: 'pv',
action: 'navigation',
from,
to
})
from = to
return result
}
}
replaceAop(window.history, 'pushState', historyReplacer)
replaceAop(window.history, 'replaceState', historyReplacer)
history
模式在前进、后退时会触发 popstate
事件,所以在 popstate
事件中上报前进、后退引发的跳转:
listen(window, 'popstate', () => {
popstateFired = true
const to = getLocationHref()
report({
eventType: 'pv',
action: 'navigation',
from,
to
})
from = to
})
需要注意的是, popstate
不仅会在 history
模式的前进、后退情况下触发,在 hash
值更改时也会触发,并且**会在 hashchange
前触发,**这样便会在 hash
模式下上报俩次。
为了避免上述的这种情况,我设置了 popstateFired
的变量来判断 popstate
事件是否已经被触发,如果是的话就不会在 hashchange
中再次上报了:
相关的文档可以看这里:
UV
uv
的统计由服务端进行计算,通过上报 pv
的 visitorId
来得到 uv
的数据
前端可以通过浏览器指纹库github.com/jackspirou/… 来生成 visitorId
传给服务端,用来区分不同用户。如果用户上传了 userId
,则使用 userId
来区分不同用户
元素曝光
元素的曝光主要有以下几种场景:
- 元素显示、隐藏
- 新增、删除元素
- 元素从屏幕外进入屏幕内
浏览器提供了 Intersection Observer API
来检测元素是否在屏幕可见,详细的介绍可以阅读下面的文章:
我也写了一个简单的 demo
,可以结合文章一起阅读,加深理解:stackblitz.com/edit/vitejs…
当监听的元素在屏幕中的可见性发生变化时,会触发传递给 Intersection Observer API
的回调函数。回调函数接收 IntersectionObserverEntry
列表作为参数,IntersectionObserverEntry
中的 intersectionRatio
属性,表示元素和屏幕的交叉比例,取值范围为 [0, 1]
。
当 intersectionRatio > 0
时,便认为元素出现在了屏幕中。当 intersectionRatio === 0
时, 表示元素在屏幕中消失:
export const createIntersection = (
el: Element, params?: Record<string, any>, options?: IntersectionObserverInit) => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// 元素出现在屏幕中,上报曝光信息
if (entry.intersectionRatio > 0) {
const target = getTarget(entry as any)
const reportParams: Omit<EventInfo, 'triggerTime'> = { eventType: 'expose', target }
if (params) {
reportParams.params = params
}
report(reportParams)
} else {
// 元素在屏幕中消失
}
})
}, options)
observer.observe(el)
return observer
}
在原生 js
中,需要手动为需要曝光的元素调用 createIntersection
。在动态新增曝光元素时,也要为新增的曝光元素调用 createIntersection
const box = document.querySelector('.box')!
const expose = document.querySelector('.expose')!
const cancelExpose = document.querySelector('.cancel-expose')!
let boxIntersectionObserver: IntersectionObserver | null = null
expose.addEventListener('click', () => {
if (boxIntersectionObserver) {
boxIntersectionObserver.unobserve(box)
}
boxIntersectionObserver = createIntersection(box)
})
cancelExpose.addEventListener('click', () => {
boxIntersectionObserver?.unobserve(box)
})
如果使用的是 react
框架,可以封装 IntersectionContainer
组件来曝光想要曝光的元素:
import React, { useEffect, useRef } from 'react'
import { createIntersection } from '@monitor-fe/core'
interface IntersectionContainerProps {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
exposeParams?: Record<string, any>
}
const IntersectionContainer = (props: IntersectionContainerProps) => {
const { children, exposeParams, ...rest } = props
const containerRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!containerRef.current) { return }
const observer = createIntersection(containerRef.current, exposeParams)
return () => {
if (containerRef.current) {
observer.unobserve(containerRef.current)
}
}
}, [])
return (
<div ref={containerRef} {...rest}>
{children}
</div>
)
}
export default IntersectionContainer
import { IntersectionContainer } from '@monitor-fe/react'
const ExposeDemo = () => {
return (
<div>
<div style={{ height: 2000 }}>
Placeholder
</div>
<IntersectionContainer
style={{
height: 100,
width: 100,
backgroundColor: 'pink',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
box
</IntersectionContainer>
</div>
)
}
export default ExposeDemo
在创建 intersectionObserver
时,支持传入 threshold
参数,当监听元素与屏幕视口的交叉比例达到 threshold
设置的比例时,触发监听函数。通过 threshold
参数,我们便可以根据需求来自己定义监听元素与屏幕的交叉比例来判断元素是否满足曝光条件。
如设置 shreshold: 0.5
,表示监听元素有超过一半出现在屏幕中时,才会认为其满足曝光条件
小结
要实现 pv
上报,要了解浏览器页面导航的相关事件:
load
hashchange
popstate
pushState
,replaceState
页面的访问 uv
要结合 userId
以及浏览器指纹来处理
对于元素曝光,浏览器提供了 Intersection Observer API
可以很方便的判断元素是否在屏幕中,并且提供了 threshold
属性控制回调函数触发时,元素与屏幕的交叉比例