Readwise Reader:让收藏的文章重新派上用场
Readwise Reader 是面向经常阅读网页、PDF 和邮件订阅的人的阅读工具。它把资料、批注和后续回顾接起来,帮助读者找回当时觉得重要的内容。
先尝试拆解问题、设计和实现,再展开思路,检查状态、边界与取舍,并设计验证用例。思路提供分析路径,不给出完整实现。
别让迟到事件恢复已删除文章
这是练习用的同步协议,不代表 Readwise 内部实现。每条事件包含 docId、正整数 version 和 action(save、archive、delete)。同一文档的 version 表示操作先后,越大越新;事件可乱序、重复到达。输入保证同一 docId/version 的 action 相同。
- 实现一个状态处理器:初始没有文档;save 使文档进入 inbox,archive 使其为 archived,delete 使其为 deleted。每个合法事件都是完整状态指令,首条收到 archive 也得到 archived。
- 输出每个 docId 的最高 version 与当前状态;重复版本或更旧事件不得覆盖状态。deleted 也必须保留最高版本,直到收到更高版本的 save 或 archive 才改变。
- 用以下顺序说明结果:A/1/save → A/3/delete → A/2/archive → A/3/delete。再加入 A/4/save,说明何时可以重新出现。
- 提交处理器、状态说明和测试;覆盖空输入、不同文档交错、重复事件、先收到删除再收到旧保存。不要实现真实网络请求。
查看思路
-
先确定比较单位是单篇文档,而不是整个事件流。A 的版本不能阻止 B 更新;每个 docId 都需要自己的最高版本与状态。由于题面保证同版本动作一致,重复事件可以忽略,不必发明冲突裁决规则。
-
手动走一遍示例:A/1 进入 inbox,A/3 变成 deleted;A/2 迟到时低于已知版本,不能让文章变成 archived;重复 A/3 不产生变化。随后 A/4/save 才使它回到 inbox。这条轨迹同时解释了为什么删除后仍要保留版本记录。
-
再构造最小反例:先收到 A/3/delete,再收到 A/1/save。若删除时直接抹掉全部状态,旧保存就会把文章错误恢复。把删除视为可比较版本的状态,才能拒绝迟到事件。这个结论只适用于题面给出的版本约束,不是现实同步系统的完整方案。
-
验证时比较输出与每篇文档最大版本事件决定的状态;把同一组事件换顺序、插入重复项,结果应保持一致。空输入得到空映射,不同文档可以独立更新。最后说明永久保存删除记录的空间代价;本题没有提供安全清理旧版本的条件,因此不应擅自丢弃它。
把浏览卡片和完成回顾分开
做一个假想的高亮回顾界面,组件接收卡片数组并允许空数组。主示例为三张卡片 A、B、C,初始显示 A,三张均未处理。卡片有原文摘录和来源标题,不要求连接 Reader。
- “上一条/下一条”只移动当前位置,到边界禁用,不循环;Keep 或 Discard 才是处理操作。成功后标记该卡片已处理,但保持当前位置,用户自行继续浏览。
- 用可控的异步保存模拟器返回成功或失败。保存期间禁用导航和两个处理按钮;成功后该卡片不能再次提交,失败后保持未处理并允许重试。进度只统计成功处理的不同卡片。
- 展示当前卡片、已处理数、保存中与错误反馈。按钮可用键盘操作;状态变化应能被辅助技术获知。
- 提交页面和交互测试:A 点 Keep 失败后重试成功,再到 B 点“下一条”到 C,最终应显示 C、已处理 1/3。另测连续双击、空数组和末尾导航;空数组应显示无内容状态,并禁用导航与处理操作。
查看思路
-
先区分当前位置、每张卡片的处理状态和当前请求状态。显示 C 不表示 A、B 已处理;导航与完成数如果共用一个计数器,就会在示例里误报进度。空输入时显示无内容状态并禁用操作,而不是访问不存在的第一张卡片。
-
按题面推演失败轨迹:A 的 Keep 请求失败后,进度仍为 0/3,卡片仍可处理;重试成功才变为 1/3。随后只用导航经过 B 到 C,进度不增加。把这个序列作为测试主线,比只测每个按钮是否能点击更容易发现状态混淆。
-
保存中禁用导航能避免请求完成时用户已经切换卡片的情况,但提交入口仍需检查是否已有请求,防止快速连点在按钮更新前发起两次保存。成功状态按卡片身份保存,不应按当前数组位置临时推断;失败时恢复可操作性并保留输入上下文。
-
用原生按钮和清楚的名称保证键盘可达,错误不要只用红色表达。通过可被读屏感知的状态区域宣布保存结果,同时保留合理焦点;不要因为更新进度就把焦点抢走。最后检验可重复的失败、成功和重复触发,说明禁用导航带来的等待成本,以及本题为何选择这个简化约束。