atom-shell

Building a Package Featuring Electron as a Stand-Alone Application

Nice buildings

This is a small tutorial to get started with Electron as a regular Node.js / Npm dependency, so you can build stand-alone app that can be published to npmjs.org.

You should be aware that Electron is the new name of the formerly known Atom-Shell. If you can't figure out what it is, please start reading First encounter with Atom-Shell first. This previous article contains valuable informations and may help you deciding which node/chromium runtime technologies you should pick... the other major contender being NW.js (formerly known as Node-Webkit, name dropped because it is now using io.js and chromium).

Let's get started!

Firstly, create your basic package structure, the usual way. Mine usually looks like this:

.
├── app.js
├── bin/
│   └── my-app
├── front/
│   ├── css
│   ├── html
│   ├── js
│   └── main.html
├── LICENSE
├── log/
├── lib/
├── Makefile
├── node_modules/
├── package.json
├── README.md
└── test/

Here, app.js is the entry point of the Electron application. When your application is not made the stand-alone way, you run with electron followed by the directory of your application, e.g. electron path/to/my/app (assuming Electron is installed globally). Electron needs a package.json, and fortunately, its package is fully compatible with node/npm package. So app.js is the entry point provided as the "main" property of your package.json, and it could be anything you want.

Have in mind that the root directory of your stand-alone application is the root directory of your package.

Also, I like app.js to be as simple and as short as possible. I choose to put Javascript code running in the browser context in the lib/ directory, and I choose to put Javascript code running in the renderer/webpage context into the front/js/ directory (the difference between both contexts has been explained here).

Any front-end code is put into the front/ directory. It could be useful to keep things separated, if one day we want an online website version of our app, and we have to bring back to life the good ol' client/server paradigm.

Secondly, add the electron-prebuilt dependency:

npm install electron-prebuilt --save

If you plan to use third-party package with native code, you should install electron-rebuild as well:

npm install electron-rebuild --save

... I will explain later how to use it properly.

Let's build it!

Finally, you need to create your own executable file. For this step, I have simply copied, adapted and reformated the launcher that you can found at ./node_modules/electron-prebuilt/cli.js, and put it into the ./bin/ directory with the name my-app.

I do not append .js to the name, because it will be the real command your user will have to type in their console, if they installed your package globally (e.g. npm install -g my-app). In fact, you CAN provide a different name for the globally installed binaries (see below), but I don't like to confuse myself, I like to stay consistent...

Here is the ./bin/my-app file:

#!/usr/bin/env node

// It just returns a path
var electronPath = require( 'electron-prebuilt' ) ;

var childProcess = require( 'child_process' ) ;

// Adjust the command line arguments: remove the "node <code.js>" part
var args = process.argv.slice( 2 ) ;
// ... and insert the root path of our application (it's the parent directory)
// as the first argument
args.unshift( __dirname + '/../' ) ;

// Run electron
childProcess.spawn( electronPath , args , { stdio: 'inherit' } ) ;

Now you can run your application using the command ./bin/my-app, and by the way, if you installed your application globally (npm install -g my-app), you would run it simply with the command: my-app. You should just add that line into your package.json to let npm know about it:

"bin": {
  "my-app": "./bin/my-app"
},

But wait? We don't have an application right now, do we?

Okey, let's copy-paste the code from the quick start documentation of Electron!

Here is our ./app.js file, slightly modified:

var app = require('app');  // Module to control application life.
var BrowserWindow = require('browser-window');  // Module to create native browser window.

// 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;

// 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({width: 800, height: 600});

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

  // Open the devtools?
  //mainWindow.openDevTools();

  // 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;
  });
});

And now our ./front/main.html file:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using io.js <script>document.write(process.version)</script>
    and Electron <script>document.write(process.versions['electron'])</script>.
  </body>
</html>

Now you can run your wonderful app! Now just run ./bin/my-app and let it shine!

Waoh! We built something really shiny!

Using package featuring native code

Electron use a version of Chromium that is different from node.js. So native packages cannot work without being rebuilt.

Rebuilding can be painful, hopefully electron-rebuild is here to do the job for us.

You should have installed it using npm install electron-rebuild --save already.

Anytime you install some packages featuring native code, you should run electron-rebuild afterward. E.g. if we want to install the package whatever, we have to do it in two step:

npm install whatever --save
./node_modules/.bin/electron-rebuild

I should warn you here:

“electron-rebuild is REALLY slow”

... and aside from that:

“electron-rebuild does NOT output ANYTHING

It's really misleading...

The first time I used it, I was thinking that it was not working at all, and I Ctrl-C it multiple times... Then, I finally give it a last chance to shine, while I was making a tea. On my two year old laptop, it takes few minutes.

Also people installing your supa-ground-breaking package are not aware of electron-rebuild, so you have to add a line into your package.json, so it will kick-in at installation time. Here is the line:

"scripts": {
    "install": "electron-rebuild"
},

You should also warn your user that the installation process can take a while...

That's all folks!

First encounter with Atom-Shell (now known as Electron)

First encounter

Update: Atom-shell is now knwon as Electron, and has a brand new website: electron.atom.io.

Atom-shell vs other node/chromium runtime technologies

Atom-shell is a framework based on node.js that include Chromium for rendering web page as well as for desktop integration. We can roughly state that it combines both the server-side and the client-side to build great multi-purpose desktop application.

From Atom-shell creators:

You could also see it as a minimal Chromium browser, controlled by JavaScript.

It is backed by Github, and looks promising. It all begins as a specific framework for the Atom IDE, but it has quickly proven that its goal was to provide a great framework for a large variety of desktop application.

It is one of the three frameworks doing that, namely nw.js, formerly known as node-webkit, and brackets-shell by Adobe.

