Electron: Cross-platform Desktop Apps Made Easy

Earlier this year, Github released Atom-Shell, the core of its famous open-source editor Atom, and renamed it to Electron for the special occasion.

Electron, unlike other competitors in the category of Node.js-based desktop applications, brings its own twist to this already well-established market by combining the power of Node.js (io.js until recent releases) with the Chromium Engine to bring us the best of both server and client-side JavaScript.

Imagine a world where we could build performant, data-driven, cross-platform desktop applications powered by not only the ever-growing repository of NPM modules, but also the entire Bower registry to fulfill all our client-side needs.

They are familiar with Node.js, Angular.js and MongoDB-like query syntax.

Getting the Goods

First things first, we will need to get the Electron binaries in order to test our app locally. We can install it globally and use it as a CLI, or install it locally in our application’s path. I recommend installing it globally, so that way we do not have to do it over and over again for every app we develop.

We will learn later how to package our application for distribution using Gulp. This process involves copying the Electron binaries, and therefore it makes little to no sense to manually install it in our application’s path.

To install the Electron CLI, we can type the following command in our terminal:

$ npm install -g electron-prebuilt

To test the installation, type electron -h and it should display the version of the Electron CLI.

At the time this article was written, the version of Electron was 0.31.2.

Setting up the Project

Let’s assume the following basic folder structure:

my-app
|- cache/
|- dist/
|- src/
|-- app.js
| gulpfile.js

… where:
- cache/ will be used to download the Electron binaries when building the app.
- dist/ will contain the generated distribution files.
- src/ will contain our source code.
- src/app.js will be the entry point of our application.

Next, we will navigate to the src/ folder in our terminal and create the package.json and bower.json files for our app:

$ npm init
$ bower init

We will install the necessary packages later on in this tutorial.

Understanding Electron Processes

Electron distinguishes between two types of processes:

The Main Process: The entry point of our application, the file that will be executed whenever we run the app. Typically, this file declares the various windows of the app, and can optionally be used to define global event listeners using Electron’s IPC module.

The Renderer Process: The controller for a given window in our application. Each window creates its own Renderer Process.

For code clarity, a separate file should be used for each Renderer Process.
To define the Main Process for our app, we will open src/app.js and include the app module to start the app, and the browser-window module to create the various windows of our app (both part of the Electron core), as such:

var app = require('app'),
BrowserWindow = require('browser-window');

When the app is actually started, it fires a ready event, which we can bind to. At this point, we can instantiate the main window of our app:

At this point, our app should be ready to run. To test it, we can simply type the following in our terminal, at the root of the src folder:

$ electron .

We can automate this process by defining the start script of the package.son file.

Building a Password Keychain Desktop App

To build a password keychain application, we need:
- A way to add, generate and save passwords.
- A convenient way to copy and remove passwords.

Generating and Saving Passwords

A simple form will suffice to insert new passwords. For the sake of demonstrating communication between multiple windows in Electron, start by adding a second window in our application, which will display the “insert” form. Since we will open and close this window multiple times, we should wrap up the logic in a method so that we can simply call it when needed:

We will need to set the show property to false in the options object of the BrowserWindow constructor, in order to prevent the window from being open by default when the applications starts.

We will need to destroy the BrowserWindow instance whenever the window is firing a closed event.

Opening and Closing the “Insert” Window

The idea is to be able to trigger the “insert” window when the end user clicks a button in the “main” window. In order to do this, we will need to send a message from the main window to the Main Process to instruct it to open the insert window. We can achieve this using Electron’s IPC module. There are actually two variants of the IPC module:

One for the Main Process, allowing the app to subscribe to messages sent from windows.

One for the Renderer Process, allowing the app to send messages to the main process.

Although Electron’s communication channel is mostly uni-directional, it is possible to access the Main Process’ IPC module in a Renderer Process by making use of the remote module. Also, the Main Process can send a message back to the Renderer Process from which the event originated by using the Event.sender.send() method.

To use the IPC module, we just require it like any other NPM module in our Main Process script:

Do not forget to check if the BrowserWindow instance is already created, if not then instantiate it.

The BrowserWindow instance has some useful methods:

isClosed() returns a boolean, whether or not the window is currently in a closed state.

isVisible(): returns a boolean, whether or not the window is currently visible.

show() / hide(): convenience methods to show and hide the window.

Now we actually need to fire that event from the Renderer Process. We will create a new script file called main.view.js, and add it to our HTML page like we would with any normal script:

<script src="./main.view.js"></script>

Loading the script file via the HTML script tag loads this file in a client-side context. This means that, for example, global variables are available via window.<var_name>. To load a script in a server-side context, we can use the require() method directly in our HTML page: require('./main.controller.js');.

Even though the script is loaded in client-side context, we can still access the IPC module for the Renderer Process in the same way that we can for the Main Process, and then send our event as such:

Generating Passwords

