Electron实现多标签页模式详解

 更新时间:2024年11月20日 10:24:19   作者:若邪  
Electron 都发展这么多年了,让人想不到的是,要实现一个多标签页的功能居然没有能用的轮子,本文就来用比较low的方案 - iframe手搓一个吧

上文介绍了 如何在 Electron 中优雅的进行进程间通讯,接下来说说如何在 Electron 实现多标签页模式,如下图。

Electron 都发展这么多年了,让人想不到的是,要实现一个多标签页的功能居然没有能用的轮子。能在 Github 上找到 Star 最多的一个轮子(Tab component for Electron)也已经不再更新,而且还是使用 Electron 建议不再使用的 WebView 实现的(Web 嵌入 | Electron)。后面也有人基于 BrowserView 实现了一套,但是现在 Electron 又不推荐使用 BrowserView 了,建议使用 WebContentsView。因为项目比较急,没有花太多时间去研究了,就用比较 low 的方案 - iframe 自己搓了一个。

直接看 HTML 的结构吧,如下

也就是一个 tab 对应一个 iframe。

界面没啥好说的,稍微有点复杂的就是主进程、渲染进程(iframe 所在的页面)、iframe 之间的通讯。

在实际的业务场景中,关闭窗口的时候需要弹框让用户确认、用户确认后 iframe 里的页面需要调接口进行登出,然后通知主进程关闭窗口。整个消息链路涉及了主进程、渲染进程、iframe 页面,而且还是双向的。

上文已经讲了如何封装主进程、渲染进程之间的通讯,下面讲讲渲染进程(iframe 所在的页面)、iframe 之间的通讯。

渲染进程监听消息、处理消息:

export const addIframeWebEventListener = () => {
  window.addEventListener("message", async (event) => {
    const message = event.data as {
      iframeWebCmd: string;
      cbid: string;
      code: number;
      data: never;
    };
    if (message.iframeWebCmd) {
      console.log(message);
      if (message.iframeWebCmd !== "postMessageCallback") {
        if (handle[message.iframeWebCmd]) {
          try {
            const res = await handle[message.iframeWebCmd](message.data);
            invokeCallback(message.cbid, res);
          } catch (ex: unknown) {
            invokeErrorCallback(message.cbid, ex);
          }
        } else {
          invokeErrorCallback(
            message.cbid,
            `方法不存在:${message.iframeWebCmd}`,
          );
        }
      } else {
        if (message.code === 200) {
          (callbacks[message.cbid] || function () {})(message.data);
        } else {
          (errorCallbacks[message.cbid] || function () {})(message.data);
        }
        delete callbacks[message.cbid]; // 执行完回调删除
        delete errorCallbacks[message.cbid]; // 执行完回调删除
      }
    }
  });
};

渲染进程主动发送消息:

function postMessage(
  data: { electronWebCmd: string; data?: any },
  cb?: (data: any) => void,
  errorCb?: (data: any) => void,
) {
  const iframe = document.getElementById(
    tabStore.currentTabId.value!,
  ) as HTMLIFrameElement;
  if (cb) {
    const cbid = Date.now();
    callbacks[cbid] = cb;
    iframe?.contentWindow?.postMessage(
      {
        ...data,
        cbid,
      },
      "*",
    );
    if (errorCb) {
      errorCallbacks[cbid] = errorCb;
    }
  } else {
    iframe?.contentWindow?.postMessage(data, "*");
  }
}

export function request<T = unknown>(params: { cmd: string; data?: any }) {
  return new Promise<T>((resolve, reject) => {
    postMessage(
      { electronWebCmd: params.cmd, data: params.data },
      (res) => {
        resolve(res);
      },
      (error) => {
        reject(error);
      },
    );
  });
}

每一个 iframe 都使用了 id 进行标识,发送消息就是给当前激活的 tab 对应的 iframe 发消息。

当需要渲染进程给 iframe 发消息的时候,就可以像调用 HTTP 请求一样发送消息,比如让 iframe 页面进行刷新:

