Introducing Electron

Desktop Apps with JavaScript

Felix Rieseberg

Introducing Electron

by Felix Rieseberg

Copyright © 2018 O’Reilly Media, Inc. All rights reserved.

Printed in the United States of America.

Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.

O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com/safari). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.

Editor: Allyson MacDonald

Production Editor: Nicholas Adams

Copyeditor: Gillian McGarvey

Interior Designer: David Futato

Cover Designer: Karen Montgomery

Illustrator: Rebecca Demarest

December 2017: First Edition

Revision History for the First Edition

2017-12-01: First Release

The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Introducing Electron, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.

While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.

978-1-491-99603-4

[LSI]

Chapter 1. What Is Electron?

Chances are high that you are already using a desktop app powered by web technologies. Popular apps like Spotify, Visual Studio Code, Atom, GitHub Desktop, and Slack all rely on a JavaScript stack—and even software powerhouses like Adobe’s Creative Suite and the Nvidia Windows Drivers come with Node.js bundled in.

Electron is an open source framework that allows developers to use web technologies to build desktop applications. Initial development started in 2013 at GitHub to support the hackable text and code editor Atom. Both were made publicly available in the spring of 2014, but Electron has since surpassed Atom in popularity and become GitHub’s biggest open source project to date. In 2017, Electron is being maintained by a team at GitHub with support from the community. Microsoft, Slack, Figma, Brave, and other companies, as well as dedicated individuals, continue to invest in the development and maintenance of the project.

This short guide introduces Electron, the most popular framework and runtime for cross-platform desktop applications built with web technologies (and, optionally, with native components too). Though far from a complete reference, it briefly introduces each aspect of developing and distributing an Electron-based app, allowing you to get the 30,000-foot overview.

Building Desktop Apps with Chromium, Node.js, and C++

At its core, Electron consists of three components. It embeds Chromium’s rendering library (known as libchromiumcontent), which is the open source foundation for Google’s browser Chrome. With it comes support for the latest web standards, a performant JavaScript engine, and constantly improving developer tools. It is a common misconception that Electron includes Chrome in its entirety; to keep Electron as lean as possible, it really does just contain Chromium’s rendering library.

The second component is Node.js, the popular JavaScript runtime built on top of V8 (the same JavaScript engine that powers libchromiumcontent and Chrome). It brings a vibrant open source ecosystem to the table: countless modules on npm offer battle-tested solutions for common development tasks and easy integration with virtually any service. Node.js rightfully claims to be the “largest ecosystem of open source libraries in the world,” a benefit that can dramatically reduce the amount of time needed to develop solutions.

The third component is a thick layer of C++, extending the usual set of available APIs with native implementations for common operating system operations such as sending native notifications, accessing system preferences, and creating native dialogs. In addition, Node.js allows the use of native modules—native components written in C, C++, Rust, or Objective-C—ensuring that the developer is never limited to JavaScript should a complicated problem require the use of a low-level programming language.

Combining Chromium and Node.js, and throwing in a large number of natively implemented APIs, Electron enables developers to build powerful desktop applications with little effort. All three technologies are cross-platform, supporting Windows, macOS, and Linux alike. Developers with experience in building desktop software are often surprised by how small the teams behind popular Electron applications are: Microsoft’s Visual Studio Code was initially built by merely seven engineers, Slack’s desktop app by just two, and GitHub’s desktop app by four. Electron’s true power is that it allows developers to build a large percentage of their desktop apps using nothing but web technologies, leaving only the truly computing-intensive tasks to be implemented natively.

Main and Renderer Processes

Electron knows two different process types: main process and renderer process. When an Electron application is started, it launches the main process. Within one application instance, there is always exactly one main process. This process has no access to DOM (Document Object Model) APIs, is windowless, and behaves a lot like a Node.js process (see Figure 1-1).

Figure 1-1. The two Electron process types

As soon as the main process has initialized, it is able to open windows. Those windows run in their own process and are referred to as renderer processes. In Electron, each web page runs in its own process. The distinction is important: Renderer processes have the familiar DOM with window and document objects, can create and render HTML elements, and have Chromium’s developer tools available, whereas the main process is really just a Node.js process. At the same time, many of Electron’s APIs can only be accessed from the main process: calling methods that perform native GUI operations from a renderer process is risky, likely to leak resources, and therefore simply not allowed.

The mysterious browser process

In older technical documentation, you will sometimes see the main process referred to as the browser process, which—naturally—can be confusing, but has its origin in Chrome’s terminology. In Chrome’s world, the main process is in charge of managing windows and tabs, thus it is dubbed the “browser process.”

The main process manages and facilitates communication with each renderer process. The renderer processes communicate in turn with the main process, but do not usually bother talking to other renderer processes. This communication is facilitated by Chromium’s multiprocess architecture, enabling developers to pass objects between processes and to even call methods on one process from another. Even though Electron imposes strict rules on which API can be called from which process, it also makes communication between processes easy enough that the restrictions are never limiting.

The difference between the main process and its attached renderer processes is important: each one has different capabilities and is responsible for different aspects of your application’s life cycle. Throughout this guide, you will find references explaining which process is responsible for a given task. This distinction is sometimes confusing for beginners but will quickly feel natural.

Node.js in the Browser

Once a window is created, web developers will quickly feel like they have been unshackled. Electron combines Node.js and Chromium, meaning you can make full use of all of Node’s APIs, including require and any module you can find on npm. Naturally, this encompasses native modules like sqlite or nodegit.

Making Node.js available inside the web context has two important implications. First, unleashing full system access onto a web page truly removes any limitations a web developer may have run into. Consider Visual Studio Code and its powerful debugger as an example: thanks to the combination of web technologies and Node.js, it is able to build, inspect, and debug C# or C++ processes. From an operating system perspective, an Electron application has full access to user space and is in its access limitations equal to a fully native application.

It also removes any safety boundaries a web developer may be used to. A compromised Electron application is far more dangerous than a compromised website, given that an Electron app has full access to the operating system. This short introduction dedicates a section to security, but as a basic rule, loading a remote web page in an Electron renderer process with Node.js integration enabled is a bad idea. To mitigate this risk, Electron allows developers to create renderer processes that have the Node.js integration entirely disabled. Later, we will discuss scenarios in which you would like to load remote content and offer a limited set of APIs only available through Node.js.

Chapter 2. Introduction to Electron

What does building an application with Electron actually look like? In this chapter, we discuss how an app is built from the ground up and how one integrates it with the operating system for a native user experience.

This introduction covers Electron’s most commonly used modules and APIs, but certainly not all of them. At the time of writing, more than a 1,700 modules containing the tag electron are available on npm, and that number will certainly grow. You will, however, learn the fundamental patterns. And once you have a grip on how Electron’s individual pieces fit together, how its modules and APIs generally work, and how it integrates with the operating system, you will understand Electron development.

Building an application with Electron is essentially Node.js development. To follow along with the exercises in this chapter, you need Node.js (at least version 7) and npm (at least version 4).

Hello World: A Small Code Editor

These days, simply printing a text line to the terminal is hardly impressive. To illustrate how an Electron application is typically developed, we will build a simple code editor (see Figure 2-1), similar to Visual Studio Code, Atom, or Sublime.

Electron’s homepage recommends a CLI tool called electron-forge or an example repository called electron-quick-start. While both are useful, knowing how an Electron application is bootstrapped is required to understand how an Electron application’s life cycle works. We will therefore build this application from scratch.

This project on GitHub

The final result of this exercise, as well as branches with implementation examples for each feature discussed in this book, is available on GitHub.

Figure 2-1. The small Electron code editor in action

Project Structure

An Electron application is a Node.js project. As such, it contains a package.json that describes the title, description, and version of the project, as well as dependencies, basic configuration, and the main entry point.

Create an empty directory for your first Electron application and a package.json at its root. Then, create a folder called src to house the app’s source code.