To keep things simple, we can just use the NPM uuid module to generate unique ID’s that will act as passwords for the purpose of this tutorial. We can install it like any other NPM module, require it in our ‘Utils’ script and then create a simple factory that will return a unique ID:

Saving Passwords

So all we really need is some kind of in-memory database that can optionally sync to file for backup. For this purpose, Loki.js seems like the ideal candidate. It does exactly what we need for the purpose of this application, and offers on top of it the Dynamic Views feature, allowing us to do things similar to MongoDB’s Aggregation module.

Dynamic Views do not offer all the functionality that MongodDB’s Aggregation module does. Please refer to the documentation for more information.

We first need to initialize the database. This process involves creating a new instance of the Loki Object, providing the path to the database file as an argument, looking up if that backup file exists, creating it if needed (including the ‘Keychain’ collection), and then loading the contents of this file in memory.

We can retrieve a specific collection in the database with the getCollection() method.

A collection object exposes several methods, including an insert() method, allowing us to add a new document to the collection.

To persist the database contents to file, the Loki object exposes a saveDatabase() method.

We will need to reset the form’s data and send an IPC event to tell the Main Process to close the window once the document is saved.

We now have a simple form allowing us to generate and save new passwords. Let’s go back to the main view to list these entries.

Listing Passwords

A few things need to happen here:

We need to be able to get all the documents in our collection.

We need to inform the main view whenever a new password is saved so it can refresh the view.

We can retrieve the list of documents by calling the getCollection() method on the Loki object. This method returns an object with a property called data, which is simply an array of all the documents in that collection:

A nice added feature would be to refresh the list of passwords after inserting a new one. For this, we can use Electron’s IPC module. As mentioned earlier, the Main Process’ IPC module can be called in a Renderer Process to turn it into a listener process, by using the remote module. Here is an example on how to implement it in main.view.js:

Copying Passwords

It is usually not a good idea to display passwords in plain text. Instead, we are going to hide and provide a convenience button allowing the end user to copy the password directly for a specific entry.

Here again, Electron comes to our rescue by providing us with a clipboard module with easy methods to copy and paste not only text content, but also images and HTML code:

Since the generated password will be a simple string, we can use the writeText() method to copy the password to the system’s clipboard. We can then update our main view HTML, and add the copy button with the copy-password directive on it, providing the index of the array of passwords:

<a href="#" copy-password="{{$index}}">copy</a>

Removing Passwords

Our end users might also like to be able to delete passwords, in case they become obsolete. To do this, all we need to do is call the remove() method on the keychain collection. We need to provide the entire doc to the ‘remove()’ method, as such:

For the scope of this current tutorial, we will see how to create a custom menu, add a custom command to it, and implement the standard quit command.

Creating & Assigning a Custom Menu to Our App

Typically, the JavaScript logic for an Electron menu would belong in the main script file of our app, where our Main Process is defined. However, we can abstract it to a separate file, and access the Menu module via the remote module:

var remote = require('remote'),
Menu = remote.require('menu');

To define a simple menu, we will need to use the buildFromTemplate() method:

The first item in the array is always used as the “default” menu item.

The value of the label property does not matter much for the default menu item. In dev mode it will always display Electron. We will see later how to assign a custom name to the default menu item during the build phase.

Finally, we need to assign this custom menu as the default menu for our app with the setApplicationMenu() method:

Menu.setApplicationMenu(appMenu);

Mapping Keyboard Shortcuts

Electron provides “accelerators”, a set of pre-defined strings that map to actual keyboard combinations, e.g.: Command+A or Ctrl+Shift+Z.

The Command accelerator does not work on Windows or Linux.
For our password keychain application, we should add a File menu item, offering two commands:

We can add a visual separator by adding an item to the array with the type property set to separator.

The CmdOrCtrl accelerator is compatible with both Mac and PC keyboards

The selector property is OSX-compatible only!

Styling Our App

You probably noticed throughout the various code examples references to class names starting with mdl-. For the purpose of this tutorial I opted to use the Material Design Lite UI framework, but feel free to use any UI framework of your choice.

Anything that we can do with HTML5 can be done in Electron; just keep in mind the growing size of the app’s binaries, and the resulting performance issues that may occur if you use too many third-party libraries.

Packaging Electron Apps for Distribution

You made an Electron app, it looks great, you wrote your e2e tests with Selenium and WebDriver, and you are ready to distribute it to the world!

But you still want to personalize it, give it a custom name other than the default “Electron”, and maybe also provide custom application icons for both Mac and PC platforms.

Building with Gulp

These days, there is a Gulp plugin for anything we can think of. All I had to do is type gulp electron in Google, and sure enough there is a gulp-electron plugin!

This plugin is fairly easy to use as long as the folder structure detailed at the beginning of this tutorial was maintained. If not, you might have to move things around a bit.

About the author

