Josh Bavari's Thoughts

Thoughts on technology and philosophy

Comparisons of nw.js and Electron

about a 6 minute read

In the last few months, I’ve been playing around with two tools to help bridge the gap between the web and native desktop applications. There are two main tools that come to mind – nw.js (formerly known as Node Webkit) and Electron (formerly known as Atom Shell).

This post focuses on using both, the differences between the two, and focusing on issues that I’ve encountered.

Outline:

  • Getting started – package.json
  • Native Menus (application menu)
  • Shell execution (child processes)
  • Packaging / run
  • Icons
  • Performance

Nw.js

Getting started

Nw.js and Electron share a lot of the same steps for getting started. The only real difference between the two is how they are run, and how they handle the node process internally.

With Nw.js, your app is bundled together. With Electron, the application is set up differently – with the main node process the handle running the browser process, and the rendering process, which handles all things from the browser (the event loop).

To get running, download the nw.js app or the electron app. Both of these applications look at your package.json file to get running by looking at the main attribute.

Bootstrapping

For nw.js, the main attribute should specify which html file to start loading when your application launched. With Electron, your main attribute should specify a JavaScript file to be run.

You also specify attributes about the nw.js window that runs via the window attribute, things like toolbar, width, and height, notably.

With Electron, the JS file that you specify will launch the browser window and specify other attributes like width, height, and other window attributes.

For convenience sake, I also created a node run script to execute the Nw.js app with my current folder. To run the node-webkit app, you simply type npm run nwjs. I also included a livereload script to watch my www folder to live reload my changes in the nw.js app.

Here’s a quick look at the package.json file used to bootstrap nw.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "name": "nwjs-app",
  "version": "1.0.0",
  "description": "",
  "main": "www/index.html",
  "scripts": {
    "nwjs": "/Applications/nwjs.app/Contents/MacOS/nwjs . & node livereload",
    "electron": "/Applications/Electron.app/Contents/MacOS/Electron . & node livereload"
  },
  "window": {
    "toolbar": true,
    "width": 800,
    "height": 500
  }
}

Here’s a quick look at the package.json file used to bootstrap Electron:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "name": "nwjs-app",
  "version": "1.0.0",
  "description": "",
  "main": "src/main.js",
  "scripts": {
    "nwjs": "/Applications/nwjs.app/Contents/MacOS/nwjs . & node livereload",
    "electron": "/Applications/Electron.app/Contents/MacOS/Electron . & node livereload"
  },
  "window": {
    "toolbar": true,
    "width": 800,
    "height": 500
  }
}

Additionally for Electron, my main.js file looks like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var app = require('app');  // Module to control application life.
var BrowserWindow = require('browser-window');  // Module to create native browser window.
var Menu = require('menu');
var ipc = require('ipc');

// var menu = new Menu();
// Report crashes to our server.
// require('crash-reporter').start();

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the javascript object is GCed.
var mainWindow = null;
var menu;

var browserOptions = {
  height: 600,
  title: 'Electron App',
  width: 800
};

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

// This method will be called when Electron has done everything
// initialization and ready for creating browser windows.
app.on('ready', function() {
  // Create the browser window.
  mainWindow = new BrowserWindow(browserOptions);

  // and load the index.html of the app.
  mainWindow.loadUrl('file://' + __dirname + '/www/index.html');

  // Emitted when the window is closed.
  mainWindow.on('closed', function() {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });

  ipc.on('update-application-menu', function(event, template, keystrokesByCommand) {
    //Go through the templates, wrap their click events back to the browser
    console.log('update-application-menu - template');
    console.log(template);
    translateTemplate(template, keystrokesByCommand);
    menu = Menu
    Menu.setApplicationMenu(menu);
  });
});

Native Menus

Electron

Due to the way electron is split up into two processes, the main process (that handles native menus) and the browser process (mainly your app), menus are mainly available to be set on the main process.

If you want your app to change your application menus, you’ll need to use the ipc module electron provides to get a message out to the main process to update the menus.

Other than that, the menu system is super easy if you wish to use static menus.

Nw.js

It’s dead simple. Since it’s all one bundled process, just call the set menu, and you’re good. It’s easy to set short cuts and modify the menus.

Shell execution

In nw.js, you’re good to go when it comes to making external shell calls.

When it comes to electron, make sure you spawn your child processes with the pipe stdio option. Without that option, you may run into some errors (due to the fact electron doesnt have a stdout it manages easily).

Packaging / running

It’s really easy on both platforms. Just set up your package.json/index.html/main.js file and run the appropriate command.

I don’t have a lot of experience with nw.js, so I cant speak to the packaging process.

For electron, to run I like to use electron-prebuilt to run my www files as an app, using electron-packager to package into an .app file, and electron-builder to create installers (dmg/setup.exe).

Icons

To get custom icons for your app files for Mac, you need an .icns file that bundles up all your icons in all the formats/sizes for your dock icon, your cmd+tab icon, and your running icon.

I used this as a walkthrough.

I first started with a size of 1024x1024 pixels, then used the following commands:

1
2
3
4
5
6
7
8
9
10
11
12
# Enter app.iconset, drop in icon.png as a 1024 x 1024 image.
# Run the following commands:
sips -z 16 16     icon.png --out ./icon_16x16.png
sips -z 32 32     icon.png --out ./icon_16x16@2x.png
sips -z 32 32     icon.png --out ./icon_32x32.png
sips -z 64 64     icon.png --out ./icon_32x32@2x.png
sips -z 128 128   icon.png --out ./icon_128x128.png
sips -z 256 256   icon.png --out ./icon_128x128@2x.png
sips -z 256 256   icon.png --out ./icon_256x256.png
sips -z 512 512   icon.png --out ./icon_256x256@2x.png
sips -z 512 512   icon.png --out ./icon_512x512.png
cp icon.png icon_512x512@2x.png

Then just run:

1
iconutil -c icns app.iconset -o ./app-dir/YourAppName.app/Contents/Resources/app.icns

You should now have your app with icons ready to go.

Performance

I didn’t see a lot of major performance bumps from using either platform. It’s JavaScript after all.

Closing words

Most of all, have fun with developing with these tools! They’re open source and free, so when you get a chance, share some knowledge, post an issue, respond to an issue, or even submit a PR.

We’re all in this together.

Comments