背景
pc + elementplus + Vue3 自用系统埋点的实践
用法
- v-trace
<el-button
v-trace="'登录按钮'"
type="default"
plain
>登录</el-button
>
<el-button
v-trace="{ evName: evName, evType: TrackerType.nav }"
type="default"
plain
>登录</el-button
>
<script setup>
// 上报存在变量的动态数据
const evName = ref("登录按钮")
</script>
- api调用
<script setup>
import { ref, getCurrentInstance } from "vue";
// 读取挂载到app上的全局对象
const { proxy } = getCurrentInstance();
// 具体上报
const reportHandler = () => {
proxy.$tracker.report({
evName: '列表-删除按钮',
evInfo: `行号: ${row.index}; 输入内容: ${row.searchInput}; 输出内容: ${row.searchOutput}`
});
}
</script>
- 组合api上报页面访问
<script setup>
import { ref, usePageTracker } from "@/hooks/index";
const formData = ref({
name: '',
type: 1
});
// 入参为当前页面的标题
// 上报元素埋点时会带上当前的页面信息
// 返回埋点实例,方便元素层级的埋点,不用再读取一下全局对象了
const { $trace } = usePageTracker("测试列表页");
const reportEle = () => {
$trace({
evName: '查询按钮',
evInfo: `输入内容:${JSON.stringify(formData.value)}`,
});
};
</script>
具体实现
- v-trace 指令实现
首先实现一个指令,然后将指令挂到app全局上,便于在项目中使用。
import { TrackerType } from "@/libs/tracker";
/** 元素埋点指令 */
export default function VTrace(app) {
return {
mounted(el, binding) {
el.addEventListener(
"click",
(e) => {
e.stopPropagation();
const value = binding.value;
// 具体事件名(操作的元素名)
let evName = value;
// 当前事件类型
let evType = TrackerType.btn;
let evInfo = "";
if (typeof value === "object") {
evName = value.evName;
evType = value.evType || TrackerType.btn;
evInfo = value.evInfo;
}
// 调用全局上报方法
app.config.globalProperties.$tracker.report(
{
evName,
evInfo,
},
evType
);
},
false
);
},
};
}
在项目入口文件中,初始化埋点实例并将实例挂载到全局对象上,将全局指令也挂载到全局对象上。
import { createApp } from "vue";
import ElementPlus from "element-plus";
import MyTracker from "@/libs/tracker";
import directives from "@/directives";
const app = createApp(App)
.use(ElementPlus)
.use(Router);
app.config.globalProperties.$tracker = new MyTracker();
Object.keys(directives).forEach((k) => {
app.directive(
k,
typeof directives[k] === "function" ? directives[k](app) : directives[k]
);
});
app.mount("#app");
初始化埋点信息以及在埋点信息中添加登录后的用户信息
import { onMounted,getCurrentInstance } from "vue";
import { useStore } from "vuex";
const store = useStore();
const { proxy } = getCurrentInstance();
/** 应用初始化
* 1. 埋点初始化
* 2. 获取用户信息并更新给埋点系统
*/
const init = async () => {
proxy.$tracker.init();
const query = getParams(location.href);
const { data } = await store.dispatch("getUserInfo");
proxy.$tracker.addUserInfo({
userId: data.id,
userName: data.username,
});
};
onMounted(() => {
init();
});
- tracker类的实现
import { getDuration, getCurrentTimeStamp } from "@/libs/date";
import { decrypt, encrypt, localSave } from "@/libs/util";
import pkg from "../../package.json";
/** 埋点类型 */
export const TrackerType = {
page: "pv",
btn: "btn-clk",
nav: "nav",
short: "short-key",
area: "area-clk",
};
const Storage_Key = "t_data";
/** 埋点 */
class MyTracker {
constructor() {
this.base = {};
this.user = {};
this.page = {};
this.timeInterval = null;
}
/** 获取浏览器 */
getSystemInfo() {
const _navigator =
typeof navigator !== "undefined" ? navigator : window.navigator;
this.base = {
app_version: pkg.version,
ua: _navigator.userAgent,
domain: window.location.origin,
};
}
/** 注入登录用户信息到埋点数据中 */
addUserInfo(user) {
this.user = user;
}
/** 注入当前页面信息 */
addPageInfo(params = {}) {
const _location = window.location;
this.page = {
url: _location.href,
sTimeStamp: getCurrentTimeStamp(),
...params,
};
this.pageStorage();
}
/** 定期缓存页面信息,在上报时清除掉 */
pageStorage() {
// 延迟缓存防止在补报前缓存就被清掉
setTimeout(() => {
this.clearPageStorage();
this.timeInterval = setInterval(() => {
// 记录时间点
this.page.eTimeStamp = getCurrentTimeStamp();
localSave(
Storage_Key,
JSON.stringify({ page: { ...this.page }, user: { ...this.user } })
);
}, 1000);
}, 5000);
}
/** 清除页面缓存 */
clearPageStorage() {
localStorage.removeItem(Storage_Key);
clearInterval(this.timeInterval);
this.timeInterval = null;
}
/** 补报之前的页面浏览信息
* 应用场景: 页面pv是在访问页面后跳转其他路由时上报的,所以这里是用户访问页面后直接关闭浏览器导致当前
* 浏览记录没能上报,在下次访问时进行补报
*/
fixReportPage() {
const tData = JSON.parse(localStorage.getItem(Storage_Key) || "{}");
if (tData.page?.url) {
const _page = tData.page;
_page.duration = getDuration(_page.sTimeStamp, _page.eTimeStamp);
console.log("tracker log=> 页面信息补报");
this.report({ evName: "页面浏览" }, TrackerType.page, {
page: _page,
...tData.user,
});
this.clearPageStorage();
}
}
/** 上报pv */
reportPage() {
const _page = this.page;
if (!_page.url) return;
// 记录停留时间
_page.duration = getDuration(_page.sTimeStamp, _page.eTimeStamp);
this.report({ evName: "页面浏览" }, TrackerType.page);
}
/** 上报数据到服务端 */
report(eventParams = {}, type = TrackerType.btn, otherParams = {}) {
const page =
type === TrackerType.page
? this.page
: { pName: this.page.pName, url: this.page.url };
const params = {
...this.base,
...this.user,
...page,
e: {
...eventParams,
},
trackType: type,
t: getCurrentTimeStamp(),
...otherParams,
};
console.log("tracker log=> ", params);
}
/** 初始化 */
init() {
this.getSystemInfo();
this.fixReportPage();
}
}
export default MyTracker;
关于页面pv上报这里,是在浏览当前路由后跳转其他路由时上报当前路由访问信息,并带上停留时长。如果不需要停留时长,可以在页面访问时直接上报。关于如何在浏览器关闭时调接口上报数据,还有待进一步优化。
欢迎评论区交流讨论更多关于vue埋点的信息~