micro app-framework - nodelive boston

26
Micro-apps with Node.js, browsers, phones (Cordova) and electron"

Upload: michael-dawson

Post on 09-Jan-2017

149 views

Category:

Technology


2 download

TRANSCRIPT

Page 1: Micro app-framework - NodeLive Boston

Micro-apps with Node.js, browsers, phones

(Cordova) and electron"

Page 2: Micro app-framework - NodeLive Boston

About Michael Dawson Loves the web and building software (with Node.js!)

Senior Software Developer @ IBMIBM Runtime Technologies Node.js Technical Lead

Node.js collaborator and CTC member

Active in LTS, build, benchmarking , apiand post-mortem working groups

Contact me:

[email protected]: @mhdawson1https://www.linkedin.com/in/michael-dawson-6051282

Page 3: Micro app-framework - NodeLive Boston

Motivation – Device like GUI

IoT CTISmall

Apps

Page 4: Micro app-framework - NodeLive Boston

Solution - Node.js !

Single page application(SPA)

Server written in Node.js

Presentation in Browser

Remotely Accessible

Deploy to Cloud

Page 5: Micro app-framework - NodeLive Boston

Well this “Ok”

Page 6: Micro app-framework - NodeLive Boston

This is a bit better

Teaser: we can do better, but that’s for later

Page 7: Micro app-framework - NodeLive Boston

• 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 !

Page 8: Micro app-framework - NodeLive Boston

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

Page 9: Micro app-framework - NodeLive Boston

<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.jsonpage.html.template

Page 10: Micro app-framework - NodeLive Boston

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

Page 11: Micro app-framework - NodeLive Boston

Good enough, create bunch of micro-apps

Page 12: Micro app-framework - NodeLive Boston

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

Page 13: Micro app-framework - NodeLive Boston

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

Page 14: Micro app-framework - NodeLive Boston

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 }

}]

}

Page 15: Micro app-framework - NodeLive Boston
Page 16: Micro app-framework - NodeLive Boston

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

});

Page 17: Micro app-framework - NodeLive Boston

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]);

}

});

Page 18: Micro app-framework - NodeLive Boston

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

Page 19: Micro app-framework - NodeLive Boston

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 }

}]

}

Page 20: Micro app-framework - NodeLive Boston

<!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

Page 21: Micro app-framework - NodeLive Boston

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

Page 22: Micro app-framework - NodeLive Boston

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();

}

});

}

Page 23: Micro app-framework - NodeLive Boston

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

}

Page 24: Micro app-framework - NodeLive Boston

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

Page 25: Micro app-framework - NodeLive Boston

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….

Page 26: Micro app-framework - NodeLive Boston

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.