Packaging Sencha ExtJs Application With Github Electron
Sencha ExtJs provides developers the potential to develop universal applications using HTML, CSS and JavaScript. Starting from version 6.0, ExtJs apps are not only limited to desktop but are also accessible on mobile and tablets as well. But what if you want to run your application as a native application like Phonegap?. Sencha provides a way to package your application into native mobile application using Sencha Command. They were also providing chromium based desktop packager before, but now this project seems to be discontinued. So what if you want to package your application into native desktop application?. There are lot of options or tools are available for this, like Github Electron, NwJs, Tide Sdk, etc.
In this blog post we will check how we can package an ExtJs application into native desktop application using Github Electron. We will also check about how to integrate the packaging process into Sencha Command.
Github’s electron framework (formerly known as Atom Shell) lets you write cross platform applications using JavaScript, HTML and CSS. It’s based on Chromium and io.js. It supports device access through native node modules directly inside the web pages .
Creating a new ExtJs App
To get started you need to create a Sencha ExtJs Application, the best way to do that is create the application using Sencha CMD, a cross platform command line application from Sencha which provides many automated tasks around the full life cycle of your applications from generating a new project to deploying an application to production. Generating a starter extjs application is as simple as follows with sencha command:
- Download ExtJs Sdk.
- Download and install sencha Cmd .
- Open your terminal and issue the following command:
sencha -sdk /path/to/extjs/framework generate app AppName /path/to/workspace
The application files generated by the above command will have the following structure
The simplest way to run your application is to run :
sencha app watch
This command will perform a development build and start watches for changes to file system. It will also start local http server to serve files from your workspace.
To access this web server use:
http://localhost:1841
Package with Github Electron
You should have node js installed on your system. Generally our application should be structured like following:
ExtJs Application
|-- app
|-- classic
|-- ext
|-- modern
|-- ...
|--Electron
|-- build
|-- app
|-- main.js
|-- package.json
|-- resources
|-- installer.nsi
|-- icon.ico
|-- setup-banner.bmp
|-- package.json
|-- node_modules
|-- build.xml
There are two package.json file in our electron application.
- For Development.
Directly under
Electron
folder. Here you can declare dependencies for your development environment and build scripts. We don’t have to distribute this file and dependencies listed in this file. - For your appliaction.
package.json
insideElectron/app
is the real manifest file for your application. Whenever you need to install some npm dependencies to be used in your application directly, you may add it into this package.json.
Create the above folder structure inside your ExtJs application.
Them format of package.json
is exactly the same as that of Node’s modules. Your application’s startup script should be specified in main
property inside your application package.son
.
Electron/app/package.json
might look like this:
{
name: "app-name",
version: "0.0.0",
main: "main.js",
productName: "app-name",
description: "your product description goes here..."
}
You can create both package.json files either by entering npm init
command or manually.
Then install npm dependencies need for packaging application by entering following command in your command line prompt:
npm install --save-dev electron-prebuilt fs-jetpack asar recedit Q
Note: You should change your current working directory to
Electron
folder in your command line prompt
Electron/app/main.js
is the entry point of our electron application. This script is resposible for creating the main window and handling the system events. Copy the code below to 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;
});
});
This script creates a chromium window and loads our ExtJs application’s index.html into the Web view.
As I mentioned above, Github Electron supports the native npm modules inside the web pages. So if you want to access the native npm modules you can do it in your client script files of html pages as shown below:
var fs = require('fs');
//reads the file content
var file = fs.readFileSync('file.txt');
// Note that, DOM can directly communicate with native node modules.
document.getElementById('id').innerHTML = file;
Creating your electron build script
We use node.js script for building the application. A typical electron distribution can be found inside Electron/node_modules/electron-prebuilt/dist
directory. Our build workflow is as follows:
-
Our very first task to do is to copy this distribution into our
build
folder insideElectron
. -
Each electron distribution contains a default application inside
dist/resources/default_app
folder. We need to replace this application with our ExtJS application. - 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.
- Rename the application exe file and change the icon and other resource files.
- Create Installer.
Let’s start writing our build.js file. Import all required modules as follows:
'use strict';
var Q = require('q');
var childProcess = require('child_process');
var jetpack = require('fs-jetpack');
var asar = require('asar');
Initialise the script:
var projectDir;
var buildDir;
var manifest;
var appDir;
function init() {
projectDir = jetpack;
buildDir = projectDir.dir('./build', { empty: true });
appDir = projectDir.dir('./app');
manifest = appDir.read('./package.json', 'json');
return Q();
}
The init
method assign directories to corresponding global variables. Here we use fs-jetpack
node module for file operations as node’s fs
modules too low level.
Copy Electron distribution
Copy the electron-prebuilt
from your development node dependencies to the build directory:
function copyElectron() {
return projectDir.copyAsync('node_modules/electron-prebuilt/dist', buildDir, { empty: true});
}
Cleanup default application
As I mentioned before Electron shipped with default application.
It can be found in default_app
inside resources folder. Remove files like:
function cleanupRuntime () {
return buildDir.removeAsync('resources/default_app');
}
Create asar package as follows:
Create asar package of your extjs application as follows:
function createAsar() {
var deferred = Q.defer();
asar.createPackage(appDir, buildDir.path('resources/app.asar'), function() {
deferred.resolve();
});
return deferred.promise;
}
Replace the default electron icon with your own and rename the application. Copy your icons into Electron/resources
folder.
function updateResources() {
var deferred = Q.defer();
// Copy your icon from resource folder into build folder.
projectDir.copy('resources/icon.ico', buildDir.path('icon.ico'));
// Replace Electron icon for your own.
var rcedit = require('rcedit');
rcedit(readyAppDir.path('electron.exe'), {
'icon': projectDir.path('resources/windows/icon.ico'),
'version-string': {
'ProductName': manifest.productName,
'FileDescription': manifest.description,
}
}, function (err) {
if (!err) {
deferred.resolve();
}
});
return deferred.promise;
}
//Rename the electron exe
function rename() {
return buildDir.renameAsync('electron.exe', manifest.productName + '.exe');
}
Create installer
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.
- Download and install NSIS.
- Create
Electron/resources/installer.nsis
, NSIS script as follows:
; NSIS packaging/install script
; Docs: http://nsis.sourceforge.net/Docs/Contents.html
!include LogicLib.nsh
!include nsDialogs.nsh
; --------------------------------
; Variables
; --------------------------------
!define dest ""
!define src ""
!define name ""
!define productName ""
!define version ""
!define icon ""
!define setupIcon ""
!define banner ""
!define exec ".exe"
!define regkey "Software\${productName}"
!define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${productName}"
!define uninstaller "uninstall.exe"
; --------------------------------
; Installation
; --------------------------------
SetCompressor lzma
Name "${productName}"
Icon "${setupIcon}"
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
Next we need to define a node function to process this script and execute create installer. So define the createInstaller
function in your build.js like following:
function createInstaller() {
var deferred = Q.defer();
var finalPackageName = manifest.name + '_' + manifest.version + '.exe';
function replace(str, patterns) {
Object.keys(patterns).forEach(function (pattern) {
var matcher = new RegExp(' + pattern + ', 'g');
str = str.replace(matcher, patterns[pattern]);
});
return str;
}
var installScript = projectDir.read('resources/installer.nsi');
installScript = replace(installScript, {
name: manifest.name,
productName: manifest.productName,
version: manifest.version,
src: buildDir.path(),
dest: projectDir.path(finalPackageName),
icon: buldDir.path('icon.ico'),
setupIcon: projectDir.path('resources/setup-icon.ico'),
banner: projectDir.path('resources/setup-banner.bmp'),
});
buildDir.write('installer.nsi', installScript);
// Note: NSIS have to be added to PATH (environment variables).
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;
};
createInstaller
method first replaces the nsis script with project specific values for name and icon. Then it executes the script with makensis
command. Note that you should have NSIS installed on your system and also it should be available in your path.
Combine all together
Combine all the tasks together and export the module:
module.exports = function () {
return init()
.then(copyRuntime)
.then(cleanupRuntime)
.then(createAsar)
.then(updateResources)
.then(rename)
.then(createInstaller);
};
Automating the process with sencha command.
It’s a big frustration switching back and forth from sencha command to node js command line each time when we build the application. So we need to integrate this nodejs build in to the sencha command. The current build artifacts should be copied into Electron
directory and build.js
` should be executed whenever you build your extjs application.
Whenever you build your application with sencha command, thhe the build.js should be executed.
Sencha command provides lot of extension points where you can hook your custom ant tasks. Here our task is to copy the current build into workspace/electron/app directory and execute our build.js file. Paste the code below into to the build.xml
file in your application root.
<target name="-after-build">
<delete includeemptydirs="true">
<fileset dir="Electron/app">
<exclude name="package.json" />
<exclude name="main.js" />
<include name="**/*" />
</fileset>
</delete>
<copy todir="Electron/app">
<fileset dir="${build.dir}" />
</copy>
<exec dir="${app.dir}/Electron" executable="node">
<arg value="build.js"/>
</exec>
</target>
You may want to change the target to -after-watch
, if you want to integrate sencha watch command with electron build.
Build your application:
sencha app build production
You can find the native desktop version of your application in Electron
folder.
Complete code for this blog post is hosted into Github.