重要说明

现在不推荐用这种方式同步,太频繁,且不易控制
推荐使用主动方式去控制pinia状态同步,代码更简洁,且更易控制,性能也更好
electron多窗口,pinia状态同步,扩展store方法,主动同步pinia的状态【推荐】

更新说明

强烈建议拉gitee上的demo,参考博客演示的图片来学习,以仓库代码为主,多次调试更新可能博客内容更新并未同步

  • 建议pinia尽量存简单的状态,不要存数据量超大的数组/map 【集合】,因为每次多窗口状态都是全量同步!!

2023/7/3
1.修复高并发情况下的pinia死循环问题
2.自定义序列化逻辑,解决ts中map对象序列化后为 {} 的问题

2023/5/19 补充版本号重置的判断,解决版本号重置后,多个窗口 都与缓存版本一致,同时触发ipc调用,导致死循环的BUG

2023/5/12 调整版本号重置值为 1 ,解决缓存版本号为null,重置值为0,其他窗口被动更新触发状态修改监听,结果缓存值和store的版本值都为0,导致主动更新,旧值覆盖新值的情况

引入

pinia是vue3官方支持的全局状态管理工具,简单易用,但是electron的多窗口虽然加载的页面是单个路由,但其实已经是另一个浏览器,所有状态无法同步更新,接下来我们写一个pinia的插件结合ipc通信实现多窗口同步更新pinia的状态。

demo项目地址

实现效果展示

请添加图片描述

并发测试

请添加图片描述

map对象测试

ts中的map对象在序列化后会变成 {} ,这里重写序列化逻辑,解决这个问题
请添加图片描述

多属性修改同步测试

请添加图片描述

问题展示

我们创建两个窗口,其中一个窗口设置counterStore.counter 增加,另一个窗口并不会跟随变化!!

如下所示:

请添加图片描述

解决方案

思路整理

1.写一个pinia插件,拦截pinia的状态修改,并在状态对象初始化时补充一个版本状态【用来控制当前状态是否需要更新】

2.初始化pinia插件的时候添加ipcRender监听,如果状态改变了通知主进程,【并且修改版本号,版本号在缓存中也存一份】

3.主进程监听状态改变,如果状态改变就通知其他窗口同步更新

4.窗口同步更新时对比版本号,对 大于、小于、等于版本号分别处理

注意:引入版本号的主要原因是渲染进程监听状态改变,进行状态同步的时候,又会触发状态修改的事件,接着通知主进程进行同步,【循环处理,不过实测,当两次更新内容完全一致时,pinia就不会触发状态修改事件,但n个窗口仍会触发 n*(n-1)次状态修改!!】所以我们引入一个版本号在状态修改时判断是否需要通知主进程进行同步

1.主进程添加handle

首先我们在主进程中添加监听,处理pinia的状态变化,可以看到这里获取三个参数

  • storeName:更新的store的名称,对应defineStore()的第一个参数
  • jsonStr:json序列化后的变化对象

electron\main\index.ts

/**pinia多窗口共享 */
ipcMain.handle(
  "pinia-store-change",
  (event, storeName: string, jsonStr: string) => {
    // 遍历window执行
    for (const currentWin of BrowserWindow.getAllWindows()) {
      const webContentsId = currentWin.webContents.id;
      if (webContentsId !== event.sender.id) {
        currentWin.webContents.send("pinia-store-set", storeName, jsonStr);
      }
    }
  }
);

2.编写pinia插件

赶时间的话可直接用后面完善的pinia插件,这里主要是演示问题,解释为什么要用版本进行控制

pinia官网

可以看到pinia插件的说明

请添加图片描述

1.我们在src/store目录下新建plugins目录,然后创建一个shareStorePlugin.ts文件:

  • 我们在状态修改时通知主进程状态修改了,并且监听状态修改了,就把变化对象序列化为json字符串传输修改的内容

  • 我们在store更新的时候输出主动更新

  • 如果更新发生在ipc的监听中时,我们输出被动更新

src\store\plugins\shareStorePlugin.ts

