Electron – Building Desktop Apps from Web-Apps

electron
Apps using Electron

I wanted to make a mobile app that would mean we had better visibility of our help-desk. The web-site we use to access all our support information works well, but I want our team – mostly developers and support – to have instant access to new information and I wanted them to be able to easily see it when they were not in the office…. primarily for when they were on call over the Christmas holidays.

This project quickly turned in to something different – and in addition to the mobile-app which never really left the ‘web page that works well on a mobile’ state – I started thinking about how the code I had written would be really useful if it was always available on the desktop.

We were using it in a browser for a while and it definitely offered some extra features that our original support site did not – but in order to really become useful, it had to be detached from the browser. Please note that this App was pure HTML and Javascript. The back-end was a PHP-based API, hosted in a different location.

This guide is for OSX, and may well have things missing or incorrectly documented. I’ll be updating it over the next few days – but I’m publishing it here so some of my colleagues can attempt to follow it. Their feedback will help me update this guide.

Why Electron?

I started looking in to all sorts of tools to build “true” desktop applications from web-browsers, and I settled on Electron. This is the framework that evolved from the Atom editor in to it’s own application. Other software – like Slack, Visual Studio IDE and Kitematic also use it – so I know its a powerful tool.

I read up on it – and one of the best things about it was that it allowed distribution on Windows, Mac and Linux (with the same codebase) and even provided tools for automatic updates.

Since I didn’t want to publish this project anywhere public, I used a program called Sinopia to host my own NPM repository. It’s very easy to set up – but it took me a while before I realised it was definitely working. I left it running with the defaults (other than permitting it to work on all IP addresses) and I start it using a program called Forever – which allows it to run as a background service.

support-appThe application

In terms of my actual App, I had written it using some jQuery – and I couldn’t really find many guides that would help me port it to Electron.

Here are the key steps in what was needed in order for me to build this application.

First make sure your application definitely works. It should work well in Chrome, which is what the Electron framework is based on.

Then take a look at the QuickStart application that the Electron site provides. You need to be fairly familiar with Node and have it installed.

Their quick start is this… it creates a simple project, and I used it as the starting block that I dropped my web-application in to.

# Clone the Quick Start repository
$ git clone https://github.com/atom/electron-quick-start

# Go into the repository
$ cd electron-quick-start

# Install the dependencies and run
$ npm install && npm start

Code Changes

I’m going to simply document the files that I have ended up with. Im sure most people reading this guide will be able to figure out what things do. If you have any questions – please ask in the comments.

This is my updated package.json file. Notice the publishConfig section. This points to the URL and port of my internal Sinopia server.

{
  "name": "my-support-app",
  "version": "0.0.43",
  "description": "My Support App",
  "main": "main.js",
  "keywords": [
    "my",
    "support"
  ],
  "author": "Sam Edney",
  "homepage": "http://www.samueledney.com",
  "devDependencies": {
    "electron-builder": "^2.6.0",
    "electron-packager": "^5.2.0",
    "electron-prebuilt": "^0.36.0"
  },
  "scripts": {
    "dev": "electron . --enable-logging",
    "clean": "rm -rf ./dist",
    "clean:osx": "rm -rf ./dist/osx",
    "clean:win": "rm -rf ./dist/win",
    "pack": "npm run clean && npm run pack:osx && npm run pack:win",
    "pack:osx": "npm run clean:osx && electron-packager . \"My Support App\" --out=dist/osx --platform=darwin --arch=x64 --version=0.36.3 --icon=assets/osx/logo.icns --ignore=dist --ignore=assets --ignore=builder.json --ignore=bower.json --ignore=README.md --ignore=.gitignore --ignore=preview.png",
    "pack:win": "npm run clean:win && electron-packager . \"My Support App\" --out=dist/win --platform=win32 --arch=ia32 --version=0.36.3 --icon=assets/win/logo.ico --ignore=dist --ignore=assets --ignore=builder.json --ignore=bower.json --ignore=README.md --ignore=.gitignore --ignore=preview.png",
    "build": "npm run build:osx && npm run build:win",
    "build:osx": "npm run pack:osx && electron-builder \"dist/osx/My Support App-darwin-x64/My Support App.app\" --platform=osx --out=\"dist/osx\" --config=builder.json",
    "build:win": "npm run pack:win && electron-builder \"dist/win/My Support App-win32-ia32\" --platform=win --out=\"dist/win\" --config=builder.json"
  },
  "dependencies": {
    "configstore": "^1.4.0",
    "electron-builder": "^2.6.0",
    "electron-plugins": "0.0.4",
    "electron-updater": "^0.2.3"
  },
  "publishConfig": {
    "registry": "http://192.168.5.49:4873/"
  }
}

This is my updated main.js file.

'use strict';

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

//this allows me to save things to a config file
const Configstore = require('configstore'); 
const pkg = require('./package.json');
const conf = new Configstore(pkg.name);

// The logger signature is essentially the same as the console.
var customLogger = {
    log: console.log,
    error: console.error,
    info: console.info,
    warn: console.warn,
    debug: console.debug
};

let mainWindow;

