前言
最近上线的人事系统,由于同步问题,HR一直认为是系统问题,而不承认自己操作不当,为了避免相互甩锅,计划把hr的操作记录下来,便于下次正面刚。
原理
还原用户行为的思路 DOM 快照 + 用户事件
难点
DOM 快照大小控制,diff合并,开始使用了保存网页的思路,后面调研发现rrweb
已经有类似实现
前端上报端代码
import Axios from '@src/utils/requestNoError';
const rrweb = require('rrweb');
export default (userName, workCode) => {
try {
(function() {
let events = [];
let isFirstFrame = true;
let whiteList = ['#/roster', '#/organizemanage', '#/api'];
rrweb.record({
emit(event, isCheckout) {
if (isCheckout) {
isFirstFrame = true;
}
const isInWhiteList = whiteList.includes(window.location.hash);
if (isInWhiteList && events.length === 301) {
save(JSON.parse(JSON.stringify(events)));
events = [];
}
isInWhiteList && events.push(event);
},
checkoutEveryNth: 299
});
// save 函数用于将 events 发送至后端存入,并重置 events 数组
function save(eventData) {
let params = {
userName,
workCode,
system: 'hrm',
events: JSON.stringify(eventData),
url: location.href,
isFirstFrame
};
let twoDomain = window.location.href.split('.')[1];
let baseUrl = '';
// 如果是线上或者测试环境才跳转
if (twoDomain === 'test' || twoDomain === 'global') {
baseUrl = `//skyeye.${twoDomain}.com/behavior/addBehavior`;
} else {
baseUrl = '//skyeye.test.com/behavior/addBehavior';
}
// 第一帧第二帧发送并缓存
if (isFirstFrame) {
isFirstFrame = false;
}
Axios({
method: 'post',
url: baseUrl,
data: params
});
}
// // 每 10 秒调用一次 save 方法,避免请求过多
// setTimeout(save, 10 * 1000);
})();
} catch (error) {
console.log(error);
}
};
管理端回放
目录结构
|____bin
| |____www // node 启动文件
|____dist
|____build
| |____build.js
| |____webpack.dev.conf.js
| |____webpack.prod.conf.js
| |____webpack.base.conf.js
| |____utils.js
|____config // webpack 配置
| |____dev.env.js
| |____index.js
| |____prod.env.js
| |____entry.js
|____mock // mock 数据配置
| |____server
| |____router
|____html // 模板html
| |____index.html
|____static // 静态文件
| |____favicon.ic
|____src
| |____entry
| | |____index.js
| |____styles // 公共样式
| |____components // 公共组件
| | |____footer
| | | |____index.less
| | | |____index.jsx
| | |____loading
| | | |____index.js
| | | |____loading.gif
| | | |____index.less
| | |____menu
| | | |____index.less
| | | |____index.jsx
| | | |____menuData.js // 侧边栏配置文件
| | |____breadcrumb
| | | |____index.less
| | | |____index.jsx
| | |____header
| | | |____index.less
| | | |____index.jsx
| |____lib // 工具库
| | |____permissionTools.js
| | |____dataFormate.js
| | |____common.js // fetch请求相关
| | |____utils.js
| | |____cookie.js
| |____containers // 业务页面
| | |____example // demo
| | | |____index.jsx
| | | |____example.less
| | | |____child
| | | | |____index.jsx
| | | |____store
| | | | |____index.js
| |____routes // 前端路由配置
| | |____nameFilter.js
| | |____index.js
| | |____riskmng.js
| | |____example.js
| | |____App.js
|____.editorconfig
|____README.md
|____yarn.lock
|____.gitignore
|____package.json
|____.eslintrc.js
|____.eslintignore
|____.babelrc
|____app.js
|____postcss.config.js
播放解析
import React from 'react';
import rrwebPlayer from 'rrweb-player';
import store from '../store';
import styles from './index.less';
import { observer } from 'mobx-react';
@observer
class Replay extends React.Component {
componentDidMount() {
store.getBehaviorDetail({ fileName: this.props.location.fileName }).then(() => this.play());
}
play = () => {
new rrwebPlayer({ // eslint-disable-line
target: document.querySelector('#replayer'), // 可以自定义 DOM 元素
data: {
events: JSON.parse(store.behaviorEvents)
}
});
};
render() {
return (
<div id="replayer" className={styles.replay_area}></div>
);
}
}
export default Replay;
node接收上报端
接收上报请求,存储
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。