It is a good idea to separate JavaScript files according to process they will be running in. Most Electron applications have some files that are exclusively used by the main process or the renderer process, and most JavaScript can be written to easily support both. The source folder should main and renderer folders to hold files exclusively written for each process type, with files for both processes residing at the root. How to organize a project is ultimately a matter of preference, you will find that most Electron tools expect this structure:

node_modules/

src/

main/

index.js

renderer/

renderer.js

index.html

style.css

package.json

Let’s start with the package manifest. Consider this most basic package.json for a Node.js application, shown in Example 2-1. It defines the entry point as index.js and describes a start script, node ., which simply instructs Node.js to run the entry script.

Example 2-1. A minimal package.json

{
  "name": "my-node-app",
  "version": "1.0.0",
  "description": "My Hello World Editor",
  "main": "src/main/index.js",
  "scripts": {
    "start": "node ."
  },
  "keywords": [ "Node" ],
  "author": "Felix Rieseberg <felix@felix.fun>",
  "license": "ISC"
}

Next, create an index.js file (Example 2-2). Let’s keep it as simple as possible; it will only contain a greeting:

Example 2-2. A minimal index.js

console.log('Hello World!')

If you run this project using npm start, you will see that Node.js dutifully logs “Hello World!” to the terminal. Turning this project into an Electron project requires only two changes: we need to acquire Electron and change how our app is being executed. First, install Electron as a development dependency:

npm install electron --save-dev
      ​

Then, change the start script from node . to electron .. Because Electron launches the entry point script using the main process (which behaves like Node.js), no further changes are necessary. If you execute this application with npm start, you will see the greeting printed to the terminal. Node.js terminates if there is no additional code to execute, whereas Electron will remain idle, waiting for either a quit signal or additional instructions from the operating system. You can either exit your brand new app by pressing Ctrl-C in your terminal or by manually terminating it from the dock or task bar.

Starting the Application

We already have a JavaScript file that is executed as soon as Electron starts. Everything you know about Node.js development still applies to working with this file, but there is one important difference: a new module called electron is now available and can be required. It contains all of Electron’s APIs in individual namespaces. One of those API modules is the app object, which controls the application’s event life cycle.

To ensure that the application has fully booted and is ready to receive instructions, wait for the ready event on the app object (see Example 2-3).

Example 2-3. Hello world from Electron

const { app } = require('electron')

app.on('ready', () => {
  console.log('Application ready!')
})

Now, if you run this application with npm start, you should see “Application ready!” printed in the terminal. From this point on, the application is ready for all kinds of mischief, like opening windows, creating dialogs, and general application business.

Other events available on app include, for instance, will-quit (emitted right before the app terminates) or open-file (emitted when the operating system tries to open a file with the application). It also contains methods like quit(). During the early days of Electron development, the app object also housed the first native integrations (like the ability to make the macOS dock bounce). Those methods are still there, but most integrations that don’t immediately affect the application’s life cycle now live in their own namespaces. As an example, the touch bar integration for macOS can be found in the TouchBar object rather than as a property on the app object.

Opening the First Window

As soon as the application is ready, it is able to open windows. Windows are opened by creating new instances of the BrowserWindow class, which is available on the electron module. They can be opened with numerous properties that can create an invisible window, a window that cannot be resized, or a window that is always on top of everything else.

When a window is created, it is important that you assign it to a variable. If the BrowserWindow instance is created and not attached to anything (see Example 2-4), V8’s JavaScript garbage collection might consider the instance unused and throw it away, in which case the window will be closed.

Example 2-4. Opening a blank BrowserWindow

const { app, BrowserWindow } = require('electron')

let mainWindow

app.on('ready', () => {
  console.log('Application ready!')
  mainWindow = new BrowserWindow()
})

Running this application will already yield results: an empty window will be created and opened. In theory, you could have already opened the Chromium developer tools, but because there is no page loaded, they would not have been able to function properly. To actually display something, we need to create a new HTML file.

In the src/renderer folder, create a new index.html file and fill it with boilerplate code, as shown in Example 2-5.

Example 2-5. Boilerplate HTML for our first BrowserWindow

<!DOCTYPE html>
<html>
  <head>
    <title>My Code Editor</title>
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <div id="container">
      Hello!
    </div>
  </body>
</html>

To enable some basic cosmetics, create a style.css file and fill it with a simple set of instructions, as shown in Example 2-6.

Example 2-6. Boilerplate CSS for our first BrowserWindow

html, body {
  margin: 0;
  overflow: hidden;
}

div#container {
  height: 100vh;
  width: 100vw;
}

Now, let’s instruct the mainWindow to load the page. We can use this opportunity to define the behavior of the BrowserWindow with a bit more attention to detail.

Each BrowserWindow instance has a host of control methods and events available. It even contains an object called webContents, which allows full control of the underlying web page. Using this object, you can, for instance, control the navigation history, execute redo/undo commands, and inject JavaScript. When creating a BrowserWindow, one can pass an object containing the options with which the window should be created. For our editor, we want to control the initial size as well as the visibility.

Loading a web page, even if it is a local file on disk, takes a few milliseconds. Ideally, you want to show the window with the containing web application loaded and ready to go. The time needed to load a local web app is likely short, but long enough to notice a possible “white flash” of emptiness in the window. This flash is caused by the brief period during which the window contains no page and displays nothing but its white background. To avoid it, create the window with the property show set to false and subscribe to the dom-ready event on the window’s web page. Then, once the window is created and the listener registered, we load our new index.html. As soon as the page is loaded, we show the window, ensuring that the user never sees the “white flash.” That sounds complex, but requires no more than 14 lines of code. Extend your existing index.js to match the one shown in Example 2-7. Figure 2-2 shows the results.

Example 2-7. Ensuring that the window is ready before it is shown

app.on('ready', () => {
  console.log('Application ready!')

  mainWindow = new BrowserWindow({
    show: false,
    width: 1000,
    height: 800
  })

  mainWindow.webContents.on('dom-ready', () => {
    // The window has loaded its contents
    mainWindow.show()
  })

  mainWindow.loadURL(`file://${__dirname}/../renderer/index.html`)
})

Figure 2-2. A BrowserWindow with developer tools and a simple index.html

Using Node.js in the Renderer Process

Since the mainWindow has full access to Node.js, you can do everything you would be able to do in a normal Node.js application. To try the Node.js integration, start your application and open the developer tools via the window menu under “View → Open Developer Tools.” Switch to the developer console and try any Node.js instruction. For example, require('fs') indeed returns the Node.js filesystem module.

We can use this to our advantage and organize files and modules with the Node.js module system. Instead of adding content directly to the HTML file, we will build a JavaScript-driven web application. You would likely choose React, Angular, or any other web framework to taste. In this short tutorial, we will get by with nothing but “vanilla JavaScript.” Clear the <div> and add a <script>require('./renderer.js')</script> tag to the body, as shown in Example 2-8.

Example 2-8. Extending index.html with a require instruction

<body>
  <div id="container"></div>
  <script>require('./renderer.js')</script>
</body>

Create a renderer.js file in the src/renderer folder and fill it with a console.log('Hello World!') statement. Launch the application again and you should see the “Hello World!” greeting printed to the console in the page’s developer tools.

Having Node.js at our disposal means that we do not have to build the editor from scratch—we can reuse the existing components available on npm. Microsoft published the core editor component of their excellent Visual Studio Code under its original code name Monaco, so in order to build our own editor app, we will install it from npm and load it in our renderer.js file.

Install the module monaco-loader and save it as a dependency of your application using the terminal command npm install monaco-loader --save​. Then, use it from renderer.js to create a new editor component. The code snippet in Example 2-9 instantiates a simple Monaco editor component with a black theme, automatic layout configuration, and language automation for JavaScript.

Example 2-9. Creating a Monaco editor in renderer.js

const loadMonaco = require('monaco-loader')

loadMonaco().then((monaco) => {
  const element = document.querySelector('#container')
  const options = {
    language: 'javascript',
    theme: 'vs-dark',
    automaticLayout: true
  }

  const editor = monaco.editor.create(element, options)
})

