2

前言

最近上线的人事系统,由于同步问题,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接收上报端

接收上报请求,存储


答案在风中飘着
302 声望6 粉丝

\失去人性,失去许多!失去兽性,失去一切!