We live in a multi-platform world. The sharks in our tech ocean have their app stores. Both Apple and Microsoft have been pushing you to use Swift or C# to write functionality for their operating systems. Both languages are supported by rich underlying platforms. Whether it be .NET or WPF or Cocoa or Metal, the names change, but our jobs don't. Our clients want us to develop applications that they can use. And, at the heart of every application is logic, rendering, communication, and storage.

What has made things challenging lately is that clients want this functionality to be across numerous platforms: mobile, desktop, and Web. Within mobile, there's iOS, Android, and Windows, within Web, you have many browsers, and on desktop, you have MacOS, Windows, and Linux. On each of these platforms, you have their respective frameworks, nuances, and annoyances.

At the same time, while these sharks are rearranging chairs on their respective sinking Titanics of native platforms, a new way of writing applications has emerged: the Web. Web-based open technologies, notably JavaScript, HTML, and CSS, have become incredibly potent. In fact, when it comes to expressing UI, in many cases Web technologies give us a better rendering platform than anything else.

Although I agree that native still offers more power, I also insist that in 99% of those cases, you don't need that power. That's a realization everyone will come to, sooner or later. When you need to tap into native platform capabilities, you have plug-ins for platforms like Cordova. You can write stuff once and run it everywhere. This is exactly what Java promised, except this time it's actually practical.

But what about desktop? We can't ignore the fact that MacOS and Windows desktops are still where 90% of the actual work gets done. Just like you have Cordova for mobile, for desktop you have Electron. And just like you have Cordova plug-ins, Electron uses node packages.

In this article, I'll introduce, dissect, and push the limits of Electron.

What is Electron?

Think of Electron as Cordova for desktops. It lets you build cross-platform desktop apps using JavaScript, HTML, and CSS. Where you need to tap into native device capabilities, such as file system, Bluetooth, or USB, you can use node packages. As you might already know, node packages can call native code. This means that electron-based desktop apps can do anything that native apps can do. It's certainly possible to bake dependencies into your node packages that are platform-dependent. For instance, you may choose to use a Mac-specific feature such as spotlight and now your app won't work on a Windows computer. You could have chosen to use a node package that provides “search” on both Windows and Macs and remained cross-platform. Luckily, there are node packages available under the generous MIT license for most conceivable needs.

Think of Electron as Cordova for desktops.

You might wonder if it's safe to bet your project on a platform like Electron. After all, it's not backed by the deep pockets of Microsoft or Apple, the big companies backing C# and Swift. Also, WPF is great isn't it? I feel quite safe recommending Electron for the following reasons:

  • It's open source, previously known as “atom shell.” and it's available under MIT license at https://github.com/electron/electron.
  • A lot of people are already using it. Slack is written using Electron, as is Visual Studio Code, and many other such examples.
  • Node and Web technologies are here to stay and are improving faster than their native counterparts.
  • There are node packages for anything you'd ever need.
  • It's quite powerful; it lets you build applications that can run as a normal application or in a system tray (or Mac menu), Mac store apps or Windows store apps, and it can even do things like notifications, etc. It can do basically anything you'd expect from a full-fledged platform, all available at your fingertips.

Getting Started with Electron

Let's write a simple “hello world” app with Electron. Before you start, you need to have Nodejs (www.nodejs.org) and Visual Studio Code (code.visualstudio.com) installed. Because the aim of this article is to demonstrate the capabilities of Electron, I won't be writing a very fancy app. So pay no attention to my UI skills, and yes, let's just go with JavaScript and not TypeScript to keep things germane.

Start by opening a terminal and creating a directory called Electron. Go into this directory and launch Visual Studio Code in this directory. Create three files:

  • index.html: the main UI
  • main.js: the main launch application file
  • package.json: where you'll specify all the app dependencies, etc.

You will use ES6. When you use Electron, you have the liberty of using ES6, as the final packaged application bundles the Chromium browser. Think of Chromium as Google Chrome minus all the Google goo. Because you're bundling the browser, you target only one browser and can make full use of that browser. So let's target ES6.

Because you'll use ES6, you don't want Visual Studio Code to annoy you with random error messages and warnings. To suppress those, you need to inform Visual Studio Code that you intend to target ES6. To facilitate that, create a fourth file in the Electron directory called jsconfig.json and put the following code in it.

{
    "compilerOptions": {
        "target": "ES6"
    }
}

You want to use jsconfig.json becase it's a subset of tsconfig.json and it provides hints and help for Visual Studio Code, helping it understand the nature of your JavaScript and TypeScript code in the project. One side benefit of jsconfig.json is that you no longer have to write /// <reference /> tags above your TypeScript code.

