设计和封装一个前端埋点上报脚本, 并逐步思考优化这个过程。
javascript
主要内容:前端
做用:java
final code:analytics.jsgit
咱们先用最直接的方式来实现这个埋点上报脚本。
建立文件并命名为 analytics.js, 在脚本里面添加一个请求,稍微包一下:github
export default function analytics (action = 'pageview') {
var xhr = new XMLHttpRequest()
let uploadUrl = `https://xxx/test_upload?action=${action}×tamp=${Date.now()}`
xhr.open('GET', uploadUrl, true)
xhr.send()
}
复制代码
这样子就能经过调用analytics()
,往咱们的统计服务端提交一条消息,并指明一个行为类型。
若是咱们须要上报的数据确实很少,如只须要‘行为/事件’,‘时间’,‘用户(id)’,‘平台环境’等,而且数据量在浏览器支持的url长度限制内,那咱们能够用简化下这个请求:ajax
// 简洁的方式
export default function analytics (action = 'pageview') {
(new Image()).src = `https://xxx/test_upload?action=${action}×tamp=${Date.now()}`
}
复制代码
用img发送请求的方法英文术语叫:image beacon
主要应用于只须要向服务器发送日志数据的场合,且无需服务器有消息体回应。好比收集访问者的统计信息。
这样作和ajax请求的区别在于:
1.只能是get请求,所以可发送的数据量有限。
2.只关心数据是否发送到服务器,服务器不须要作出消息体响应。而且通常客户端也不须要作出响应。
3.实现了跨域chrome
或者咱们直接用新标准fetch
方式上传api
// 简洁的方式
export default function analytics (action = 'pageview') {
fetch(`https://www.baidu.com?action=${action}×tamp=${Date.now()}`, {method: 'get'})
}
复制代码
考虑到上报数据过程咱们并不关心返回值,只须要知道上报成功与否,咱们能够用Head请求来更高效地实现咱们的上报过程:跨域
// 高效的方式
export default function analytics (action = 'pageview') {
fetch(`https://www.baidu.com?action=${action}×tamp=${Date.now()}`, {method: 'head'})
}
复制代码
head
请求方式和参数传递方式与get
请求一致,也会受限于浏览器,但由于其不须要返回响应实体,其效率要比get方式高得多。单上述示例的简单请求在chrome下表现大概就有20ms的优化。数组
若是要上传的数据确实比较多,拼接参数后的url长度超出了浏览器的限制,致使请求失败。则咱们采起post的方式:
// 通用的方式 (能够采用fetch, 但fetch默认不带cookie, 可能有认证问题)
export default function analytics (action = 'pageview', params) {
let xhr = new XMLHttpRequest()
let data = new FormData()
data.append('action', action)
for (let obj in params) {
data.append(obj, params[obj])
}
xhr.open('POST', 'https://xxx/test_upload')
xhr.send(data)
}
复制代码
不管单个埋点的数据量多少,如今假设页面为了作用户行为分析,有多处埋点,频繁上报可能对用户正常功能的访问有必定影响。
解决这个问题最直接思路就是减小上报的请求数。所以咱们来实现一个批量上传的feature,一个简单的思路是每收集完10条数据就打包上报:
// 每10条数据数据进行打包
let logs = []
/** * @params {array} 日志数组 */
function upload (logs) {
console.log('send logs', logs)
let xhr = new XMLHttpRequest()
let data = new FormData()
data.append('logs', logs)
xhr.open('POST', this.url)
xhr.send(data)
}
export default function analytics (action = 'pageview', params) {
logs.push(Object.assign({
action,
timeStamp: Date.now()
}, params))
if (logs.length >= 10) {
upload(logs)
logs = []
}
}
复制代码
在埋点的位置,咱们先执行个几十次看看
import analy from '@/vendor/analytics1.js'
for (let i = 33; i--;) {
analy1('pv')
}
复制代码
ok, 正常的话应该上报成功了,而且每条请求都包含了10个数据。
但问题很快也暴露了,这种凑够N条数据再统一发送的行为会出现断层,若是在没有凑够N条数据的时候用户就关掉页面,或者是超过N倍数但凑不到N的那部分,若是不处理的话这部分数据就丢失了。
一种直接的解决方案是监听页面beforeunload
事件,在页面离开前把剩余不足N条的log所有上传。所以,咱们添加一个beforeunload事件,顺便整理下代码,将其封装成一个类:
export default class eventTrack {
constructor (option) {
this.option = Object.assign({
url: 'https://www.baidu.com',
maxLogNum: 10
}, option)
this.url = this.option.url
this.maxLogNum = this.option.maxLogNum
this.logs = []
// 监听unload事件,
window.addEventListener('beforeunload', this.uploadLog.bind(this), false)
}
/** * 收集日志,集满 maxLogNum 后上传 * @param {string} 埋点行为 * @param {object} 埋点附带数据 */
analytics (action = 'pageview', params) {
this.logs.push(Object.assign({
action,
timeStamp: Date.now()
}, params))
if (this.logs.length >= this.maxLogNum) {
this.send(this.logs)
this.logs = []
}
}
// 上报一个日志数组
send (logs, sync) {
let xhr = new XMLHttpRequest()
let data = new FormData()
for (var i = logs.length; i--;) {
data.append('logs', JSON.stringify(logs[i]))
}
xhr.open('POST', this.url, !sync)
xhr.send(data)
}
// 使用同步的xhr请求
uploadLog () {
this.send(this.logs, true)
}
}
复制代码
目前为止咱们初步实现了功能,在进一步新增feature前,先继续优化下当前代码,结合前面的过程,咱们能够考虑优化这几点:
analytics.head
(单条上报), analytics.post
(默认)关于sendBeacon
, 该方法能够将少许数据异步传输到Web服务器。在上述代码的uploadLog
方法中,咱们使用了同步的xhr请求,这样作是为了防止页面因关闭或者切换,脚原本不及执行致使最后的日志没法上报。
beforeunload的场景下,同步xhr
和sendBeacon
的特色
值得一提的是,单页应用中,路由的切换并不会对漏报形成太大影响,只要确保上报脚本是挂载到全局,并处理好页面关闭和跳转到其余域名的状况就好。
总之,根据这两点优化,咱们在增长新功能前再完善下代码:
export default class eventTrack {
constructor (option) {
this.option = Object.assign({
url: 'https://www.baidu.com',
maxLogNum: 10
}, option)
this.url = this.option.url
this.maxLogNum = this.option.maxLogNum
this.logs = []
// 拓展analytics,容许单个上报
this.analytics['head'] = (action, params) => {
return this.sendByHead(action, params)
}
this.analytics['post'] = (action, params) => {
return this.sendByPost(action, params)
}
// 监听unload事件,
window.addEventListener('beforeunload', this.unloadHandler.bind(this), false)
}
/** * 收集日志,集满 maxLogNum 后上传 * @param {string} 埋点行为 * @param {object} 埋点附带数据 */
analytics (action = 'pageview', params) {
this.logs.push(JSON.stringify(Object.assign({
action,
timeStamp: Date.now()
}, params)))
if (this.logs.length >= this.maxLogNum) {
this.sendInPack(this.logs)
this.logs = []
}
}
/** * 批量上报一个日志数组 * @param {array} logs 日志数组 * @param {boolean} sync 是否同步 */
sendInPack (logs, sync) {
let xhr = new XMLHttpRequest()
let data = new FormData()
for (var i = logs.length; i--;) {
data.append('logs', logs[i])
}
xhr.open('POST', this.url, !sync)
xhr.send(data)
}
/** * POST上报单个日志 * @param {string} 埋点类型事件 * @param {object} 埋点附加参数 */
sendByPost (action, params) {
let xhr = new XMLHttpRequest()
let data = new FormData()
data.append('action', action)
for (let obj in params) {
data.append(obj, params[obj])
}
xhr.open('POST', this.url)
xhr.send(data)
}
/** * Head上报单个日志 * @param {string} 埋点类型事件 * @param {object} 埋点附加参数 */
sendByHead (action, params) {
let str = ''
for (let key in params) {
str += `&${key}=${params[key]}`
}
fetch(`https://www.baidu.com?action=${action}×tamp=${Date.now()}${str}`, {method: 'head'})
}
/** * unload事件触发时,执行的上报事件 */
unloadHandler () {
if (navigator.sendBeacon) {
let data = new FormData()
for (var i = this.logs.length; i--;) {
data.append('logs', this.logs[i])
}
navigator.sendBeacon(this.url, data)
} else {
this.sendInPack(this.logs, true)
}
}
}
复制代码
思考一个问题,假如咱们的页面处于断网离线状态(好比就是信号很差),用户在这期间进行了操做,而咱们又想收集这部分数据会怎样?
咱们能够尝试增长“失败重传”的功能,比起网络不稳定,更多的状况是某个问题致使的稳定错误,重传不能解决这类问题。设想咱们在客户端进行数据收集,咱们能够很方便地记录到log文件中,因而一样的考虑,咱们也能够把数据暂存到localstorage上面,有网环境下再继续上报,所以解决这个问题的方案咱们能够概括为:
navigator.onLine
判断网络情况localstorage
, 延时上报咱们修改下sendInPack
, 并增长对应方法
sendInPack (logs, sync) {
if (navigator.onLine) {
this.sendMultiData(logs, sync)
this.sendStorageData()
} else {
this.storageData(logs)
}
}
sendMultiData (logs, sync) {
console.log('sendMultiData', logs)
let xhr = new XMLHttpRequest()
let data = new FormData()
for (var i = logs.length; i--;) {
data.append('logs', logs[i])
}
xhr.open('POST', this.url, !sync)
xhr.send(data)
}
storageData (logs) {
console.log('storageData', logs)
let data = JSON.stringify(logs)
let before = localStorage['analytics_logs']
if (before) {
data = before.replace(']', ',') + data.replace('[', '')
}
localStorage.setItem('analytics_logs', data)
}
sendStorageData () {
let data = localStorage['analytics_logs']
if (!data) return
data = JSON.parse(data)
this.sendMultiData(data)
localStorage['analytics_logs'] = ''
}
复制代码
注意
navigator.onLine
在不一样浏览器开发环境下的问题,好比chrome下localhost访问时候,navigator.onLine值总为false, 改用127.0.0.1则正常返回值
PV是日志上报中很重要的一环。
目前为止咱们基本实现完上报了,如今再回归到业务层面。pv的目的是什么,以及怎样更好得达到咱们的目的? 推荐先阅读这篇关于pv的文章:
为何说你的pv统计是错的
在大多数状况下,咱们的pv上报假设每次页面浏览(Page View)对应一次页面加载(Page Load),且每次页面加载完成后都会运行一些统计代码, 然而这状况对于尤为单页应用存在一些问题
为了遵循更好的PV,咱们能够在脚本增长下列状况的处理:
this.option = Object.assign({
url: 'https://baidu.com/api/test',
maxLogNum: 10,
stayTime: 2000, // ms, 页面由隐藏变为可见,而且自上次用户交互以后足够久,能够视为新pv的时间间隔
timeout: 6000 // 页面切换间隔,小于多少ms不算间隔
}, option)
this.hiddenTime = Date.now()
···
// 监听页面可见性
document.addEventListener('visibilitychange', () => {
console.log(document.visibilityState, Date.now(), this.hiddenTime)
if (document.visibilityState === 'visible' && (Date.now() - this.hiddenTime > this.option.stayTime)) {
this.analytics('re-open')
console.log('send pv visible')
} else if (document.visibilityState === 'hidden') {
this.hiddenTime = Date.now()
}
})
···
复制代码
考虑咱们是一个hash模式的单页应用,即路由跳转以 ‘#’加路由结尾标识。 若是咱们想对每一个路由切换进行追踪,一种作法是在每一个路由组件的进行监听,也能够在上报文件中直接统一处理:
window.addEventListener('hashchange', () => {
this.analytics()
})
复制代码
但这样子有个问题,如何判别当前hash跳转是个有效跳转。好比页面存在重定向逻辑,用户从A页面进入(弃用页面),咱们代码把它跳转到B页面,这样pv发出去了两次,而实际有效的浏览只是B页面一次。又或者用户只是匆匆看了A页面一眼,又跳转到B页面,A页面要不要做为一次有效PV?
一种更好的方式是设置有效间隔,好比小于5s的浏览不做为一个有效pv,那由此而生的逻辑,咱们须要调整咱们的 analytics
方法:
// 封装一个sendPV 专门用来发送pv
constructor (option) {
···
this.sendPV = this.delay((args) => {
this.analytics({action: 'pageview', ...args})
})
window.addEventListener('hashchange', () => {
this.sendPV()
})
this.sendPV()
···
}
delay (func, time) {
let t = 0
let self = this
return function (...args) {
clearTimeout(t)
t = setTimeout(func.bind(this, args), time || self.option.timeout)
}
}
复制代码
ok, 到这里就差很少了,完整示意在这里 analytics.js,加了点调用测试 考虑到不一样业务场景,咱们还有有更多空间能够填补,数据闭环其实也是为了更好的业务分析服务,虽然是一个传统功能,但值得细细考究的点仍是挺多的吧