首页 / 资讯中心 / 文章详情

在真实应用中使用 react-broadcast-sync:仪表盘在标签页间进行筛选

在真实应用中使用 react-broadcast-sync:仪表盘在标签页间进行筛选 ★ FEATURED ARTICLE
在第 1 部分中我们发现了问题React 应用会在浏览器标签页之间悄然失去同步。在第 2 部分中我们构建了抽象层。现在让我们真正用起来。想象一个仪表盘在两个标签页中同时打开。你在其中一个里改了状态筛选条件切到另一个它仍然显示旧视图。数据来自同一个 API但每个标签页都持有自己的一份 UI 状态副本。让我们用react-broadcast-sync来修复它安装、一个真实示例、我踩过的坑以及几个值得了解的选项。完整的选项参考在 README 里这里不再重复。1. 安装 react-broadcast-syncnpm install react-broadcast-sync除非你设置telemetry: true否则使用遥测是关闭的。无论哪种方式同步功能都一样工作。有两种接入方式。useBroadcastChannel(channelName, options)是一个给需要自己专属 channel 的单个组件使用的 hook。BroadcastProvider只调用一次这个 hook并通过 context 共享结果这样任何后代组件都可以用useBroadcastProvider()读取它。我在接近根部的地方使用 provider作为一个共享的应用级 channel。import { BroadcastProvider } from react-broadcast-sync; import Dashboard from ./Dashboard; export default function App() { return ( BroadcastProvider channelNamedashboard Dashboard / /BroadcastProvider ); }Dashboard是你自己的组件。在它内部postMessage(type, payload)负责发送传入的 payload 以message.message形式到达。在同一个源origin的两个标签页中打开应用你们就连上了。2. 一个真实世界的模式仪表盘筛选仪表盘有两个控件条目状态和日期范围。一个标签页里的改动应该更新另一个。我每次都会发送完整的选中值这样接收方应用一个对象就够了而不用从各个字段变更中重新拼装。我把筛选条件的形状、默认值和消息名放在一个地方export type Filters { status: all | open | closed; range: 7d | 30d; }; export const initialFilters: Filters { status: all, range: 7d }; export const FILTERS_APPLY filters/apply;这个库的postMessage接收一个字符串 type 和一个 payload。它不会推断出类型化协议TypeScript 类型也不会在运行时校验传入数据所以接收方要自己检查值。下面是App.tsx里的完整功能。channel 是dashboard命名空间是filters-v1拼接成dashboard-filters-v1。每个参与的标签页都使用相同的一对。import { useState } from react; import { BroadcastProvider, useBroadcastProvider } from react-broadcast-sync; import { FILTERS_APPLY, initialFilters } from ./filters; import type { Filters } from ./filters; export default function App() { const [filters, setFilters] useStateFilters(initialFilters); return ( BroadcastProvider channelNamedashboard options{{ namespace: filters-v1, registeredTypes: [FILTERS_APPLY], onMessage: { [FILTERS_APPLY]: message { const next message.message; if ( next (next.status all || next.status open || next.status closed) (next.range 7d || next.range 30d) ) { setFilters({ status: next.status, range: next.range }); } }, }, }} FilterPanel filters{filters} onApply{setFilters} / /BroadcastProvider ); } function FilterPanel({ filters, onApply }: { filters: Filters; onApply: (next: Filters) void; }) { const { postMessage, error } useBroadcastProvider(); function applyFilters(next: Filters) { onApply(next); postMessage(FILTERS_APPLY, next); } return ( section h1Dashboard filters/h1 label Status select value{filters.status} onChange{event { const status event.target.value; if (status all || status open || status closed) { applyFilters({ ...filters, status }); } }} option valueallAll/option option valueopenOpen/option option valueclosedClosed/option /select /label label Date range select value{filters.range} onChange{event { const range event.target.value; if (range 7d || range 30d) { applyFilters({ ...filters, range }); } }} option value7dLast 7 days/option option value30dLast 30 days/option /select /label pShowing {filters.status} items for {filters.range}./p {error p rolealert{error}/p} /section ); }这个流程有两个入口。用户改了一个控件applyFilters先更新当前标签页然后发送这份选择。另一个标签页发来一份选择onMessage先校验它再更新当前标签页。我在用户操作里广播并让传入的处理器只专注于应用状态这样收到的变更永远不会被再次发送。Tab A: user changes a filter | | applyFilters: update local state, then postMessage(filters/apply, next) v BroadcastChannel dashboard-filters-v1 | v Tab B: onMessage validates the payload, then setFilters(next)通过 channel 传输的是 UI 状态的快照完整的筛选选择而不是一长串字段编辑。在真实的仪表盘中filters还会喂给你的查询或图表 props。示例停在 UI 层是为了让同步代码保持可见。筛选条件很少是仪表盘唯一要同步的东西。一旦第二个功能需要跨标签页通信你就得决定两者如何共享一个 channel。Channel、namespace、registeredTypes当第二个功能出现时比如一个主题切换你有三种工具它们各司其职。channel 名加上namespace决定哪些标签页和 hook 互相通信解析后的 BroadcastChannel 名称是channelName-namespace所以dashboard加filters-v1就变成dashboard-filters-v1。registeredTypes只在接收时过滤未列出的类型在到达后被丢弃。如果这些类型属于一个功能、且由相同的组件使用那就保持一个 channel 并设置registeredTypes。如果这些功能相互独立监听器或选项不同那就给每个功能自己的 channel 或 namespace。如果解析出的名称只有一个、但每个功能各有一个 hook浏览器仍然会把每条消息投递给两个连接只是被忽略的那些会在 JavaScript 里被丢弃。而独立的 channel 根本不会投递它们。3. 踩过的坑下面的浏览器观察来自在 React StrictMode 下、两个 Chrome 标签页中的测试。它们描述的是被测环境不代表所有浏览器或应用。广播不是持久化后打开的标签页什么也收不到——在它连接之前发送的内容都不会到达。在我的测试中第三个标签页以应用默认值开始。请从你的常规数据源加载当前选择。先本地应用再广播一次发送方 hook 不会收到它自己发出的消息所以要先更新本地状态。永远不要重新广播你刚收到的内容。让sourceName自动生成在我的测试中两个名字相同的标签页会互相忽略。投递顺序不是冲突策略两次几乎同时发生的编辑可能让不同标签页持有不同的最终值。对于业务记录服务器保持权威。过期不是应用状态过期的通知会从接收方的messages中消失但它不会撤销你的onMessage已经设置的状态而且它仍保留在发送方的列表里。expirationDuration: 0表示不过期。路由不是安全namespace和registeredTypes决定哪些消息到达。它们不校验 payload也不认证发送方。请保留运行时检查并且永远不要把一个 channel 消息当作服务器操作的授权。4. 值得了解的选项README 列出了每个选项及其默认值。下面这些是这个仪表盘真正用到的namespace追加到 channel 名后面channelName-namespace用于分离功能或协议版本。每个标签页都用相同的一对。registeredTypes在未列出的传入类型到达messages或onMessage之前就丢弃它们。它只在接收时过滤。我在每个功能 channel 上都设置了它。onMessage处理被接受的消息可以是单个函数也可以是以 type 为键的映射。它接收信封对象。React 可能还没渲染出新的messages所以要用参数。keepLatestMessage跨所有类型只保留一个槽位所以另一种类型的消息会替换掉前一条。适合单一最新值流比如主题不适合筛选条件加通知共用一个 provider 的情况。要实现按类型保留最新用registeredTypes或独立 channel。telemetry默认关闭。省略它保持使用事件不开启只有想主动加入时才设置telemetry: true。两种方式同步功能都一样。postMessage上的expirationDuration用于短命通知。使用一个正的毫秒数。时间类选项cleaningInterval、cleanupDebounceMs、deduplicationTTL、batchingDelayMs、excludedBatchMessageTypes可以等你有了具体理由再碰。对我来说默认值就够了。如果你想碰它们有一点要知道一个为正的、且大于cleaningInterval的cleanupDebounceMs可能会无限期推迟清理。配方让其他标签页刷新这是一种模式不是库的功能。保存一条记录后把它的 ID 作为提示发送出去。接收方校验它然后通过你的数据层重新拉取。channel 负责传递提示服务器负责提供真相。const { postMessage } useBroadcastChannel(records, { registeredTypes: [record/invalidated], onMessage: { record/invalidated: message { const value message.message; if (value typeof value.id string) { // 在这里调用你应用的重新拉取/失效函数。 } }, }, }); // 在你的保存处理器里在现有请求成功后 postMessage(record/invalidated, { id: record-42 });如果你用 TanStack Query那行注释就变成一行代码queryClient.invalidateQueries({ queryKey: [records, value.id] })。加分项如果由编码智能体来写你的代码这个包附带一个给编码智能体用的SKILL.mdagentskills.io 格式还有一个从 README 生成的llms.txt。不会自动安装任何东西。如果你使用智能体请自己把 skill 复制到你的项目里并检查你的智能体从哪里读取 skillsmkdir -p .agents/skills cp -r node_modules/react-broadcast-sync/skills/react-broadcast-sync .agents/skills/README 里有一个 Agent Skill 章节包含详细信息。把 README 和这篇文章指给你的智能体也很有帮助因为上面这些坑正是智能体最容易犯的错误。总结我们一开始是各自活在气泡里的标签页。修复方案结果很小UI 用本地 React 状态一个负责传递变更通知的 channel一个共享子树的 provider以及当功能不再共享协议时使用的独立 channel。校验传入的值先本地更新再发送并提前决定新标签页和冲突编辑应该如何表现——而不是假设 channel 会处理它们。至此这个系列完结问题、抽象层现在是用法。你的标签页不再需要被隔离了。在一个筛选栏上试试它然后告诉我哪些标签页开着、你发了什么、你期望另一个标签页做什么。完整的选项参考在 README 里。相关阅读延伸外链以下为推荐的相关技术教程来自致知笔记如何将笔记本电脑连接到外接显示器如何检查 IP 地址是静态还是动态Static/Dynamic IP如何在 Windows 11/10 中创建密码重置盘
阅读完成 · 觉得有帮助?
咨询建站