前端多实例数据同步

起因

写扩展的时候遇到个头疼的问题:一个扩展有好几个独立的 JavaScript 环境在同时运行。

比如:

  • background script 在后台跑
  • 好几个 content script 注入到不同网页里
  • 用户点出来的 popup 窗口
  • 设置页面 options page

这些环境都是隔离的,但数据需要共享。用户在 popup 改了个设置,所有 content script 要立刻知道;background 在处理的任务,其他实例也要能感知到。

github 代码实例

传统的做法

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'); // 自动同步,不用手动调用存储接口

回响问题的解决

回响问题特别烦人:

  1. popup 修改数据 → 写入存储
  2. 所有实例(包括 popup 自己)收到变化事件
  3. popup 处理事件 → 再次写入存储
  4. 又触发事件 → 死循环

解决办法是每个实例都有个唯一标识,通过版本号过滤自己的变更:

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、多窗口应用,甚至是简单的分布式应用。

  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    736 引用 • 1307 回帖 • 2 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...