Save the file and restart your application. Congratulations, you built your first desktop application with Electron! Granted, core features are missing—it can neither open nor save files, cannot be compiled to a standalone binary, and does not integrate properly with the operating system in the way a user would expect an editor to. This introduction will continue to build upon this example to illustrate the usage of APIs and Electron features, so this is your opportunity to build the editor of your dreams.

Extending the Monaco Editor

Since the editor serves as an example for a web application, this introduction will not go into any additional details around the Monaco editor, but if you are curious about its capabilities, you can find documentation in its GitHub repository.

Chapter 3. Working with Electron

User Interfaces with BrowserWindows

BrowserWindows are at the forefront of user interfaces built with Electron. They are they main way to display visuals to the user. Given their flexibility, you are not limited to the classic rectangular window—configured properly, they can be any shape, size, or opacity, and take on any Z-Index in the desktop’s window manager. If a situation requires that a window is heart-shaped, half-transparent, and on top of the macOS dock, Electron will make that work.

When creating a window with new BrowserWindow(options), numerous properties can be passed in to define where, how, and in what size the window will be created. Let’s take a look at some of the most important properties. For a full list of the 150+ options for any given version of Electron, please consult the official API documentation; if you are building a traditional desktop app, the following list has you fully covered:

width, height (number)

Window’s width and height in pixels, with a default of 800 x 600.

x, y (number)

Left and top offset of the window from the screen. Default is that the window is centered on the current screen.

minWidth, minHeight (number)

If given, these numbers will define the smallest size the window can be resized to by the user.

maxWidth, maxHeight (number)

These two numbers define the biggest size the window can be resized to by the user.

resizable (boolean)

If set to false, the window will not be resizable at all.

movable (boolean)

In a similar fashion, setting this to false will fix the window’s position on screen.

minimizable, maximizable, closable (boolean)

Setting any of these settings to false will disable the matching operation.

alwaysOnTop (boolean)

If set to true, the window will always be on top of other windows, even if another app is focused.

fullscreen, fullscreenable (boolean)

Setting the fullscreen to true will have the window open in fullscreen mode. Setting the latter to false will keep users from entering the state manually.

title (string)

The default window title (overriden by the HTML document’s title).

icon (NativeImage or string)

The window icon used in the taskbar. If left undefined, the application’s icon will be used.

show (boolean)

Show the window. If set to false, the window will be hidden upon creation.

frame (boolean)

If set to false, the window will be created without the operating system’s default window frame. This allows you to define all aspects of the window’s UI.

parent (BrowserWindow)

If the window should be a child to another window, this informs both Electron and the operating system about the relationship.

modal (boolean)

If a window is a child window, enabling this property turns into an attached modal, which is affixed to the parent window.

backgroundColor (string)

The window’s background color as a hexadecimal value.

opacity (number)

The initial opacity of the window, ranging from 0 (for 0%) to 1 (for 100%).

transparent (boolean)

The window will be transparent if set to true.

Showing Windows Gracefully

To ensure that the app feels snappy, a window needs to load gracefully, without visible “loading flashes.” If your web application is able to load within a few milliseconds, waiting for either the dom-ready event on the webContents or the ready-to-show-event on the browserWindow itself might be the best options. The first corresponds to the dom-ready event as emitted on the web page, while the later will be emitted when the page has been rendered for the first time but has not been shown yet.

If the web application is more complex and takes a longer time to load, delaying the window visibility might not be an option and could make the application launch feel sluggish. In those cases, the BrowserWindow can be created with a backgroundColor to ensure that the app can load into a window that already has the correct color (see Example 3-1).

In the example below, the window is created with the same background color as the web application it is supposed to hold. This avoids the “white flash of emptiness”—and though it does not actually impact how quickly the app loads, avoiding the flash leads to a better user experience.

Example 3-1. Opening a BrowserWindow with a predetermined background color

mainWindow = new BrowserWindow({
  backgroundColor: '#1e1e1e'
})

If none of these approaches are feasible, one has the option to show the window from the renderer process using the remote object. The remote object can be used from renderer processes to access methods and objects on the main process. Since operations on the current BrowserWindow are fairly common, the helper method getCurrentWindow()​ allows access to the current window. Note that the actual call is still made from the main process—behind the curtains, the remote object notifies the main process and executes operations there.

In Example 3-2, the BrowserWindow was instantiated with the option show: false. It can then be shown as soon as the web app has finished loading:

Example 3-2. Showing the window from the renderer process

const { remote } = require('electron')
const loadMonaco = require('monaco-loader')

loadMonaco().then((monaco) => {
  const element = document.querySelector('#container')
  const options = { ... }
  const editor = monaco.editor.create(element, options)

  // The editor is live, let's show the window
  remote.getCurrentWindow().show()
})

Configuring the Web Page with webPreferences

BrowserWindows allow for fine-grained control of how the web page behaves and which features it will have available. Though some of libchromiumcontent’s more obscure feature flags are rarely necessary (image support, for instance, can be disabled), feature flags are an excellent tool to harden application security for remotely loaded content.

Node.js integration and preload scripts

The two most powerful options available through webPreferences are preload scripts and the ability to turn off the Node.js integration. When nodeIntegration: false is set, Electron will disable the Node.js integration, thus not granting the loaded website the far-reaching powers it would otherwise have. A script passed to the preload property will be executed before any web content is loaded and has Node.js integration available, even if Node.js integration is otherwise disabled.

This enables developers to build a custom API, slimmed down to just those features needed by the web page. One of the more common use cases for Electron is the extension of web applications. The idea is simple: Create a web app, available at www.my-great-app.com, and simply load it inside a BrowserWindow. Loading remote content comes with security risks that need to be carefully considered (see “Ensuring Security” for more information), but combining preload scripts and nodeIntegration greatly reduces that danger.

Consider the following example. Using the configuration shown in Example 3-3, the loaded web page will have no access to Node.js APIs whatsoever, but it will have access to the custom API window.myAPI.readApplications(), which reads the applications directory. Even if the web page is taken over by a malicious actor, the worst native operation they could possibly do is to read the contents of this folder.

Example 3-3. Creating a custom API with preload scripts

// In the main process: Create the window
mainWindow = new BrowserWindow({
  webPreferences: {
    preload: require.resolve('./preload.js'),
    nodeIntegration: false
  },
});

// In preload.js: Create a custom api
const fs = require('fs')

window.myAPI = {
  readApplications() {
    return fs.readdirSync('/Applications')
  }
}

BrowserWindows continue to evolve. The Electron maintainers, as well as Google, continue to improve the capabilities, methods, and events as operating systems evolve. The full API depends on the version of Electron used and can always be found in the detailed API documentation.

Multiple Windows and Threads

Each BrowserWindow is powered by its own renderer process. In Electron, each process can communicate with the main thread (and thus all other processes) using Electron’s powerful inter-process communication (borrowed from libchromiumcontent). This enables the composition of multithreaded applications that use multiple windows and processes to split up work, allowing the operating system to allocate resources appropriately.

Consider the email client Nylas Mail, written entirely in Electron. To ensure that each managed email is only a few milliseconds away, it uses a SQLite database, which is available as a native C++ module that can be called from Electron. One could just require the native module in the application’s main window, but that would have dire consequences: because the SQLite database instance would live in the same process as the application’s user interface, long-running operations in the database would be able to block the user interface. For example, a full-text search on a bigger database could, for a few seconds, block scrolling. Obviously, that would be unacceptable, so Nylas runs SQLite in its own process and uses IPC to pass objects between the database and the application’s main window.

Electron developers often use hidden BrowserWindows, IPC, and the powerful JavaScript Proxy to compose multiprocess applications.

Inter-Process Communication

The require('electron') module has two sub-modules responsible for enabling inter-process communication. ipcMain is used exclusively by the main process to receive messages from renderer processes. To send data to a render window, the main process uses the send() method available on a window’s webContents property. In the renderer process, ipcRenderer can be used to send and receive messages to and from the main process.