import { ipcRenderer } from "electron";
import { PiniaPluginContext } from "pinia";

declare module "pinia" {
  export interface PiniaCustomProperties {
    storeUpdateVersion: number; // 标记store变更的版本
  }
}

// 处理electron多窗口,pinia共享问题
export function shareStorePlugin({ store }: PiniaPluginContext) {
  // 初始化本地缓存版本
  const storeName: string = store.$id;
  // 监听数据变化
  store.$subscribe(() => {
    console.log(`主动更新 ${storeName} 的状态`);
     // 通知主线程更新
    ipcRenderer.invoke(
      "pinia-store-change",
      storeName,
      JSON.stringify(store.$state)
    );
  });

 // 监听数据同步修改
  ipcRenderer.on(
    "pinia-store-set",
    (event, targetStoreName: string, jsonStr: string) => {
      // 监听到状态改变后,同步更新状态
      if (storeName === targetStoreName) {
        console.log("被动更新状态:" + storeName);

        const obj = JSON.parse(jsonStr);
        const keys = Object.keys(obj);
        const values = Object.values(obj);

        /// 更新各个key对应的值的状态
        for (let i = 0; i < keys.length; i++) {
          store.$state[keys[i]] = values[i];
        }
      }
    }
  );
}

2.在pinia中注册插件,我们调整src/store/index.ts:

import { createPinia } from "pinia";
import { shareStorePlugin } from "./plugins/shareStorePlugin";

const pinia = createPinia();

// 添加状态共享插件
pinia.use(shareStorePlugin);

export default pinia;

3.我们看一下测试效果

我们打开三个窗口,分别点击三个窗口中的counter自增,可以看到值已经能够正常同步,但有一个问题,当我们点击一个窗口的自增时,内容是这样的:

  • 被点击窗口 , 主动更新一次,被动更新两次
  • 其他窗口,被动更新一次,主动更新一次,被动更新一次

由此,我们可知,我们窗口被主进程通知更新状态的监听中,修改store的值,又会触发store值改变的事件,然后又会通知主进程告知其他窗口同步更新,但是当我们两次修改值完全一致时,则不会触发store值改变的事件!!

**注意:**也就是说,当前写法,如果我们有n个窗口,我们值修改一个值,在这个窗口中,这个值就会被动更新 n-1次,而主进程会被通知n次
请添加图片描述

3.完善pinia插件

虽然上面我们已经能够实现状态管理同步,但如果窗口很多的化,循环调用多次是很可怕的,所以我们使用一个版本号来控制状态更新。

  • 用版本号,而不是用 true/false变量来管控,主要考虑极端情况,窗口被动更新还没同步,又触发了主动更新时,方便根据版本号进行 状态同步的微调

  • 这里通过版本号来判断状态更新时,如果是被动更新导致的,则不需要通知主进程

  • 我们在ipcRender.invoke执行前输出 主动更新

  • 在ipcRender.on中输出 被动更新

import { ipcRenderer } from "electron";
import cacheUtils from "@/utils/cacheUtils";
import { PiniaPluginContext } from "pinia";

// 预设本地store版本缓存时间为50s  实际开发中可以设置很大,缓存时间的限制,目的是为了让版本归零,避免自增超过上限
const STORE_CACHE_TIME = 50;
// 设置本地store缓存的key
const STORE_CACHE_KEY_PREFIX = "store_";
const STORE_CACHE_VERSION_KEY_PREFIX = STORE_CACHE_KEY_PREFIX + "version_";
// 2023/5/19 补充版本号重置的判断,解决版本号重置后,多个窗口 都与缓存版本一致,同时触发ipc调用,导致死循环的BUG
let isResetVersion = false;
// 2023/07/03 补充是否能主动更新判断,解决多窗口高频率同时触发ipc调用,导致死循环的BUG
let canUpdate = true;

declare module "pinia" {
  export interface PiniaCustomProperties {
    storeUpdateVersion: number; // 标记store变更的版本
  }
}