app.on('ready', function() {

    updater.on('ready', function() {

        //put the window back to where it last was
        mainWindow = new BrowserWindow({
            name: "my-support-app",
            width: 450,
            height: 1050,
            x: conf.get("my_window_x"),
            y: conf.get("my_window_y"),
            alwaysOnTop: true
        });

        mainWindow.loadURL('file://' + __dirname + '/index.html');

        //this simply saves the last position of the window
        mainWindow.on('move', function(e) {

            var pos = mainWindow.getPosition();

            if (pos) {
                conf.set("my_window_x", pos[0]);
                conf.set("my_window_y", pos[1]);
            }
        });

        mainWindow.on('closed', function() {
            mainWindow = null;
        });
    });

    //this will push messages to the main application
    //and we are able to listen for them in jquery.
    //for example - we can show a message when an 
    //update becomes available (which we do)

    updater.on('error', function(err) {
       if (mainWindow) {
            mainWindow.webContents.send('update-failed');
        }
    });

    updater.on('updateAvailable', function() {
        if (mainWindow) {
            mainWindow.webContents.send('update-available');
        }
    });

    updater.start(customLogger);

});

//quit when windows are closed down
app.on('window-all-closed', function() {
    app.quit();
});

The rest of my code is simply my HTML / JS project. The index.html page is the same index.html page that my main project used. However, in order to really make use of the Electron framework – I made some changes to my javascript.

Since I want to also allow this code to work in my regular web-app, I need to test if things are running in Electron and if so – do things the Electron way!

This is my my_app.js file.

//is this running in electron? 
if (window && window.process && window.process.type) {

    function get_config_value(key_name) {

        var Configstore = require('configstore');
        var pkg = require('./package.json');
        var conf = new Configstore(pkg.name);

        return conf.get(key_name);

    }

    function set_config_value(key_name, value) {

        var Configstore = require('configstore');
        var pkg = require('./package.json');
        var conf = new Configstore(pkg.name);

        conf.set(key_name, value);

    }

    const electron = require('electron');
    const app = electron.app;
    const remote = require('electron').remote;
    const Menu = remote.Menu;
    const MenuItem = remote.MenuItem;
    const shell = require('electron').shell;

    var menu = new Menu();

    // NOTE: this is actually a real menu structure in my real code.
    //       its just too big to include here. Ill add a link to something

    var template = [{}]; // ** REPLACE WITH REAL MENU **

    menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);

    var pjson = require('./package.json');
    var plugins = require('electron-plugins'),
        ipc = require('electron').ipcRenderer;

    //gets the version from the package.json file
    var app_version = pjson.version;

    document.addEventListener('DOMContentLoaded', function() {

        var context = {
            document: document
        }

        plugins.load(context, function(err, loaded) {
            if (err) return console.error(err)
            console.log('Plugins loaded successfully.');
        });

    });

    //listen for the messages from the electron updater 
    ipc.on('update-available', function() {
        $('#div_updater_ready').fadeIn('slow');
    });

    ipc.on('update-failed', function() {
        $('#div_updater_error').fadeIn('slow');
    });

    //this will load up links in a browser rather than electron
    $(document).on('click', 'a[href^="http"]', function(event) {
        event.preventDefault();
        shell.openExternal(this.href);
    });

    console.log("config-based");

} else {

    console.log("local-based");

    //this has get_config_value and set_config_value for local storage
    includeJs('js/storage_local.js');  

}

And thats genuinely all thats needed.

To run your application, open a terminal and move in to the app folder.

Then try:

npm install

node_modules/.bin/electron .

This will run your application in development mode! Changes you make to your code can be picked up if you refresh the application.

Automatic Updates

In order to get automatic updates working, you need to publish any changes to the Sinopia server. In order to do that, you need to register a user on it. This is simply a couple of commands:

npm config set registry http://192.168.5.49:4873

npm adduser --registry http://192.168.5.49:4873

Check that things are working by visiting the url in your browser. If its not up – auto updates wont work, so figure out how to get Sinopia working.

Now we need to publish your App to the Sinopia service. This is so easy, it almost brings me to tears thinking about how difficult it used to be to issue updates.

Make sure you update the version number in the package.json file to be a new one, then back in the terminal, within your app’s folder, execute the following:

npm pack
npm pub

The first command makes a *.tgz file of all things in your app folder. The second pushes it up to the server.

Note: Make sure that once it has uploaded you delete the *.tgz file, otherwise it will be included in the next *.tgz file you make, causing exponential growth in file-sizes.

Building an executable

There are a few ways of doing this. The easiest is to get the pre-compiled version of the OSX Electron app from their website, and replace the /Resources/app folder with your own. Otherwise you can try some of these scripts to build your application file.

npm run build:osx

If you scroll back up to the top – you will see that in our package.json file there are some ‘scripts’. These are shortcuts to running batch commands, and this one creates the OSX application in a new folder called dist.

When running, the application checks for updates every few minutes, and the code above means that if you add the right HTML to your index.html page – your users will be informed when one becomes available.

Leave a Reply

Your email address will not be published. Required fields are marked *