Ptoceti's SPA – From GWT to a JS stack – Deploy on ...



Ptoceti's SPA – From GWT to a JS stack – Deploy on ...

0 0


PtocetiSpaPres

Ptoceti Single Page Application presentation

On Github lathil / PtocetiSpaPres

Ptoceti's SPA

From GWT to a JS stack

https://github.com/lathil/Ptoceti

What is it ?

  • a Obix server
  • a Single Page Application UI

Runs on ...

  • an OSGI R4 container (Felix, ...)
  • a JRE 7
  • a HTML5 browser

Deploy on ...

my powermac G4 ... ?

Deploy on ...

probably more something like this ...

What does it do ?

Open Building Infomation eXchange

  • an OASIS standard since 2006
  • IBM provide an implementation in Tivoli Power management suite
  • in Tridium Niagara AX framwork ...
  • Dubai airport, Olympic village & district at Beijing...
A simple Rest servicevery open

Obix: a web service ...

.. inspired from Rest and HATEOAS

  • Small object model that can be combined to describe the domain
  • Resources identified by URL
  • Links between resources
  • No fixed url, resources are navigated thought links and hypermedia

Root entry point: the Lobby

							
								<obj href="http://localhost:8080/obix/rest/" is="obix:Lobby">
									<op name="batch" href="/batch/" is="obix:Op" in="obix:BatchIn" out="obix:BatchOut"></op>
									<ref name="about" href="/about/" is="obix:About"></ref>
									<ref name="watchService" href="/watchservice/" is="obix:WatchService"></ref>
								</obj>
							
						

/about/ leads to :

							
								<obj name="about" href="http://localhost:8080/obix/rest/about/" status="ok" is="obix:About">
									<abstime name="serverBootTime" val="2014-04-07T19:31:30+02:00"></abstime>
									<abstime name="serverTime" val="2014-04-07T21:58:42+02:00"></abstime>
									<str name="obixVersion" val=""></str>
									<str name="productName" val="Obix-Lib"></str>
									<uri name="productUrl" val="www.ptoceti.com"></uri>
									<str name="productVersion" val="1.0.0.SNAPSHOT"></str>
									<str name="vendorName" val="ptoceti"></str>
									<uri name="vendorUrl" val="www.ptoceti.com"></uri>
									<str name="serverName" val="org.ops4j.pax.web.pax-web-jetty-bundle"></str>
								</obj>
							
						

Ptoceti's SPA stack:

Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.

Backbone's approach:

There's more than one way do do it

  • Minimalistic by design
  • Does not force a top level pattern for the application architecture
  • MVC, MVP, MVVM, .. YOU choose your fix

Backbone provides these objects

  • Backbone.Model
  • Backbone.Collection
  • Backbone.View
  • Backbone.Event
  • Backbone.Router
  • Backbone.History

For every thing else, there's ...

Github

Using Backbone: challenge 1

No support for parsing nested models

Backbone provides only for Models or Collection of Models structures.

override model : function(attrs, option) in Backbone.Collection and override parse() & implements initialise() in Backbone.Collection

Why do we need nested models ?

Obix responses are structured by Entities :

							
								<obj href="http://localhost:8080/obix/rest/" is="obix:Lobby">
									<op name="batch" href="/batch/" is="obix:Op" in="obix:BatchIn" out="obix:BatchOut">
									<ref name="about" href="/about/" is="obix:About">
									<ref name="watchService" href="/watchservice/" is="obix:WatchService">
								</ref></ref></op></obj>
							
						

We map each entity to a model, that allows us to define and use helper functions :

							
								var aboutRef = this.lobby.getAbout().getHref());
							
						

For each Model of known type, re-assigne proper Obix Model

							
								var Objs = Backbone.Collection.extend({
									...
								    model : function(attrs, option) {
								        switch (attrs.type) {
								             case "abstime":
								                 return new Abstime(attrs, option);
								             ...
								        }
								    }
									...
								})
							
						

By default parse() convert response to a hash of attributes, here, we convert it into a Model when appropriate.

							
								var Obj = Backbone.Model.extend({
								    ...
								    parse : function(response, options) {
								       if (response !== null) {
								            ...
								            if (response.childrens !== null)
								               response.childrens = new Objs(response.childrens, {
								               urlRoot : _.result(this, 'urlRoot'),
								               parent : this
								            });
								            ...
								        }
								        return response;
								    },
								    ...
								})
							
						

