Creating Desktop Applications With AngularJS and GitHub Electron

Jasim Muhammed
👁️ 347,664 views
💬 comments

GitHub's Electron framework (formerly known as Atom Shell) lets you write cross platform desktop application using HTML, CSS and JavaScript. It's a variant of io.js run-time which is focused on desktop applications instead of web servers.

Electron's rich native APIs enables us to access native things directly from our pages with JavaScript.

This tutorial shows us how to build a desktop application with Angular and Electron. The steps for this tutorial are as follows:

Table of Contents

    1. Create a simple Electron application
    2. Use Visual Studio Code Editor to manage our project and tasks
    3. Integrate an Angular Customer Manager App with Electron
    4. Use Gulp tasks to build our application and create installers

    Creating Your Electron Application

    To get started, install Node if you don't have it in your system already. Our application should be structured as follows:

    project-structure

    There are two package.json files in this project.

    • For development

      The package.json directly inside the project root contains the configurations, dependiencies for your development environment and build scripts. These dependencies and package.json file will not be included inside the production build.

    • For your application

      The package.json inside app folder is the manifest file for your application. So whenever you need to install npm dependencies to be used in your application directly, you should install it against this package.json

    The format of package.json is exactly same as that of Node's module. Your application’s startup script should be specified in main property inside your app/package.json.

    app/package.json might look like this:

     
    { 
        name: "AngularElectron", 
        version: "0.0.0", 
        main: "main.js" 
    } 
    
    

    You can create both package.json files either by entering the npm init command. You can also manually create these files. Install npm dependencies that are required for packaging the application by entering following command in your command line prompt:

    
    npm install --save-dev electron-prebuilt fs-jetpack asar rcedit Q
    

    Creating your Startup Script

    app/main.js is the entry point of our application. This script is responsible for creating the main window and handling the system events. main.js should look like the following:

     // app/main.js
    
    // Module to control application life.
    var app = require('app'); 
    
    // Module to create native browser window.
    var BrowserWindow = require('browser-window');
    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 finished
    // initialization and is ready to create 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 + '/index.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;
      });
    
    });
    

    Native access from the DOM

    As I mentioned above, Electron enables you to access local npm modules and native APIs directly from your web pages. Create your app/index.html file as follows:

     
    <html>
    <body> 
      <h1>Hello World!</h1>
       We are using Electron 
       <script> document.write(process.versions['electron']) </script> on 
       <script> document.write(process.platform) </script>
       <script type="text/javascript"> 
          var fs = require('fs');
          var file = fs.readFileSync('app/package.json'); 
          document.write(file); 
       </script>
    
    </body> 
    </html> 
    

    app/index.html is a simple HTML page. Here it reads your package.json using Node's fs (file system) module and writes the content into the document body.

    Run the Application

    Once you have created the project structure, app/index.html, app/main.js, app/package.json , you'll probably want to try running your initial Electron application to test it and make sure it's working as expected.

    If you've installed electron-prebuilt globally in your system, you can run our application with following command:

    
    electron app 
    

    Here electron is the command to run electron shell and app is our application folder name. If you don't want to install Electron into your global npm modules, then run with your local modules installed into npm_modules folder as follows in your command line prompt:

    
    "node_modules/.bin/electron" "./app" 
    

    While you could do it that way, I recommend you create a gulp task to run your application in your gulpfile.js, so that you can integrate your task into Visual Studio Code Editor which we will check in next section.

     
    // get the dependencies
    var gulp        = require('gulp'), 
      childProcess  = require('child_process'), 
      electron      = require('electron-prebuilt');
    
    // create the gulp task
    gulp.task('run', function () { 
      childProcess.spawn(electron, ['./app'], { stdio: 'inherit' }); 
    });
    

    Run your gulp task: gulp run Our application might looks like this:

    electron-app

    Configuring Development Environment With Visual Studio Code

    Visual Studio Code is a cross platform code editor from Microsoft. Behind the scene of VS Code is Electron and Microsoft's proprietary monaco code editor. You can download Visual Studio Code here.

    Open your electron application in VS Code.

    open-application

    Configure Visual Studio Code Task Runner

    Lots of tools exist to automate our tasks like building, packaging or testing our application. Mostly we run these tools from the command line. VS Code's built in task runner enables you to integrate your custom tasks into your project. You can run your grunt, gulp, MsBuild or any other tasks directly from within your project without moving to the command line.

    VS Code can detect your grunt and gulp tasks automaticaly. Press ctrl + shift + p then type Run Task followed by Enter.

    run-task

    You will get all available tasks from your gulpfile.js or gruntfile.js.

    Note that, you should have your gulpfile.js in root directory of your application.

    run-task-gulp

    ctrl + shift + b will execute build task from your task runner. You can override this intergration using the task.json file. Press ctrl + shift + p then type Configure Task followed by Enter. This will create .setting folder and a task.json in your project . You need to configure the tasks in tasks.json if you want to do more than simply run the task. For example you might want to run the application when you press the Ctrl + Shift + B . To do this edit the task.json file as follows:

     
    { 
      "version": "0.1.0", 
      "command": "gulp", 
      "isShellCommand": true, 
      "args": [ "--no-color" ], 
      "tasks": [ 
        { 
          "taskName": "run", 
          "args": [], 
          "isBuildCommand": true 
        } 
      ] 
    } 
    

    Root section says that the command is gulp. You can have more tasks inside tasks section as you want. Setting isBuildCommand true for a task will bind the Ctrl + Shift + B to that task. Currently VS Code supports one top level task only.

    Now if you press Ctrl + Shift + B, gulp run will be executed.

    You can read more about visual studio code tasks here

    Debugging Electron Application

    Open the debug panel and click configure button which will create a launch.json file inside .settings folder with debug configuration.

    debug

    We don't need launch app.js configuration, so remove it.

    Now your launch.json should be as follows:

     
    { 
      "version": "0.1.0", 
      // List of configurations. Add new configurations or edit existing ones. 
      // ONLY "node" and "mono" are supported, change "type" to switch. 
      "configurations": [
        { 
          "name": "Attach", 
          "type": "node", 
          // TCP/IP address. Default is "localhost". 
          "address": "localhost", 
          // Port to attach to.
          "port": 5858, 
          "sourceMaps": false 
         } 
       ] 
    }
    

    Change your gulp run task that we created before as follows, so that our electron will start in debug mode and will listen to port 5858:

     
    gulp.task('run', function () { 
      childProcess.spawn(electron, ['--debug=5858','./app'], { stdio: 'inherit' }); 
    }); 
    

    In the debug panel choose "Attach" configuration and click run or press F5. After few seconds you should see the debug command panel in the top.

    debug-star

    Creating the AngularJS Application

    New to AngularJS? Check out the official website here or some of the Scotch Angular tutorials.

    This section explains how to create a simple Customer Manager application using AngularJS with a MySQL database as the backend. The goal of this application is not to highlight the core concepts of AngularJS but to demonstrate how to use the AngularJS and NodeJS together with MySQL backend inside GitHub Electron.

    Our Customer Manager application is as simple as the following:

    • List the customers
    • Add New Customer
    • Option to delete a customer
    • Search for the customer

    Project Structure

    Our application is located inside app folder, and the structure of the application is shown below.

    angular-project-structure

    The home page is the app/index.html file. The app/scripts folder contains all the key scripts and views used in this application. There are several techniques that can be used for organizing application files.

    Here I prefer scripts organized by features. Each feature has its own folder with templates and controllers inside the same folder. For more info on folder structure, read: AngularJS Best Practices: Directory Structure

    Before get started with the AngularJS application, we're going to install client side dependencies using bower. Install Bower if you don't have it already. Change the current working directory to the root of the application in your terminal then install dependencies as follows from your command line prompt :

    
    bower install angular angular-route angular-material --save 
    

    Setting Up the Database

    For this demo I'll be using a database called customer-manager and a table called customers. Here is the dump of database so that you can get up and run quickly.

    
    CREATE TABLE `customer_manager`.`customers` ( 
      `customer_id` INT NOT NULL AUTO_INCREMENT, 
      `name` VARCHAR(45) NOT NULL, 
      `address` VARCHAR(450) NULL, 
      `city` VARCHAR(45) NULL, 
      `country` VARCHAR(45) NULL, 
      `phone` VARCHAR(45) NULL, 
      `remarks` VARCHAR(500) NULL, PRIMARY KEY (`customer_id`) 
    ); 
    

    Creating an Angular Service to Interact with MySQL

    Once you have your database and table ready, let's create an AngularJS service to access the data directly from this database. The service connects to database using node-mysql npm module - a NodeJs driver for MySql written in JavaScript. Install node-mysql module in your app/ folder where your Angular application resides.

    Note , we install node-mysql module inside app folder, not in the application root, as we need to include this module inside the final distribution.

    Change the current working directory to app folder in your command prompt install using following command.

    
    npm install --save mysql 
    

    Our angular service - app/scripts/customer/customerService.js, should looks like following:

     
    (function () {
        'use strict';
        var mysql = require('mysql');
    
        // Creates MySql database connection
        var connection = mysql.createConnection({
            host: "localhost",
            user: "root",
            password: "password",
            database: "customer_manager"
        });
    
        angular.module('app')
            .service('customerService', ['$q', CustomerService]);
    
        function CustomerService($q) {
            return {
                getCustomers: getCustomers,
                getById: getCustomerById,
                getByName: getCustomerByName,
                create: createCustomer,
                destroy: deleteCustomer,
                update: updateCustomer
            };
    
            function getCustomers() {
                var deferred = $q.defer();
                var query = "SELECT * FROM customers";
                connection.query(query, function (err, rows) {
                    if (err) deferred.reject(err);
                    deferred.resolve(rows);
                });
                return deferred.promise;
            }
    
            function getCustomerById(id) {
                var deferred = $q.defer();
                var query = "SELECT * FROM customers WHERE customer_id = ?";
                connection.query(query, [id], function (err, rows) {
                    if (err) deferred.reject(err);
                    deferred.resolve(rows);
                });
                return deferred.promise;
            }
    
            function getCustomerByName(name) {
                var deferred = $q.defer();
                var query = "SELECT * FROM customers WHERE name LIKE  '" + name + "%'";
                connection.query(query, [name], function (err, rows) {
                    if (err) deferred.reject(err);
    
                    deferred.resolve(rows);
                });
                return deferred.promise;
            }
    
            function createCustomer(customer) {
                var deferred = $q.defer();
                var query = "INSERT INTO customers SET ?";
                connection.query(query, customer, function (err, res) 
                    if (err) deferred.reject(err);
                    deferred.resolve(res.insertId);
                });
                return deferred.promise;
            }
    
            function deleteCustomer(id) {
                var deferred = $q.defer();
                var query = "DELETE FROM customers WHERE customer_id = ?";
                connection.query(query, [id], function (err, res) {
                    if (err) deferred.reject(err);
                    deferred.resolve(res.affectedRows);
                });
                return deferred.promise;
            }
    
            function updateCustomer(customer) {
                var deferred = $q.defer();
                var query = "UPDATE customers SET name = ? WHERE customer_id = ?";
                connection.query(query, [customer.name, customer.customer_id], function (err, res) {
                    if (err) deferred.reject(err);
                    deferred.resolve(res);
                });
                return deferred.promise;
            }
        }
    })();
    

    customerService is a simple custom angular service that provides basic CRUD operations on customers table . It uses node's mysql module directly inside the service. If you already have a remote data service, you can use it instead.

    Controller & Template

    OurcustomerControllerinsideapp/scripts/customer/customerController is as follows:

     
    (function () {
        'use strict';
        angular.module('app')
            .controller('customerController', ['customerService', '$q', '$mdDialog', CustomerController]);
    
        function CustomerController(customerService, $q, $mdDialog) {
            var self = this;
    
            self.selected = null;
            self.customers = [];
            self.selectedIndex = 0;
            self.filterText = null;
            self.selectCustomer = selectCustomer;
            self.deleteCustomer = deleteCustomer;
            self.saveCustomer = saveCustomer;
            self.createCustomer = createCustomer;
            self.filter = filterCustomer;
    
            // Load initial data
            getAllCustomers();
    
            //----------------------
            // Internal functions 
            //----------------------
    
            function selectCustomer(customer, index) {
                self.selected = angular.isNumber(customer) ? self.customers[customer] : customer;
                self.selectedIndex = angular.isNumber(customer) ? customer: index;
            }
    
            function deleteCustomer($event) {
                var confirm = $mdDialog.confirm()
                                       .title('Are you sure?')
                                       .content('Are you sure want to delete this customer?')
                                       .ok('Yes')
                                       .cancel('No')
                                       .targetEvent($event);
    
                $mdDialog.show(confirm).then(function () {
                    customerService.destroy(self.selected.customer_id).then(function (affectedRows) {
                        self.customers.splice(self.selectedIndex, 1);
                    });
                }, function () { });
            }
    
            function saveCustomer($event) {
                if (self.selected != null && self.selected.customer_id != null) {
                    customerService.update(self.selected).then(function (affectedRows) {
                        $mdDialog.show(
                            $mdDialog
                                .alert()
                                .clickOutsideToClose(true)
                                .title('Success')
                                .content('Data Updated Successfully!')
                                .ok('Ok')
                                .targetEvent($event)
                        );
                    });
                }
                else {
                    //self.selected.customer_id = new Date().getSeconds();
                    customerService.create(self.selected).then(function (affectedRows) {
                        $mdDialog.show(
                            $mdDialog
                                .alert()
                                .clickOutsideToClose(true)
                                .title('Success')
                                .content('Data Added Successfully!')
                                .ok('Ok')
                                .targetEvent($event)
                        );
                    });
                }
            }
    
            function createCustomer() {
                self.selected = {};
                self.selectedIndex = null;
            }
    
            function getAllCustomers() {
                customerService.getCustomers().then(function (customers) {
                    self.customers = [].concat(customers);
                    self.selected = customers[0];
                });
            }
    
            function filterCustomer() {
                if (self.filterText == null || self.filterText == "") {
                    getAllCustomers();
                }
                else {
                    customerService.getByName(self.filterText).then(function (customers) {
                        self.customers = [].concat(customers);
                        self.selected = customers[0];
                    });
                }
            }
        }
    
    })();
    

    Our Customer template(app/scripts/customer/customer.html) uses angular material components to build user interface, and its is as follows:

    
    <div style="width:100%" layout="row">
        <md-sidenav class="site-sidenav md-sidenav-left md-whiteframe-z2"
                    md-component-id="left"
                    md-is-locked-open="$mdMedia('gt-sm')">
    
            <md-toolbar layout="row" class="md-whiteframe-z1">
                <h1>Customers</h1>
            </md-toolbar>
            <md-input-container style="margin-bottom:0">
                <label>Customer Name</label>
                <input required name="customerName" ng-model="_ctrl.filterText" ng-change="_ctrl.filter()">
            </md-input-container>
            <md-list>
                <md-list-item ng-repeat="it in _ctrl.customers">
                    <md-button ng-click="_ctrl.selectCustomer(it, $index)" ng-class="{'selected' : it === _ctrl.selected }">
                        {{it.name}}
                    </md-button>
                </md-list-item>
            </md-list>
        </md-sidenav>
    
        <div flex layout="column" tabIndex="-1" role="main" class="md-whiteframe-z2">
    
            <md-toolbar layout="row" class="md-whiteframe-z1">
                <md-button class="menu" hide-gt-sm ng-click="ul.toggleList()" aria-label="Show User List">
                    <md-icon md-svg-icon="menu"></md-icon>
                </md-button>
                <h1>{{ _ctrl.selected.name }}</h1>
            </md-toolbar>
    
            <md-content flex id="content">
                <div layout="column" style="width:50%">
                    <br />
                    <md-content layout-padding class="autoScroll">
                        <md-input-container>
                            <label>Name</label>
                            <input ng-model="_ctrl.selected.name" type="text">
                        </md-input-container>
                        <md-input-container md-no-float>
                            <label>Email</label>
                            <input ng-model="_ctrl.selected.email" type="text">
                        </md-input-container>
                        <md-input-container>
                            <label>Address</label>
                            <input ng-model="_ctrl.selected.address"  ng-required="true">
                        </md-input-container>
                        <md-input-container md-no-float>
                            <label>City</label>
                            <input ng-model="_ctrl.selected.city" type="text" >
                        </md-input-container>
                        <md-input-container md-no-float>
                            <label>Phone</label>
                            <input ng-model="_ctrl.selected.phone" type="text">
                        </md-input-container>
                    </md-content>
                    <section layout="row" layout-sm="column" layout-align="center center" layout-wrap>
                        <md-button class="md-raised md-info" ng-click="_ctrl.createCustomer()">Add</md-button>
                        <md-button class="md-raised md-primary" ng-click="_ctrl.saveCustomer()">Save</md-button>
                        <md-button class="md-raised md-danger" ng-click="_ctrl.cancelEdit()">Cancel</md-button>
                        <md-button class="md-raised md-warn" ng-click="_ctrl.deleteCustomer()">Delete</md-button>
                    </section>
                </div>
            </md-content>
    
        </div>
    </div>
    

    app.js contains module initialization script and application route config as follows:

    
    
    (function () {
        'use strict';
    
        var _templateBase = './scripts';
    
        angular.module('app', [
            'ngRoute',
            'ngMaterial',
            'ngAnimate'
        ])
        .config(['$routeProvider', function ($routeProvider) {
                $routeProvider.when('/', {
                    templateUrl: _templateBase + '/customer/customer.html' ,
                    controller: 'customerController',
                    controllerAs: '_ctrl'
                });
                $routeProvider.otherwise({ redirectTo: '/' });
            }
        ]);
    
    })();
    

    Finally here is our index page app/index.html

    
    
    <html lang="en" ng-app="app">
        <title>Customer Manager</title>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge"gt;
        <meta name="description" content="">
        <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
        <!-- build:css assets/css/app.css -->
        <link rel="stylesheet" href="../bower_components/angular-material/angular-material.css" />
        <link rel="stylesheet" href="assets/css/style.css" />
        <!-- endbuild -->
    <body>
        <ng-view></ng-view>
        <!-- build:js scripts/vendor.js -->
        <script src="../bower_components/angular/angular.js"></script>
        <script src="../bower_components/angular-route/angular-route.js"></script>
        <script src="../bower_components/angular-animate/angular-animate.js"></script>
        <script src="../bower_components/angular-aria/angular-aria.js"></script>
        <script src="../bower_components/angular-material/angular-material.js"></script>
        <!-- endbuild -->
    
        <!-- build:app scripts/app.js -->
        <script src="./scripts/app.js"></script>
        <script src="./scripts/customer/customerService.js"></script>
        <script src="./scripts/customer/customerController.js"></script>
        <!-- endbuild -->
    </body>
    </html>
    

    Run your application using gulp run command or press Ctrl + Shif + B if you already configured VS Code task runner as above.

    angular-app

    Build the AngularJS Application

    To build our Angular application install gulp-uglify, gulp-minify-css and gulp-usemin packages.

    
    npm install --save gulp-uglify gulp-minify-css gulp-usemin
    

    Open your gulpfile.js and import required modules

    
      var childProcess = require('child_process'); 
      var electron     = require('electron-prebuilt'); 
      var gulp         = require('gulp'); 
      var jetpack      = require('fs-jetpack'); 
      var usemin       = require('gulp-usemin'); 
      var uglify       = require('gulp-uglify');
    
      var projectDir = jetpack; 
      var srcDir     = projectDir.cwd('./app'); 
      var destDir    = projectDir.cwd('./build');
    

    Clean build directory if it already exists.

     
    gulp.task('clean', function (callback) { 
        return destDir.dirAsync('.', { empty: true }); 
    });
    
    

    Copy files into build directory, We don't need to copy the angular application code using copy function. usemin will do this for us in next section:

    
    gulp.task('copy', ['clean'], function () { 
        return projectDir.copyAsync('app', destDir.path(), { 
            overwrite: true, matching: [ 
                './node_modules/**/*', 
                '*.html', 
                '*.css', 
                'main.js', 
                'package.json' 
           ] 
        }); 
    });
    

    Our build task takes our app/index.html with gulp.src() and then we pipe it to usemin. It then writes the output into build directory and replace references in index.html with optimized version of code.

    Note: Don't forget to define usemin blocks inside app/index.html as follows:

    
    <!-- build:js scripts/vendor.js -->
    <script src="../bower_components/angular/angular.js"></script>
    <script src="../bower_components/angular-route/angular-route.js"></script>
    <script src="../bower_components/angular-animate/angular-animate.js"></script>
    <script src="../bower_components/angular-aria/angular-aria.js"></script>
    <script src="../bower_components/angular-material/angular-material.js"></script>
    <!-- endbuild -->
    
    <!-- build:app scripts/app.js -->
    <script src="./scripts/app.js"></script>
    <script src="./scripts/customer/customerService.js"></script>
    <script src="./scripts/customer/customerController.js"></script>
    <!-- endbuild -->
    

    The build task should look as follows:

     
    gulp.task('build', ['copy'], function () { 
      return gulp.src('./app/index.html') 
        .pipe(usemin({ 
          js: [uglify()] 
        })) 
        .pipe(gulp.dest('build/')); 
        }); 
    
    

    Preparing for Distribution

    In this section we will package our Electron application for production. Create your build script in root directory as build.windows.js. This script is intended to be used on Windows. For other platform you should create scripts specific to that platform and should run according to your platform.

    A typical electron distribution can be found inside node_modules/electron-prebuilt/dist directory. Here is our steps to build the electron application:

    • Our very first task to do is to copy electron distribution into our dist folder.

    • Each electron distribution contains a default application inside dist/resources/default_app folder. We need to replace this application with our final angular application build.

    • To protect our application’s source code and resources from users, you can choose to package your app into an asar archive with little changes to your source code. An asar archive is a simple tar-like format that concatenate files into a single file, Electron can read arbitrary files from it without unpacking the whole file.

    Note: this section describes about packaging in windows platform. All these steps are same for other platforms but paths and files used here is different for other platforms. You can get the full build scripts for OSx and linux here in github.

    Install dependencies required to build the electron as following: npm install --save q asar fs-jetpack recedit.

    Next initialize our build script as follows:

     
    var Q = require('q'); 
    var childProcess = require('child_process'); 
    var asar = require('asar'); 
    var jetpack = require('fs-jetpack');
    var projectDir;
    var buildDir; 
    var manifest; 
    var appDir;
    
    function init() { 
        // Project directory is the root of the application
        projectDir = jetpack; 
        // Build directory is our destination where the final build will be placed 
        buildDir = projectDir.dir('./dist', { empty: true }); 
        // angular application directory 
        appDir = projectDir.dir('./build'); 
        // angular application's package.json file 
        manifest = appDir.read('./package.json', 'json'); 
        return Q(); 
    } 
    

    Here we use fs-jetpack node module for the file operation. It gives more flexibility on file operations.

    Copy Electron Distribution

    Copy the default electron distribution from electron-prebuilt/dist to our dist directory.

     
     function copyElectron() { 
         return projectDir.copyAsync('./node_modules/electron-prebuilt/dist', buildDir.path(), { overwrite: true }); 
    } 
    

    Cleanup the Default Application

    You can find a default HTML application inside resources/default_app folder. We need to replace this application with our angular application. Remove it as follows:

    Note: Here path is specific to windows platform. For other platforms process is same but path is different. In OSX it's Contents/Resources/default_app

     
     function cleanupRuntime() { 
         return buildDir.removeAsync('resources/default_app'); 
    } 
    

    Create asar package

     
    function createAsar() { 
         var deferred = Q.defer(); 
         asar.createPackage(appDir.path(), buildDir.path('resources/app.asar'), function () { 
             deferred.resolve(); 
         }); 
         return deferred.promise; 
    } 
    

    This combines all your angular application files into single asar package file. You can find the destination asar file in dist/resources/ directory.

    Replace application resources with your own

    Next step is to replace the default electron icon with your own, update the product information and rename the application.

     
    function updateResources() {
        var deferred = Q.defer();
    
        // Copy your icon from resource folder into build folder.
        projectDir.copy('resources/windows/icon.ico', buildDir.path('icon.ico'));
    
        // Replace Electron icon for your own.
        var rcedit = require('rcedit');
        rcedit(buildDir.path('electron.exe'), {
            'icon': projectDir.path('resources/windows/icon.ico'),
            'version-string': {
                'ProductName': manifest.name,
                'FileDescription': manifest.description,
            }
        }, function (err) {
            if (!err) {
                deferred.resolve();
            }
        });
        return deferred.promise;
    }
    //Rename the electron exe 
    function rename() {
        return buildDir.renameAsync('electron.exe', manifest.name + '.exe');
    }
    
    

    Creating Native Installers

    You can either use wix or NSIS to create windows installer. Here we use NSIS which is designed to be small and flexible as possible and is very suitable for internet distribution. With NSIS you can create such installers that are capable of doing everything that is needed to setup your software.

    Create NSIS script in resources/windows/installer.nsis

        !include LogicLib.nsh
        !include nsDialogs.nsh
    
        ; --------------------------------
        ; Variables
        ; --------------------------------
    
        !define dest "{{dest}}"
        !define src "{{src}}"
        !define name "{{name}}"
        !define productName "{{productName}}"
        !define version "{{version}}"
        !define icon "{{icon}}"
        !define banner "{{banner}}"
    
        !define exec "{{productName}}.exe"
    
        !define regkey "Software\${productName}"
        !define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${productName}"
    
        !define uninstaller "uninstall.exe"
    
        ; --------------------------------
        ; Installation
        ; --------------------------------
    
        SetCompressor lzma
    
        Name "${productName}"
        Icon "${icon}"
        OutFile "${dest}"
        InstallDir "$PROGRAMFILES\${productName}"
        InstallDirRegKey HKLM "${regkey}" ""
    
        CRCCheck on
        SilentInstall normal
    
        XPStyle on
        ShowInstDetails nevershow
        AutoCloseWindow false
        WindowIcon off
    
        Caption "${productName} Setup"
        ; Don't add sub-captions to title bar
        SubCaption 3 " "
        SubCaption 4 " "
    
        Page custom welcome
        Page instfiles
    
        Var Image
        Var ImageHandle
    
        Function .onInit
    
            ; Extract banner image for welcome page
            InitPluginsDir
            ReserveFile "${banner}"
            File /oname=$PLUGINSDIR\banner.bmp "${banner}"
    
        FunctionEnd
    
        ; Custom welcome page
        Function welcome
    
            nsDialogs::Create 1018
    
            ${NSD_CreateLabel} 185 1u 210 100% "Welcome to ${productName} version ${version} installer.$\r$\n$\r$\nClick install to begin."
    
            ${NSD_CreateBitmap} 0 0 170 210 ""
            Pop $Image
            ${NSD_SetImage} $Image $PLUGINSDIR\banner.bmp $ImageHandle
    
            nsDialogs::Show
    
            ${NSD_FreeImage} $ImageHandle
    
        FunctionEnd
    
        ; Installation declarations
        Section "Install"
    
            WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR"
            WriteRegStr HKLM "${uninstkey}" "DisplayName" "${productName}"
            WriteRegStr HKLM "${uninstkey}" "DisplayIcon" '"$INSTDIR\icon.ico"'
            WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$INSTDIR\${uninstaller}"'
    
            ; Remove all application files copied by previous installation
            RMDir /r "$INSTDIR"
    
            SetOutPath $INSTDIR
    
            ; Include all files from /build directory
            File /r "${src}\*"
    
            ; Create start menu shortcut
            CreateShortCut "$SMPROGRAMS\${productName}.lnk" "$INSTDIR\${exec}" "" "$INSTDIR\icon.ico"
    
            WriteUninstaller "${uninstaller}"
    
        SectionEnd
    
        ; --------------------------------
        ; Uninstaller
        ; --------------------------------
    
        ShowUninstDetails nevershow
    
        UninstallCaption "Uninstall ${productName}"
        UninstallText "Don't like ${productName} anymore? Hit uninstall button."
        UninstallIcon "${icon}"
    
        UninstPage custom un.confirm un.confirmOnLeave
        UninstPage instfiles
    
        Var RemoveAppDataCheckbox
        Var RemoveAppDataCheckbox_State
    
        ; Custom uninstall confirm page
        Function un.confirm
    
            nsDialogs::Create 1018
    
            ${NSD_CreateLabel} 1u 1u 100% 24u "If you really want to remove ${productName} from your computer press uninstall button."
    
            ${NSD_CreateCheckbox} 1u 35u 100% 10u "Remove also my ${productName} personal data"
            Pop $RemoveAppDataCheckbox
    
            nsDialogs::Show
    
        FunctionEnd
    
        Function un.confirmOnLeave
    
            ; Save checkbox state on page leave
            ${NSD_GetState} $RemoveAppDataCheckbox $RemoveAppDataCheckbox_State
    
        FunctionEnd
    
        ; Uninstall declarations
        Section "Uninstall"
    
            DeleteRegKey HKLM "${uninstkey}"
            DeleteRegKey HKLM "${regkey}"
    
            Delete "$SMPROGRAMS\${productName}.lnk"
    
            ; Remove whole directory from Program Files
            RMDir /r "$INSTDIR"
    
            ; Remove also appData directory generated by your app if user checked this option
            ${If} $RemoveAppDataCheckbox_State == ${BST_CHECKED}
                RMDir /r "$LOCALAPPDATA\${name}"
            ${EndIf}
    
        SectionEnd
    

    Create a function called createInstaller in your build.windows.js file as follows:

     
    
    function createInstaller() {
        var deferred = Q.defer();
    
        function replace(str, patterns) {
            Object.keys(patterns).forEach(function (pattern) {
                console.log(pattern)
                  var matcher = new RegExp('{{' + pattern + '}}', 'g');
                str = str.replace(matcher, patterns[pattern]);
            });
            return str;
        }
    
        var installScript = projectDir.read('resources/windows/installer.nsi');
    
        installScript = replace(installScript, {
            name: manifest.name,
            productName: manifest.name,
            version: manifest.version,
            src: buildDir.path(),
            dest: projectDir.path(),
            icon: buildDir.path('icon.ico'),
            setupIcon: buildDir.path('icon.ico'),
            banner: projectDir.path('resources/windows/banner.bmp'),
        });
        buildDir.write('installer.nsi', installScript);
    
        var nsis = childProcess.spawn('makensis', [buildDir.path('installer.nsi')], {
            stdio: 'inherit'
        });
    
        nsis.on('error', function (err) {
            if (err.message === 'spawn makensis ENOENT') {
                throw "Can't find NSIS. Are you sure you've installed it and"
                + " added to PATH environment variable?";
            } else {
                throw err;
            }
        });
    
        nsis.on('close', function () {
            deferred.resolve();
        });
    
        return deferred.promise;
    
    }
     

    You should have NSIS installed, and should be available in your path. creaeInstaller function reads installer script and execute it against NSIS runtim with makensis command.

    Putting It All Together

    Create a function to put all pieces together, then export it to be accessed from gulp task:

    
       function build() { 
        return init()
                .then(copyElectron) 
                .then(cleanupRuntime) 
                .then(createAsar) 
                .then(updateResources) 
                .then(rename) 
                .then(createInstaller); 
        }
        module.exports = { build: build };
    

    Next create gulp task in gulpfile.js to execute this build script:

    
    
     var release_windows = require('./build.windows'); 
     var os = require('os'); 
     gulp.task('build-electron', ['build'], function () { 
         switch (os.platform()) { 
             case 'darwin': 
             // execute build.osx.js 
             break; 
             case 'linux': 
             //execute build.linux.js 
             break; 
             case 'win32': 
             return release_windows.build(); 
         } 
    }); 
    
    

    You can have your final product by running:

    
    gulp build-electron
    

    Your final electron application should be in dist folder and folder structure should be some thing similar to following.

    dist-structure

    Summary

    Electron is not just a native web view that let's you to wrap your web application into desktop application . It now includes automatic app updates, Windows installers, crash reporting, notifications, and other useful native app features — all exposed through JavaScript APIs.

    So far there is huge range of apps created using electron including chat apps, database explorers, map designers, collaborative design tools, and mobile prototyping apps.

    Here is some useful resources about Github Electron:

    Jasim Muhammed

    1 post

    Full stack web developer with over seven years of experience in building web applications. Passionate about Front-End development . Focused on React, Angular and Sencha ExtJs