起因
写扩展的时候遇到个头疼的问题:一个扩展有好几个独立的 JavaScript 环境在同时运行。
比如:
- background script 在后台跑
- 好几个 content script 注入到不同网页里
- 用户点出来的 popup 窗口
- 设置页面 options page
这些环境都是隔离的,但数据需要共享。用户在 popup 改了个设置,所有 content script 要立刻知道;background 在处理的任务,其他实例也要能感知到。
传统的做法
1. 用扩展存储
// background 里存数据
chrome.storage.local.set({ data: { count: 1 } });
// content script 监听变化
chrome.storage.onChanged.addListener((changes) => {
console.log('数据变了', changes.data);
});
2. 用消息传递
// popup 发消息
chrome.runtime.sendMessage({ type: 'UPDATE_DATA', data: { count: 1 } });
// background 收到后转发给所有人
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 转发给所有 content scripts
});
但这些办法都不太好用:
- 消息传递要自己管理路由,很容易漏掉某些实例
- 没有版本控制,几个实例同时写数据会冲突
- 改个嵌套对象不会自动触发同步
- 最坑的是回响问题:实例 A 写入 → 收到自己写入的事件 → 再次写入 → 死循环
我的解决思路
核心是三个东西:版本控制 + 响应式代理 + 适配器模式。
版本控制解决冲突
给每个数据加上版本号和写入者标识:
interface VersionedStorage<T> {
value: T;
version: number;
writerId: string;
}
多个实例同时写的时候,版本号大的胜出,版本号小的丢弃。如果版本号一样,就用 writerId 来决定。
响应式代理实现深度监听
用 Vue 的 customRef + Proxy 做到深度响应式:
const data = reactiveStorage({ items: [] });
data.items.push('新item'); // 自动同步,不用手动调用存储接口
回响问题的解决
回响问题特别烦人:
- popup 修改数据 → 写入存储
- 所有实例(包括 popup 自己)收到变化事件
- popup 处理事件 → 再次写入存储
- 又触发事件 → 死循环
解决办法是每个实例都有个唯一标识,通过版本号过滤自己的变更:
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 });
}
}
这样即使收到自己写入的事件,也会因为版本号一样被忽略。
适配器模式支持多种存储
任何存储源只要实现三个方法就行:
interface StorageAdapter<T> {
getValue(): Promise<VersionedStorage<T>>;
setValue(value: VersionedStorage<T>): void;
watch(callback: Function): () => void;
}
然后通用逻辑就可以在所有存储源上工作。
实际用起来
API 统一
不管用什么存储,用法都一样:
// 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、多窗口应用,甚至是简单的分布式应用。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于