Stéphane is a front-end engineer with over seven years' experience who specializes in building performant and scalable JavaScript-based web applications. He enjoys working in a team of talented developers, sharing his experience and knowledge with others, and learning new technologies. [click to continue...]

Comments

Venelin Ivanov

What would be the best way to migrate web app ( node.js + html5) into electron based windows app? Can we migrate all features developed in web version?

boriscy

Excellent man I have started some time ago a project with electron https://github.com/boriscy/timecheck using babel I must say that you don't need browserify because you can require files like in io.js (node), I will try lokijs, tried other solution but didn't work.

Zǝus Glǝiskǝttǝ

Instructions are not very clear on where to place code....I downloaded
the github code and ran npm install and it just did not work at all.

Stéphane P. Péricat

Hi,
In theory you should be able to migrate your web app straight into an Electron app. I have not tested apps that use modules based on native C code, so I cannot confirm those would work. But basically you will need to write a new js file specifically for the Main Process, and you might need to refactor your other modules in case you would like to use Electron's specific features such as the IPC module.
As far as the frontend goes, anything that the Chromium engine can render, you can reuse right away in your electron app.
Hope this helps!

Venelin Ivanov

Thanks. i will ask more questions soon. Btw, your app on github didn't work. when I do npm install window pops up and it never ends.

Stéphane P. Péricat

Which platform are you running on ?

Stéphane P. Péricat

Do you have a stack trace of the npm error ? Which platform are you running on ?

Stéphane P. Péricat

Loki is really easy and straight forward to use, that's why i love it. Plus, you can also use it in the browser with indexedDB.

Stéphane P. Péricat

I updated the demo code; please update your git checkout and follow the updated instructions in the readme: https://github.com/stephanepericat/toptal-electron-loki-demo/blob/master/README.md

I discovered that my internet provider was blocking my git:// access for I had to change it to http:// by using this command "git config --global url."https://".insteadOf git://
then I followed the updated instructions and this time it worked .
Thanks :)

sandeep kumar patel

Nice article.
There is also a book on electron explaining most of the important feature with coded exaample:- https://leanpub.com/electron

Carlos Esquivel

i've been following this cases of creating Desktop web apps, and is there any reason or pros on convert your web app to desktop? it improves performance? or is it just a new trend on make it more user friendly?

John Paul Baric

Hello @sppericat:disqus I am currently getting this error when trying to create a password.https://imgur.com/gLt3aQt Followed github instructions but seems to be stuck and help would be appreciated.

Élysson Mendes Rezende

I think you should review this tutorial... It is so confuse for people who don't know this very well

Евгений Терехов

@sppericat:disqus Thank you for this very helpful article. However, can you explain what is the point of loading AngularJS controllers code in server context (using require()) and not in client context?

Arzan Razdan

I have a web application (HTML + JS + JQuery + Node.JS) which I can successfully package using Electron as ASAR and run it as a desktop application. My problem is that if I distribute the application, my source code (JavaScript) shall be exposed. Please advise how (if) I can hide / secure my code.

Mycroft Jones

Does electron give me total keyboard control? When doing Javascript in a regular browser, a lot of function keys are off limits, the browser grabs them for itself so I can't use them in my javascript app.

Sai Sasank

I have multiple C++ programs and I want them to read input from the index.html web page and using the computed output should in turn modify some GUI elements on the index.html page. How can I do this? Thank you.

Sumit Kumar

I followed each of your steps
I am getting following error when I do electron .
sumit-mac:src sumit$ electron .
App threw an error during load
Error: Cannot find module 'app'
at Module._resolveFilename (module.js:455:15)
at Function.Module._resolveFilename (/usr/local/lib/node_modules/electron-prebuilt/dist/Electron.app/Contents/Resources/electron.asar/common/reset-search-paths.js:35:12)
at Function.Module._load (module.js:403:25)
at Module.require (module.js:483:17)
at require (internal/module.js:20:19)
at Object.<anonymous> (/Users/sumit/toptal-electron-loki-demo/src/app.js:9:11)
at Module._compile (module.js:556:32)
at Object.Module._extensions..js (module.js:565:10)
at Module.load (module.js:473:32)
at tryModuleLoad (module.js:432:12)

Sebastian Scholle

It's very hard to follow on this tutorial step by step as some steps are not clear. I find myself referring to the github code to check against what the explanation is. However then I am confused once again, as the code is not what I am seeing in the Tutorial. So I guess I will just have to follow on in theory, and then download and run the code (and fix and deprecations)

Succhi Singh

I feel you've tried to explain everything, a very good attempt. When i learnt Electron, i used these resources - https://hackr.io/tutorials/learn-electron
Submit it here, this tutorial.

Stéphane is a front-end engineer with over seven years' experience who specializes in building performant and scalable JavaScript-based web applications. He enjoys working in a team of talented developers, sharing his experience and knowledge with others, and learning new technologies.