Both ipcMain and ipcRenderer are Node.js EventEmitters and have all the usual event-related methods and properties one would expect. Messages follow a simple protocol and are always sent on a channel, represented by a single string. Let’s put both into action and play a simple game of ping pong (Examples 3-4 and 3-5).

Example 3-4. Sending and receiving messages in the main process

// In the main process: Create the window and send a ping
const { ipcMain, BrowserWindow, app } = require('electron')

let mainWindow = null

app.on('ready', () => {
  mainWindow = new BrowserWindow();
  mainWindow.loadURL(`file://${__dirname}/src/renderer/index.html`)

  ipcMain.on('ping-pong', (event, data) => {
    console.log('Pong received:', data)
  })

  setTimeout(() => {
    mainWindow.webContents.send('ping-pong', { ping: true })
  }, 1000)
})

Example 3-5. Sending and receiving messages in the renderer process

// In the main process: Listen to the ping event
const { ipcRenderer } = require('electron')

ipcRenderer.on('ping-pong', (event, data) => {
  console.log('Ping received:', data)

  ipcRenderer.send('ping-pong', { pong: true })
})

By default, messages will be sent asynchronously. Methods to send them synchronously are available, as well as methods to respond directly to a renderer process’s logical host or a sender of a message.

Performance Considerations

Additional windows are not free. A renderer process has a small footprint and a life cycle that needs to be managed. While the used CPU resources of idle windows approaches zero, each window always holds on to some memory, no matter how light the content. Multiple strategies have evolved; which one is most appropriate depends on each individual situation. Applications that need to create and destroy renderer processes at a higher frequency tend to use a task pool, meaning that they always have an “empty” window loaded and ready.

Performance needs might also give way to other considerations. Visual Studio Code, for instance, runs each extension in its own renderer process to ensure that no single editor extension can slow down either the editor or any other extension. In short, composing an Electron application with multiple processes might be the correct solution to provide an excellent user experience, but each additional process comes at the cost of resources taken from the operation system and other applications.

Threads with Web Workers

If an operation is too small for its own process but too big to run in the same thread as your other JavaScript, consider Web Workers, which are supported by Electron. This recently standardized object allows the creation of individual threads from JavaScript and are excellent for expensive for loops and similarly costly operations.

There are some caveats. Node.js integration in Web Workers is disabled by default. While it can be enabled with the nodeIntegrationInWorker property on a BrowserWindow’s webPreferences object, it’s not ready for prime time: even if enabled, Electron’s APIs are not accessible. Loading native Node.js modules will not immediately throw any errors, but instead will lead to issues down the road. Most of them are written with a single-threaded environment in mind, and even if the module is thread-safe, Node.js’s own process.dlopen operation is not. In short, use Web Workers for web-safe JavaScript and simple Node.js tasks, but do not attempt to perform native operations.

Native Menus

Electron uses the Menu module to represent all native menus. This includes the application menu, which is visible in the menu bar on macOS and some Linux distributions; the window menu, which will be shown by macOS in the menu bar and by Windows directly underneath the window frame; and contextual menus. The Menu module lives in the main process but can be safely called from the renderer process using Electron’s remote module.

Default Menus

During development, Electron will provide default menus when none are otherwise set. This is not the case in production; in order to have any menu in your final app, you will need to manually set them. To include it in production, use the module electron-default-menu.

In Electron, menus are created using an array as a template, no matter where you intend to use them. Let’s take a look at this basic example, which creates a menu with three top-level items and then uses it as a context menu (see Example 3-6). A Menu instance is created by calling the buildFromTemplate() method with a simple array as the template. Each item in the array represents a MenuItem. Opening the menu is straightforward: each instance has a popup() method opening the menu as a context menu in the active BrowserWindow (i.e., the window that would be returned by a getFocusedWindow() call) at the current mouse position, allowing for either to be overridden with parameters.

Example 3-6. Creating a simple context menu

const { remote } = require('electron')
const { Menu } = remote

const template = [
  {
    label: 'Minimize Window',
    click(item, browserWindow, event) {
      browserWindow.minimize()
    }
  },
  {
    type: 'separator'
  },
  {
    label: 'Quit',
    click() {
      require('electron').remote.app.quit()
    }
  }
]

const menu = Menu.buildFromTemplate(template)

window.addEventListener('contextmenu', (e) => {
  e.preventDefault()
  menu.popup(remote.getCurrentWindow())
}, false)

The most obvious properties are the label and the click handler. The latter will be called with the MenuItem, the current BrowserWindow, and the event as parameters whenever the user invokes the item. An individual item can also be passed another array as a submenu. Other properties allow the configuration of icons, keyboard shortcuts, and fine-grained positioning control (see Example 3-7).

Electron provides shortcuts for implementing the most common operations—actions like copy, paste, or cut can be added to a menu using the role property. While the MenuItem API itself is fairly stable, the list of predefined roles is still evolving. Before implementing a common operation, always check the API documentation to see if a custom implementation is really necessary.

Example 3-7. Advanced menu template

const template = [
  { role: 'cut' },
  { role: 'copy' },
  { role: 'paste' },
  { type: 'separator ' },
  {
    label: 'Language...',
    submenu: [
      {
        label: 'English',
        accelerator: 'CmdOrCtrl+Shift+E',
        icon: './images/english.png',
        click: (item) => setLanguageFromMenuItem(item)
      },
      {
        label: 'French',
        accelerator: 'CmdOrCtrl+Shift+F',
        icon: './images/french.png',
        click: (item) => setLanguageFromMenuItem(item)
      },
      {
        label: 'Enable Spellchecking',
        type: 'checkbox',
        checked: true,
        click: (item) => toggleSpellCheckforWindow(item.checked)
      }
    ]
  }
]

Once created, the Menu instance can be used as an application menu by calling Menu.setApplicationMenu(menuInstance); or as a menu for a BrowserWindow by calling browserWindow.setMenu(menuInstance). As you can see in Example 3-7, each item accepts an accelerator property, which describes an application-wide or window-wide shortcut. The menu is responsible for enabling classic behavior like cut, copy, and paste, so if you want your users to be able to copy with a Cmd-C on macOS (or Ctrl-C on Windows and Linux), then you need to implement a menu.

Native Dialogs

The dialog module enables developers to show native message, error, open, and save dialogs with little effort. All of them are implemented with the same pattern: each type of dialog has its own method on the dialog module, which can be called with a short set of options and a callback. As soon as the user has closed the dialog, the callback is called with the user’s input as parameters.

The dialog module is called from the main process, but can be safely accessed from a renderer process using the remote module. All dialogs can be optionally passed a BrowserWindow as the first parameter, turning the dialogs into modals that are directly attached to the window. A modal forces the user to deal with the dialog before they can continue interacting with the window, making them useful for errors and questions that are supposed to interrupt the user.

Warning, Error, and Information Dialogs

Any dialog that is supposed to merely display information or ask a question is considered a message box. Different operating systems know different types of message boxes, but the choices are generally info, error, and question. A message box has multiple string fields (title, message, detail) and can hold custom buttons (see Figure 3-1). The only place for any custom design is the dialog’s icon, which defaults to the current application’s icon. Exactly one checkbox with a label can be added to the mix (see Example 3-8).

Figure 3-1. A message dialog with two buttons

Example 3-8. A confirmation message box with checkbox

const { dialog } = require('electron')

const buttons = ['Red', 'Green', 'Yellow']

const options = {
  type: 'question',
  buttons: buttons,
  title: 'My Gummy Bear App',
  message: 'Which gummy bear has the best color?',
  detail: 'Choose carefully!',
  checkboxLabel: 'Remember my choice',
  checkboxChecked: true,
}

dialog.showMessageBox(options, (response, checkboxChecked) => {
  const selected = buttons[response]

  console.log(`User responded with ${selected}`)
  console.log(`Remember choice: ${checkboxChecked}`)
})

