[{"data":1,"prerenderedAt":331},["ShallowReactive",2],{"content:\u002Fposts\u002F2025\u002Fbuilding-a-pinia-plugin-for-cross-tab-state-syncing":3,"surround:\u002Fposts\u002F2025\u002Fbuilding-a-pinia-plugin-for-cross-tab-state-syncing":320},{"id":4,"title":5,"body":6,"canonical":19,"categories":291,"date":293,"description":294,"draft":295,"extension":296,"image":297,"meta":298,"navigation":302,"path":303,"permalink":304,"readingTime":305,"recommend":304,"references":304,"seo":310,"sitemap":311,"stem":314,"tags":315,"type":304,"updated":318,"__hash__":319},"content\u002Fposts\u002F2025\u002Fbuilding-a-pinia-plugin-for-cross-tab-state-syncing.md","【翻译】构建 Pinia 插件进行跨标签页状态同步",{"type":7,"value":8,"toc":271},"minimark",[9,23,26,30,33,37,40,56,60,63,66,80,84,87,99,102,107,110,121,127,134,137,143,150,153,159,162,179,182,188,192,195,201,204,207,210,224,228,231,245,248,262],[10,11,12],"auths-alex",{},[13,14,15,16],"p",{},"原文地址：",[17,18,22],"a",{"href":19,"rel":20},"https:\u002F\u002Falexop.dev\u002Fposts\u002Fbuilding-pinia-plugin-cross-tab-sync\u002F",[21],"nofollow","Building a Pinia Plugin for Cross-Tab State Syncing",[24,25],"hr",{},[27,28,29],"h2",{"id":29},"简介",[13,31,32],{},"在现代 Web 应用程序中，用户经常在打开多个浏览器选项卡的情况下工作。当使用 Pinia 进行状态管理时，我们有时需要确保一个选项卡中的状态变化反映在我们应用程序的所有打开实例中。这篇文章将指导你创建一个插件，将跨表状态同步添加到你的 Pinia store 中。",[27,34,36],{"id":35},"了解-pinia-插件","了解 Pinia 插件",[13,38,39],{},"Pinia 插件是扩展 Pinia Stores 功能的函数。插件是强大的工具，可帮助：",[41,42,43,47,50,53],"ul",{},[44,45,46],"li",{},"减少代码重复",[44,48,49],{},"跨 Stores 添加可重复使用的功能",[44,51,52],{},"保持 Store 定义简洁明了",[44,54,55],{},"实现横切关注点 （Implement cross-cutting concerns）",[27,57,59],{"id":58},"不同标签页通过-broadcastchannel-通信","不同标签页通过 BroadcastChannel 通信",[13,61,62],{},"BroadcastChannel API 提供了一种在同一来源的不同浏览器上下文（选项卡、窗口或 iframe）之间发送消息的简单方法。它非常适合我们在选项卡之间同步状态的使用案例。",[13,64,65],{},"BroadcastChannel 的主要特点：",[41,67,68,71,74,77],{},[44,69,70],{},"浏览器内置 API",[44,72,73],{},"同源安全模型",[44,75,76],{},"简单的发布\u002F订阅消息收发模式",[44,78,79],{},"无需外部依赖",[27,81,83],{"id":82},"broadcastchannel-的工作原理","BroadcastChannel 的工作原理",[13,85,86],{},"BroadcastChannel API 的运行原理很简单：任何浏览上下文（窗口、选项卡、iframe 或 worker）都可以通过创建具有相同频道名称的 BroadcastChannel 对象来加入频道。加入后：",[88,89,90,93,96],"ol",{},[44,91,92],{},"使用 postMessage（） 方法发送消息",[44,94,95],{},"消息是通过 onmessage 事件处理程序接收的",[44,97,98],{},"上下文可以使用 close（） 方法离开通道",[27,100,101],{"id":101},"实现插件",[103,104,106],"h3",{"id":105},"store-配置","Store 配置",[13,108,109],{},"要使用我们的插件，store 需要通过配置选择加入状态共享：",[111,112,118],"pre",{"className":113,"code":115,"language":116,"meta":117},[114],"language-ts","import { defineStore } from 'pinia'\nimport { computed, ref } from 'vue'\n\nexport const useCounterStore = defineStore('counter', () => {\n    const count = ref(0)\n    const doubleCount = computed(() => count.value * 2)\n\n    function increment() {\n        count.value++\n    }\n\n    return {\n        count,\n        doubleCount,\n        increment\n    }\n},\n{\n    share: {\n        enable: true,\n        initialize: true,\n    },\n})\n","ts","",[119,120,115],"code",{"__ignoreMap":117},[13,122,123,126],{},[119,124,125],{"code":125},"share"," 选项启用跨 Tab 同步，并控制存储是否应从其他选项卡初始化其状态。",[103,128,130,131],{"id":129},"插件注册-maints","插件注册 ",[119,132,133],{"code":133},"main.ts",[13,135,136],{},"在创建 Pinia 实例时注册插件：",[111,138,141],{"className":139,"code":140,"language":116,"meta":117},[114],"import { createPinia } from 'pinia'\n\nimport { PiniaSharedState } from '.\u002Fplugin\u002Fplugin'\n\nconst pinia = createPinia()\npinia.use(PiniaSharedState)\n",[119,142,140],{"__ignoreMap":117},[103,144,146,147],{"id":145},"插件实现-pluginplugints","插件实现 ",[119,148,149],{"code":149},"plugin\u002Fplugin.ts",[13,151,152],{},"这是我们支持 TypeScript 的完整插件实现：",[111,154,157],{"className":155,"code":156,"language":116,"meta":117},[114],"import type { DefineStoreOptions, PiniaPluginContext, StateTree } from 'pinia'\n\ninterface Serializer\u003CT extends StateTree> {\n    serialize: (value: T) => string\n    deserialize: (value: string) => T\n}\n\ninterface BroadcastMessage {\n    type: 'STATE_UPDATE' | 'SYNC_REQUEST'\n    timestamp?: number\n    state?: string\n}\n\ninterface PluginOptions\u003CT extends StateTree> {\n    enable?: boolean\n    initialize?: boolean\n    serializer?: Serializer\u003CT>\n}\n\nexport interface StoreOptions\u003CS extends StateTree = StateTree, G = object, A = object>\n    extends DefineStoreOptions\u003Cstring, S, G, A> {\n    share?: PluginOptions\u003CS>\n}\n\n\u002F\u002F Add type extension for Pinia\ndeclare module 'pinia' {\n    export interface DefineStoreOptionsBase\u003CS, Store> {\n        share?: PluginOptions\u003CS>\n    }\n}\n\nexport function PiniaSharedState\u003CT extends StateTree>({\n    enable = false,\n    initialize = false,\n    serializer = {\n        serialize: JSON.stringify,\n        deserialize: JSON.parse,\n    },\n}: PluginOptions\u003CT> = {}) {\n    return ({ store, options }: PiniaPluginContext) => {\n        if (!(options.share?.enable ?? enable))\n            return\n\n        const channel = new BroadcastChannel(store.$id)\n        let timestamp = 0\n        let externalUpdate = false\n\n        \u002F\u002F Initial state sync\n        if (options.share?.initialize ?? initialize) {\n            channel.postMessage({ type: 'SYNC_REQUEST' })\n        }\n\n        \u002F\u002F State change listener\n        store.$subscribe((_mutation, state) => {\n            if (externalUpdate)\n                return\n\n            timestamp = Date.now()\n            channel.postMessage({\n                type: 'STATE_UPDATE',\n                timestamp,\n                state: serializer.serialize(state as T),\n            })\n        })\n\n        \u002F\u002F Message handler\n        channel.onmessage = (event: MessageEvent\u003CBroadcastMessage>) => {\n            const data = event.data\n            if (\n                data.type === 'STATE_UPDATE'\n                && data.timestamp\n                && data.timestamp > timestamp\n                && data.state\n            ) {\n                externalUpdate = true\n                timestamp = data.timestamp\n                store.$patch(serializer.deserialize(data.state))\n                externalUpdate = false\n            }\n\n            if (data.type === 'SYNC_REQUEST') {\n                channel.postMessage({\n                    type: 'STATE_UPDATE',\n                    timestamp,\n                    state: serializer.serialize(store.$state as T),\n                })\n            }\n        }\n    }\n}\n",[119,158,156],{"__ignoreMap":117},[13,160,161],{},"该插件的工作原理是：",[88,163,164,167,170,173,176],{},[44,165,166],{},"为每个 store 创建一个 BroadcastChannel",[44,168,169],{},"订阅存储更改和广播更新",[44,171,172],{},"处理来自其他标签页的传入消息",[44,174,175],{},"使用时间戳来阻止更新周期",[44,177,178],{},"支持复杂状态的自定义序列化",[103,180,181],{"id":181},"通信流程图",[13,183,184],{},[185,186],"img",{"alt":117,"src":187},"https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002F67aedc0a7d20d-78.webp",[27,189,191],{"id":190},"使用同步-store","使用同步 Store",[13,193,194],{},"组件可以像任何其他 Pinia store 一样使用 synchronized store：",[111,196,199],{"className":197,"code":198,"language":116,"meta":117},[114],"import { useCounterStore } from '.\u002Fstores\u002Fcounter'\n\nconst counterStore = useCounterStore()\n\u002F\u002F State changes will automatically sync across tabs\ncounterStore.increment()\n",[119,200,198],{"__ignoreMap":117},[27,202,203],{"id":203},"结论",[13,205,206],{},"通过这个 Pinia 插件，我们添加了 cross-tab state synchronization，只需最少的配置。该解决方案是轻量级的、类型安全的，并利用内置的 BroadcastChannel API。对于用户经常跨多个选项卡工作并需要一致的状态体验的应用程序，此模式特别有用。",[13,208,209],{},"使用此插件时，请记住考虑以下事项：",[41,211,212,215,218,221],{},[44,213,214],{},"仅为真正需要的 Store 启用共享",[44,216,217],{},"注意大型 state 对象的性能",[44,219,220],{},"考虑对复杂数据结构进行自定义序列化",[44,222,223],{},"在不同的浏览器场景中全面测试",[27,225,227],{"id":226},"未来优化web-worker","未来优化：Web Worker",[13,229,230],{},"对于具有大量交叉表通信或复杂状态转换的应用程序，请考虑将 BroadcastChannel 处理卸载到 Web Worker。此方法可以通过以下方式提高性能：",[41,232,233,236,239,242],{},[44,234,235],{},"将消息处理移出主线程",[44,237,238],{},"在不阻塞 UI 的情况下处理复杂的状态转换",[44,240,241],{},"在同步大型状态对象时减少主线程负载",[44,243,244],{},"缓冲和批处理状态更新以获得更好的性能",[13,246,247],{},"这在以下情况下特别有用：",[41,249,250,253,256,259],{},[44,251,252],{},"您的应用程序同时打开了多个选项卡",[44,254,255],{},"状态更新很频繁或计算密集型",[44,257,258],{},"您需要对同步数据执行验证或转换",[44,260,261],{},"应用程序处理需要同步的大型数据集",[13,263,264,265,270],{},"您可以在 ",[17,266,269],{"href":267,"rel":268},"https:\u002F\u002Fgithub.com\u002Falexanderop\u002FpluginPiniaTabs",[21],"GitHub 存储库","中找到此插件的完整代码。它还提供了如何将它与 Web Worker 一起使用的示例。",{"title":117,"searchDepth":272,"depth":272,"links":273},4,[274,276,277,278,279,288,289,290],{"id":29,"depth":275,"text":29},2,{"id":35,"depth":275,"text":36},{"id":58,"depth":275,"text":59},{"id":82,"depth":275,"text":83},{"id":101,"depth":275,"text":101,"children":280},[281,283,285,287],{"id":105,"depth":282,"text":106},3,{"id":129,"depth":282,"text":284},"插件注册 main.ts",{"id":145,"depth":282,"text":286},"插件实现 plugin\u002Fplugin.ts",{"id":181,"depth":282,"text":181},{"id":190,"depth":275,"text":191},{"id":203,"depth":275,"text":203},{"id":226,"depth":275,"text":227},[292],"译","2025-02-14 11:32:59","创建一个 Pinia 插件，使用 BroadcastChannel API 实现跨浏览器选项卡的状态同步。该插件允许您标记特定存储以进行跨 Tab 同步，并通过基于时间戳的冲突解决自动处理状态更新。",false,"md","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002F67aed7b7cb893-78.webp",{"aside":299},[300,301],"toc","auth-alex",true,"\u002Fposts\u002F2025\u002Fbuilding-a-pinia-plugin-for-cross-tab-state-syncing",null,{"text":306,"minutes":307,"time":308,"words":309},"7 min read",6.395,383700,1279,{"title":5,"description":294},{"loc":303,"images":312},[313],{"loc":187},"posts\u002F2025\u002Fbuilding-a-pinia-plugin-for-cross-tab-state-syncing",[316,317],"Pinia","Vue","2026-02-07 23:23:50","-yMgbBj0DpvwG-bFfYc_tNXxVJVz2H81Qoz7WsA8-WY",[321,326],{"title":322,"path":323,"stem":324,"date":325,"type":304,"children":-1},"Drizzle ORM 使用","\u002Fposts\u002F2025\u002Fdrizzle.orm-use","posts\u002F2025\u002Fdrizzle.orm-use","2025-02-12 10:35:29",{"title":327,"path":328,"stem":329,"date":330,"type":304,"children":-1},"【翻译】如何选择 Vue 项目架构","\u002Fposts\u002F2025\u002Fhow-to-structure-vue-projects","posts\u002F2025\u002Fhow-to-structure-vue-projects","2025-02-14 14:18:31",1789642590353]