/**获取本地缓存的store的修改版本 */
function getLocalStoreUpdateVersion(storeCacheKey: string) {
  let currentStoreUpdateVersion: number = cacheUtils.get(storeCacheKey);
  // 如果本地没有,就初始化一个
  if (
    currentStoreUpdateVersion === null ||
    currentStoreUpdateVersion === undefined
  ) {
    currentStoreUpdateVersion = 0;
    cacheUtils.set(storeCacheKey, currentStoreUpdateVersion, STORE_CACHE_TIME);
  }
  return currentStoreUpdateVersion;
}

// 处理electron多窗口,pinia共享问题
export function shareStorePlugin({ store }: PiniaPluginContext) {
  // 初始化本地缓存版本
  const storeName: string = store.$id;
  /// 缓存key
  const storeCacheVersionKey = STORE_CACHE_VERSION_KEY_PREFIX + storeName;
  let currentStoreUpdateVersion: number =
    getLocalStoreUpdateVersion(storeCacheVersionKey);
  // 初始化同步store版本
  store.storeUpdateVersion = currentStoreUpdateVersion;

  // 初始化store
  initStore(store);

  // 监听数据变化
  store.$subscribe(() => {
    // 获取本地存储的最新状态
    currentStoreUpdateVersion = cacheUtils.get(storeCacheVersionKey);
    /// 如果本地缓存过期,则重置一个缓存,并且通知主进程让其他窗口更新状态
    if (
      currentStoreUpdateVersion === null ||
      currentStoreUpdateVersion === undefined
    ) {
      currentStoreUpdateVersion = 0;
      store.storeUpdateVersion = currentStoreUpdateVersion;
      console.log(`主动更新 ${storeName} 的状态`);

      // 主动更新
      updateStoreSync(
        stringify(store.$state),
        storeName,
        store.storeUpdateVersion,
        true
      );
    } else {
      // 如果版本一致,则增加版本号,且更新本地存储版本 ,并且通知主线程告知其他窗口同步更新store状态
      if (store.storeUpdateVersion === currentStoreUpdateVersion) {
        if (!canUpdate) {
          canUpdate = true;
          return;
        }
        /// 补充版本号重置的判断,解决版本号重置后,多个窗口 都与缓存版本一致,同时触发ipc调用,导致死循环的BUG
        if (isResetVersion) {
          store.storeUpdateVersion = currentStoreUpdateVersion;
          isResetVersion = false;
        } else {
          store.storeUpdateVersion++;
          console.log(`主动更新 ${storeName} 的状态`);

          // 主动更新
          updateStoreSync(
            stringify(store.$state),
            storeName,
            store.storeUpdateVersion,
            false
          );
        }
      } else {
        // 如果当前store的版本大于本地存储的版本,说明本地版本重置了【过期重新创建】,此时重置store的版本
        // 如果当前store的版本小于本地存储的版本,说明是被动更新引起的state变动回调,此时仅更新版本即可
        store.storeUpdateVersion = currentStoreUpdateVersion;
        canUpdate = true;
      }
    }
  });

  // 监听数据同步修改
  ipcRenderer.on(
    "pinia-store-set",
    (
      event,
      targetStoreName: string,
      jsonStr: string,
      isReset: boolean,
      storeUpdateVersion: number
    ) => {
      console.log("被动更新哦");
      // 监听到状态改变后,同步更新状态
      if (storeName === targetStoreName) {
        // 补充版本号是否重置标识
        isResetVersion = isReset;
        console.log("被动更新状态:" + storeName);
        // 2023/07/03  会有本地存储的版本没有即时同步的情况,这里手动设置一遍 最新版本
        setStoreVersion(storeName, storeUpdateVersion);

        const obj = JSON.parse(jsonStr);
        const keys = Object.keys(obj);
        const values = Object.values(obj);
        canUpdate = false;

        /// 更新各个key对应的值的状态
        for (let i = 0; i < keys.length; i++) {
          changeState(store.$state, keys[i], values[i]);
        }
      }
    }
  );
}

/**
 * 状态更新同步
 * @param stateJsonStr 序列化的状态修改字符串
 * @param storeName  修改的状态的名称
 * @param storeUpdateVersion  状态修改的版本号
 * @param isResetVersion 是否重置了版本号
 */
