前端多实例数据同步
起因
写扩展的时候遇到个头疼的问题:一个扩展有好几个独立的 JavaScript 环境在同时运行。
好几个 content script 注入到不同网页里
这些环境都是隔离的,但数据需要共享。用户在 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'); // 自动同步,不用手动调用存储接口
回响问题的解决
解决办法是每个实例都有个唯一标识,通过版本号过滤自己的变更:
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 });
// 用户改设置,所有实例立即更新
可以支持不同存储方式
云端数据 - 自行实现适配器支持用户数据存在在服务器
核心思路
像用普通 vue ref 变量一样,不用管同步细节。这个方案不只适用于扩展,稍微改改就能用在其他需要多实例数据同步的场景,比如 Web Workers、多窗口应用,甚至是简单的分布式应用。