埋点

就是数据采集、数据处理、数据分析和挖掘,如用户停留时间,用户点击情况等

库设计

webpack臃肿,可读性差,但适合开发一些项目

rollup打包干净,所以非常适合开发SDK和一些框架

此处就使用rollup作为打包工具

目录结构

依赖

1
2
3
4
npm install rollup -D
npm install rollup-plugin-dts -D
npm install rollup-plugin-typescript2 -D
npm install typescript -D

打包工具配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import ts from "rollup-plugin-typescript2"
import dts from 'rollup-plugin-dts' // 生成声明文件

import path from "path"

export default [
// 输出主要代码
{
input: "./src/core/index.ts",
// es: import export
// cjs: require exports
// umd: AMD CMD global
output: [
{
file: path.resolve(__dirname, "./dist/index.esm.js"),
format: "es"
},
{
file: path.resolve(__dirname, "./dist/index.cjs.js"),
format: "cjs"
},
{
file: path.resolve(__dirname, "./dist/index.js"),
format: "umd",
name: "tracker"
},
],
plugins: [
ts(),
]

},
// 输出声明文件
{
input: "./src/core/index.ts",
output: {
file: path.resolve(__dirname, "./dist/index.d.ts"),
format: "es"
},
plugins: [
dts(),
]
}
]

类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 配置项的类型
* @requestUrl 接口地址
* @historyTracker history上报
* @hashTracker hash上报
* @domTracker 携带Tracker-key 点击事件上报
* @sdkVersionsdk版本
* @extra透传字段
* @jsError js和promise报错异常上报
*/
export interface DefaultOptons {
uuid: string | undefined,
requestUrl: string | undefined,
historyTracker: boolean,
hashTracker: boolean,
domTracker: boolean,
sdkVersion: string | number,
extra: Record<string, any> | undefined,
jsError:boolean
}

//requestUrl必传,其他配置项可不传
export interface Options extends Partial<DefaultOptons> {
requestUrl: string,
}

//版本
export enum TrackerConfig {
version = '1.0.0'
}
//上报必传参数
export type reportTrackerData = {
[key: string]: any,
event: string,
targetKey: string
}

核心功能

页面访问量

即PageView,简称PV

用户每次对网站的访问均会被记录,记录时主要监听了history和hash

hash可以使用使用hashchange监听

history只能通过popstate监听,无法通过pushState、replaceState这些API监听,只能重写其函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// T:History对象的所有键名
export const createHistoryEvent = <T extends keyof History>(type: T)=> {
// origin:History对象中对于传入键名的值
const origin = history[type]

return function(this:any) {
// 调用原版函数
const res = origin.apply(this,arguments)

// 创建自定义事件(发布订阅模式)
const e = new Event(type)

// dispatchEvent:派发事件
window.dispatchEvent(e)

return res
}
}

// addEventListener:监听事件
// window.addEventListener("pushState",console.log)
// removeEventListener:删除事件

监听:

1
2
3
4
5
6
7
8
9
10
11
// 事件捕获器
// mouseEventList:事件列表 target:后台定义的枚举值
private captureEvents <T>(mouseEventList:string[],targetKey:string,data?:T) {
mouseEventList.forEach(event=> {
window.addEventListener(event,()=> {
console.log("监听到了")
// 捕获到时上报
this.reportTracker({event,targetKey,data})
})
})
}

独立访客

即Unique Visitor,简称UV,访问网站的一台电脑客户端为一个访客

需要生成用户唯一表示,这个是后台的工作,前台只需要获取并使用接口返回的id即可

1
2
3
4
5
6
7
8
9
// 设置用户id
public setUserId <T extends DefaultOptons["uuid"]>(uuid:T) {
this.data.uuid = uuid
}

// 设置用户自定义参数
public setExtra <T extends DefaultOptons["extra"]>(extra:T) {
this.data.extra = extra
}

也可以使用canvas指纹追踪技术

使用navigator.sendBeacon上报访问记录,即使页面关闭了,也会完成请求,而XMLHTTPRequest不一定

1
2
3
4
5
6
7
8
9
10
11
12
// 上报
private reportTracker <T>(data:T) {
const params = Object.assign(this.data,data,{time: new Date().getTime()})

let headers = {
type: "application/x-www-form-unlencoded"
}
let blob = new Blob([JSON.stringify(params)],headers)

// 不支持json,所以使用blob,请求类型是ping
navigator.sendBeacon(this.data.requestUrl,blob)
}

DOM事件监听

给需要监听的元素添加一个属性,用来区分是否需要监听

1
2
3
4
5
6
7
8
9
10
11
12
13
// DOM事件监听
private targetKeyReport() {
MouseEventList.forEach(ev => {
window.addEventListener(ev,(e)=>{
const target = e.target as HTMLElement
// 如果元素上有这个属性,说明此次操作需要被上报
const targetKey = target.getAttribute("target-key")
if(targetKey) {
this.reportTracker({event:ev,targetKey})
}
})
})
}