Open and Save Dialogs

All operating systems have made efforts to standardize dialogs for opening and saving files, so naturally an Electron application should not deviate from those norms. Opening either dialog is done similarly to how question dialogs work: the dialog module has two methods, showOpenDialog() and showSaveDialog(), which can be called with a number of configuration options.

The open dialog can be used to open files or folders, optionally with multiple selections (see Example 3-9). The options also accept an array of valid file extensions, should your application only handle certain file types. To support all file types, a * wildcard can be used.

Example 3-9. An open dialog with basic configuration

const { dialog } = require('electron')

const options = {
  filters: [
    { name: 'JavaScript', extensions: ['js', 'jsx'] },
    { name: 'All Files', extensions: ['*'] }
  ],
  properties: [
    'openFile',
    'openDirectory',
    'multiSelections'
  ]
}

dialog.showOpenDialog(options, (paths) => {
  console.log(`The user selected the following path: ${paths[0]}`)
})

Save dialogs are created using the same pattern. A save dialog does not actually save anything; it simply returns the path selected by the user (see Example 3-10). Again, a list of valid file extensions can be submitted to restrict the file types offered by the dialog.

Example 3-10. A save dialog with basic configuration

const { dialog } = require('electron')

const options = {
  filters: [
    { name: 'Source Code', extensions: ['txt', 'js', 'jsx'] },
    { name: 'All Files', extensions: ['*'] }
  ]
}

dialog.showSaveDialog(options, (path) => {
  console.log(`The user selected the following path: ${path}`)
})

Both dialog types are called asynchronously when a callback is provided, but they can be called synchronously by simply not providing a callback. If used without a callback, they will return the selected paths directly.

Native Notifications

Notifications are core to the user experience on any platform: Windows, macOS, and most Linux distributions added the native notifications first made popular on mobile devices. If your application has things to tell the user about, an Electron app’s ability to integrate with the operating system’s native notification system is a feature that distinguishes it from a mere website. While Chrome uses custom notifications on some systems, Electron uses each operating system’s native notification method.

On macOS, Electron uses the default notifications that show up in the upper right corner and in the system’s notifications flyout (opened by clicking the icon right next to the Siri icon in the menu bar). Your app will thus show up in the Notifications section of System Preferences, where the user can fine-tune how notifications show up (see Figure 3-2). Interactive notifications (that accept a reply) are also supported.

On Windows, Electron attempts to use the Action Center where available. On Windows 10, notifications just work out of the box. Annoyingly, on Windows 8 and 8.1, applications need to have a shortcut with an Application User Model ID in the start menu to be able to send notifications. That sounds tedious, and installers creating shortcuts will do this for you automatically. Windows 7 does not have a native notification system, but Microsoft contributed a custom implementation to Electron that allows developers to send native notifications similar to those on Windows 8 and later. Instead of disregarding notifications on that older system, Electron will now display a toast notification in the lower right corner of the display.

Figure 3-2. Native macOS notifications interface

Newer versions of Windows allow the creation of notifications with XML, allowing developers to add custom buttons, text fields, images, and other elements to a single notification. Electron provides a basic notification out of the box, while the electron-windows-notifications module enables the use of XML. If your application requires advanced usage of all of Window’s notification capabilities, this module offers full flexibilty through Microsoft’s proprietary XML notification schema.

On Linux, Electron uses libnotify to send notifications, thus supporting Unity, GNOME, KDE, and any other environment that follows the Gnome notification specification.

Notifications are typically sent from the renderer process, with the option to send them from the main process added in Electron 1.7.0. In the renderer process, notifications follow the HTML5 Notifications standard, meaning that creating a native notification is identical to creating a web notification (see Example 3-11).

Example 3-11. Creating a notification from a renderer process

const myNotification = new Notification('Hello!', {
  body: 'This is a simple notification'
})

myNotification.onclick = () => console.log('Notification clicked!')

In the main process, notifications can be created with the Notification module (see Example 3-12). It does not follow the HTML5 spec and instead uses the same pattern as other Electron modules and methods, accepting properties in a single options parameter and emitting events using Node.js’ EventEmitter. Notifications are ultimately sent using the same underlying native code, so the behavior does not differ fundamentally.

Example 3-12. Creating a notification from the main process

const { Notification } = require('electron')

const toast = new Notification({
  title: 'Hello!',
  body: 'This is a simple notification',
  hasReply: true,
  actions: []
});

toast.once('close', (event) => console.log('Closed!'));
toast.once('click', (event) => console.log('Clicked!'));
toast.once('reply', (event, response) => {
  console.log(`Responded: ${response}`)
});

Ensuring Security

Electron’s technical advantages are a double-edged sword when it comes to security: with open access to the whole operating system comes both power and danger. Seasoned developers of desktop software are used to the responsibilities that come with that power, whereas web developers enjoyed a blissfully limited environment for years—the features offered by a browser may be lacking, but the environment is generally secure. Browsers already assume that there might be malicious websites, so an attacker taking over your website has limited access to your user’s machines.

However, Electron is not a browser. The untethered access to the operating system makes it powerful, but letting remote web content take control of those powers is irresponsible and open your users up to dangers that Electron is not meant to handle. For that reason, it is heavily recommended that applications be as self-contained as possible. Ideally, an application ships with all JavaScript that will ever be executed, using the web exclusively for API calls.

A BrowserWindow is a blank canvas, available with broad powers out of the box. It is up to the developer to cut those powers down to what is actually needed. A security issue in Electron exists whenever code is downloaded and executed. In practice, many developers find themselves in a position where that cannot be entirely avoided, so this section discusses strategies and practical techniques to mitigate the inherent security risks.

Safely Displaying Remote Content

A renderer process can be created with one three isolation types:

Node.js integration enabled

Without additional configuration, each BrowserWindow or WebView will have full access to Node.js by default. Using this option for a remote website loaded via HTTP or HTTPS is irresponsible and, while possible, should never be done in production.

Node.js integration disabled

Using webPreferences, Node.js integration can be disabled. Preloaded scripts still have access to Node.js and can define a custom API. The renderer process still has access to the operating system, but the JavaScript context might have a limited set of APIs available to call methods from within a website.

Sandbox enabled

In this mode, Electron will create a renderer process that is contained in the same sandbox used by Chrome to ensure that a website cannot modify or access system resources. Electron will not create a Node.js context for the renderer. Using sandboxed renderers is an advanced use of Electron that comes with many caveats and is still considered experimental.

The sandbox option is most interesting to developers using Electron to build another browser, and is not used in many other scenarios. In fact, the development and maintenance of sandbox-related features are championed by Brave, an Electron-based browser developed by the inventor of JavaScript, Brendan Eich.

Given those three options, selectively disabling Node.js integration is currently considered best practice whenever remote content is loaded.

A common setup involves using a BrowserWindow (with Node.js integration enabled) that hosts a WebView (with Node.js integration disabled; see Figure 3-3). The window’s renderer process has the ability to ship as much logic and user interface with the app as possible, and can selectively load remote content in the WebView. Through a combination of controlling the WebView’s navigation using Electron’s navigation events as well as preload scripts, developers are able to ensure:

The WebView only loads known hosts via HTTPS

The WebView has only the precise APIs needed

The WebView integrates with the user interface inside the BrowserWindow

Figure 3-3. Using a BrowserWindow, a WebView, and preload scripts in combination

Since the preload script has access to the IPC module, it can set up API methods that communicate either with the renderer process hosting the WebView or with the main process. It is up to the developer to write a preload script that does not expose dangerous methods, so vigilance is in order whenever Node.js access is given to a web resource, even if it is your own. The application should never fully trust any web content—instead, it should verify, escape, and clean every instruction.

The Future of WebViews

Google is in the process of replacing WebViews with alternative implementations for safely displaying content inside a website. At some point in the future, the WebView will be officially superseded by a technology called Out-of-Process iframes (OOPIFs). In the meantime, motivation for fixing issues with the WebView tag is dropping, meaning that developers will constantly find little bugs that are only present when a WebView is used.

