我正在参加「掘金·启航计划」
什么是埋点
埋点就是数据采集和数据上报的行为,例如:用户页面停留时间、按钮点击、页面跳转等
为什么要做埋点
埋点可以收集大量的用户行为数据,经过数据人员的分析和挖掘就能够为运营人员提供有力的数据,从而获取用户的喜好和交互习惯,从而优化流程、提升用户体验和提高核心用户转化率
技术栈
本次项目使用typescript + rollup
- 使用typescript可以在开发编译的时候就发现问题
- 使用rollup打出来的包更加简洁
初始化项目
初始化package.json和ts
npm init -y
tsc --init
创建目录
根据下面的目录结构创建文件夹和文件
.
├── src
│ ├── core
| | |── index.ts
│ ├── types
| | |── index.ts
│ ├── utils
| | |── pv.ts
├── package.json
├── rollup.config.js
├── tsconfig.json
安装依赖
npm install typescript -D
npm install rollup -D
npm install rollup-plugin-dts -D // 用于生成类型声明文件
npm install rollup-plugin-typescript2 -D // 支持使用TS来进行开发
配置rollup.config.js
import path from "path"
import ts from "rollup-plugin-typescript2"
import dts from "rollup-plugin-dts"
export default [
{
input: "./src/core/index.ts", // 入口文件
// 输出三个类型:es、cjs和umd
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()
]
},
]
配置package.json
{
"name": "tracker",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c"
},
"keywords": [],
"author": "",
"files": [
"dist"
],
"license": "ISC",
"devDependencies": {
"rollup": "^2.76.0",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-typescript2": "^0.32.1",
"typescript": "^4.7.4"
}
}
测试
在core/index.ts文件写一段测试代码,执行build命令,打包成功切根目录下的dist文件夹有输出文件说明初始化项目成功
// core/index.ts
const fn = (num: number): number => {
return num * 2
}
const a: number = 10
console.log(fn(a));
实现核心代码
定义类型
在types/core.ts文件夹下定义我们需要使用到类型
- DefaultOptions - 默认参数类型
- Options - 用户传递参数类型
- TrackerConfig - sdk的版本号
// types/core.ts
/**
* 默认参数类型
* @requestUrl 接口地址
* @historyTracker history上报
* @hashTracker hash上报
* @domTracker 携带Tracker-key标识的dom元素,点击上报
* @sdkVersion sdk版本号
* @extra 上报的数据
* @jsError js和promise异常上报
*/
export interface DefaultOptions {
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<DefaultOptions> {
requestUrl: string
}
// sdk的版本号
export enum TrackerConfig {
version = '1.0.0'
}
初始化Tracker类
在core/index.ts中导出Tracker
// core/index.ts
import {DefaultOptions, TrackerConfig, Options} from "../types/core"
export default class Tracker {
public data: Options // 参数
constructor(options: Options) {
// 合并参数
this.data = Object.assign({}, this.initDefaultOptions(), options)
}
// 返回默认参数
private initDefaultOptions(): DefaultOptions {
return <DefaultOptions>{
sdkVersion: TrackerConfig.version,
historyTracker: false,
hashTracker: false,
domTracker: false,
jsError: false
}
}
}
core核心功能
PV
PageView的缩写,即页面访问量,实现的功能是将用户对网页的访问记录并上报
单页应用分为两种路由模式:history和hash
- hash模式我们使用 hashchange 监听来实现功能
- history模式无法通过 popstate 监听 pushState replaceState,所以我们只能重写其函数来实现功能
// utils/pv.ts
// 捕获history事件,再自定义事件派发出去,已达到监听的目的
export const createHistoryEvnent = <T extends keyof History>(type: T): () => any => {
const origin = history[type];
return function (this: any) {
const res = origin.apply(this, arguments)
var e = new Event(type)
window.dispatchEvent(e)
return res;
}
}
// core/index.ts
// 重写history函数
private rewriteHistoryFn() {
window.history['pushState'] = createHistoryEvnent('pushState')
window.history['replaceState'] = createHistoryEvnent('replaceState')
}
// 捕获事件,上报数据
private captureEvents<T>(MouseEventList: string[], targetKey: string, data?: T) {
MouseEventList.forEach(event => {
window.addEventListener(event, () => {
// 执行数据上报方法
})
})
}
// 根据传参开启事件的捕获
private installTracker() {
if (this.data.historyTracker) {
if (this.data.historyTracker) {
this.captureEvents(['pushState', 'replaceState', 'popstate'], 'histpry-pv')
}
if (this.data.hashTracker) {
this.captureEvents(['hashchange'], 'hash-pv')
}
}
}
UV
unique view的缩写,即访问网站的自然人,用户的唯一标识,一般是用户登录后给定的userid,我们给一个函数进行userid的绑定,每次上报数据携带此参数
// core/index.ts
public setUserId<T extends DefaultOptions['uuid']>(uuid: T) {
this.data.uuid = uuid
}
数据上报
sdk采用 navigator.sendBeacon 的方式上报数据,原因是使用sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。通俗的讲就是跟 XMLHttrequest 对比 navigator.sendBeacon 即使页面关闭了 也会完成请求 而XMLHTTPRequest 不一定,通过MDN了解更多
我们编写一个上报函数
// core/index.ts
// 上报数据
private reportTracker<T>(data: T) {
const params = Object.assign(this.data, data, { time: new Date().getTime() })
let headers = {
type: 'application/x-www-form-urlencoded'
};
let blob = new Blob([JSON.stringify(params)], headers);
console.log(blob);
navigator.sendBeacon(this.data.requestUrl, blob)
}
我们在根目录下新建一个server文件夹,使用express编写一个测试的上报接口
- 初始化
npm init -y
- 安装依赖
// cors解决跨域问题
npm install express cors -S
- 在根目录下新建index.js编写接口
// index.js
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors())
app.use(express.urlencoded({ extended: false }))
app.post('/tracker', (req, res) => {
console.log(req.body);
res.send(200)
})
app.listen(9000, () => {
console.log('服务启动在9000');
})
- 启动服务
node index.js
测试一下
- 修改捕获事件上报的函数
// core/index.ts
// 捕获事件,上报数据
private captureEvents<T>(MouseEventList: string[], targetKey: string, data?: T) {
MouseEventList.forEach(event => {
window.addEventListener(event, () => {
this.reportTracker({
event,
targetKey,
data
})
})
})
}
- 打包
npm run build
- 根目录下新建index.html,引入打包后的代码,使用Live Server开启一个静态服务器(需要提前安装一下Live Server这个插件)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button target-key="btn">有标识按钮</button>
<button>无标识按钮</button>
<script src="./dist/index.js"></script>
<script>
new tracker({
requestUrl: "http://127.0.0.1:9000/tracker",
historyTracker: true,
domTracker: true,
jsError: true
})
</script>
</body>
</html>
- 在控制台执行history.pushState('', '', '/a'),可以看到网络的all里面有一条请求记录,类型是ping,同时后台接口也会打印body
DOM事件监听
给需要监听上报的dom元素添加一个属性来标识
<button target-key="btn">有标识按钮</button>
<button>无标识按钮</button>
在core/index.ts里面编写dom监听上报的代码
// cord/index.ts
const MouseEventList: string[] = ['click', 'dblclick', 'contextmenu', 'mousedown', 'mouseup', 'mouseenter', 'mouseout', 'mouseover']
private targetKeyReport() {
MouseEventList.forEach(event => {
window.addEventListener(event, (e) => {
const target = e.target as HTMLElement
const targetValue = target.getAttribute('target-key')
if (targetValue) {
this.sendTracker({
targetKey: targetValue,
event
})
}
})
})
}
js异常上报
我们上报error事件和promise的报错unhandledrejection
// cord/index.ts
private jsError() {
this.errorEvent()
this.promiseReject()
}
private errorEvent() {
window.addEventListener('error', (event) => {
this.reportTracker({
event: 'error',
targetKet: "error",
message: event.message
})
})
}
private promiseReject() {
window.addEventListener("unhandledrejection", (event) => {
event.promise.catch(error => {
this.reportTracker({
event: 'promise',
targetKet: "error",
message: error
})
})
})
}
用户手动上报
再新增一个函数,让用户能够手动上报
// cord/index.ts
// 手动上报
public sendTracker<T>(data: T) {
this.reportTracker(data)
}
完整代码
- src/core/index.ts
import { DefaultOptions, TrackerConfig, Options } from "../types/core"
import { createHistoryEvnent } from "../utils/pv"
const MouseEventList: string[] = ['click', 'dblclick', 'contextmenu', 'mousedown', 'mouseup', 'mouseenter', 'mouseout', 'mouseover']
export default class Tracker {
public data: Options // 参数
constructor(options: Options) {
// 合并参数
this.data = Object.assign({}, this.initDefaultOptions(), options)
this.rewriteHistoryFn()
this.installTracker()
}
// 返回默认参数
private initDefaultOptions(): DefaultOptions {
return <DefaultOptions>{
sdkVersion: TrackerConfig.version,
historyTracker: false,
hashTracker: false,
domTracker: false,
jsError: false
}
}
// 重写history函数
private rewriteHistoryFn() {
window.history['pushState'] = createHistoryEvnent('pushState')
window.history['replaceState'] = createHistoryEvnent('replaceState')
}
// 手动上报
public sendTracker<T>(data: T) {
this.reportTracker(data)
}
// 捕获事件,上报数据
private captureEvents<T>(MouseEventList: string[], targetKey: string, data?: T) {
MouseEventList.forEach(event => {
window.addEventListener(event, () => {
this.reportTracker({
event,
targetKey,
data
})
})
})
}
// 根据传参开启事件的捕获
private installTracker() {
if (this.data.historyTracker) {
this.captureEvents(['pushState', 'replaceState', 'popstate'], 'histpry-pv')
}
if (this.data.hashTracker) {
this.captureEvents(['hashchange'], 'hash-pv')
}
if (this.data.domTracker) {
this.targetKeyReport()
}
if (this.data.jsError) {
this.jsError()
}
}
// 上报数据
private reportTracker<T>(data: T) {
const params = Object.assign(this.data, data, { time: new Date().getTime() })
let headers = {
type: 'application/x-www-form-urlencoded'
};
let blob = new Blob([JSON.stringify(params)], headers);
console.log(blob);
navigator.sendBeacon(this.data.requestUrl, blob)
}
public setUserId<T extends DefaultOptions['uuid']>(uuid: T) {
this.data.uuid = uuid
}
public setExtra<T extends DefaultOptions['extra']>(extra: T) {
this.data.extra = extra
}
private jsError() {
this.errorEvent()
this.promiseReject()
}
private errorEvent() {
window.addEventListener('error', (event) => {
this.reportTracker({
event: 'error',
targetKet: "error",
message: event.message
})
})
}
private promiseReject() {
window.addEventListener("unhandledrejection", (event) => {
event.promise.catch(error => {
this.reportTracker({
event: 'promise',
targetKet: "error",
message: error
})
})
})
}
private targetKeyReport() {
MouseEventList.forEach(event => {
window.addEventListener(event, (e) => {
const target = e.target as HTMLElement
const targetValue = target.getAttribute('target-key')
if (targetValue) {
this.sendTracker({
targetKey: targetValue,
event
})
}
})
})
}
}
- src/types/core.ts
/**
* @requestUrl 接口地址
* @historyTracker history上报
* @hashTracker hash上报
* @domTracker 携带Tracker-key标识的dom元素,点击上报
* @sdkVersion sdk版本号
* @extra 上报的数据
* @jsError js和promise异常上报
*/
export interface DefaultOptions {
uuid: string | undefined,
requestUrl: string | undefined,
historyTracker: boolean,
hashTracker: boolean,
domTracker: boolean,
sdkVersion: string | number,
extra: Record<string, any> | undefined,
jsError: boolean
}
export interface Options extends Partial<DefaultOptions> {
requestUrl: string
}
export enum TrackerConfig {
version = '1.0.0'
}
- src/utils/pv.ts
// 捕获history事件,再自定义事件派发出去,已达到监听的目的
export const createHistoryEvnent = <T extends keyof History>(type: T): () => any => {
const origin = history[type];
return function (this: any) {
const res = origin.apply(this, arguments)
var e = new Event(type)
window.dispatchEvent(e)
return res;
}
}