首页

前端多实例数据同步

起因

写扩展的时候遇到个头疼的问题:一个扩展有好几个独立的 JavaScript 环境在同时运行。
比如:
background script 在后台跑
好几个 content script 注入到不同网页里
用户点出来的 popup 窗口
设置页面 options page
这些环境都是隔离的,但数据需要共享。用户在 popup 改了个设置,所有 content script 要立刻知道;background 在处理的任务,其他实例也要能感知到。

传统的做法

1. 用扩展存储

javascript
// background 里存数据 chrome.storage.local.set({ data: { count: 1 } }); // content script 监听变化 chrome.storage.onChanged.addListener((changes) => { console.log('数据变了', changes.data); });

2. 用消息传递

javascript
// popup 发消息 chrome.runtime.sendMessage({ type: 'UPDATE_DATA', data: { count: 1 } }); // background 收到后转发给所有人 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { // 转发给所有 content scripts });
但这些办法都不太好用:
消息传递要自己管理路由,很容易漏掉某些实例
没有版本控制,几个实例同时写数据会冲突
改个嵌套对象不会自动触发同步
最坑的是回响问题:实例A写入→收到自己写入的事件→再次写入→死循环

我的解决思路

核心是三个东西:版本控制 + 响应式代理 + 适配器模式

版本控制解决冲突

给每个数据加上版本号和写入者标识:
typescript
interface VersionedStorage { value: T; version: number; writerId: string; }
多个实例同时写的时候,版本号大的胜出,版本号小的丢弃。如果版本号一样,就用 writerId 来决定。

响应式代理实现深度监听

用 Vue 的 customRef​ + Proxy​ 做到深度响应式:
typescript
const data = reactiveStorage({ items: [] }); data.items.push('新item'); // 自动同步,不用手动调用存储接口

回响问题的解决

回响问题特别烦人:
1.
popup 修改数据 → 写入存储
2.
所有实例(包括 popup 自己)收到变化事件
3.
popup 处理事件 → 再次写入存储
4.
又触发事件 → 死循环
解决办法是每个实例都有个唯一标识,通过版本号过滤自己的变更:
typescript
class ExtensionStorageSync { private instanceId = Math.random().toString(36).substring(2, 11); private currentVersion = 0; handleStorageChange(changes, areaName) { const newData = changes.data?.newValue; if (!newData) return; // 关键:只处理其他实例的变更 if (newData.version > this.currentVersion) { this.currentVersion = newData.version; this.updateLocalData(newData.value); } // 如果是自己写入的,version <= currentVersion,直接忽略 // 这样就不会回响了 } writeData(value) { const newVersion = this.currentVersion + 1; const versionedValue = { value, version: newVersion, writerId: this.instanceId }; // 先更新本地版本号(这步很重要!) this.currentVersion = newVersion; // 再写入存储 chrome.storage.local.set({ data: versionedValue }); } }
这样即使收到自己写入的事件,也会因为版本号一样被忽略。

适配器模式支持多种存储

任何存储源只要实现三个方法就行:
typescript
interface StorageAdapter { getValue(): Promise>; setValue(value: VersionedStorage): void; watch(callback: Function): () => void; }
然后通用逻辑就可以在所有存储源上工作。

实际用起来

API 统一

不管用什么存储,用法都一样:
typescript
// background script const globalState = useReactiveStorage(wxtAdapter, { enabled: true, tasks: [], settings: { theme: 'dark' } }); // content script const state = useReactiveStorage(wxtAdapter, { fallback: defaultState }); // 自动和 background 同步 // popup const state = useReactiveStorage(wxtAdapter, { fallback: defaultState }); // 用户改设置,所有实例立即更新

可以支持不同存储方式

小数据 - localStorage
扩展数据 - WXT 存储,跨扩展上下文同步
云端数据 - 自行实现适配器支持用户数据存在在服务器

核心思路

像用普通 vue ref 变量一样,不用管同步细节。这个方案不只适用于扩展,稍微改改就能用在其他需要多实例数据同步的场景,比如 Web Workers、多窗口应用,甚至是简单的分布式应用。