export function refresh() {
  return request({
    cmd: "refresh",
  });
}

完整代码:

/* eslint-disable no-case-declarations */
/* eslint-disable no-shadow */

import { useTabsStore } from "@/store/tabs";
import handle from "./handle";

/* eslint-disable @typescript-eslint/no-explicit-any */
const callbacks: { [propName: string]: (data: any) => void } = {};
const errorCallbacks: { [propName: string]: (data: any) => void } = {};

const tabStore = useTabsStore();

function postMessage(
  data: { electronWebCmd: string; data?: any },
  cb?: (data: any) => void,
  errorCb?: (data: any) => void,
) {
  const iframe = document.getElementById(
    tabStore.currentTabId.value!,
  ) as HTMLIFrameElement;
  if (cb) {
    const cbid = Date.now();
    callbacks[cbid] = cb;
    iframe?.contentWindow?.postMessage(
      {
        ...data,
        cbid,
      },
      "*",
    );
    if (errorCb) {
      errorCallbacks[cbid] = errorCb;
    }
  } else {
    iframe?.contentWindow?.postMessage(data, "*");
  }
}

export function request<T = unknown>(params: { cmd: string; data?: any }) {
  return new Promise<T>((resolve, reject) => {
    postMessage(
      { electronWebCmd: params.cmd, data: params.data },
      (res) => {
        resolve(res);
      },
      (error) => {
        reject(error);
      },
    );
  });
}

function invokeCallback<T = unknown>(cbid: string, res: T) {
  (
    document.getElementById(tabStore.currentTabId.value!) as HTMLIFrameElement
  )?.contentWindow?.postMessage(
    {
      electronWebCmd: "postMessageCallback",
      cbid,
      data: res,
      code: 200,
    },
    "*",
  );
}

function invokeErrorCallback(cbid: string, res: unknown) {
  (
    document.getElementById(tabStore.currentTabId.value!) as HTMLIFrameElement
  )?.contentWindow?.postMessage(
    {
      electronWebCmd: "postMessageCallback",
      cbid,
      data: res,
      code: 400,
    },
    "*",
  );
}

export const addIframeWebEventListener = () => {
  window.addEventListener("message", async (event) => {
    const message = event.data as {
      iframeWebCmd: string;
      cbid: string;
      code: number;
      data: never;
    };
    if (message.iframeWebCmd) {
      console.log(message);
      if (message.iframeWebCmd !== "postMessageCallback") {
        if (handle[message.iframeWebCmd]) {
          try {
            const res = await handle[message.iframeWebCmd](message.data);
            invokeCallback(message.cbid, res);
          } catch (ex: unknown) {
            invokeErrorCallback(message.cbid, ex);
          }
        } else {
          invokeErrorCallback(
            message.cbid,
            `方法不存在:${message.iframeWebCmd}`,
          );
        }
      } else {
        if (message.code === 200) {
          (callbacks[message.cbid] || function () {})(message.data);
        } else {
          (errorCallbacks[message.cbid] || function () {})(message.data);
        }
        delete callbacks[message.cbid]; // 执行完回调删除
        delete errorCallbacks[message.cbid]; // 执行完回调删除
      }
    }
  });
};

iframe 页面监听消息、处理消息:

export const addElectronWebWebEventListener = () => {
  window.addEventListener("message", async (event) => {
    const message = event.data as {
      electronWebCmd: string;
      cbid: string;
      code: number;
      data: never;
    };
    if (message.electronWebCmd) {
      if (message.electronWebCmd !== "postMessageCallback") {
        if (handle[message.electronWebCmd]) {
          try {
            const res = await handle[message.electronWebCmd](message.data);
            invokeCallback(message.cbid, res);
          } catch (ex: unknown) {
            invokeErrorCallback(message.cbid, ex);
          }
        } else {
          invokeErrorCallback(
            message.cbid,
            `方法不存在:${message.electronWebCmd}`,
          );
        }
      } else {
        if (message.code === 200) {
          (callbacks[message.cbid] || function () {})(message.data);
        } else {
          (errorCallbacks[message.cbid] || function () {})(message.data);
        }
        delete callbacks[message.cbid]; // 执行完回调删除
        delete errorCallbacks[message.cbid]; // 执行完回调删除
      }
    }
  });
};

