背景
某天突然在线上环境发现了一个 id 重复的问题,具体如下。
由于框以 ID 作为区分标识,重复框 ID 会引起多个选中,从而引起标注时的不便。于是我就开始了 debug 之旅。
原因
首先我们定位到源码,从 frameId 定义角度进行排查, 发现 frameId 主要在标注框时进行定义,其逻辑如下:
其中关键的 setFeatureFrameId函数具体如下:
setFeatureFrameId(feature: Feature, frameId: number) {
let id = frameId
if (!id) {
this.maxFrameId++
id = this.maxFrameId
}
feature.set('frameId', id)
}
主要是根据 maxFrameId作为当页的唯一值标识进行赋值,这一逻辑乍一看没啥问题,但是细细思索,却是个很糟糕的设计。
关于 ID 前后端对接
一般而言,ID 都是从后端获取的,通过后端数据库的自增以确保唯一值。但常常的情况是用户操作新增时会先在前端进行数据的新增,而后再发送到后端,那么在这个“前端已新增但后端未新增”时,怎么去唯一标识新增数据呢?
根据个人经验及业内相关实践,主要比较好的方式有以下两种:
- 等待后端生成 ID
如图,ID 生成完全交由后端实现,优点是前端实现简单并准确,但代价是延迟 & 一定程度上的用户体验影响。
- 前端生成临时 ID 后在后端同步
这一步通过前端使用 UUID,nano ID等生成唯一值 ID,以保证独一标识,先行更新界面数据而不是等待后端的 ID 更新。这一方法优点很明显是缩短了数据更新的时间,优化体验,但代价是后端可能要稍微做一下兼容。
这两种方法都立基于让后端签发 ID的思想。但原代码中关于 id 更新的逻辑却不是这两种中的任何一种,它是完全由前端签发的:
- 完全前端签发 ID
如图,纯前端签发时,后端需新增一个字段进行存储,这种方法虽然和完全后端签发不同,但如果能确保 ID 的唯一性也不失为一种方法。
问题定位
但问题就出现在 frameId 并不是一个唯一值。
如前代码所示,frameId通过 maxFrameId作为当页的唯一值标识进行赋值。根据业务逻辑,不同页的 frameId 可以相同,同页的frameId不同即可,预计原代码是根据此逻辑进行设计的 ID 赋值逻辑。
但由于可以存在跨页标记框,那么 frameId为了适应这一需求需要去对跨页的赋值打补丁。但如果当初选择后端签发 ID 或 唯一值 ID,那都不会出现这个 bug。
解决方案
由于代码耦合比较多,不好对 frameId 赋值逻辑进行重构,只能在跨页的函数出加上唯一值maxFrameId的兼容:
const maxFrameId = features.length
? _.max(_.map(features, feature => feature.values_.frameId))
: 0
this.maxFrameId = _.max([maxFrameId, this.maxFrameId])
当前最佳的方案私以为是直接将
maxFrameId的赋值逻辑用唯一值替代,以避免后续的更改再出问题,成本也不大。
小结
id 签发这块逻辑最好还是让后端做,在前端做非常容易让逻辑变得复杂化。