In the meantime, Electron has started work on an experimental technology called BrowserViews, which composes a renderer on top of an existing BrowserView. This enables developers to implement the same security architecture outlined in “Safely Displaying Remote Content”. It offers less flexibility (currently, only one BrowserView per BrowserWindow is allowed, for instance) but does not include the subtle bugs present in WebViews. In short: the “embedding content in your page” space is in flux.

Chapter 4. Binaries, Installers, and Updates

During development, an Electron application behaves just like a Node process. The Electron runtime, launched by executing Electron.exe or Electron.app, loads the entry point JavaScript file and only then starts behaving like “your” application. However, once you ship your application to users, the name Electron should never show up in any Windows task manager or macOS activity monitor. You will likely also want to change the icon and create a single installer file, ready to be downloaded by potential customers.

Shipping an Electron application therefore consists of two steps. First, the application needs to be packaged. In this process, a binary with the app’s name and icon is created for each platform, bundling up the whole application, including all JavaScript files and dependencies. Then, installers need to be created. In the spirit of Node.js development, the solution for both tasks comes in the form of npm modules. Traditionally, developers would use bite-sized modules to build custom build pipelines, combining modules such as electron-packager, electron-winstaller, or electron-installer-dmg into artisanal scripts. As interest in Electron grew and support for more distribution methods was added, creating a build pipeline from scratch became an increasingly daunting task. To solve that problem, the community has started work on a CLI tool called electron-forge. Its ability to support developers with automatic scaffolding, rebuilding of native modules, packaging, and creation of installers is amazing and has this guide’s recommendation.

On electron-builder

Readers with Electron experience may wonder why electron-builder is not being discussed here. While offering an impressive one-stop solution, electron-builder developer Vladimir Krivosheev did so at the cost of reimplementing all Electron dependencies instead of using those provided by the Electron maintainers (like, for instance, electron/windows-installer). This means that improvements and bug fixes introduced by the Electron developers in these auxiliary packages are not reflected in electron-builder. For that reason, this guide recommends electron-forge instead.

To install electron-forge, run npm i -g electron-forge. Once installed, it can either scaffold a new project with the electron-forge init command or import an exiting project with the electron-forge import command. If you decide to scaffold a new project, it offers multiple templates—one for each major frontend framework. Popular ones are React, Angular 2, and Vue, but by default it will use a plain vanilla JavaScript template that does not include any additional dependencies.

Troubles with electron-forge

electron-forge is a powerful tool that is pushing both Node.js and npm to its limits and is not always compatible with the latest version of either. Should you run into any trouble, consult the readme in the GitHub repository as well as the list of known issues for help.

Importing the Electron Code Editor

To turn the Electron-based code editor from this guide’s short “Hello World” exercise into an electron-forge project, you need to import it. First, install the tool globally with npm (npm i -g electron-forge). Then, navigate your terminal to the root of your code editor and execute electron-forge import. The CLI will start a wizard and walk you through the import steps. Given how simple the little code editor is, all default assumptions made by electron-forge will be correct.

Packaging the Application

To package an Electron application, binaries need to be created for three platforms: Windows, macOS, and Linux. On Windows and Linux, you might decide to offer 64-bit binaries in addition to 32-bit ones. As a rule of thumb, you should always try to package an application on the target platform—though there are many reports of successfully building Windows binaries using Wine on a Mac, installing virtual machines for each target platform is usually easier, quicker, and guaranteed to actually result in working binaries.

For Windows, Microsoft offers free virtual machines for VirtualBox, Parallels, VMWare, and Hyper-V. If you do not have a preference, consider that VirtualBox is freeware and cross-platform. Alternatively, consider a service like AppVeyor, offering Windows-based continuous integration services in the cloud.

macOS is a bit trickier. macOS runs in a virtual machine, but Apple’s licenses usually require that an actual Apple computer is used. If you do not have an Apple machine available, the cloud is your only option. Travis CI offers free CI services for open source software and includes both macOS and Linux machines.

For Linux, solutions are plentiful. Ubuntu in a virtual machine is a safe choice, but so are the many cloud CI services offering automatic build services on Linux machines. If you want to try something new, the Linux Subsystem for Windows, which allows the installation of a command-line version of Ubuntu from the Windows Store, works too.

The actual packaging is done by a module called electron-packager, which can be either used directly from the command line or manually integrated into a custom build process. Using electron-forge, one simply has to call electron-forge package to package the current architecture and platform.

Configuration

Even though electron-forge accepts command-line arguments, configuring all modules involved in the build process from a central location allows for easy reproducible builds. After running either the import or init command, your package.json file should contain a configuration skeleton (see Example 4-1)

Example 4-1. Configuration skeleton for electron-forge

"config": {
  "forge": {
    "make_targets": {
      "win32": [
        "squirrel"
      ],
      "darwin": [
        "zip"
      ],
      "linux": [
        "deb",
        "rpm"
      ]
    },
    "electronPackagerConfig": {},
    "electronWinstallerConfig": {
      "name": ""
    },
    "electronInstallerDebian": {},
    "electronInstallerRedhat": {},
    "github_repository": {
      "owner": "",
      "name": ""
    },
    "windowsStoreConfig": {
      "packageName": ""
    }
  }
}

The properties in the electronPackagerConfig will be passed down to electron-packager (see Example 4-2). Its API is extensive and allows for fine-tuned control of all kinds of options, but developers should at least include the following critical metadata used by operating systems to determine the name, version, author, and copyright information of a binary.

appCopyright (string)

A human-readable copyright line used by both Windows and macOS.

icon (string)

Path to an icon file used to replace Electron’s default icon. Windows requires an ico file, macOS an icns file. Provide a path without extension but have both file types at the path available.

osxSign (object)

If you are in possession of an Apple Developer Certificate, electron-packager can sign the binary for you. For more information, see the documentation of electron-osx-sign.

Example 4-2. Basic configuration for electron-packager

"electronPackagerConfig": {
  "appCopyright": "Copyright (C) 2017 O’Reilly Media, Inc.",
  "icon": "static/icon",
  "osxSign": {
    "identity": "6RQE5FT22A"
  }
}

Once configured, run npm run package or the equivalent electron-forge package . to package your application into a binary. On macOS, the result will be self-contained, meaning that the .app bundle in the output folder is all that is required to run the application. On Windows, the resulting .exe requires all files in the output folder to be present.

Code-Sign Your Applications

Both Windows and macOS include mechanisms to verify the signature of binaries, displaying warning dialogs to users if the authenticity of an application cannot be verified. This is not unique to Electron, but if this is your first foray into software development for the desktop, make sure to get an Apple Developer ID Certificate and a Microsoft Authenticode Certificate. See the Microsoft Introduction to Code Signing and the Apple Code Signing Guide for more information.

Creating Installers

Not all platforms require installers, but in the parlance of the Electron world, any way to bundle up the application is considered an installer. The list of possibilities includes the traditional Windows installer, packages for the Windows Store and Mac App Store, and Linux app packages. Multiple open source modules are available for each imaginable target, with the following supported by electron-forge:

zip (all platforms)

Creates a simple zip file containing the application and all required files.

squirrel (Windows)

Creates Squirrel.Windows installers, including both the classic exe installer as well as Microsoft’s modern MSI format. Uses electron-winstaller.

appx (Windows)

Creates a Windows appx package either for sideloading or submission to the Windows Store. Uses electron-windows-store.

dmg (macOS)

Creates a dmg image. Uses electron-installer-dmg.

deb (Linux)

Creates a deb package for Ubuntu and other Debian-based distributions. Uses electron-installer-debian.

rpm (Linux)

Creates an rpm package for Red Hat and other Fedora-based distributions. Uses electron-installer-redhat.

flatpak (Linux)

Creates a flatpak package for the Flatpak distribution mechanism, which describes itself as “the future of application distribution.” Uses electron-installer-flatpak.

