Micro-apps with Node.js,
browsers, phones
(Cordova) and electron"
About Michael Dawson
Loves the web and building software (with Node.js!)
Senior Software Developer @ IBM
IBM Runtime Technologies Node.js Technical Lead
Node.js collaborator and CTC member
Active in LTS, build, benchmarking , api
and post-mortem working groups
Contact me:
michael_dawson@ca.ibm.com
Twitter: @mhdawson1
https://www.linkedin.com/in/michael-dawson-6051282
Motivation – Device like GUI
IoT CTI
Small
Apps
Solution - Node.js !
 Single page application(SPA)
 Server written in Node.js
 Presentation in Browser
 Remotely Accessible
 Deploy to Cloud
Well this “Ok”
This is a bit better
Teaser: we can do better, but that’s for later
• Code available from GitHub and published to npm.
• https://github.com/mhdawson/micro-app-framework
• Configuration
• Authentication
• Encryption (SSL)
• Templates
• Pop-ups
micro-app-framework is born !
micro-app-framework - components
<TITLE>
<PAGE_WIDTH>
<PAGE_HEIGHT>
Configuration
• serverPort
• title
• scrollBars
• tls
• authenticate
• authinfo
Methods
• getDefaults()
• getTemplateReplacements()
• startServer(server)
• handleSupportingPages(request, response)
Files
• server.js
• page.html.template
• config.json
• package.json
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
<title><TITLE></title>
</head>
<body style="overflow-x:hidden;overflow-y:hidden;">
<script>
var socket = new io.connect('<URL_TYPE>://' +
window.location.host);
socket.on('data', function(data) {
var parts = data.split(":");
var topic = parts[0];
var value = parts[1];
var targetTD = document.getElementById(topic);
if (null != targetTD) {
targetTD.innerHTML=value;
}
})
</script>
<table BORDER="10" width="100%" style="font-size:25px">
<tbody>
<DASHBOARD_ENTRIES>
</tbody>
</table>
</body>
</html>
{
"title": “Cottage Data",
"serverPort": 3000,
"mqttServerUrl": "<add your mqtt server here>",
"dashboardEntries": [ {"name": "Inside temp", "topic": "house/temp2"},
{"name": "Outside temp", "topic": "house/lacrossTX141/20/temp"},
{"name": "Timestamp", "topic": "house/time"} ]
}
config.json
page.html.template
var fs = require('fs');
var mqtt = require('mqtt');
var socketio = require('socket.io');
const BORDERS = 55;
const HEIGHT_PER_ENTRY = 34;
const PAGE_WIDTH = 320;
var eventSocket = null;
var latestData = {};
var Server = function() {
}
Server.getDefaults = function() {
return { 'title': 'House Data' };
}
var replacements;
Server.getTemplateReplacments = function() {
if (replacements === undefined) {
var config = Server.config;
var height = BORDERS;
var dashBoardEntriesHTML = new Array();
for (i = 0; i < config.dashboardEntries.length; i++) {
dashBoardEntriesHTML[i] = '<tr><td>' + config.dashboardEntries[i].name + ':</td><td id="' +
config.dashboardEntries[i].topic + '">pending</td></tr>';
height = height + HEIGHT_PER_ENTRY;
}
replacements = [{ 'key': '<TITLE>', 'value': Server.config.title },
{ 'key': '<UNIQUE_WINDOW_ID>', 'value': Server.config.title },
{ 'key': '<DASHBOARD_ENTRIES>', 'value': dashBoardEntriesHTML.join("") },
{ 'key': '<PAGE_WIDTH>', 'value': PAGE_WIDTH },
{ 'key': '<PAGE_HEIGHT>', 'value': height }];
}
return replacements;
}
Server.startServer = function(server) {
var topicsArray = new Array();
var config = Server.config;
for (i = 0; i < config.dashboardEntries.length; i++) {
topicsArray.push(config.dashboardEntries[i].topic);
}
var mqttOptions;
if (Server.config.mqttServerUrl.indexOf('mqtts') > -1) {
mqttOptions = { key: fs.readFileSync(path.join(__dirname, 'mqttclient', '/client.key')),
cert: fs.readFileSync(path.join(__dirname, 'mqttclient', '/client.cert')),
ca: fs.readFileSync(path.join(__dirname, 'mqttclient', '/ca.cert')),
checkServerIdentity: function() { return undefined }
}
}
var mqttClient = mqtt.connect(Server.config.mqttServerUrl, mqttOptions);
eventSocket = socketio.listen(server);
eventSocket.on('connection', function(client) {
for (var key in latestData) {
var value = latestData[key];
if (value.trim().indexOf(" ") === -1) {
value = Math.round(value * 100) / 100;
}
eventSocket.to(client.id).emit('data', key + ":" + value);
}
});
mqttClient.on('connect',function() {
for(nextTopic in topicsArray) {
mqttClient.subscribe(topicsArray[nextTopic]);
}
});
mqttClient.on('message', function(topic, message) {
var timestamp = message.toString().split(",")[0];
var parts = message.toString().split(":");
if (1 < parts.length) {
var value = parts[1].trim();
latestData[topic] = value;
if (value.trim().indexOf(" ") === -1) {
value = Math.round(value * 100) / 100;
}
eventSocket.emit('data', topic + ':' + value );
}
});
}
if (require.main === module) {
var path = require('path');
var microAppFramework = require('micro-app-framework');
microAppFramework(path.join(__dirname), Server);
}
server.js
Good enough, create bunch of micro-apps
But some things still bug me
 Desktop
