H5可回溯方案浅析 -- 以保险投保为例
在金融行业搬过砖的同学应该多少都有听说过【可回溯】这个词,它主要是为了记录用户所有的操作轨迹以及交互状态,大都用在与用户发生纠纷时的举证,并且也是金融监管部门的监管红线(必须要有)。
首先,大家先看一下可回溯最终实现的效果,下面的GIF是从rrweb官网的demo中录制的。
如上图,所谓的可回溯就是把我们在页面上的所有操作都“录制”下来,然后在其他地方进行“回放”,可以看到录制和回放我都加了引号,因为这并不是真正的视频录制和回放,而是收集用户操作,再通过特殊的解析器,模拟当时用户的界面以及操作。这是怎么实现的呢?
可回溯实现方式
可回溯的本质其实也是一种日志上报,只是它需要上报的范围比普通的日志上报要大得多,不仅是用户的所有交互,还有页面模板本身的变化,都需要记录起来,形成一个个HTML快照,然后再输出。
HTML快照
关于快照的完整官方文档
在rrweb中快照分为两种,全量快照和增量快照。在初始化的时候会将当前的DOM树全量收集一次,包括引入的CSS,这就是全量快照。
后续的变动都为增量快照,其核心是MutationObserver,通过这个API我们可以清晰的感知到页面DOM树的变化,并且会把变化的内容通过数组的形式返回。
快照序列化
关于序列化的完整官方文档
快照其实是一个个DOM节点,这并不是一种适合存储、使用的数据格式,所以需要将它们转换成描述性质的数据结构,这个过程有点像逆虚拟DOM。序列化后的快照就可以被rrweb提供的播放器解析,随后完成回放。
回溯数据存储方案
可回溯功能本身其实rrweb
已经能完全cover住了,但是要在实际项目中使用,我们还需要设计后续的存储、拉取方案。
回溯维度
在讨论如何存储数据之前,我们需要先明确数据的维度,这直接影响我们存储的方式。
我们将会以一个保险投保的项目为例:
2022年8月8号,用户A在投保详情页填写表单,阅读了健康告知,进入收银台付款,最终完成了一份健康险的购买。
我们可以分析一下上述的流程,不难看出其中一共有两个核心维度用户
、时间
和一个可选维度页面
。
后续对可回溯数据的调取必然也会按照这些维度来进行的,可能是调取某用户的所有操作记录,也可能是调取某用户某个时间段内的操作记录,甚至可以精细到某页面的记录(大多数情况下我们都是应用级别的回溯)。
所以我们的存储内容也需要与上述维度进行关联,不限于存储的库表名、文件夹、文件名或者是具体内容。
数据上报
前端产生数据之后就应该进行数据的上报,回溯数据的上报原理和笔者之前写的日志上报十分类似。
我们会按一定的时间频率
或数据条目
对已产生的数据进行数据传输上报。
数据存储
这里其实是整个存储方案最核心的地方,我们收集来的数据到底应该怎么持久化。
可回溯数据是否需要落库?不落库还能怎么办?
在笔者的实践中是没有使用数据库做持久化的,采用的是腾讯云的对象存储COS进行回溯数据存储,不使用数据库的原因有以下两点:
① 成本更低
可回溯数据是极低频的数据,但是存储时长又很长(通常是3年以上),如果用数据库做持久化成本会变得很高,而COS则很适合做这种事情。
② 前端自闭环
使用COS做持久化可以不需要后端的人力介入,靠前端人力可以实现自闭环。
确定持久化方式之后,我们需要考虑的就是具体的实现方式了,下面将会介绍两种实现方式。
纯前端
十分不推荐这种方式
对象存储服务都会提供H5接入的SDK,也就是说我们可以直接C端将可回溯的数据传输到COS的存储桶里,但是这样会将访问存储桶的密钥将暴露在前端,十分的不安全,很容易被做成图床。
并且后续的回溯数据拉取也需要借助后端服务,我们终究是没法完全避开后端服务(云函数)。
后端服务(云函数)
推荐这种方式,如果没有条件部署Node服务可以用云函数来替代
对象存储服务一定会提供Node接入的SDK,接入的流程也比较简单,并且后续的回溯数据调取也可以借助该服务来进行。
const COS = require('cos-nodejs-sdk-v5')
const cos = new COS({ SecretId, SecretKey })
cos.putObject({
Bucket,
Region,
StorageClass,
Key: body.targetFile, // 2022/08/${userId}/${recordId}/${index}.json
Body: body.content, // 前端产生的可回溯数据
})
因为腾讯云的对象存储是支持在线新建文件的,所以我们可以不用在服务端将文件写好再上传,而是直接将回溯数据上传到桶里并生成JSON文件。
存储名称
确定在对象存储里持久化之后,我们就需要考虑怎么维护文件的存储路径,按照上面分析出的储存维度,以及后续调取数据的模式,我们可以梳理出这样的储存路径。
年/月/用户标识/录制标识/切片
每个/
都代表一层文件夹,下面介绍一下每一层的用途:
第一层是年份,因为在保险业务内可回溯的数据是按年来管理的,进行数据删除也是以年为维度删除的,这一层可以方便后续的维护。
第二层是月份,一个用户保险投保行为基本都能在一个月内完成,所以我们加了一层月份维度,让调取更加方便。
然后是用户标识,这里一般指用户的唯一标识,它可能是userId也可能是openId,具体视业务而定,核心就是能够定位到唯一的用户,因为大多数调取可回溯数据的时候,提供给我们的就只有时间和用户信息。
再接着就是录制标识,这也是比较核心的一环,具体的实现将会在下面的Record-id中阐述,这个标识主要的作用就是将前端分段上报的数据串联起来,组成一个完整的回溯数据。
最后一个就是最终的文件名,前端在上传的时候会维护一个index,用于标识这次上传的是录制标识中的第几个切片,主要用于保证回溯数据的顺序准确。
接入方式
rrweb官方提供了十分简约的接入方式,这种方式适用于大部分的情况。
let events = [];
rrweb.record({
emit(event) {
// 将 event 存入 events 数组中
events.push(event);
},
});
// save 函数用于将 events 发送至后端存入,并重置 events 数组
function save() {
const body = JSON.stringify({ events });
events = [];
fetch('http://YOUR_BACKEND_API', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
});
}
// 每 10 秒调用一次 save 方法,避免请求过多
setInterval(save, 10 * 1000);
初始化时机
录制初始化的时机主要取决于录制的维度,是页面维度还是应用维度,两者的初始化时机完全不同,我们需要具体情况具体分析。
不过初始化的实现是完全一致的,我们可以封装好初始化的方法,然后就可以灵活使用了。
如果是应用级别的可回溯收集,我们就可以在SPA初始化的生命周期中完成这个初始化;如果是页面级的话也是在相应的页面生命周期中开始初始化。
record-id
按官方推荐的接入方式,接入后会发现每次save
的数据之间其实是没有关联关系的,我们需要维护一个用于关联的标识,也就是标题中的record-id
字段。
在初始化网页应用的时候,我们会随机生成一串字符串,并以record-id
为key值存入sessionStorage中,后续每次的save
操作都会带上该字符串,用于标识本次数据的归属。
除此之外,在初始化的时候,还需要初始化一个index
属性,初始值为0或1都可以,每次save
的时候也需要将其带上作为切片值,并且是自增的。
回放方案
const events = YOUR_EVENTS;
const replayer = new rrweb.Replayer(events);
replayer.play();
和录制一样,rrweb提供了配套的回放解决方案,使用方式也十分简单。这里我们需要解决的是如何准确获取到需要回放的数据。
一般在提取可回溯数据时我们会拿到两个数据用户信息、操作时间,然后则需要我们通过这两个数据定位到一个范围内的数据。
在本案例中,我们上传回溯数据时会将当时的record-id
做一次日志上报,所以我们可以通过用户信息+操作时间在日志系统中查找到对应的record-id
,然后可以拼出COS中完整的文件夹路径:
年/月/用户标识/录制标识
但是我们并不知道这个record-id
下有多少个切片文件,接下来可以用COS提供的查询文件夹内容的APIgetBucket
,查询到对应record-id
下的切片文件有多少个,然后将所有切片文件批量拉取回来并在服务端完成拼接,最后输出给前端。
小结
整个H5的可回溯方案至此已经介绍完毕,用一张图来总结大概是这样的:
整体来说,我们需要开发的地方并不多,因为大头已经有成熟的第三方插件帮我们处理好了,我们要做的是沉下心来考虑中间串联的部分。
毕竟一个完整的可回溯方案,数据收集、数据传输、数据持久化以及数据使用,是缺一不可的。
转载自:https://juejin.cn/post/7132729131568988196