function updateStoreSync(
  stateJsonStr: string,
  storeName: string,
  storeUpdateVersion: number,
  isResetVersion: boolean
) {
  // 更新本地缓存的store版本号
  setStoreVersion(storeName, storeUpdateVersion);

  // 通知主线程更新
  ipcRenderer.invoke(
    "pinia-store-change",
    storeName,
    stateJsonStr,
    isResetVersion,
    storeUpdateVersion
  );

  // 更新本地缓存的store
  cacheUtils.set(STORE_CACHE_KEY_PREFIX + storeName, stateJsonStr);
}

/**
 * 更新本地缓存的store版本号
 * @param storeName  更新的状态名称
 * @param storeUpdateVersion  状态修改的版本号
 */
function setStoreVersion(storeName: string, storeUpdateVersion: number) {
  // 更新本地缓存的store版本号
  const storeCacheVersionKey = STORE_CACHE_VERSION_KEY_PREFIX + storeName;
  cacheUtils.set(storeCacheVersionKey, storeUpdateVersion, STORE_CACHE_TIME);
}

/**
 * 修改state的值
 * 补充 如果反序列化的字段是map类型,需要额外处理
 */
function changeState(state: any, key: any, value: any) {
  if (state[key] instanceof Map) {
    if (value instanceof Array) {
      state[key] = new Map(value);
    } else {
      state[key] = new Map(Object.entries(value as object));
    }
  } else {
    state[key] = value;
  }
}


/**
 * 初始化状态对象
 * @param store
 */
function initStore(store: any) {
  const cacheKey = STORE_CACHE_KEY_PREFIX + store.$id;
  // 从本地缓存中读取store的值
  const stateJsonStr = cacheUtils.get(cacheKey);
  if (stateJsonStr) {
    const stateCache = JSON.parse(stateJsonStr);
    const keys = Object.keys(stateCache);
    const values = Object.values(stateCache);

    /// 更新各个key对应的值的状态
    for (let i = 0; i < keys.length; i++) {
      changeState(store.$state, keys[i], values[i]);
    }
  }
}

/**
 * 2023/07/03 自定义序列化方式, 处理ts中map类型/对象序列化后为 {} 的情况
 */
function stringify(obj: any): string {
  return JSON.stringify(cloneToObject(obj));
}

// 将字段包含map的对象转为json对象的格式
function cloneToObject(obj: any): any {
  let newObj: any = obj;
  if (obj instanceof Map) {
    return Object.fromEntries(obj);
  }
  if (obj instanceof Object) {
    newObj = {};
    const keys = Object.keys(obj);
    const values = Object.values(obj);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const value = values[i];
      newObj[key] = cloneToObject(value);
    }
  }
  if (obj instanceof Array) {
    newObj = [];
    for (let i = 0; i < obj.length; i++) {
      newObj[i] = cloneToObject(obj[i]);
    }
  }
  return newObj;
}


4.完善handle

2023/5/19为了解决版本号重置后,多个窗口 都与缓存版本一致,同时触发ipc调用,导致死循环的BUG,我们相应的也需要再handle中补充一个版本号重置标识:

/**pinia多窗口共享 */
ipcMain.handle(
  "pinia-store-change",
  (
    event,
    storeName: string,
    jsonStr: string,
    isResetVersion: boolean,
    storeUpdateVersion: number
  ) => {
    // 遍历window执行
    for (const currentWin of BrowserWindow.getAllWindows()) {
      const webContentsId = currentWin.webContents.id;
      if (webContentsId !== event.sender.id && !currentWin.isDestroyed()) {
        currentWin.webContents.send(
          "pinia-store-set",
          storeName,
          jsonStr,
          isResetVersion,
          storeUpdateVersion
        );
      }
    }
  }
);

5.最终实现效果

  • 如果本地没有缓存,第一次调用可能会出现一次循环调用
  • 正常情况下,一次窗口的状态改变,只会触发一次主动更新,其他窗口只会触发一次被动更新
    请添加图片描述
Logo

前往低代码交流专区

更多推荐