背景
enX 的 Chrome 扩展会给查过的生词画下划线。主窗口点一个词会弹出浮层,浮层里有 Mark Known(标为已认识)按钮,点了之后这个词的下划线消失。
后来侧边栏的整句翻译也加上了同样的下划线(ADR-053),于是有了一个新问题:在主窗口标为已认识之后,侧边栏里这个词的下划线也应该一起消失。
原来的做法
content script → background → POST /api/mark → enx-api 写库
↓ 成功
content script 收到回复 → 去掉主窗口下划线
主窗口要等接口成功才去掉下划线,侧边栏完全不知道这件事。
改成什么样
content script:先去掉主窗口下划线
└─ runtime.sendMessage({ type: 'markAcquainted', word })
├─ 侧边栏:收到后立即去掉下划线,不回复
└─ background:调 enx-api,完成后回复 content script
两点变化:
- 主窗口先改界面,再让 background 在后台调接口,也就是常说的乐观更新(optimistic update),和它相对的是原来那种等服务端确认再改界面的做法。
- 侧边栏不需要新的通道。
chrome.runtime.sendMessage从 content script 发出时,会投递给扩展里所有监听runtime.onMessage的页面,background 和侧边栏都能收到。侧边栏只需要多注册一个监听。
为什么可以先改界面
标为已认识失败的代价很低:服务端没记上,用户下次读别的文章时这个词还会带下划线,再标一次就行。为了这个小概率的失败让用户每次都等一个网络往返,不划算。
如果改成「background 调接口成功后再通知侧边栏」,状态会更严格一致,但侧边栏要多等一个接口的时间。对这个操作来说,一致性没有那么重要。
多个接收方只能有一个回复
runtime.sendMessage 允许多个页面监听,但发送方只会收到第一个调用 sendResponse 的回复,其余的被忽略。侧边栏如果也回复,会抢在 background 前面,content script 就看不到接口的真实结果了。
所以约定:侧边栏只监听、不回复(不调用 sendResponse,也不 return true),只有 background 回复。
顺带发现的问题:接口不能重试
原来的 /api/mark 是切换:每调用一次,已认识和未认识互相翻转。服务端已经写入、但回复丢了的时候,如果客户端重试,这个词就被翻回了未认识。所以这次改动先不做重试,另开了一个 issue:把接口拆成两个幂等的操作,PUT /api/me/words/:word/known 和 DELETE /api/me/words/:word/known,之后再给 background 加重试。