您现在的位置是:首页 >其他 >electron+vue3全家桶+vite项目搭建【16】electron多窗口,pinia状态无法同步更新问题解决网站首页其他

electron+vue3全家桶+vite项目搭建【16】electron多窗口,pinia状态无法同步更新问题解决

编程小龙 2023-07-08 04:00:03
简介electron+vue3全家桶+vite项目搭建【16】electron多窗口,pinia状态无法同步更新问题解决

引入

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

demo项目地址

实现效果展示

请添加图片描述

问题展示

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

如下所示:

请添加图片描述

解决方案

思路整理

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

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

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

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

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

1.主进程添加handle

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

  • storeName:更新的store的名称,对应defineStore()的第一个参数
  • keys:更新内容的对象key集合 【json序列化后】
  • values:更新内容的对象值集合 【json序列化后】

electronmainindex.ts

/**pinia多窗口共享 */
ipcMain.handle(
  "pinia-store-change",
  (event, storeName: string, keys, values) => {
    // 遍历window执行
    for (const currentWin of BrowserWindow.getAllWindows()) {
      const webContentsId = currentWin.webContents.id;
        // 只更新其他窗口 【这里排除通知主进程的窗口】
      if (webContentsId !== event.sender.id) {
        currentWin.webContents.send("pinia-store-set", storeName, keys, values);
      }
    }
  }
);

2.编写pinia插件

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

pinia官网

可以看到pinia插件的说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T3Pj0Y7w-1683017496931)(D:我的坚果云logimgsimage-20230428094713910.png)]

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

  • 我们在状态修改时通知主进程状态修改了,并且监听状态修改了,就以 keys,values进行传输修改的内容

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

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

srcstorepluginsshareStorePlugin.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,
      Object.keys(store.$state),
      Object.values(store.$state)
    );
  });

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

        /// 更新各个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 = "storeUpdateVersion_";

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 storeCacheKey = STORE_CACHE_KEY_PREFIX + storeName;
  let currentStoreUpdateVersion: number =
    getLocalStoreUpdateVersion(storeCacheKey);
  // 初始化同步store版本
  store.storeUpdateVersion = currentStoreUpdateVersion;

  // 监听数据变化
  store.$subscribe(() => {
    // 获取本地存储的最新状态
    currentStoreUpdateVersion = cacheUtils.get(storeCacheKey);
    /// 如果本地缓存过期,则重置一个缓存,并且通知主进程让其他窗口更新状态
    if (
      currentStoreUpdateVersion === null ||
      currentStoreUpdateVersion === undefined
    ) {
      currentStoreUpdateVersion = 0;
      cacheUtils.set(
        storeCacheKey,
        currentStoreUpdateVersion,
        STORE_CACHE_TIME
      );
      store.storeUpdateVersion = currentStoreUpdateVersion;
      console.log(`主动更新 ${storeName} 的状态`);
      // 通知主线程更新
      ipcRenderer.invoke(
        "pinia-store-change",
        storeName,
        Object.keys(store.$state),
        Object.values(store.$state)
      );
    } else {
      // 如果版本一致,则增加版本号,且更新本地存储版本 ,并且通知主线程告知其他窗口同步更新store状态
      if (store.storeUpdateVersion === currentStoreUpdateVersion) {
        store.storeUpdateVersion++;
        cacheUtils.set(
          storeCacheKey,
          store.storeUpdateVersion,
          STORE_CACHE_TIME
        );
        console.log(`主动更新 ${storeName} 的状态`);

        // 通知主线程更新
        ipcRenderer.invoke(
          "pinia-store-change",
          store.$id,
          Object.keys(store.$state),
          Object.values(store.$state)
        );
      } else {
        // 如果当前store的版本大于本地存储的版本,说明本地版本重置了【过期重新创建】,此时重置store的版本
        // 如果当前store的版本小于本地存储的版本,说明是被动更新引起的state变动回调,此时仅更新版本即可
        store.storeUpdateVersion = currentStoreUpdateVersion;
      }
    }
  });

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

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

4.最终实现效果

  • 如果本地没有缓存,第一次调用可能会出现一次循环调用
  • 正常情况下,一次窗口的状态改变,只会触发一次主动更新,其他窗口只会触发一次被动更新
    请添加图片描述
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。