iframe 发送消息:

function postMessage(
  data: { iframeWebCmd: string; data?: unknown },
  cb?: (data: unknown) => void,
  errorCb?: (data: unknown) => void,
) {
  if (cb) {
    const cbid = Date.now();
    callbacks[cbid] = cb;
    window.parent?.postMessage(
      {
        ...data,
        cbid,
      },
      "*",
    );
    if (errorCb) {
      errorCallbacks[cbid] = errorCb;
    }
  } else {
    window.parent?.postMessage(data, "*");
  }
}

export function request<T = unknown>(params: { cmd: string; data?: unknown }) {
  return new Promise<T>((resolve, reject) => {
    postMessage(
      { iframeWebCmd: params.cmd, data: params.data },
      (res) => {
        resolve(res as T);
      },
      (error) => {
        reject(error);
      },
    );
  });
}

如此一来 iframe 页面发消息的时候也很简单:

/**
 * @description 获取 mac 地址
 * @returns
 */
export const getMac = () => {
  return request<string>({
    cmd: "getMac",
  });
};

获取 mac 地址,消息的传递过程是:iframe 页面 -> 渲染进程 -> 主进程,主进程 -> 渲染进程 -> iframe 页面,属于双向通讯。如果没有做好通讯的封装,处理起来想想都麻烦,而现在只需要关注业务代码就好了。

完整代码:

import handle from "./handle";

/* eslint-disable no-shadow */
const callbacks: { [propName: string]: (data: unknown) => void } = {};
const errorCallbacks: { [propName: string]: (data: unknown) => void } = {};
function postMessage(
  data: { iframeWebCmd: string; data?: unknown },
  cb?: (data: unknown) => void,
  errorCb?: (data: unknown) => void,
) {
  if (cb) {
    const cbid = Date.now();
    callbacks[cbid] = cb;
    window.parent?.postMessage(
      {
        ...data,
        cbid,
      },
      "*",
    );
    if (errorCb) {
      errorCallbacks[cbid] = errorCb;
    }
  } else {
    window.parent?.postMessage(data, "*");
  }
}

export function request<T = unknown>(params: { cmd: string; data?: unknown }) {
  return new Promise<T>((resolve, reject) => {
    postMessage(
      { iframeWebCmd: params.cmd, data: params.data },
      (res) => {
        resolve(res as T);
      },
      (error) => {
        reject(error);
      },
    );
  });
}

function invokeCallback<T = unknown>(cbid: string, res: T) {
  window.parent?.postMessage(
    {
      iframeWebCmd: "postMessageCallback",
      cbid,
      data: res,
      code: 200,
    },
    "*",
  );
}

function invokeErrorCallback(cbid: string, res: unknown) {
  window.parent?.postMessage(
    {
      iframeWebCmd: "postMessageCallback",
      cbid,
      data: res,
      code: 400,
    },
    "*",
  );
}
export const addElectronWebWebEventListener = () => {
  window.addEventListener("message", async (event) => {
    const message = event.data as {
      electronWebCmd: string;
      cbid: string;
      code: number;
      data: never;
    };
    if (message.electronWebCmd) {
      if (message.electronWebCmd !== "postMessageCallback") {
        if (handle[message.electronWebCmd]) {
          try {
            const res = await handle[message.electronWebCmd](message.data);
            invokeCallback(message.cbid, res);
          } catch (ex: unknown) {
            invokeErrorCallback(message.cbid, ex);
          }
        } else {
          invokeErrorCallback(
            message.cbid,
            `方法不存在:${message.electronWebCmd}`,
          );
        }
      } else {
        if (message.code === 200) {
          (callbacks[message.cbid] || function () {})(message.data);
        } else {
          (errorCallbacks[message.cbid] || function () {})(message.data);
        }
        delete callbacks[message.cbid]; // 执行完回调删除
        delete errorCallbacks[message.cbid]; // 执行完回调删除
      }
    }
  });
};