Next, open package.json, which is guidance to nodejs, about your project. Here, you specify the node commands you wish to support, the node packages you depend upon, etc. Put the code shown in Listing 1 into package.json.

Listing 1: Contents of package.json

{
    "name"    : "electron-hello-world",
    "version" : "0.1.0",
    "main"    : "main.js",
    "devDependencies": {
    "electron-prebuilt": "^0.35.0"
    },
    "scripts": {
    "start": "electron main.js"
    }
}

As can be seen in Listing 1, you're depending upon only one node package at the moment, called “electron-prebuilt”. You might find typing this into Visual Studio Code a lot easier if you had IntelliSense for all of these files. Luckily, you do; just press CTRL_SPACE at any point for good help and IntelliSense, and even node packages and their versions fetched from the Web, shown as IntelliSense, as can be seen in Figure 1. Figure 1 shows that there is a node package available for Bluetooth communication. This could get very interesting!

Figure 1: IntelliSense in Visual Studio Code
Figure 1: IntelliSense in Visual Studio Code

Let's give our index.html some logic. Go ahead and put the code shown in Listing 2 into index.html.

Listing 2: Index.html barebones showing versions of what you're using

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Hello World!</title>
    </head>
    <body>
        <pre>
        Node version: <script>document.write(process.versions.node)</script>
        Chrome version: <script>document.write(process.versions.chrome)</script>
        Electron version: <script>document.write(process.versions.electron)</script>
        </pre>
    </body>
</html>

And now we come to the meat of the application. To start an Electron app, you need to understand some basics of how Electron works.

Electron relies on a main process, and optionally one or more renderer processes. The idea is that the main process is very lightweight, and the renderer process is where the actual work gets done. Each Web page in the application, or, for our purposes here, a screen is a renderer process. The fun part is that in normal Web pages, the Web page can't tap outside the sandboxed environment, but in Electron, it has full liberty to do so.

The main process is represented by a BrowserWindow object. Each BrowserWindow instance runs the Web page in its own render process. When a BrowserWindow is destroyed, the underlying renderer processes are also killed. Typically speaking, on Windows, hitting the “X” button destroys the process. On a Mac, hitting “X” doesn't destroy the process; you have to go to the dock and right-click/exit. You have to accommodate these nuances in your code, but don't worry, there isn't going to be a lot of if ‘mac’ then else if ‘Windows’ then sort of messy logic in your application.

The main process and render process can communicate with each other. One common scenario in which they need to communicate is where you may have a danger of leaking resources, and you'd rather manage that lifetime through the main process. In those instances, the render process can communicate back to the main process via facilities provided within the electron framework.

There's one more thing to understand. Globals are bad. Here, you control the browser completely. You can make use of globals to your advantage. Plus, you can use ES6 features to ensure that those globals don't change.

Okay, so now that you're armed with the above explanation, start by writing the following code in your main.js:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

As you can see, you're loading Electron as a constant first. When you flush out the node_modules directory, requirejs picks Electron from there. Secondly, you're creating a const app to control the app's lifecycle. Then you create a BrowserWindow instance.

Next, let's handle the window-all-closed event where the user quits the app. The idea is that when the user hits the “X” button, or, on a Mac, has done a right click/exit from the dock, the application should quit. This can be achieved using the code below:

app.on('window-all-closed', function() {
    if (process.platform != 'darwin') {
        app.quit();
    }
});

Next, create a mainWindow variable in the global namespace. You want to put this in the global namespace because you don't want this variable to get garbage collected. If it did get garbage collected, the window would be closed and that wouldn't be a very nice application. Here's the code:

var mainWindow = null;

Finally, you need to handle the ready event to get your application going. Place the code shown in Listing 3 in your application's main.js to get your application spinning nicely.

Listing 3: Contents of main.js

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

app.on('window-all-closed', function() {
    if (process.platform != 'darwin') {
        app.quit();
    }
});

var mainWindow = null;
app.on('ready', function() {
    mainWindow = new BrowserWindow({width: 400, height: 400});
    mainWindow.loadURL('file://' + __dirname + '/index.html');
    mainWindow.on('closed', function() {
        mainWindow = null;
    });
});

Running Your App

Running your app could mean running it for debugging purposes or packaging it for distribution. Let's focus on the debugging part for now. Back in terminal, run npm install to install the devDependencies you specified in your package.json file. Verify that a node_modules directory appears, as shown in Figure 2. This node_modules directory can be overwhelming - there's a lot of stuff in it. It's the kind of thing that I might show to my manager to prove how hard I'm working, but I rarely ever look into it otherwise. The reality is, this is a black box, and well-written node_modules usually maintain pretty good backward compatibility. You don't check this directory into source control. Its only purpose, besides running your application, is to scare your non-technical manager.

