无埋点日志记录
背景: 给开发自己看的一个系统日志记录功能。
原理,dom变化后,更新一份观察者,观察者是需要记录的element
js原生支持dom树更改的监听,MDN
observer = new MutationObserver((mutationList: any, obs: any) => {
domChange(mutationList, obs);
});
// dom 变化后回调函数
const domChange = (mutationsList: any[], observerParams: any) => {
// 检查页面loading
checkSpinStatus();
// 检查页面中antd message alert 等元素内容,
checkMessageWarningStatus();
// 给白名单中的 元素挂载点击事件
addWhiteClassClickListener();
// 给icon,i标签挂载点击事件
addTagIClickListener();
// 给button 元素挂载点击事件
addButtonClickListener();
};
给button元素添加click 事件
const addButtonClickListener = () => {
// 1. 获取所有页面上的按钮元素
const buttons = document.querySelectorAll('button');
// 2. 创建一个函数来处理按钮点击事件
const handleButtonClick = (event: any) => {
debounceSendLog({
type: 'button',
event: event,
});
};
// 3. 遍历所有按钮并添加点击事件监听器
buttons.forEach((button) => {
button.addEventListener('click', _.throttle(handleButtonClick, 2000));
});
};
给白名单中的类做click事件绑定
const addWhiteClassClickListener = () => {
// 1. 给白名单业务逻辑元素添加绑定
let eleList = whiteClassList.reduce((result: any[], item: any) =>{
return [...result, ...Array.from(document.querySelectorAll(`[class*="${item.className}"]`))];
},[]);
const handleClick = (event: any) => {
debounceSendLog({
type: 'custom',
event: event,
});
};
// 3. 遍历所有按钮并添加点击事件监听器
eleList.forEach((ele) => {
ele.addEventListener('click', _.throttle(handleClick, 2000));
});
};
// 找到点击时,目标的关键字,如果没有就找父元素的。
const getText = (event: any) =>{
// SVG 元素直接返回,没有关键字, 使用path 类命定位
if(event.target.nodeName === 'path' || event.target.nodeName === 'svg'){
return 'icon';
}else{
let targetDom = event.target;
for(const white of whiteClassList){
let target = event.path.slice(0,6).find((item: any) => item.className.includes(white.className));
if(target){
targetDom = target;
break;
}
}
return targetDom ? targetDom.textContent : '未找到关键字';
}
};
将日志写入到*.log 文件夹中
const sendLog = (params: any) => {
let { type, event } = params;
// 打印8个元素,基本可以定位点击的具体是谁
const pathStr = event.path.slice(0,8).reduce((res: string, path: any) =>{
return `${res} > ${path.className ? path.className : ''}`;
},'');
let text = event.target.textContent;
if(type === 'custom' && !text){
text = getText(event);
}
// setHistoryLog 是对 fs.writeFile(path, text, writeOptions, callback);
setHistoryLog(`窗口位置: ${pathName},操作类型: ${type},关键字:{${text}} 位置:{screenX:${event.screenX},screenY:${event.screenY}}. path: ${pathStr}`);
};
// lodash debounce
const debounceSendLog = _.debounce(sendLog, 1500, { maxWait: 3, leading: true, trailing: false });
使用方式
- 初始化,建议监听路由变化,每次变化1秒,做一次初始化observer。
history.listen((location) =>{
initObserver(location.pathName);
})
export const initObserver = (pathname: string) => {
destoryObserver();
pathName = pathname;
window.fileAPI.setHistoryLog(`页面切换:${pathname}`);
observer = new MutationObserver((mutationList: any, obs: any) => {
domChange(mutationList, obs);
});
const targetNode = document.body;
observer.observe(targetNode, config);
};
const destoryObserver = () => {
if (observer) {
observer.disconnect();
}
};
- 白名单最好写到配置项中,避免修改代码。
const whiteClassList = [
{
className: 'ng_alert',
text: '页面alert警告信息',
},
{
className: 'ng_radio',
text: 'radio或者radioButton',
},
{
className: 'router_header_item',
text: '头部路由卡片',
},
{
className: 'anticon',
text: 'icon',
},
];