I was using the <webview>
tag instead of iFrames, but couldn't find much details on the NWJS docs, the Electron docs, nor on the actual <webview>
docs around accessing the content inside of it.
I wanted to retrieve the document.title
from within the <webview>
, and send that back to the main process.
The basic solution I went with for communicating between a main process and inside of a <webview>
, is to use Webview's ContentWindow.postMessage()
method. This is very similar to window.postMessage()
. By using postMessage()
—specifically tracking the event.source
—we create a communication bridge between the main process and the <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);
}
How it works (at least one way I've found):
<webview>
and the window (to listen later on for a message coming from within the <webview>
)<webview>
element loads a URL, it triggers contentload()
contentload()
will inject an EventListener into the <webview>
and setup the data/DOM elements we want to get from inside of the <webview>
.<webview>
finishes loading, it triggers loadstop()
loadstop()
will send a message to the <webview>
to establish a bridge. It's important to note that here I use webview.contentWindow.postMessage()
instead of window.postMessage()
.<webview>
responds with the data we setup on step 1<webview>
, it triggers receiveHandshake()
receiveHandshake()
you now have access the data that came from inside of the <webview>
. This can be the page title—or whatever you configured in the webviewInjectScript
.removeListeners()
to remove all the EventListeners we setup, but you could keep sending messages back-and-forth.FYI—in the context of Electron and NWJS, the <webview>
tag allows you to render websites (like an iframe), with the benefit that it runs in a separate process. This is much better for performance than a bunch of iframes. A <webview>
contains a standard HTML document, the complication over say an iframe is because it runs in a separate process.
There's also another thread with some other solutions, such as using IPC messages, and using the preload
tag.
There's another way to do this that's more proper for Electron. In the code below, I'm using Vue (2) and Webpack, but the key differences from the above implementation are:
ipcRenderer
and ipcMain
to send messages, instead of postMessage
preload
property of Webview, instead of using executeScript()
. I've binded the :preload
value to the Vue computed property (injectScript
), which returns the path of the external injectWebPageScript.js
file.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://stackoverflow.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
});
});
});