The templates included with electron-forge create Squirrel installers for Windows, zip files for macOS, and deb/rpm files for Linux by default. To configure which targets to create, use the make_targets property inside the configuration in package.json. Each module responsible for these targets can be configured individually—they all offer numerous options for fine-tuned control, but in practice only a few options are relevant to most developers. Once you have committed to Electron as a platform and have reason to deviate from the defaults, you can find all the configurable options in the documentation.

To create installers, run npm run make or the equivalent electron-forge make in a project’s directory. Creating installers will also automatically kick off packaging. Unless instructed to behave otherwise, electron-forge will create installers for the platform it is currently running on.

Enabling Automatic Updates

A quick chat with anyone who was involved in the development of earlier Internet Explorer versions reveals the tremendous importance of updates. Users are, at large, too lazy to update, and no mechanism will save you more often from yourself than Electron’s included automatic updating mechanism. It is based on the Squirrel updating framework, which combines two native implementations for Windows and macOS. Automatic updates for Linux are currently not supported. Squirrel is not Electron-specific, but Electron promotes Squirrel as the updating framework of choice.

Automatically updating an Electron application means that the application communicates with a server to determine whether a new update is available. If so, it needs to be downloaded, unpacked, and verified. The tricky part comes at the end: the application needs to replace itself with the new downloaded version. Electron offers an autoUpdater module that reduces this complex process to a few lines of JavaScript.

Updating Is Hard

Automatic updates are complex and depend on many factors out of any developer’s control—operating system, internet connection, antivirus software, user interaction, and your update server all need to perfectly work together for an update to actually succeed. Many developers of Electron-based applications have reported that a small percentage of users will always be left behind each time an update is released. Many solutions are available, including the traditional warning dialog that prompts users to update. Whatever choice you make, be aware that Electron’s integrated updating mechanism is not perfect.

On both macOS and Windows, auto-updating requires that the application was previously installed in an update-ready way. On macOS, the currently installed version and the version to be downloaded need to be signed with the same Apple-provided developer certificate and the user needs to have sufficient write access to overwrite the application. On Windows, the update is done by the Squirrel Installer, so if the app was not installed with a Squirrel Installer, automatic updates are out of the question.

Providing an Update Server

Auto-updates are downloaded from a developer-provided server. Sadly, the two updater frameworks inside Electron’s auto updater (Squirrel.Mac and Squirrel.Windows) share nothing but the name and expect completely different endpoints. The macOS updater expects an API that returns JSON-formatted information about a possibly available update for a given version—in short, the server determines whether or not a new version should be downloaded. The Windows updater does not need an API at all; instead, it expects a so-called RELEASES file that contains information about all available versions.

That is obviously a terrible developer experience, so the community sprung into action to bridge that gap and multiple turnkey server solutions exists today. One of the first solutions was Nuts by Gitbooks.io, a fully automated server package that uses GitHub Releases as the source for new releases. The newest and possibly easiest solution is Zeit’s Hazel, a lightweight and ready-to-deploy framework that also uses GitHub Releases. If GitHub Releases are not an option for you, consider electron-release-server. It lacks the corporate sponsorship keeping Nuts and Hazel in good shape, but offers far more flexibility.

If none of those solutions work for you, know that the Squirrel.Mac framework has recently started discussing the support of file-based updates. In the very near future, an update server framework might not be necessary at all—you can already host Squirrel.Windows updates entirely on a CDN, and a file-based updating solution for Squirrel.Mac would remove the need for any server-side logic.

Implementing the Update Logic

In Electron’s JavaScript, auto-updating is an almost trivial process. Once the auto-updater is given a feed url, it can immediately go to work and does not require any additional configuration. The updater will emit events for each finding that it makes, but even in its most basic implementation, updates will be downloaded automatically and installed whenever the user quits the application.

The autoUpdater module is only available in the main process, but communication with it is possible from any renderer using the remote module. The four events emitted are checking-for-update, update-available, update-not-available, and finally update-downloaded. Once the update is downloaded, Electron’s auto-updater will install the update automatically as soon as the app quits. To do so programmatically, developers can call autoUpdater.quitAndInstall() (see Example 4-3).

Example 4-3. Basic implementation of Electron’s auto-updater

const { autoUpdater } = require('electron')

autoUpdater.setFeedURL('https://my-update-server.com')

autoUpdater.on('update-available', () => {
  // An update is available! If desired, this
  // information could be surfaced to the user
})

autoUpdater.on('update-not-available', () => {
  // An update is not available! If desired, this
  // information could be surfaced to the user
})

autoUpdater.on('update-downloaded', () => {
  // At this point, the update is ready and will
  // be installed when the app restarts. To do so
  // immediately:
  autoUpdater.quitAndInstall()
})

autoUpdater.checkForUpdates()

Once an update becomes available, it will typically take a few weeks for users to update. To determine how long it will take a typical user to receive a new version, consider how often the application is opened, how long it is usually open for (if it is closed before the update is downloaded, it will have to restart the download the next time it is opened), and what kind internet connection your users typically have.

App Stores as an Alternative to Automatic Updates

Electron apps can be submitted to both the Mac App Store and the Windows Store. While a bit of work is required to become a registered developer with each store, their respective update-mechanism is unmatched by any custom updating framework. Baked into the operating system, updates can be downloaded without the application running, and because they are installed by the operating system, chances for failures are dramatically reduced.

Of course, there are plenty of arguments for using Squirrel. Electron Apps are only supported in the Windows Store from Windows 10 with the Anniversary Update on, and the Mac App Store requires apps to run in the macOS sandbox.

Chapter 5. Considering Electron

Should you build an application with Electron? For most developers and teams, that question comes down to three core considerations:

How performant can a JavaScript application really be, and how much work is it to achieve a fluid interface?

How much support can one expect from the community and peers: how much open source code is available, how much information about proven strategies can be found, how well does the stack integrate with other technologies used at the organization?

Lastly, where does Electron fall short: where should a developer expect to do more work than other frameworks might require? This closing chapter aims to answer exactly those questions.

Performant Web Applications

The prevailing and popular argument against building applications with any JavaScript-based solution (including Electron) is performance. JavaScript on the desktop is like CGI in movies: whenever it is applied correctly and with craftsmanship, it goes by unnoticed, meaning that JavaScript is only judged by its weaker implementations.

Electron’s biggest strengths are worth repeating. The web is the dominant platform for building cross-platform user interfaces. The combination of a modern JavaScript engine and built-in support for native code allows developers to build cross-platform applications that make maximum use of web technologies without giving up the ability to use native code for performance-intensive operations. In other words, whenever an Electron application is not performant, a developer has missed an opportunity to optimize code.

In short, all rules that apply to building performant web applications for browsers equally apply to building applications for Electron, with the exception of optimizing for a small JavaScript bundle. Given that Electron apps have all resources available locally, size is a concern best measured in megabytes rather than kilobytes.

Two skill sets are crucial for building a performant Electron applications. First, understand the end-to-end flow of rendering pixels for your app. If you are using a JavaScript framework like React or Angular, make sure to be aware of common performance-killers and easy optimization wins. When does it refresh the layout? When does it force Chromium to repaint the whole page? Then, analyze your own code. Are there any long-running operations that could block the rendering thread? Are you making use of the many new technologies available to improve performance, like web workers or APIs like requestAnimationFrame or requestIdleCallback?¹

Electron comes with Chromium’s excellent developer tools. The timeline recordings offer a detailed look at how JavaScript and Chromium work together, allowing developers to quickly spot operations that simply take too long, block other operations, force Chromium to recalculate layout, or introduce paint bottlenecks.

Optimizing with Performance Profiles and Developer Tools

Developer tools for any given Electron renderer process can be opened by calling openDevTools() on its respective webContents. Once opened, you have the same developers tools available that you might already know from working with Google Chrome. Behind the Performance tab is a powerful profiler, offering a detailed look at how the application is executed. For more information on how to use the profiling tool, consider Google’s excellent developer documentation. It is a vital tool that is useful for both web development and app development with Electron.