I have discarded brackets-shell early in the process, because Adobe seems to develop it only for the brackets IDE purpose. There was no guarantee that it will be improved in the long run, neither that it will be developed as a multi-purpose framework in mind. Also in the past, I have been highly frustrated with the Adobe's way of documenting things... I remember Adobe AIR, and frankly: I don't want to experience such a pain again...

Also from their own overview:

Note: The brackets-shell is only maintained for use by the Brackets project. Although some people have definitely had success using it as an app shell for other projects, we don't provide any official support for that and we haven't done a ton of work to make the app shell easily reusable. Many people will likely find it easier to use a project like node-webkit, which is more generic by design.

This notice alone convinced me to stay away from that, forever.

But here is the serious challenger: node-webkit a.k.a. nw.js.

This framework already have a dozen of real life application, and there is even a game in the Steam store.

Node-webkit was built on top of node.js and webkit, hence the name. However it doesn't use any of them anymore: now it's based on io.js, the node.js' fork featuring Harmony (ECMA-Script 6), and Chromium. So they decided to rename it nw.js to avoid confusion.

The governance of nw.js looks more open than it used to be, which is a very good news. On the other hand Github is THE reference of open source collaboration: so we can expect a lot from the atom-shell's community in the future.

Technically, both projects are awesome.

It is not clear which one is better.

But I tend to prefer Atom-shell because its paradigm is more natural to me, particularly those points:

  • The entry point of the application is a Javascript file, not an HTML file: in my opinion this is cleaner

  • We can roughly state that it is the back-end that controle the entire application and I like that (I can be biased here, since I'm a back-end developer)

  • Consequently the back-end can create as many windows as it is needed... If I'm not wrong, nw.js has a main window that cannot be closed without closing the whole application, and new windows should be created by this main window. I found that a bit hacky...

  • The way node.js and Chromium are integrated together is easier and the KISS principle is something I care a lot in computer science... See the details of the integration here.

  • Multi-context, see the Atom-shell documentation:

    If you are an experienced Node-Webkit user, you should be familiar with the concept of Node context and web context, these concepts were invented because of how the Node-Webkit was implemented.

    By using the multi-context feature of Node, atom-shell doesn't introduce a new JavaScript context in web pages.

  • I found the API more cleaner and straightforward, typically the back-end side (named browser-side in the doc) controle a browser, can create windows, close them, define how they look, it can make it interact with the user's desktop... Everything look less hacky and natural. With nw.js it's like a webpage having eventually access to node.js' stuff, with Atom-shell we code like we will do in any other technologies (like for example the C++/GTK combo): it's more like coding a node.js application that can create windows with webpage. I prefer that coding paradigm, the other way is like looking upside-down.

For all those reasons, despite the popularity of nw.js, I decided to go with Atom-shell, after experimenting both of them few hours. There is no large application using Atom-shell at the moment, except Atom itself, but I bet on it...

Letter

Atom-shell: the browser-side and the renderer-side concept

The key to understand Atom-shell is the concept of browser-side (our good ol' back-end/server side) and renderer-side (also called web page, our good ol' front-end/client side).

The first deals with the system and controle the application globally, the second renders a web application in the window created by the first.

Both of them have access to node.js nonetheless, however it is possible to forbid that for a web page, for security reason, if our application is designed to browse some arbitrary website on the internet, rather than run local web content.

For browser to client interaction, we have to use either the IPC module (Inter-Process Communication) which is asynchronous, or the remote module for synchronous RPC (Remote Procedure Call).

The IPC module is familiar to any web developer: it's more or less the same paradigm as the client/server one. Asynchronous content is sent back and forth.

The remote module is easier, we can typically call any method of the browser-side from the renderer-side, just like if both side was the same entity... but be careful: there are some drawbacks!

Let's get our hands dirty!!!

After a week with Atom-shell, playing and coding seriously, there are few things worth mentioning here.

The remote module is really handy, however the only way to gain access to an object of the browser-side is to use remote.getGlobal(name) which bind a global browser-side variable to its renderer-side counterpart. I don't like to pollute the global scope just to provide some objects to the renderer's remote module...

I would prefer passing those objects at the BrowserWindow's instanciation, or as an extra parameter of .loadUrl().

I have created an issue for that, if you like the idea, please contribute to the discussion.

Also there is the problem of callbacks passed to the browser, see here for details.

While I understand what happen and how to avoid it, this forbid the usage of my lovely node.js' EventEmitter. I came up with this kind of design, before reading why it should be avoided:

// Renderer-side code:
var bridge = remote.getGlobal( 'bridge' ) ;
bridge.on( 'update' , function() {
    // do something
} ) ;

That kind of design is really neat, but forbidden... I wonder if it's worth trying to fix the potential exceptions issues with some boilerplated try-catch block, or if it's a definitive no-go...

Trouble with jQuery and workaround

In Atom-shell, we cannot include jQuery like we do usually, i.e. putting <script src="../js/jquery.js"></script> into the main .html file of the renderer-side.

At first glance, it was a bit frustrating, since the renderer/web page side claims to behave like any normal browser. However, it is not an Atom-shell issue. Actually jQuery detect a module and a module.exports global, and therefore expect a CommonJS context, thus decide to not install itself into window.$ as usual.

After a bit of googling, the workaround is easy, we have to require jQuery in the main .js file of the web page, this way:
window.$ = require( '../js/jquery.js' ) ;

For the record, here is the related issue on Github, giving all the details.

Conclusions?

I cannot say I'm an experienced Atom-shell developer at the moment. There are many things to explore, and a great app to build.

But I can say I enjoy it: it just does the job, and that's what we like. There is no fuss, I didn't find anything opinionated, or anything.

Just node.js and Chromium, together.

... to be continued...

Do not miss the other post on Atom-Shell / Electron: