真实未读状态必须由服务端接口返回,data-read仅是HTML映射;localStorage仅作瞬时缓存,不可替代后端同步;CSS用[data-read="0"]控制显隐更稳定;左滑标记已读需阈值判断与服务端回写;未读数必须调接口获取,禁止DOM计数。

真实未读状态必须由服务端接口返回,data-read 属性只是它的 HTML 显性映射;前端用 localStorage 缓存 ID 列表仅用于加载瞬时体验,不能替代后端状态同步。
用 data-read 控制样式而非 class 切换
CSS 选择器依赖属性值做显隐控制,比操作 classList 更稳定,尤其在 Vue/React 的 diff 过程中不会因 key 变化或重渲染丢失状态。
-
data-read值必须是字符串"0"或"1",不能是布尔false或数字0,否则属性会被框架自动移除 - 服务端模板中直接输出
<li data-read="0">,纯前端渲染时用el.dataset.read = "0"注入 - CSS 示例:
[data-read="0"] .badge { display: block; }、[data-read="1"] .badge { display: none; } - 禁止用
el.remove()或清空innerHTML来“隐藏未读项”,这会破坏滚动位置和无障碍语义
左滑标记已读必须带位移阈值与服务端回写
纯手势交互容易误触,且滑动完成 ≠ 状态变更完成。关键不是“怎么滑”,而是“滑多少才认”和“滑完怎么同步”。
- 监听
touchstart/touchmove/touchend,记录起始 X 坐标,计算deltaX = currentX - startX - 设定有效左滑阈值(如
-80),仅当deltaX < -80且方向稳定时触发markAsRead(id) -
markAsRead必须先发请求到后端(如POST /api/messages/123/read),成功后再更新dataset.read和localStorage - 滑动中用
transform: translateX(${deltaX}px)提供视觉反馈,松手后补transition回弹或滑出,避免卡顿感
localStorage 只存 ID 和时间戳,不存正文或大字段
它不是数据库,是轻量缓存层。存错结构会导致内存膨胀、解析失败、跨设备不同步。
立即学习“前端免费学习笔记(深入)”;
- 只缓存必要字段:例如
{id: 123, time: "2026-04-30T14:22"},绝不塞content或 base64 图片 - 写入前做截断:
JSON.stringify(msgs.slice(-20)),防止无限累积 - 读取加兜底:
JSON.parse(localStorage.getItem('unreadMsgs') || '[]'),避免null报错 - 检测兼容性:
if (typeof localStorage !== 'undefined'),降级到内存数组const cache = []
最容易被忽略的是:右上角铃铛里的未读数字,永远不能靠前端数 DOM 上 data-read="0" 的个数来显示——用户可能已在其他设备点开、后端延迟落库、甚至消息本身已被撤回。每次进入页面或下拉刷新,都得调一次 /api/messages/unread-count 接口拿真实值。



















