Applications need to communicate one way or another. Electron ships with two ipc (Inter Process Communication) modules for that purpose. ipcMain is the module used on the backend or main process side. Render processes like a BrowserWindow or BrowserView can use the ipcRenderer module. These modules establish a communication channel between the main and render process. Messages can flow synchronously or asynchronously on that channel.

With ipc, it is possible to propagate a BrowserView’s events to the main process. A click on a call-to-action button in the BrowserView could trigger an action in the main process. Some data wrangling mechanism for example. After computation, it would pass down the result in an asynchronous manner.

This is a great technique to write reactive code that keeps the event-loop spinning. But it comes with limitations. The same project must contain ipcRenderer and ipcMain. Otherwise, electron’s ipc is not useable. Thus, this form of communication is only available in apps built in electron’s context. It’s impossible to communicate from an external WebApp with the electron app via ipc.

But there are other solutions to achieve communication with an external WebApp. One of them is electron’s ability to use custom protocols, which I want to elaborate on in this post.

Before I go ahead and dive into implementing a custom protocol within electron, I want to outline a demo scenario. If you are in a hurry, feel free to jump to the code part and skip the intro.

Scenario

For demo’s sake, we will keep it simple. Imagine, we have a WebApp called “foogle”:
It allows users to search the internet via a simple interface. The user has an input field to type the term to search for and a button to conduct the search.
The stakeholder informs us, that part of the user base still refuses to use a WebApp. They express the need for a solid piece of trustworthy software. Besides, they desire a way to clear the search form from the software. And the app should also display the form’s focus state, next to the current search term.

   

electron app target state

   

This requires updating the app based on events happening in the “foogle” render process. Since “foogle” is an external WebApp, it’s impossible to propagate this events with ipc. However, establishing a way of communication is possible. It is achievable with custom protocols and JavaScript injection into the render process. And electron’s ability to handle this.

Before we get our hands dirty coding, we define the requirements of such a protocol:

  • name: obvious choice — demoprotocol
  • message: structure of the message
  • send: a method to send a message
  • receive: a method to handle incoming messages

   

Visualization of events the protocol will handle

   

The render process will receive the protocol during its preload phase. Electron will inject it as JavaScript file, hence it will be available in the contents window context. A sole implementation on the external app side is also possible. But this way is assuring the interface is available, and offering a way to detect the context of an electron app.

At this point, we have a solid plan and a good understanding of what we want to achieve. It’s time to code.

Demoprotocol

Defining the message structure is the first step in implementing the solution. The message will use a simple structure to keep the protocol as lean and flexible as possible:

// message structure
{
	"type": string;
	"payload": any;
}

The send method will use demoprotocol to deliver messages to the electron app. This is a custom protocol request and thus has a URL structure.

const url = `demoprotocol://${type}?payload=${JSON.stringify(data)}`;

The receive method will be a normal JavaScript function and invoked via injection.

demoprotocol.receive = (type, payload) => {
	console.log("Received msg of type ", type, "with payload ", payload);
};

Register the protocol with electron

Before an external app can use the custom protocol, electron needs to know about it. The protocol module exposes different methods to register a protocol. It is necessary to register the protocol as privileged to use the fetch API.

// index.js

// Must happen before the "app ready" event
protocol.registerSchemesAsPrivileged([
	{
		scheme: "demoprotocol",
		privileges: {
			bypassCSP: true,
			supportFetchAPI: true,
		},
	},
]);

The webPreference’s preload property injects the protocol file into the render process.

const bv = new BrowserView({
	webPreferences: {
		preload: "/absolute/path/to/protocol.js",
	},
});

The protocol’s core

The interface adds the property demoprotocol to the window context of the render process. It exposes the send method covered earlier.

// protocol.js

window.demoprotocol = {
	send: (type, data) =>
		fetch(`demoprotocol://${type}?payload=${JSON.stringify(data)}`, {
			method: "GET",
			// Instead of get and queryparam, using post is also an option.
			/* method: "POST",
			body: JSON.stringify({
				data,
			}), */
		}).then((res) => res.json()),
};

The protocol on electrons’s side

The protocol module registers the function for handling incoming protocol messages. It accepts the name of the protocol and a handler function. Different underlying protocol types are available for registration. Buffer is a good choice when responding with a JSON object.

When receiving a request on the protocol, the handler parses the type and payload from it. Because of the URL format, retrieving the message is a simple task.

// index.js

// this happens after the "app ready" event
protocol.registerBufferProtocol("demoprotocol", (req, cb) => {
	const url = new URL(req.url);
	const type = url.hostname;

	try {
		const payload = JSON.parse(url.searchParams.get("payload"));

		switch (type) {
			case "focus":
				if (payload.data) {
					win.webContents.send("focus", true);
				} else {
					win.webContents.send("focus", false);
				}
				break;
			case "term":
				win.webContents.send("term", payload.data);
				break;
		}

		cb({
			mimeType: "application/json",
			data: Buffer.from(JSON.stringify({ success: true, msg: payload })),
		});
	} catch (error) {
		cb({
			mimeType: "application/json",
			data: Buffer.from(JSON.stringify({ success: false, msg: error })),
		});
	}
});

To send a message the main process injects JavaScript:

// An if check and try-catch block can prevent some unwanted errors.
win
	.getBrowserView()
	.webContents.executeJavaScript(`window.demoprotocol.receive("clear", null)`);

The protocol on the external side

The external app can check the window object for the demoprotocol. Adding the receive method, if the property is present.

// foogle.html

if (demoprotocol) {
	demoprotocol.receive = (type, payload) => {
		console.log("Received msg of type ", type, "with payload ", payload);

		if (type === "clear") {
			document.getElementById("Term").value = "";
			document.querySelector("#Results").innerHTML = "";
		}
	};
}

Or listen for events and calling the send method:

// foogle.html

document.getElementById("Term").addEventListener("focus", function (e) {
	e.preventDefault();
	if (demoprotocol) {
		demoprotocol
			.send("focus", { data: true })
			.then(console.log)
			.catch(console.error);
	}
});

demoprotocol in action

For the shortness of the post, I will omit the other parts of the implementation. The complete working code of the example app is available here. Or try to implement it by yourself. All you need is a BrowserWindow, a BrowserView, a little HTML, and some ipc sprinkled on top of it.

Congratulations, you did it. You hopefully got a better understanding of how electron can communicate with external apps. And how you can leverage custom protocols for that.

Going one step further

Improve on the above example by:

  • try out the other protocol types
  • how to retrieve the message when using fetch and post
  • embed protocol.js into the external app
  • use WebSockets instead of a custom protocol
  • inject a protocol interface and event listener script into a public page

Let me end this excursion with a quote for your meditation:

Any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure.
— Robert C. Martin