– Browser Bar
– Pop-ups
– Having to re-open all those windows
– Having to position the windows
– Remembering URL
 Phone
– Single browser with tabs
– UI issues
– Browser Bar
– Pop-ups
– Having to open browser/then tab
– Remembering URL
Electron – Desktop solution
 electron.atom.io
 Build cross platform desktop apps
– With JavaScript, HTML and CSS
 Uses Node.js, Chromium and V8 !
 Happiness
– No pop-ups
– No browser bar
– Position on startup
– No URL to remember
– Binary package possible
– No URL to remember
micro-app-electron-launcher
 https://github.com/mhdawson/micro-app-electron-launcher
 npm install micro-app-electron-launcher
 vi config.json
 npm start
 Future: create native binary
{ "apps": [
{ "name": "home dashboard",
"hostname": "X.X.X.X",
"port": "8081",
"options": { "x": 3350, "y": 10, "resizable": false }
},
{ "name": "phone",
"hostname": "X.X.X.X",
"port": "8083",
"options": { "x": 15, "y": 1850, "sizable": false }
},
{ "name": "Alert Dashboard",
"hostname": "X.X.X.X",
"port": "8084",
"options": { "x": 3065, "y": 10, "sizable": false }
},
{ "name": "totp",
"tls": true,
"hostname": "X.X.X.X",
"port": "8082",
"auth": "asdkweivnaliwerld8welkasdfiuwerasdkllsdals9=",
"options": { "x": 2920, "y": 10, "sizable": false }
}
]
}
'use strict';
var http = require('http');
var https = require('https');
var os = require('os');
var util = require('util');
var path = require('path');
var CryptoJS = require('crypto-js');
var prompt = require('prompt');
// get configuration options
prompt.start();
prompt.get({ properties: { password: { hidden: true } } },
(err, passwordPrompt) => {
var config = require(path.join(__dirname, 'config.json'));
var decryptConfigValue = function(value) {
var passphrase = passwordPrompt.password + passwordPrompt.password;
return CryptoJS.AES.decrypt(value, passphrase).toString(CryptoJS.enc.Utf8);
}
// object used to keep global reference to window objects alive
// until window is closed
var windows = new Object();
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
// for now don't verify the certificates as we know they
// simply the self-signed certificate for our server
app.on('certificate-error', (event, webContents, url, error,
certificate, callback ) => {
event.preventDefault();
callback(true);
});
// OS X specific stuff as recommended in the electron start guide
app.on('window-all-closed', () => {
// When all windows are closed, then quit
if ('darwin' !== process.platform ) {
app.quit();
}
});
function createWindow (appConfig) {
// setup based on configured options
var httpHandler = http;
var urlPrefix = "http://";
if (appConfig.tls === true) {
httpHandler = https;
urlPrefix = "https://";
}
// setup the options for the window that will be created
var windowOptions = appConfig.options;
if (windowOptions === undefined) {
windowOptions = new Object();
}
if (windowOptions.webPreferences === undefined) {
windowOptions.webPreferences = new Object();
}
if (windowOptions.webPreferences.nodeIntegration === undefined) {
// disable Node integration by default as its more secure
// to not allow the application to access the environment
windowOptions.webPreferences.nodeIntegration = false;
}
var extraHeadersString = '';
var extraHeadersObject;
if (appConfig.auth !== undefined) {
// the app must use basic authentication so set up the required
// objects need to add the authentication header to the requests
extraHeadersObject = { 'Authorization': 'Basic ' +
new Buffer(decryptConfigValue(appConfig.auth)).toString('base64') };
extraHeadersString = 'Authorization: ' + extraHeadersObject.Authorization;
}
// first make the request to get the size of the window for the app
var req = httpHandler.request({ 'hostname': appConfig.hostname,
'port': appConfig.port,
path: '/?size',
rejectUnauthorized: false,
headers: extraHeadersObject }, (res) => {
var sizeData = '';
res.on('data', (chunk) => {
sizeData = sizeData + chunk;
});
res.on('end', () => {
var sizes = JSON.parse(sizeData);
windowOptions.width = sizes.width;
windowOptions.height = sizes.height + platformHeightAdjust;
var mainWindow = new BrowserWindow(windowOptions);
windows[mainWindow] = mainWindow;
// work around what looks like a bug in respecting the config
if (windowOptions.resizable !== undefined) {
mainWindow.setResizable(windowOptions.resizable);
}
// we want minimal window without the menus
mainWindow.setMenu(null);
// ok all set up open the window now
mainWindow.loadURL(urlPrefix + appConfig.hostname + ':' +
appConfig.port + '?windowopen=y',
{ extraHeaders: extraHeadersString });
// clean up
mainWindow.on('closed', () => {
windows[mainWindow] = null;
});
app.on('ready', () => { createWindow(appConfig) });
// os specific stuff recommended by electron quickstart
app.on('activate', () => {
if (null === mainWindow) {
createWindow(appConfig);
};
});
});
});
req.end();
};
// launch all of the configured applications
for (var i = 0; i < config.apps.length; i++) {
createWindow(config.apps[i]);
}
});
Cordova – Mobile solution
 https://cordova.apache.org/
 Uses Node.js !
 Build cross platform mobile apps