Figure 2: The node_modules directory after the npm install
Figure 2: The node_modules directory after the npm install

To run the app, run npm start in terminal. Verify that your application runs, as shown in Figure 3.

Figure 3: The Hello World application is running.
Figure 3: The Hello World application is running.

What fun is a desktop app that doesn't do anything? Let's modify the index.html to list the files in the directory in which the app runs. The new index.html can be seen in Listing 4. For brevity, I've only shown the Body tag.

Listing 4: Modified index.html listing files in the current directory

<body>
    <pre>
        Node version: <script>document.write(process.versions.node)</script>
        Chrome version: <script>document.write(process.versions.chrome)</script>
        Electron version: <script>document.write(process.versions.electron)</script>
    </pre>
    <strong>List of files:</strong>
    <blockquote>
        <span id="files"/>
    </blockquote>
    <script>
        var glob = require('glob');
        glob("*.*", null, function(er, files){
            var filesElement = document.getElementById("files");
            files.forEach(function(file) {
                filesElement.innerText =
                filesElement.innerText + "\n" + file;
            });
        });
    </script>
</body>

Because you're depending upon a new node module called “glob”, modify the devDependencies of package.json to include glob, as shown below:

"devDependencies": {
    "electron-prebuilt": "^0.35.0",
    "glob": "^6.0.1"
},

Now go ahead and do an “npm install” and “npm start” from terminal again, and verify that you are able to see the files in the current directory, as shown in Figure 4.

Figure 4: Hello World shows the list of files.
Figure 4: Hello World shows the list of files.

What Else Can You Do?

Besides the fact that you can tap into all native capabilities, you can also:

  • Give your app a custom icon.
  • Give your app a custom thumbnail and progress bar in its icon.
  • Tap into OS level notifications.
  • Detect online versus offline.
  • Show jump lists/recent documents.
  • Customize the dock menu in OSX, or the tasks menu in Windows.
  • Show thumbnail toolbars in Windows like media player does for the current running video.
  • Show the currently edited file in the title bar.
  • Run in system tray or Mac's menu.
  • Create a frameless window and completely take over the UI with custom areas defined for dragging and dropping.
  • List your app in app stores, both Windows and Mac.

In other words, you can do everything you'd expect a native app to do. Sure, there's fine print, but in most scenarios you can achieve most business requirements on three platforms, Mac, Windows, and Linux. While you fight NIBs or WPF on a single platform, I'll be sitting at the bar, missing you - or not.

One article isn't enough to go into every single detail. At the very least, you'd like to brand, package, and ship your app. Let's do that next.

Packaging Your App

So far, you've been running the app by specifying a script in package.json. What's actually happening behind the scenes is that you're running the following command:

./node_modules/.bin/electron main.js

Go ahead: Try running exactly the above from terminal. You should see the app come up. How can you hand this over as an .app or .exe to your users?

If you expand the node_modules folder, and go to ./node_modules/electron-prebuilt/dist, you should see a file called “Electron.app” there (in Windows, you should see an equivalent exe). In Visual Studio Code, expand Electron.app, and you should find a folder called “Contents”. Under Contents\Resources, create a folder called “app”, and xcopy the contents of your app (index.html, main.js, and package.json) in that directory.

Figure 5: The contents of the app
Figure 5: The contents of the app

Now, from Finder (or Windows Explorer), run the Electron.app (or Electron.exe) by double clicking on it. You should see your app running just like before, except the file list isn't being shown. The code is still working, it just doesn't have a Start directory to work with. You could hardcode a Start directory or pass it in as a command line parameter, but I'll keep this article germane to Electron and not spend too much time on the actual app functionality.

This Electron.app (or Electron.exe) is something you could xcopy to your end user's computers, but there are two problems:

  • How can you prevent users from snooping inside the contents of the app or maybe even editing it?
  • It would be really nice to brand the app with your own icon, name etc.

Asar