Of most interest to developers of desktop applications should be how many frames per second the app renders at, especially for users with weaker computers. Chromium allows for CPU throttling and records the number of frames per second in profiles. Use the Frames section to find frames that took particularly long to render and consider how you could perform the same operations while freeing up resources for Chromium to render (see Figure 5-1). Consider the use of different threads and processes to offload expensive operations.

Figure 5-1. Chromium’s profiler in action

Make it your routine to check existing code and new features for performance bottlenecks. Building an application with expensive operations that will still be just as fluid as a native application is not a matter of obsessive optimization but of routinely checking code for mistakes and bottlenecks.

If Chromium’s included developer tools do not provide enough detail, Electron allows a deeper level of data collection from libchromiumcontent directly. Few developers of Electron applications will ever need to do so, but know that it is possible to debug an app down to each event that Chromium’s content module operates on. To play with the tracing feature, open the Google Chrome browser and navigate to chrome://tracing/ to record and inspect traces. To perform the same operation in your Electron application, check out the official documentation on using Electron’s contentTracing module.

In summary, building a performant Electron application is up to the developer. Both Chromium and Node.js are extremely powerful—in the right hands, they deliver excellent performance. At the same time, JavaScript is a scripting language without a compiler to bark at developers whenever they write code that cannot be optimized. That does not mean that such a compiler does not exist, nor does it mean that the compiler does not bark error messages. Microsoft’s Visual Studio Code is a wonderful example of an application that makes full use of V8’s optimizations and, at least at the time of writing, also an excellent example of an application that is likely close to the ceiling of how performant it could be.²

The Ecosystem and Community

If all software projects are limited only by the amount of time developers can dedicate to them, open source projects are limited by the amount of sponsorship they receive from companies that consider it in their best interest to sponsor development. Whenever a technology needs to be chosen, it is only fair to ask who else has already “bought in”—or, in other words, which organizations will jump in and support the project should it ever encounter a crisis.

Electron is used by multiple big players: Microsoft alone is using Electron to build Visual Studio Code, the installer for Visual Studio, and the next version of Skype (currently dubbed Skype Preview). Under GitHub’s leadership, most of Electron’s contributors are paid to do work on the project, elevating the stability and quality of the code committed.

GitHub has proven to be welcoming to new players willing to put in work. Therefore, Electron turns out to be an almost perfect solution for your problem, pull requests adding features or fixing issues have a decent chance of making it into the next release.

In addition to strong leadership, Electron’s community benefits from an “open source mentality,” where solutions to common problems are shared. At the time of writing, npm hosts about 1,700 modules that enhance the functionality of Electron, making it easy for developers to reuse existing solutions. Combined with the abundance of high-quality packages available for Node.js and browsers, developers have a wide variety of libraries to build upon.

Where Electron Falls Short

Electron is not without flaws, and some of them may never be overcome.

An Outsider on All Platforms

There is no way around it: Electron is not the primary way to build applications on any platform. Despite Microsoft’s support and usage of Electron, Microsoft’s own documentation will steer you towards building applications using the Universal Windows Platform. Most APIs available to UWP apps are also available to win32 applications (and therefore Electron), but once you start running into trouble, chances are high that the common troves of developer wisdom, like StackOverflow, will have more information about how to fix an issue with C# than with C++. The same is true for macOS and Linux: the chances that Apple’s documentation will ever include information for Electron developers targeting macOS are slim.

How painful being a “system outsider” is depends on the individual project. If you expect your application to consume a large number of Win32/COM APIs or fundamental macOS *Kits that are not already made available directly by Node.js or Electron, a native implementation may be the better solution.

Code Outside of Your Control

Google’s libchromiumcontent is at the core of Electron, and your application will inherit bugs that are present there. There is no doubt that Google builds excellent software, but it is entirely possible that a small bug in Chromium will have negative effects on your application.

In early 2017, Visual Studio Code received negative press coverage following a bug report showing that disabling the blinking cursor in the editor could save up 13% of CPU usage. The Google Chrome team responded publically that the issue was due to Chromium re-painting the whole screen at a higher frequency than necessary, which was a bug that got eventually fixed. However, it is important to point out that it was difficult for Microsoft and Google to debug and fix this issue and that the fix was ultimately out of Microsoft’s control.

Millions of web applications and websites rely on Google’s engineers to run properly inside Google Chrome. If the thought of Chromium being a foundation for your application raises hairs, Electron is not the right framework.

Bigger Than Necessary

Every Electron application ships with its own copy of libchromiumcontent and Node.js. In practice, that means that a zipped Electron app is never smaller than roughly 65 megabytes (give or take the exact size of any given Electron release). If download size is a big concern and the functionality of the project limited, a fully native implementation will likely be dramatically smaller in size.

The Electron community has previously discussed a runtime mode in which multiple Electron apps could share a common libchromiumcontent and Node.js core. However, no code has been written to support that goal and few companies seem to be interested enough to sponsor the development, so an Electron runtime is currently an unrealized dream.

Native Interfaces

Users enjoy the native look and feel of their operating systems. Though it is possible to build an Electron app that looks and feels exactly like a Windows³ or macOS⁴ app, it is certainly harder to do so than it would be using the system’s respective native implementations.

This point is most relevant when you have not already envisioned an interface with web technologies for your application. It is no accident that many Electron apps had their roots as web apps, Chrome apps, or were otherwise compelled to build an interface with web technologies. Line-of-business applications often do not really care about how a certain control actually looks as long as it is usable. When building an application natively with UWP (Windows) or AppKit (macOS), developers are offered a ready-to-use library of buttons, panels, menus, and other user interface elements. Furthermore, since all those elements look the same across the operating system, users are more likely to already be familiar with the behavior of, say, a datepicker.

The usual counterargument is that there are thousands of high-quality user interface components available for websites (often as open source). However, if use of the operating system’s native user interface components is a must, Electron is probably not the right choice.

Closing Remarks

This short guide aimed to give you an overview of what development with Electron feels like. Naturally, any application developer is faced with more challenges than creating notifications, opening native dialogs, or embedding remote web content. That said, the short examples provided here give a complete tour of the levels of complexity, underlying patterns behind interacting with Electron, and fundamental idea of combining Node.js with web content.

In summary, Electron offers the following benefits like few other frameworks can:

Reuse and extend existing web application patterns, components, and designs

Reuse and sharpen existing web application development skills

Use the latest web technologies available in the latest version of libchromiumcontent without having to worry about users’ browsers

Build desktop applications for macOS, Windows, and Linux using a common code base

Break through existing boundaries of what is possible with Node.js and web technologies using native code

Build upon a vast library of excellent open source components, both for Node.js in general and specifically for Electron

Participate in a vibrant community of developers actively sharing code, best practices, and solutions

Should you decide to dive deeper into Electron development, you can always find the full source code as well as API documentation for the project in its GitHub repository. To see bigger applications and their respective source code, check out the repositories of Microsoft’s Visual Studio Code, GitHub’s Atom, or Nylas Mail.

¹ These APIs enable developers to schedule work cooperatively. For more information, see Mozilla’s excellent developer documentation (requestAnimationFrame, requestIdleCallback).

² This compliment is not extended to extensions. If you are a user of Visual Studio Code and currently bewildered about why your installation is not as performant as promised here, chances are high that you have installed a number of extensions that were not written with the same rigor as the editor itself.

³ Microsoft actually implemented most UWP interface components in JavaScript and released them as open source libraries. The official WinJS library offers native Windows UI components; Office Fabric does the same for the unique Office look.

⁴ While Apple has not released components, inofficial projects like PhotonKit ported various macOS UI components to JavaScript.

About the Author

Felix Rieseberg is a staff engineer at Slack, core contributor to Ghost, and an Electron contributor. Previously, he was a senior open source engineer at Microsoft and executive editor at Apfeltalk. You can find Felix on Twitter, GitHub, or on his website.