Using Backbone: challenge 2

No support for nested models event

Events are not generated on nested attributes

uses backbone-deep-model or override set() in Backbone.Model

last solution chosen because of the nested nature of Obix models

For each attribute property that is an Model, invoke set() to generate the events.

							
								var Obj = Backbone.Model.extend({
									set: function(key, val, options) {
										...
										 // For each `set` attribute, update or delete the current value.
									     for (attr in attrs) {
									        val = attrs[attr];
									        
									        var type = Object.prototype.toString.call(current[attr]);
									        if( current[attr] instanceof Objs && val instanceof Objs){
									            current[attr].set(val.models,options);
									        } else {
										       ...
									        }
									     }
									     ...
									}
								})
							
						

Using Backbone: challenge 3

Two-way binding

No binding mechanism for generated events betwen DOM element and model

Backbone let it to you to install your handlers Many plugins availables from Epoxy.js to Bacbone.ModelBinder to ease the pain

On model initialisation, create a new binder.

							
							initialize : function() {
							    ...
							    // initialize Backbone.ModelBinder for dual binding
							    this.modelbinder = new ModelBinder();
							    ...
							}
							
						

once the view is rendered, bind models attributes to dom elements

							
							onRender : function() {
						        this.modelbinder.bind(this.model, this.el, {
							        count: {selector: '[name=count]'},
							    });
							    ...
							},
							
						

Using Backbone: challenge 4

Nested Views

View management is very light in Backbone

To avoid re-creating the wheel, use a third part plugin such as Chaplin, or ...

Enter ...

Marionette

Marionnette is ...

An library on top of Backbone that provides bricks to build large scale web app.

  • View component: ItemView, CollectionView, CompositeView, Layout
  • View management: Region, Renderer, TemplateCache
  • Application structure: Application, Controller, AppRouter, EventAgregator, Command, ...

Nested Views becomes trivial

Use Marionnette's region for subviews placement

							
								regions : {
								    header : '#header',
								    content : '#content',
								    footer : '#footer'
								},
							
						

Insert subview when ready

							
								layout.header.show(new HeaderView({
								    model : about
								}));
								
								layout.footer.show(new FooterView({
								    model : about
								}));
							
						

Templating handled by ..

