Electron 内嵌 Iframe 的数据通信

5,691 阅读1分钟

Electron 内嵌 IFrame 的数据通信

  • 背景:项目A打包成网页,然后套壳在 Electron (下面成为项目B)和其他平台,平台差异化的代码逻辑在各自平台实现。

  • 环境:Vue2 + electron-builder

  • 遇到问题:使用 Electron 自带的 webview ,同样的代码,webview 无法加载到 preload.js ,网上找了很久都没法实现,只能切换成 iframe ,也不知道正常是否是用此逻辑来处理通信。

主要步骤

  1. Electron 配置 preload.js ,在 preload.js 配置可供 iframe 调用的方法。
  2. iframe 里按需调用,ipcMain 接受事件,并转发给 ipcRenderer
  3. ipcRenderer 收到消息,通过 postMessage 发送给 iframe
  4. iframe 收到回调消息,处理数据

1648881307039.jpg

具体实现

  1. 配置 preload.js ,监听事件
// B/src/background.ts
const win = new BrowserWindow({
  //...其他配置
  webPreferences: {
      webviewTag: true,
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true,
      nodeIntegrationInSubFrames: true,
      // 对应的文件在 public/repload.js 
      preload: path.join(__static, "preload.js"),
  }
});
// 加载 Electron 项目里的 app.vue,后续会用到这个 win
const containerHtml = process.env.WEBPACK_DEV_SERVER_URL as string;
if (containerHtml) {
  await win.loadURL(containerHtml);
} else {
  createProtocol("app");
  await win.loadURL("app://./index.html");
}
// 这里监听来自 iframe 传回的消息,并转发给 win.webContents,即 Electron 项目里的 app.vue
ipcMain.on("event_from_iframe", (evt, data) => {
  win.webContents.send("event_from_iframe", data);
});
// B/public/preload.js
const { ipcRenderer } = require("electron");
window.isElectron = true;
window.$bridge = {
  ipcRenderer,
};
  1. iframe 里通过调用 NativeElectron.send 按需传递信息,ipcMain 接受事件,并转发给 ipcRenderer
<!-- A/src/index.vue -->
<!-- 项目A 里的代码,假设最后打包的地址是 http://a.com , 后续会使用到 -->
<script lang="ts">
  import { onMounted, onUnmounted } from "@vue/composition-api";
  import {NativeElectron} from "@/utils/native/native.electron";
  
  onMounted(() => {
    NativeElectron.addMessageListener({
      xxx: (res) => {
        console.log("xxx response: ", res);;
      }
    })
  })
  onUnmounted(() => {
    NativeElectron.removeMessageListener();
  })
  handleXXX() {
    NativeElectron.send("xxx", {data: "data"});
  }
</script>
// A/src/utils/native/native.electron.ts
interface NativeElectronCallback {
  [key: string]: Function;
}

// 和 Electron 外壳约定好的数据方式,其他不做响应
interface NativeElectronData {
  from: "electron" | any;
  name: string; // 回调方法名
  payload: any;
}

export class NativeElectron {
  static callbacks: NativeElectronCallback = {}; // 注册回调函数

  static isElectron(): boolean {
    const is = window && window.isElectron;
    console.log("isElectron : ", is);
    return is;
  }

  static addMessageListener (callbacks: NativeElectronCallback) {
    if (!NativeElectron.isElectron()) {
      return;
    }
    window.addEventListener("message", NativeElectron.messageHandler)
    NativeElectron.callbacks = callbacks;
  }

  static removeMessageListener() {
    if (!NativeElectron.isElectron()) {
      return;
    }
    window.removeEventListener("message", NativeElectron.messageHandler)
  }

  // 接收 Electron 里通过的 postMessage
  static messageHandler = (e) => {
    const data = e.data as NativeElectronData;
    if (typeof data !== "object") {
      return;
    }
    const { from = "", name = "", payload } = data;
    if (from !== "electron") {
      return;
    }
    const callback = NativeElectron.callbacks[name];
    callback && callback(payload);
  }
  
  // iframe 里传消息给 ipcMain
  static send(type: string, payload: any) {
    if (!NativeElectron.isElectron()) {
      return;
    }
    // @ts-ignore
    window.$bridge.ipcRenderer.send("event_from_iframe", {
      type,
      payload
    });
  }
}
  1. ipcRenderer 收到消息,通过 postMessage 发送给 iframe
<!-- B/src/app.vue -->
<template>
  <div>这是 Electron 项目,下面使用 iframe 标签包裹的 A 项目</div>
  <iframe src="http://a.com"></iframe>
</template>
<script>
  mounted() {
    const webView = document.querySelector("iframe");
    // 监听从 ipcMain 传来的消息,并转发给 iframe
    ipcRenderer.on("event_from_iframe", (e, data) => {
      webView.contentWindow.postMessage(
        { from: "electron", name: "xxx", payload: data },
        "http://a.com" // postMessage 的第二个参数是 iframe 里加载的源地址
      );
    });
  }
</script>

具体流程

文件加载流程:

  1. B/src/background.ts 里创建 win,加载 B/public/preload.jsB/src/app.vue
  2. B/src/app.vue 里通过 iframe 加载打包之后的 A/src/index.vue
  3. A/src/index.vuewindow 对象上,能拿到 B/public/preload.js 上挂载的函数。

数据流转过程(具体看文章开头的流程图)

  1. A/src/index.vue 调用 handleHelloWorld 把消息 {data: "data"} 传给 B/src/background.ts 里的 ipcMain.on("event-from-iframe")
  2. ipcMain.on("event-from-iframe") 把数据转发给 B/src/app.vue 里的 ipcRenderer.on("event_from_iframe")
  3. ipcRenderer.on("event_from_iframe") 通过 webView.contentWindow.postMessage 传给 iframe
  4. iframe 通过 window.addEventListener("message") 的回调拿到数据。