平均一台笔记本同时开着30个标签页,你的应用只是其中之一。用户打开它,切去Slack读消息,15分钟后回来,却忘了哪个标签是你的。如果标题还是"My App",图标还是那个灰色方块,这15分钟就白等了——新消息来了、构建完成了、上传结束了,用户一无所知。
浏览器其实给了你一套小而强的注意力召回工具:标签标题、favicon、可见性状态、焦点事件、系统通知。配置得当,一个非活动标签可以显示"(3) New messages — Acme Chat",favicon闪烁红色徽章,隐藏时暂停昂贵的轮询,回来时立即刷新,紧急事件触发原生系统通知。配置不当,同样的代码会泄漏事件监听器、与React渲染周期冲突、在SSR首屏就报hydration不匹配。
本文梳理六个构建注意力感知UI的原语,全部基于ReactUse的专注型钩子。每个原语先看手动实现、再看陷阱、最后看封装好的钩子。文末会把六个原语组合成一个聊天标签组件,行为逼近原生应用。
一、标签标题作为通知渠道
元素是网页上最被低估的通知界面。Gmail、GitHub、Linear、Discord都在用:前导的(N)计数或•圆点,让你不用切标签就知道有事发生。实现只有一行——document.title = "..."——但在React组件里用错方式,标题会被最后一帧渲染的内容卡住,即使组件已卸载。</p>
手动实现的问题很隐蔽:previous变量捕获的是effect执行瞬间的标题,如果父组件在渲染间隙改了标题,cleanup函数恢复的就是个过期值。修复方案要么统一标题的数据源,要么干脆不写cleanup让下一帧覆盖。多数应用选了后者,然后忘了写cleanup,六个月后有人开启React StrictMode,effect跑两遍,就曝出了标题卡死的bug。
ReactUse的useTitle接管了这些细节:传入字符串,自动同步到document.title,组件卸载时恢复原标题。无需手动管理previous,无需担心StrictMode的双重执行。
二、favicon动态徽章
标题之外,favicon是第二个视觉锚点。用户可能开着十几个标签,标题被截断成"Ac...",但16×16的图标始终可见。动态favicon可以画数字徽章、变色闪烁、甚至简单动画——全部用Canvas生成Data URL替换的href。
手动实现要处理:Canvas生成、多尺寸图标适配、原始favicon备份、组件卸载恢复、内存泄漏防护。ReactUse的useFavicon把这一堆收进一个字符串参数,内部处理所有副作用清理。
三、页面可见性与焦点状态
Page Visibility API和window的focus/blur事件,让标签知道自己是前台还是后台。关键用途:后台时暂停setInterval轮询、暂停视频/动画节省资源、回来时刷新数据。但这两个API有微妙差异——visibilitychange在标签被遮挡时触发(即使窗口仍有焦点),focus/blur只在窗口层级触发。混用容易重复触发或遗漏。
ReactUse提供usePageLeave(鼠标离开页面区域)、useWindowFocus(窗口焦点状态)、useDocumentVisibility(文档可见性)三个粒度不同的钩子,按需取用。
四、系统通知
Notification API是最后手段——需要用户授权,且现代浏览器对非安全上下文(非HTTPS/localhost)直接拒绝。但一旦获得权限,这是唯一能突破浏览器边界的注意力召回方式。实现要点:权限请求时机(不能页面加载就弹)、通知点击行为(聚焦标签还是打开新窗口)、通知去重(相同内容不要堆叠)。
useNotification封装了权限检查和通知创建,返回一个触发函数,调用前自动校验权限状态。
五、组合实战:聊天标签组件
把六个原语串起来:未读消息驱动标题前缀计数;紧急消息触发favicon红闪;标签隐藏时暂停消息轮询、改为WebSocket推送;切回前台时拉取离线期间的消息;@提及触发系统通知;通知点击聚焦标签并滚动到对应消息。
代码结构:一个主组件用useTitle、useFavicon、useDocumentVisibility、useWindowFocus、useNotification,visibility为hidden时调clearInterval暂停轮询,visible时立即fetch,window聚焦时额外做一次刷新防数据过期。
这套组合的边界情况:SSR时document不存在,所有钩子内部要判断typeof document !== "undefined";StrictMode下effect双重执行,清理函数必须幂等;用户拒绝通知权限时优雅降级,不阻塞核心功能。
浏览器标签页的竞争本质是注意力经济的微观战场。30个标签在抢同一块屏幕空间,你的应用要么主动召回用户,要么被永久遗忘。ReactUse这类钩子库的价值,是把"能工作"的代码变成"能放心用"的代码——副作用清理、边界情况、框架兼容,全部封装在声明式接口之下。
热门跟贴