错误上报

需要处理error事件和promise的unhandledrejection报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private jsError() {
this.errorEvent()
this.promiseReject()
}

// error事件错误上报
private errorEvent() {
window.addEventListener("error",(event)=> {
this.reportTracker({
event: "error",
targetKey: "message",
message: event.message
})
})
}

// promise错误上报
private promiseReject() {
window.addEventListener("unhandledrejection",(event)=> {
event.promise.catch(error=> {
this.reportTracker({
event: "promise",
targetKey: "message",
message: error
})
})
})
}

全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { DefaultOptons, TrackerConfig, Options } from "../types/index"
import { createHistoryEvent } from "../utils/pv"
const MouseEventList: string[] = ['click', 'dblclick', 'contextmenu', 'mousedown', 'mouseup', 'mouseenter', 'mouseout', 'mouseover']

export default class Tracker {
public data:Options
// options:配置项
constructor(options:Options) {
// 用户传入的options中的项会覆盖默认同名项
this.data = Object.assign(this.initDef(),options)
this.installTracker()
}

// 用户不传配置项时的兜底逻辑(设置默认项)
private initDef():DefaultOptons {
// 用重写的history方法替换原生
window.history["pushState"] = createHistoryEvent("pushState")
window.history["replaceState"] = createHistoryEvent("replaceState")

return<DefaultOptons> {
sdkVersion: TrackerConfig.version,
historyTracker: false,
hashTracker: false,
domTracker: false,
jsError: false
}
}

// 设置用户id
public setUserId <T extends DefaultOptons["uuid"]>(uuid:T) {
this.data.uuid = uuid
}

// 设置用户自定义参数
public setExtra <T extends DefaultOptons["extra"]>(extra:T) {
this.data.extra = extra
}

// 初始化监听器的函数
private installTracker() {
// 如果开启了history监听
if(this.data.historyTracker) {
this.captureEvents(["pushState","replaceState","popstate"],"history-pv")
}
// 如果开启了hash监听
if(this.data.hashTracker) {
this.captureEvents(["hashchange"],"hash-pv")
}
// 如果开启了DOM监听
if(this.data.domTracker) {
this.targetKeyReport()
}
// 如果开启了错误监听
if(this.data.jsError) {
this.jsError()
}
}

// 手动发起上报
public sendTracker <T>(data:T) {
this.reportTracker(data)
}

// DOM事件监听
private targetKeyReport() {
MouseEventList.forEach(ev => {
window.addEventListener(ev,(e)=>{
const target = e.target as HTMLElement
// 如果元素上有这个属性,说明此次操作需要被上报
const targetKey = target.getAttribute("target-key")
if(targetKey) {
this.reportTracker({event:ev,targetKey})
}
})
})
}

// 路由事件监听
// mouseEventList:事件列表 target:后台定义的枚举值
private captureEvents <T>(mouseEventList:string[],targetKey:string,data?:T) {
mouseEventList.forEach(event=> {
window.addEventListener(event,()=> {
console.log("监听到了")
// 捕获到时上报
this.reportTracker({event,targetKey,data})
})
})
}

// 开启错误处理
private jsError() {
this.errorEvent()
this.promiseReject()
}

// error事件错误上报
private errorEvent() {
window.addEventListener("error",(event)=> {
this.reportTracker({
event: "error",
targetKey: "message",
message: event.message
})
})
}

// promise错误上报
private promiseReject() {
window.addEventListener("unhandledrejection",(event)=> {
event.promise.catch(error=> {
this.reportTracker({
event: "promise",
targetKey: "message",
message: error
})
})
})
}

// 向后台上报
private reportTracker <T>(data:T) {
const params = Object.assign(this.data,data,{time: new Date().getTime()})

let headers = {
type: "application/x-www-form-unlencoded"
}
let blob = new Blob([JSON.stringify(params)],headers)

// 不支持json,所以使用blob
navigator.sendBeacon(this.data.requestUrl,blob)
}
}

发布npm

发布设置

package.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "tracker", // 包名,传到npm时就是这个名字
"version": "1.0.0", // 版本号
"description": "",
// import的查找规则:browser+mjs→module→browser+cjs→main
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"browser": "dist/index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c"
},
"keywords": [], // 搜索关键词
"author": "",
"files": ["dist"], // 要上传的目录,不写就默认上传整个工程目录
"license": "ISC",
"devDependencies": {
// ...省略
}
}

上传

npm的源一定要是默认源

注册

1
npm adduser

依次输入用户名、密码、邮箱,输入正确邮箱验证码后注册完成

登录

1
npm login

依次输入用户名、密码、邮箱,输入正确邮箱验证码后登录完成

发布

1
npm publish

发布完成后会收到npm的提示邮件