You can package your app into an Asar (https://github.com/electron/asar) archive. Asar is a simple extensive archive format, similar to tar. It concatenates all files together into a single file without compression, while allowing random access support.

To use an Asar archive instead of the app folder, first you need to create the app.asar file, which is the Asar archive of the app. Once you have such an archive, you need to copy it to Electron.app/Contents/Resources/app.asar. This effectively bundles the app. Of course, you can also do uglification (Gulp-uglify), and minification (Gulp-minify) to help further protect your IP, launch times, and performance.

You need to create an Asar archive and xcopy it to a specific location. Doing so is quite easy. You edit your package.json and make two changes there:

  • Include a devDependency for “asar”.
  • Add a script called package that runs “asar pack . app.asar”.

That's it! Now you need to copy app.asar to Electron.app\Contents\Resources\app.asar and distribute Electron.app.

All of this can be easily automated using a Gulp task in Visual Studio Code.

Branding Your App

The application works and is distributable. Now, how do you brand it? It would be nice if it had a name like “MyFancyApp.exe” and your own icon!

On Windows, you just rename the exe and edit its icon using a resource editor, and you're done!

On a Mac, there are some additional steps you need to follow.

  1. Edit the Info.plist file in Electron.app/Contents and make the changes shown in Figure 6.
  2. Edit the Info.plist file in the Electron.app/Contents/Frameworks/Electron Helper.app/Contents folder and make the changes shown in Figure 7. Repeat these changes for the Frameworks/Electron Helper NP.app and Frameworks/Electron Helper EH.app folders.
  3. Rename Electron.app to MyFancyApp.app.
  4. Rename Electron Helper.app to MyFancyApp Helper.app and repeat this for Electron Helper NP.app and Electron Helper EH.app.
  5. Rename the MyFancyApp Helper.app\Contents\MacOS\Electron Helper.app to MyFancyApp Helper.app\Contents\MacOS\MyFancyApp Helper.app and repeat this for MyFancyApp Helper NP.app and MyFancyApp Helper EH.app.
  6. Finally, if you want your own icon, drop your own .icns file in the MyFancyApp.app folder, and edit the Info.plist file to use the new icns file as the icon of your app. I'm skipping this step.
Figure 6: Editing the app's plist file.
Figure 6: Editing the app's plist file.
Figure 7: Editing the helper app Info.plist file
Figure 7: Editing the helper app Info.plist file

That's it! Go ahead and run your application. Verify that your application's name shows up in the dock, as shown in Figure 8.

Figure 8: The app's proper name shown in the Dock
Figure 8: The app's proper name shown in the Dock

Also verify that the Activity Monitor shows your new app name's identification, as can be seen in Figure 9.

Figure       9      : Your branded app in the Activity Monitor
Figure 9 : Your branded app in the Activity Monitor

Submit Your App to the Mac App Store

This is a real-world app and you can submit it to the Mac App Store. Hopefully it will make you millions and you'll write me a thank you note for introducing you to Electron. In order to submit to the Mac App Store, you should first familiarize yourself with Apple's guidelines for the Mac App Store. Once you understand those, follow the following steps:

  1. Request a certificate from Apple using the guide shown here: https://github.com/nwjs/nw.js/wiki/MAS%3A-Requesting-certificates.
  2. Sign your app. The important difference here is that you need to sign every Electron dependency.
  3. Upload your app for review, and if you did everything right, your app should appear in the App Store soon.

Because Mac App Store's apps are sandboxed, you have to disable the crash-reporter, which sends all crash reports to the creators of Electron and the auto-update facilities. A lot of developers are also frustrated by the limitations that app sandboxing puts on them. It might be worth your time reading about these limitations here https://developer.apple.com/documentation/security/app_sandbox.

Summary

Web and open standards will win. I know there's a lot of muscle behind C# and Swift, WPF and Cocoa, DirectX, and Metal. Even though there's a place and time for those technologies, the reality is that the vast majority of application needs can be fully satisfied with open-Web technologies. Frameworks such as AngularJS, AngularJS2, or React, along with a bundled database, make you incredibly productive and very powerful. A good designer can also create a user interface using CSS3 and such Web technologies that put any WPF Metro UI to shame, or for that matter the white box that Jony Ive has been trapped in for the past 15 years.

Although it's definitely possible to create not merely equivalent, but better-than-pure native counter-part apps using open Web technologies, let's not forget the biggest advantage: a single codebase for 99% of your code. This is something that Java promised and that JavaScript delivers.

Due to tools such as Visual Studio Code, Gulp, and TypeScript, I have a very hard time justifying opening XCode anymore. Sometimes I still open Visual Studio because frankly, it's still the best dev environment on Windows.

In this article, I gave you a high-level introduction to Electron. I realize that I skimmed over many details, but it should be enough to pique your interest. In subsequent articles, I'll tackle some real-world scenarios and apps where I'll have a chance to dive deeper into the details I skimmed over this time.

Until then, keep on coding!