Support expressions, helpers, model attribute mappings, ...

							
								<div class="row">
									<div class="col-md-8 col-sm-8 col-xs-10">
										<p><span name="displayName" {{#if="" "contenteditable"="" }}contenteditable="true" {{="" if}}="">{{displayName}}</span></p>
									</div>
									<div class="col-md-3 col-sm-3 hidden-xs">
									</div>
									<div class="col-md-1 col-sm-1 col-xs-2">
										<p><a data-toggle="collapse" data-target="#monitoredDetails{{elemId}}"><span name="detailsCollapseControl" class="glyphicon glyphicon-chevron-down h3"></span></a></p>
									</div>
								</div>
							
						
  • {{#if ... }} {{/if}} -> expression with if helper
  • {{displayname}} -> mapping of attached model's 'displayName' attribute.

Integration with Marionnette

  • Marrionette provides a template cache for compiled templates
  • Templates are compiled on demand on the browser and stored in the cache
  • Facilitate development, as just need to do refresh on the browser to get a new compiled template

Give your application structure ...

Event driven architecture

Reactive programming

Favor loose coupling between the application components

  • Use a event mediator to handle application messages
  • use Publish / Subscribe event communication
  • One main controller for booting up and application level control
  • Lighter messaging using Courier between nested views
  • Ease scalability

Example

In header subview, generate event to go to history display ...

							
								var HeaderView = Marionette.ItemView.extend({
								...
								    historyMenuClicked : function() {
								        this.ui.lobbyMenu.parent().removeClass('active');
								        this.ui.watchesMenu.parent().removeClass('active');
								        this.ui.historyMenu.parent().addClass('active');
								        ventAggr.trigger("app:goToHistory");
								...
								});
							
						
headerview.js is controller of the header, listen to history tab being click. We need to 'activate' through css the menu, Bootstrap failing to do this in our setup.

... in main app, event is caught and we ask the controller to do something in response

							
								var app = new Marionette.Application();
								
								app.on('start', function() {
								    ...
								    ventAggr.on("app:goToHistory", function() {
								        if( Backbone.history.fragment != "history" ) {
								            Backbone.history.navigate("history", {});
								            app.controller.goToHistory();
								        }
								    });
								    ...
								});
	
							
						

Modular Application

All components (views, controllers, ..) are AMD modules

Require.js used to link them at runtime and optimization phase

  • Application loaded peace-meal
  • Most important first - user don't have to wait too long
  • Resilience

Why AMD ?

I've got big walls ..

... wi-fi don't go well throught them

Modular Application

Example: module declaration

							
								define(['backbone', 'marionette', 'underscore', 'eventaggr', 'views/obixview','models/obix', 'models/watcheslocal', 'models/watches'], function(
		Backbone, Marionette, _, ventAggr, ObixView, Obix, WatchesLocal, Watches) {

								    var AppController = Marionette.Controller.extend({
								    ...
								    });

								    return AppController;
								});
							
						

Modular Application

Example: loading a module asynchronously

							
								goToHistory : function() {
								    var controller = this;
								    require(['views/historyview'], function(HistoryView) {
								        controller.obixView.content.show(new HistoryView());
								    });
								}
							
						

AMD loading

No optimisation

  • each module loaded separatly
  • perfect for dev phase

AMD loading

Redefine your modules contents

Enter your modules definitions in require.js config inside your grunt.js file (if your are using Grunt)

							
								requirejs: {
								    compile_build: {
								        options: {
								            optimize:"uglify",
								            optimizeCss: "standard",
								            modules : [
								                {name: 'main', include: ["i18n", "text",'courier','moment','mediaenquire','jquery.enterkeyevent']},
								                {name:'views/headerview', exclude: [...]},
								                {name:'views/footerview', exclude: [...]},
								                ...
								            ]
								        }
								    }
								}
							
						

AMD loading

with optimisation

  • 1st module: common libs + core
  • each subview as own module
  • modules are loaded as needed

Make the application responsive

  • Bootstrap 3 handle responsive grid and css
  • Modernizr detect device capabilities ( svg,mq,websocket...) and provides polyfil in some case
  • Javascript media queries ( enquire.js) gives the application opportunites to present alternatives components

Example: using Modernizer to test for svg support

(we use d3.js to display graph - not supported on gingerbread default browser)
							
								if( Modernizr.svg) {
								    var region = this.historyRegion;
								    require(['views/historyrollupchartview'], function(HistoryRollupChartView){
								        ...
								    });
								}
							
						

This avoid sending d3.js + xcharts.js + xcharts.css to the browser if it can't use it.

Project building

we use:

  • Bower for javacript dependencies
  • Grunt for scheduling building of ihm project
  • Maven for builing the osgi Bundle

Project layout

  • in bower_component the dependencides loaded by bower
  • in js/app the application code
  • in js/lib the cleaned up dependencies
  • in js_optimized, the application uglified and modularized by require.js

Maven launch Grunt, which in turn launch Bower, Modernizr an then RequireJs

Build in a CI environment

We make use of a Jenkins platform at

Simply need to add and configure the NodeJS plugin in 'Administer Jenkins'

Develoment, debugging ...

  • Locally Osgi server is started with pax runner
  • Embedded Jetty is configured to serve dev portion of js application ( build is minified )
  • Then we use Chrome dev tools to map application to source folder. Development and debugging made in Chrome

Develoment, debugging on remote

Server is deployed on target using

Application js served with npm serve on developemnt machine, make access through CORS to remote server.

Summary

Positive sides of a Backbone stack:

  • Communauty around it still vibrant
  • Plenty of choices for plugins
  • Lightweight library does not get into your way -> very flexible
  • Force you to build your application structure
  • Integrate well with an AMD structure
  • Imperative style vs Declarative (depends where you stand)

Summary

Not so positive sides of a Backbone stack:

  • Needs to select your plugins depending on your need ( or use yeoman for a standard stack)
  • Backbone force 1 Model per View structure; ( With Angular you can have N Controller/Model per View)
  • Model.toJson() used for serialisation to view and to server ( via REST ). Difficulties to display extrat attributes
  • No separation between Models and provider. Would be nice to have disconnected Models
  • Backbone is not RAD ( if you are in this kind of thing).

Summary

Compare to Angular ?