– With JavaScript, HTML and CSS
 Happiness
– Better UI experience
– apk (and equivalent for ios)
– No URL to remember
– No browser bar
– No pop-ups
– No URL to remember
micro-app-cordova-launcher
 https://github.com/mhdawson/micro-app-cordova-launcher/
 Install android SDK
 npm install -g cordova
 cordova create launcher myorg "Micro App Launcher"
 cordova platform add android
 patch for untrusted domains
 update www directory which project contents
 update domain limitations
 cordova build --release android -> apk
 Sign the application -> signed apk
 Install on phone
{ "apps": [
{ "name": "home",
"hostname": "X.X.X.X",
"port": "8081"
},
{ "name": "cottage",
"hostname": "X.X.X.X",
"port": "8081"
},
{ "name": "phone",
"hostname": "X.X.X.X",
"port": "8083"
},
{ "name": "totp",
"tls": true,
"hostname": "X.X.X.X",
"port": "8082",
"auth": "XXXXXXXXXXXXXX",
"options": { "x": 2920, "y": 10, "sizable": false }
}
]
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' *;script-src * 'unsafe-eval'">
<meta id='theViewport' name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Micro-app Launcher</title>
</head>
<body onresize="doResize()">
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" />
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" src="aes.js"></script>
<script type="text/javascript" src="index.js"></script>
<table appWindow cellpadding="0" cellspacing="0">
<tr><td><table cellpadding="0" cellspacing="0" id="buttons"></table></td></tr>
<tr><td><table cellpadding="0" cellspacing="0" id="frames"></table></td></tr>
</table>
</body>
</html>
Index.html
const BUTTON_ROW_SIZE = 50;
const FRAME_ADJUST = 10;
var currentApp;
function showApp(event) {
for (var i = 0; i < event.data.config.apps.length; i++) {
if (event.data.showId !== i) {
$('#frame' + i).hide();
$('#framebutton' + i).show();
} else {
$('#frame' + i).show();
$('#framebutton' + i).hide();
currentApp = i;
}
}
}
function showNext() {
var nextApp = currentApp + 1;
if (nextApp >= config.apps.length) {
nextApp = 0;
}
showApp({ data: {config: config, showId: nextApp}});
}
function showPrevious() {
var nextApp = currentApp - 1;
if (nextApp < 0 ) {
nextApp = config.apps.length - 1;
}
showApp({ data: {config: config, showId: nextApp}});
}
var decryptConfigValue = function(value, pass) {
var passphrase = pass + pass;
return CryptoJS.AES.decrypt(value, passphrase).toString(CryptoJS.enc.Utf8);
}
Index.js
var config;
function readConfig(launchApps) {
window.resolveLocalFileSystemURL(cordova.file.applicationDirectory + "www/config.json", function(configFile) {
configFile.file(function(theFile) {
var fileReader = new FileReader();
fileReader.onloadend = function(event) {
try {
// parsing directly with JSON.parse resulted in errors, this works
config = eval("(" + event.target.result + ")");
} catch (e) {
alert('Bad configuration file:' + e.message);
throw (e);
}
launchApps();
}
fileReader.readAsText(theFile);
}, function() {
alert('Cannot read configuration file');
});
}, function(err) {
alert('Configuration file does not exist');
});
}
var authNeeded = false;
function readConfigAndAuth(launchApps) {
readConfig(function() {
// first check if there is a need to authenticate
for (var i = 0; i < config.apps.length; i++) {
if (config.apps[i].auth != undefined) {
authNeeded = true;
}
}
if (authNeeded) {
$("#buttons").html('<tr height=' + BUTTON_ROW_SIZE + 'px"><td>Password:' +
'<input id="authpassword" type="password"></input>' +
'<button id="authbutton">go</button></td></tr>');
$('#authbutton').click(launchApps);
} else {
launchApps();
}
});
}
var startHeight;
var startWidth;
var app = {
// constructor
initialize: function() {
this.bindEvents();
},
bindEvents: function() {
document.addEventListener('deviceready', this.start, false);
},
// load and run the micro-apps
start: function() {
startHeight = window.innerHeight;
startWidth = window.innerWidth;
readConfigAndAuth(function() {
try {
var pass;
if (authNeeded) {
pass = $('#authpassword').val();
}
var viewport= document.querySelector('meta[name="viewport"]');
window.resizeTo(startWidth, startHeight);
viewport.content = 'width=device-width minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0'
// create the frames for the applications
var frames = new Array();
for (var i = 0; i < config.apps.length; i++) {
frames[i] = '<table id="frame' + i + '"></table>';
}
$("#frames").hide();
$("#frames").html('<tr><td>' + frames.join('n') + '</td></tr>');
$("#frames").height(startHeight - BUTTON_ROW_SIZE - FRAME_ADJUST*2);
$("#frames").width(startWidth - FRAME_ADJUST);
$("#frames").show();
// basic setup of the frames
for (var i = 0; i < config.apps.length; i++) {
var frameId = '#frame' + i;
$(frameId).hide();
$(frameId).height(startHeight - BUTTON_ROW_SIZE - FRAME_ADJUST*2);
$(frameId).width(startWidth - FRAME_ADJUST);
// enable swipe to move through the configured apps
$(frameId).bind('swipeleft', showNext);
$(frameId).bind('swiperight', showPrevious);
}
// ok now fill in the content for the frames
var frameButtons = new Array();
for (var i = 0; i < config.apps.length; i++) {
var method = 'http://';
if (config.apps[i].tls) {
method = 'https://';
};
if (config.apps[i].auth !== undefined) {
method = method + decryptConfigValue(config.apps[i].auth, pass) + '@';
};
var frameId = '#frame' + i;
var content = '<tr><td><iframe height="100%" width="100%" src="' +
method +
config.apps[i].hostname +
':' +
config.apps[i].port +
'?windowopen=y' +
'" frameborder="0" scrolling="yes"></iframe></td></tr>';
$(frameId).html(content);
frameButtons[i] = '<td><button id="framebutton' + i + '" type="button">' + config.apps[i].name + '</button></td>';
}
// setup the buttons
$("#buttons").html('<tr height=' + BUTTON_ROW_SIZE + 'px">' + frameButtons.join('') + '</tr>');
for (var i = 0; i < config.apps.length; i++) {
$('#framebutton' + i).click({config: config, showId: i}, showApp);
}
// enable swipe to move through the configured apps
$('#buttons').bind('swipeleft', showNext);
$('#buttons').bind('swiperight', showPrevious);
showApp({ data: {config: config, showId: 0}});
} catch (e) {
alert('Failed to start micro-app-launcher ' + e.message);
throw (e);
}
});
}
};
app.initialize();
Where to deploy micro-app server ?
 To the Cloud of course
 http://www.ibm.com/cloud-computing/bluemix/
 Lots of add on services to
– Watson
– Twillio (sms)
– Database
– Any many many more….
Copyrights and Trademarks
© IBM Corporation 2016. All Rights Reserved
IBM, the IBM logo, ibm.com are trademarks or registered
trademarks of International Business Machines Corp.,
registered in many jurisdictions worldwide. Other product and
service names might be trademarks of IBM or other companies.
A current list of IBM trademarks is available on the Web at
“Copyright and trademark information” at
www.ibm.com/legal/copytrade.shtml
Node.js is an official trademark of Joyent. IBM SDK for Node.js is not formally
related to or endorsed by the official Joyent Node.js open source or
commercial project.
Java, JavaScript and all Java-based trademarks and logos are trademarks or
registered trademarks of Oracle and/or its affiliates.
Apache Cordova is an official trademark of the Apache Software Foundation
npm is a trademark of npm, Inc.

Micro app-framework

  • 1.
    Micro-apps with Node.js, browsers,phones (Cordova) and electron"
  • 2.
    About Michael Dawson Lovesthe web and building software (with Node.js!) Senior Software Developer @ IBM IBM Runtime Technologies Node.js Technical Lead Node.js collaborator and CTC member Active in LTS, build, benchmarking , api and post-mortem working groups Contact me: michael_dawson@ca.ibm.com Twitter: @mhdawson1 https://www.linkedin.com/in/michael-dawson-6051282
  • 3.
    Motivation – Devicelike GUI IoT CTI Small Apps
  • 4.
    Solution - Node.js!  Single page application(SPA)  Server written in Node.js  Presentation in Browser  Remotely Accessible  Deploy to Cloud
  • 5.
  • 6.
    This is abit better Teaser: we can do better, but that’s for later
  • 7.
    • Code availablefrom GitHub and published to npm. • https://github.com/mhdawson/micro-app-framework • Configuration • Authentication • Encryption (SSL) • Templates • Pop-ups micro-app-framework is born !
  • 8.
    micro-app-framework - components <TITLE> <PAGE_WIDTH> <PAGE_HEIGHT> Configuration •serverPort • title • scrollBars • tls • authenticate • authinfo Methods • getDefaults() • getTemplateReplacements() • startServer(server) • handleSupportingPages(request, response) Files • server.js • page.html.template • config.json • package.json
  • 9.
    <html> <head> <script src="/socket.io/socket.io.js"></script> <title><TITLE></title> </head> <body style="overflow-x:hidden;overflow-y:hidden;"> <script> varsocket = new io.connect('<URL_TYPE>://' + window.location.host); socket.on('data', function(data) { var parts = data.split(":"); var topic = parts[0]; var value = parts[1]; var targetTD = document.getElementById(topic); if (null != targetTD) { targetTD.innerHTML=value; } }) </script> <table BORDER="10" width="100%" style="font-size:25px"> <tbody> <DASHBOARD_ENTRIES> </tbody> </table> </body> </html> { "title": “Cottage Data", "serverPort": 3000, "mqttServerUrl": "<add your mqtt server here>", "dashboardEntries": [ {"name": "Inside temp", "topic": "house/temp2"}, {"name": "Outside temp", "topic": "house/lacrossTX141/20/temp"}, {"name": "Timestamp", "topic": "house/time"} ] } config.json page.html.template
  • 10.
    var fs =require('fs'); var mqtt = require('mqtt'); var socketio = require('socket.io'); const BORDERS = 55; const HEIGHT_PER_ENTRY = 34; const PAGE_WIDTH = 320; var eventSocket = null; var latestData = {}; var Server = function() { } Server.getDefaults = function() { return { 'title': 'House Data' }; } var replacements; Server.getTemplateReplacments = function() { if (replacements === undefined) { var config = Server.config; var height = BORDERS; var dashBoardEntriesHTML = new Array(); for (i = 0; i < config.dashboardEntries.length; i++) { dashBoardEntriesHTML[i] = '<tr><td>' + config.dashboardEntries[i].name + ':</td><td id="' + config.dashboardEntries[i].topic + '">pending</td></tr>'; height = height + HEIGHT_PER_ENTRY; } replacements = [{ 'key': '<TITLE>', 'value': Server.config.title }, { 'key': '<UNIQUE_WINDOW_ID>', 'value': Server.config.title }, { 'key': '<DASHBOARD_ENTRIES>', 'value': dashBoardEntriesHTML.join("") }, { 'key': '<PAGE_WIDTH>', 'value': PAGE_WIDTH }, { 'key': '<PAGE_HEIGHT>', 'value': height }]; } return replacements; } Server.startServer = function(server) { var topicsArray = new Array(); var config = Server.config; for (i = 0; i < config.dashboardEntries.length; i++) { topicsArray.push(config.dashboardEntries[i].topic); } var mqttOptions; if (Server.config.mqttServerUrl.indexOf('mqtts') > -1) { mqttOptions = { key: fs.readFileSync(path.join(__dirname, 'mqttclient', '/client.key')), cert: fs.readFileSync(path.join(__dirname, 'mqttclient', '/client.cert')), ca: fs.readFileSync(path.join(__dirname, 'mqttclient', '/ca.cert')), checkServerIdentity: function() { return undefined } } } var mqttClient = mqtt.connect(Server.config.mqttServerUrl, mqttOptions); eventSocket = socketio.listen(server); eventSocket.on('connection', function(client) { for (var key in latestData) { var value = latestData[key]; if (value.trim().indexOf(" ") === -1) { value = Math.round(value * 100) / 100; } eventSocket.to(client.id).emit('data', key + ":" + value); } }); mqttClient.on('connect',function() { for(nextTopic in topicsArray) { mqttClient.subscribe(topicsArray[nextTopic]); } }); mqttClient.on('message', function(topic, message) { var timestamp = message.toString().split(",")[0]; var parts = message.toString().split(":"); if (1 < parts.length) { var value = parts[1].trim(); latestData[topic] = value; if (value.trim().indexOf(" ") === -1) { value = Math.round(value * 100) / 100; } eventSocket.emit('data', topic + ':' + value ); } }); } if (require.main === module) { var path = require('path'); var microAppFramework = require('micro-app-framework'); microAppFramework(path.join(__dirname), Server); } server.js
  • 11.
    Good enough, createbunch of micro-apps
  • 12.
    But some thingsstill bug me  Desktop – Browser Bar – Pop-ups – Having to re-open all those windows – Having to position the windows – Remembering URL  Phone – Single browser with tabs – UI issues – Browser Bar – Pop-ups – Having to open browser/then tab – Remembering URL
  • 13.
    Electron – Desktopsolution  electron.atom.io  Build cross platform desktop apps – With JavaScript, HTML and CSS  Uses Node.js, Chromium and V8 !  Happiness – No pop-ups – No browser bar – Position on startup – No URL to remember – Binary package possible – No URL to remember
  • 14.
    micro-app-electron-launcher  https://github.com/mhdawson/micro-app-electron-launcher  npminstall micro-app-electron-launcher  vi config.json  npm start  Future: create native binary { "apps": [ { "name": "home dashboard", "hostname": "X.X.X.X", "port": "8081", "options": { "x": 3350, "y": 10, "resizable": false } }, { "name": "phone", "hostname": "X.X.X.X", "port": "8083", "options": { "x": 15, "y": 1850, "sizable": false } }, { "name": "Alert Dashboard", "hostname": "X.X.X.X", "port": "8084", "options": { "x": 3065, "y": 10, "sizable": false } }, { "name": "totp", "tls": true, "hostname": "X.X.X.X", "port": "8082", "auth": "asdkweivnaliwerld8welkasdfiuwerasdkllsdals9=", "options": { "x": 2920, "y": 10, "sizable": false } } ] }
  • 16.
    'use strict'; var http= require('http'); var https = require('https'); var os = require('os'); var util = require('util'); var path = require('path'); var CryptoJS = require('crypto-js'); var prompt = require('prompt'); // get configuration options prompt.start(); prompt.get({ properties: { password: { hidden: true } } }, (err, passwordPrompt) => { var config = require(path.join(__dirname, 'config.json')); var decryptConfigValue = function(value) { var passphrase = passwordPrompt.password + passwordPrompt.password; return CryptoJS.AES.decrypt(value, passphrase).toString(CryptoJS.enc.Utf8); } // object used to keep global reference to window objects alive // until window is closed var windows = new Object(); const electron = require('electron'); const app = electron.app; const BrowserWindow = electron.BrowserWindow; // for now don't verify the certificates as we know they // simply the self-signed certificate for our server app.on('certificate-error', (event, webContents, url, error, certificate, callback ) => { event.preventDefault(); callback(true); }); // OS X specific stuff as recommended in the electron start guide app.on('window-all-closed', () => { // When all windows are closed, then quit if ('darwin' !== process.platform ) { app.quit(); } }); function createWindow (appConfig) { // setup based on configured options var httpHandler = http; var urlPrefix = "http://"; if (appConfig.tls === true) { httpHandler = https; urlPrefix = "https://"; } // setup the options for the window that will be created var windowOptions = appConfig.options; if (windowOptions === undefined) { windowOptions = new Object(); } if (windowOptions.webPreferences === undefined) { windowOptions.webPreferences = new Object(); } if (windowOptions.webPreferences.nodeIntegration === undefined) { // disable Node integration by default as its more secure // to not allow the application to access the environment windowOptions.webPreferences.nodeIntegration = false; } var extraHeadersString = ''; var extraHeadersObject; if (appConfig.auth !== undefined) { // the app must use basic authentication so set up the required // objects need to add the authentication header to the requests extraHeadersObject = { 'Authorization': 'Basic ' + new Buffer(decryptConfigValue(appConfig.auth)).toString('base64') }; extraHeadersString = 'Authorization: ' + extraHeadersObject.Authorization; } // first make the request to get the size of the window for the app var req = httpHandler.request({ 'hostname': appConfig.hostname, 'port': appConfig.port, path: '/?size', rejectUnauthorized: false, headers: extraHeadersObject }, (res) => { var sizeData = ''; res.on('data', (chunk) => { sizeData = sizeData + chunk; });
  • 17.
    res.on('end', () =>{ var sizes = JSON.parse(sizeData); windowOptions.width = sizes.width; windowOptions.height = sizes.height + platformHeightAdjust; var mainWindow = new BrowserWindow(windowOptions); windows[mainWindow] = mainWindow; // work around what looks like a bug in respecting the config if (windowOptions.resizable !== undefined) { mainWindow.setResizable(windowOptions.resizable); } // we want minimal window without the menus mainWindow.setMenu(null); // ok all set up open the window now mainWindow.loadURL(urlPrefix + appConfig.hostname + ':' + appConfig.port + '?windowopen=y', { extraHeaders: extraHeadersString }); // clean up mainWindow.on('closed', () => { windows[mainWindow] = null; }); app.on('ready', () => { createWindow(appConfig) }); // os specific stuff recommended by electron quickstart app.on('activate', () => { if (null === mainWindow) { createWindow(appConfig); }; }); }); }); req.end(); }; // launch all of the configured applications for (var i = 0; i < config.apps.length; i++) { createWindow(config.apps[i]); } });
  • 18.
    Cordova – Mobilesolution  https://cordova.apache.org/  Uses Node.js !  Build cross platform mobile apps – With JavaScript, HTML and CSS  Happiness – Better UI experience – apk (and equivalent for ios) – No URL to remember – No browser bar – No pop-ups – No URL to remember
  • 19.
    micro-app-cordova-launcher  https://github.com/mhdawson/micro-app-cordova-launcher/  Installandroid SDK  npm install -g cordova  cordova create launcher myorg "Micro App Launcher"  cordova platform add android  patch for untrusted domains  update www directory which project contents  update domain limitations  cordova build --release android -> apk  Sign the application -> signed apk  Install on phone { "apps": [ { "name": "home", "hostname": "X.X.X.X", "port": "8081" }, { "name": "cottage", "hostname": "X.X.X.X", "port": "8081" }, { "name": "phone", "hostname": "X.X.X.X", "port": "8083" }, { "name": "totp", "tls": true, "hostname": "X.X.X.X", "port": "8082", "auth": "XXXXXXXXXXXXXX", "options": { "x": 2920, "y": 10, "sizable": false } } ] }
  • 20.
    <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Security-Policy"content="default-src 'self' *;script-src * 'unsafe-eval'"> <meta id='theViewport' name='viewport' content='width=device-width, initial-scale=1.0'> <title>Micro-app Launcher</title> </head> <body onresize="doResize()"> <link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" /> <script src="http://code.jquery.com/jquery-1.11.1.min.js"></script> <script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script> <script type="text/javascript" src="cordova.js"></script> <script type="text/javascript" src="aes.js"></script> <script type="text/javascript" src="index.js"></script> <table appWindow cellpadding="0" cellspacing="0"> <tr><td><table cellpadding="0" cellspacing="0" id="buttons"></table></td></tr> <tr><td><table cellpadding="0" cellspacing="0" id="frames"></table></td></tr> </table> </body> </html> Index.html
  • 21.
    const BUTTON_ROW_SIZE =50; const FRAME_ADJUST = 10; var currentApp; function showApp(event) { for (var i = 0; i < event.data.config.apps.length; i++) { if (event.data.showId !== i) { $('#frame' + i).hide(); $('#framebutton' + i).show(); } else { $('#frame' + i).show(); $('#framebutton' + i).hide(); currentApp = i; } } } function showNext() { var nextApp = currentApp + 1; if (nextApp >= config.apps.length) { nextApp = 0; } showApp({ data: {config: config, showId: nextApp}}); } function showPrevious() { var nextApp = currentApp - 1; if (nextApp < 0 ) { nextApp = config.apps.length - 1; } showApp({ data: {config: config, showId: nextApp}}); } var decryptConfigValue = function(value, pass) { var passphrase = pass + pass; return CryptoJS.AES.decrypt(value, passphrase).toString(CryptoJS.enc.Utf8); } Index.js
  • 22.
    var config; function readConfig(launchApps){ window.resolveLocalFileSystemURL(cordova.file.applicationDirectory + "www/config.json", function(configFile) { configFile.file(function(theFile) { var fileReader = new FileReader(); fileReader.onloadend = function(event) { try { // parsing directly with JSON.parse resulted in errors, this works config = eval("(" + event.target.result + ")"); } catch (e) { alert('Bad configuration file:' + e.message); throw (e); } launchApps(); } fileReader.readAsText(theFile); }, function() { alert('Cannot read configuration file'); }); }, function(err) { alert('Configuration file does not exist'); }); } var authNeeded = false; function readConfigAndAuth(launchApps) { readConfig(function() { // first check if there is a need to authenticate for (var i = 0; i < config.apps.length; i++) { if (config.apps[i].auth != undefined) { authNeeded = true; } } if (authNeeded) { $("#buttons").html('<tr height=' + BUTTON_ROW_SIZE + 'px"><td>Password:' + '<input id="authpassword" type="password"></input>' + '<button id="authbutton">go</button></td></tr>'); $('#authbutton').click(launchApps); } else { launchApps(); } }); }
  • 23.
    var startHeight; var startWidth; varapp = { // constructor initialize: function() { this.bindEvents(); }, bindEvents: function() { document.addEventListener('deviceready', this.start, false); }, // load and run the micro-apps start: function() { startHeight = window.innerHeight; startWidth = window.innerWidth; readConfigAndAuth(function() { try { var pass; if (authNeeded) { pass = $('#authpassword').val(); } var viewport= document.querySelector('meta[name="viewport"]'); window.resizeTo(startWidth, startHeight); viewport.content = 'width=device-width minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0' // create the frames for the applications var frames = new Array(); for (var i = 0; i < config.apps.length; i++) { frames[i] = '<table id="frame' + i + '"></table>'; } $("#frames").hide(); $("#frames").html('<tr><td>' + frames.join('n') + '</td></tr>'); $("#frames").height(startHeight - BUTTON_ROW_SIZE - FRAME_ADJUST*2); $("#frames").width(startWidth - FRAME_ADJUST); $("#frames").show(); // basic setup of the frames for (var i = 0; i < config.apps.length; i++) { var frameId = '#frame' + i; $(frameId).hide(); $(frameId).height(startHeight - BUTTON_ROW_SIZE - FRAME_ADJUST*2); $(frameId).width(startWidth - FRAME_ADJUST); // enable swipe to move through the configured apps $(frameId).bind('swipeleft', showNext); $(frameId).bind('swiperight', showPrevious); }
  • 24.
    // ok nowfill in the content for the frames var frameButtons = new Array(); for (var i = 0; i < config.apps.length; i++) { var method = 'http://'; if (config.apps[i].tls) { method = 'https://'; }; if (config.apps[i].auth !== undefined) { method = method + decryptConfigValue(config.apps[i].auth, pass) + '@'; }; var frameId = '#frame' + i; var content = '<tr><td><iframe height="100%" width="100%" src="' + method + config.apps[i].hostname + ':' + config.apps[i].port + '?windowopen=y' + '" frameborder="0" scrolling="yes"></iframe></td></tr>'; $(frameId).html(content); frameButtons[i] = '<td><button id="framebutton' + i + '" type="button">' + config.apps[i].name + '</button></td>'; } // setup the buttons $("#buttons").html('<tr height=' + BUTTON_ROW_SIZE + 'px">' + frameButtons.join('') + '</tr>'); for (var i = 0; i < config.apps.length; i++) { $('#framebutton' + i).click({config: config, showId: i}, showApp); } // enable swipe to move through the configured apps $('#buttons').bind('swipeleft', showNext); $('#buttons').bind('swiperight', showPrevious); showApp({ data: {config: config, showId: 0}}); } catch (e) { alert('Failed to start micro-app-launcher ' + e.message); throw (e); } }); } }; app.initialize();
  • 25.
    Where to deploymicro-app server ?  To the Cloud of course  http://www.ibm.com/cloud-computing/bluemix/  Lots of add on services to – Watson – Twillio (sms) – Database – Any many many more….
  • 26.
    Copyrights and Trademarks ©IBM Corporation 2016. All Rights Reserved IBM, the IBM logo, ibm.com are trademarks or registered trademarks of International Business Machines Corp., registered in many jurisdictions worldwide. Other product and service names might be trademarks of IBM or other companies. A current list of IBM trademarks is available on the Web at “Copyright and trademark information” at www.ibm.com/legal/copytrade.shtml Node.js is an official trademark of Joyent. IBM SDK for Node.js is not formally related to or endorsed by the official Joyent Node.js open source or commercial project. Java, JavaScript and all Java-based trademarks and logos are trademarks or registered trademarks of Oracle and/or its affiliates. Apache Cordova is an official trademark of the Apache Software Foundation npm is a trademark of npm, Inc.