如何访问 <webview> (Electron/NW.JS) 的内容?

How can I access the contents of a <webview> (Electron/NW.JS)?

我使用的是 <webview> 标签而不是 iFrame,但是找不到关于 NWJS docs, the Electron docs, nor on the actual <webview> docs 访问其中内容的详细信息。

我想从 <webview> 中检索 document.title,并将其发送回主进程。

我在主进程和 <webview> 内部进行通信的基本解决方案是使用 Webview 的 ContentWindow.postMessage() method. This is very similar to window.postMessage()。通过使用 postMessage()——专门跟踪 event.source——我们在主进程和 <webview>.

之间创建了一个通信桥梁
const webview = document.getElementById('your-webview-element');

// <webview> Content is loaded
function contentload() {
  // The following will be injected in the webview
  const webviewInjectScript = `
      var data = {
        title: document.title,
        url: window.location.href
      };

      function respond(event) {
        event.source.postMessage(data, '*');
      }

      window.addEventListener("message", respond, false);
  `;

  webview.executeScript({
    code: webviewInjectScript
  });
}

// <webview> Loading has finished
function loadstop() {
  webview.contentWindow.postMessage("Send me your data!", "*"); // Send a request to the webview
}

// Bind events
webview.addEventListener("contentload", contentload);
webview.addEventListener("loadstop", loadstop);
window.addEventListener("message", receiveHandshake, false); // Listen for response

function receiveHandshake(event) {
  // Data is accessible as event.data.*
  // This is the custom object that was injected during contentload()
  // i.e. event.data.title, event.data.url
  console.log(event.data)

  // Unbind EventListeners
  removeListeners();
}

// Remove all event listeners
function removeListeners() {
  webview.removeEventListener("contentload", contentload);
  webview.removeEventListener("loadstop", loadstop);
  window.removeEventListener("message", receiveHandshake);
}

它是如何工作的(至少我找到了一种方法):

  1. 首先,将 EventListeners 从主进程绑定到 <webview> 和 window(稍后监听来自 <webview> 的消息)
  2. <webview> 元素加载 URL 时,它会触发 contentload()
  3. contentload() 会将 EventListener 注入 <webview> 并设置我们想要从 <webview>.
  4. 内部获取的 data/DOM 元素
  5. 一旦 <webview> 完成加载,它会触发 loadstop()
  6. loadstop() 将向 <webview> 发送消息以建立桥接。需要注意的是这里我用的是webview.contentWindow.postMessage()而不是window.postMessage().
  7. <webview> 使用我们在步骤 1 中设置的数据进行响应
  8. 当主进程收到来自<webview>的响应(通过EventListener"message")时,它触发receiveHandshake()
  9. receiveHandshake() 内部,您现在可以访问来自 <webview> 内部的数据。这可以是页面标题——或者您在 webviewInjectScript.
  10. 中配置的任何内容
  11. 最后,我调用 removeListeners() 删除我们设置的所有 EventListener,但您可以继续发送消息 back-and-forth。

仅供参考——在 Electron 和 NWJS 的上下文中,<webview> 标签允许您呈现网站(如 iframe),好处是它在单独的进程中运行。这比一堆 iframe 的性能要好得多。一个 <webview> 包含一个标准的 HTML 文档,说 iframe 的复杂性是因为它在一个单独的进程中运行。

还有 another thread 一些其他的解决方案,例如使用 IPC 消息,以及使用 preload 标签。


更新:使用 front-end 框架(即 Vue)

还有另一种方法更适合 Electron。在下面的代码中,我使用了 Vue (2) 和 Webpack,但与上述实现的主要区别是:

  • 使用 Electron ipcRendereripcMain 发送消息,而不是 postMessage
  • 使用 Webview 的 preload 属性 加载注入脚本,而不是使用 executeScript()。我已经将 :preload 值绑定到 Vue 计算的 属性 (injectScript),其中 returns 外部 injectWebPageScript.js 文件的路径。

components/myComponent.vue

<template>
  <webview ref="frame" class="frame" :preload="injectScript"/>
</template>

<script>
export default {
  computed: {
    injectScript() {
      const appPath = require("electron").remote.app.getAppPath();
      return `file://${require("path").resolve(
        __dirname,
        "../../mixins/injectWebPageScript.js"
      )}`;
    }
  },
  methods: {
    mySiteLoaderScript(url) {
      const frame = this.$refs.frame;

      // Initialize event listeners on the Webview
      addListeners();

      // Set the URL, start loading
      frame.setAttribute("src", url);

      // Bind events
      function addListeners() {
        frame.addEventListener("dom-ready", contentloaded);
        frame.addEventListener("ipc-message", receiveHandshake);
      }

      // Remove all event listeners
      function removeListeners() {
        frame.removeEventListener("dom-ready", contentloaded);
        frame.removeEventListener("ipc-message", receiveHandshake);
      }

      // Once webview content is loaded, request its data
      function contentloaded() {
        frame.send("requestData");
      }

      // Triggered when we receive a response from the Webview
      // This is the `ipc-message` event
      function receiveHandshake(event) {
        // Only listen to replyData messages
        if (event.channel !== "replyData") return false;

        const data = event.args[0];
        const title = data.title;
        const favicon = data.favicon;

        // Remove listeners once data has been received
        removeListeners();
      }
    }
  },
  mounted() {
    this.mySiteLoaderScript("https://whosebug.com");
  }
};
</script>

mixins/injectWebPageScript.js

const { ipcRenderer } = require("electron");

// Once the Webview's document has been loaded, notify the ipcRenderer
document.addEventListener("DOMContentLoaded", () => {
  ipcRenderer.on("requestData", () => {
    ipcRenderer.sendToHost("replyData", {
      title: document.title,
      url: window.location.href
    });
  });
});