在 Electron 里基于 iframe 的方案实现多标签页,有一个致命的缺陷就是,如果 iframe 里的页面属于第三方,那么就无法与里面的页面进行同通讯,比如我在实现刷新标签页的时候,是给 iframe 里的页面发送消息,页面收到消息后执行下面的代码:

refresh: () => {
    const iframeID = getIframeId();
    if (iframeID) {
      let href = location.href;
      if (href.indexOf("?") === -1) {
        href = href + `?iframeId=${iframeID}`;
      } else {
        if (href.indexOf("iframeId") === -1) {
          href = href + `&iframeId=${iframeID}`;
        }
      }
      location.href = href;
      setTimeout(() => {
        location.reload();
      }, 500);
    } else {
      location.reload();
    }
  }

到此这篇关于Electron实现多标签页模式详解的文章就介绍到这了,更多相关Electron多标签页模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • javascript验证邮件地址和MX记录的方法

    javascript验证邮件地址和MX记录的方法

    这篇文章主要介绍了javascript验证邮件地址和MX记录的方法,涉及javascript正则验证的相关技巧,需要的朋友可以参考下
    2015-06-06
  • JavaScript中的this指向问题详解

    JavaScript中的this指向问题详解

    这篇文章主要给大家介绍了关于JavaScript中this指向问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • js中getter和setter用法实例分析

    js中getter和setter用法实例分析

    这篇文章主要介绍了js中getter和setter用法,结合实例形式分析了javascript中getter和setter的功能、使用方法及相关操作注意事项,需要的朋友可以参考下
    2018-08-08
  • JavaScript中具名函数的多种调用方式总结

    JavaScript中具名函数的多种调用方式总结

    这篇文章主要介绍了JavaScript中具名函数的多种调用方式总结,本文总结了4种方法,需要的朋友可以参考下
    2014-11-11
  • JavaScript实现H5接金币功能(实例代码)

    JavaScript实现H5接金币功能(实例代码)

    这篇文章主要介绍了JavaScript实现H5接金币功能,本文分步骤通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-02-02
  • js实现HashTable(哈希表)的实例分析

    js实现HashTable(哈希表)的实例分析

    本文详细介绍javascript哈希表的实例分析及用法。下面就跟小编一起来学习下吧
    2016-11-11
  • 浅谈javascript属性onresize

    浅谈javascript属性onresize

    这篇文章主要介绍了浅谈javascript属性onresize的详细使用方法,十分的实用,这里推荐给大家,有需要的小伙伴可以参考下。
    2015-04-04
  • js实现点击左右按钮轮播图片效果实例

    js实现点击左右按钮轮播图片效果实例

    这篇文章主要介绍了js实现点击左右按钮轮播图片效果的方法,涉及click事件相应、animate方法等使用技巧,需要的朋友可以参考下
    2015-01-01
  • 用js+iframe形成页面的一种遮罩效果的具体实现

    用js+iframe形成页面的一种遮罩效果的具体实现

    用js形成页面的一种遮罩效果,选择想要进行遮罩的窗口,在这里想要遮罩的是一个iframe窗口,具体的实现如下,感兴趣的朋友可以参参考下
    2013-12-12
  • Javascript跨域请求的4种解决方式

    Javascript跨域请求的4种解决方式

    如果所请求的域名跟这个域名不致,这种情况就是跨域,由于跨域存在漏洞,所以一般来说正常的跨域请求方式是请求不到的,所以有了本文的出现,感兴趣的你可以参考下哈,希望可以帮助到你
    2013-03-03

最新评论