eeDepart Architecture overview

Core libraries

  • jQuery
  • Underscore
  • Backbone
  • Marionette
  • Require

Core custom libraries

  • Backbone.Components
  • Backbone.EventsHub
  • Backbone.Services
  • Backbone.Resources
  • Backbone.AjaxCommands

Repository structure

Core principles

  • Modularity
  • Extensibility
  • Scalability
  • Separation of concerns
  • Code re-use
  • Code quality increase

Modularity

Achieved by creating Modules and Components. Modules and Components are self contained parts of functionality that don't depend on anything else.

 

Modules encapsulate a single route - every route is added a module, and withing that module logic is implemented by utilizing Components. 

Extensibility

Modules and Components in eeDepart are completely override-able. They can either be slightly modified in the custom project or behavior can be completely different ( like for SWR )

 

Extensibility is achieved by using require.js and a plugin we modified for this purpose. Exact details are in the documentation

 

Override process is very straightforward and when done once becomes very familiar.

 

Scalability

Scalability in this sense means that the project can have as many or as little modules and components as necessary. Since every module and every component are completely separate, and usually don't depend on each other, it's perfectly safe to add or remove modules from the application.

 

Separation of concerns

Along Modules and Components we have introduced specialized parts of application that serve specific purposes:

 

* Services - influenced by Anguar.js services

* EventsHub - Flux like dispatcher but with more control

* AjaxCommands - utility to ease preparation of ajax request

* Resources - separate each resource in its own container

 

Code quality

Since eeDepart serves as a CORE for all other Check-ins that are build on this architecture we go to great lengths that only necessary changes are added and we try to improve the code quality overall. We do this by:

* New branching mechanism - that allows proper versioning

* (Manually) taking care that only smallest set of common functionality gets into the core

* Using code style tools: jshint and jscs 

* Writing Unit Tests ( inprogress )

* Frequent reviews of our processes

 

How does it work

Documentation covers the process pretty well but just to summarize:

 

1. We load all of the dependencies

2. Using pegasus.js we fetch config and resource data

3. When config is ready we start the application

4. On application start ALL modules and standalone components are instantiated

5. Modules set up route listeners and wait to be activated

6. Once a route is reached the module starts and calls the controller to prepare the screen data and render.

What we solved:

* Routing via modules - it's trivially easy to add a new route / module to the app

* Extendible by default

* Modules / Components system - as long as the same principles are followed

* Screen rendering process - already set up for mustache and Marionette

* Local storage handling by default

* Error handling

* Loading screen handling

* Component shareable across projects by default - again as long as principles are followed.

 

 RetrieveApp.js - set up route action

        var routerAPI = {
            loadRoute: function() {
                var route = eeDepart.getCurrentRoute();

                Backbone.EventsHub.useHub( 'stateComponentHub' )
                    .trigger( 'setActiveScreenState', route, {});
            }
        };    
...
...
         /**
         * Add the route bindings for this module and pass the callback method.
         */
        eeDepart.addRoute( routerAPI, routes );

 RetrieveApp.js - add route callbacks

        function appStateChanged( stateModel, actionData ) {
            if ( stateModel.get( 'currentPage' ) === ee.Constants.RouteEvents.RETRIEVE ||
                stateModel.get( 'currentPage' ) === '' ) {
                API.loadRetrieve( actionData );
            }
        }
...
...
        /**
         * Listen to a 'route state' change. When a route is changed in the
         * browser, or when a new state is triggered programatically listen
         * for the new state change and react accordingly.
         */
        Backbone.EventsHub.useHub( 'stateComponentHub' )
            .on( 'eeDepart:stateContainer:stateChanged', appStateChanged, this );

        /**
         * Add the route bindings for this module and pass the callback method.
         */
        

 RetrieveApp.js - when route is reached fire up the screen loading process

            loadRetrieve: function( data ) {
                var that = this;
                require([ 'replace!ROOT_MODULE/retrieve/main/retrieveController:retrieveController' ],

                    function( RetrieveController ) {
                        eeDepart.navigate( ee.Constants.RouteEvents.RETRIEVE, data );

                        if ( !that.controller ) {
                            that.controller = new RetrieveController();
                        }

                        that.controller.loadContent( data );
                    }
                );
            }

http://eedepart/app/ck.fly#ck_retrieve

 RetrieveController.js - Instantiate the model and the Layout view.

        loadContent       : function( data ) {

            var isDeeplink = Backbone.EventsHub.useHub( 'stateComponentHub' ).request( 'getStateValue', 'isDeeplink' );

            this.model = new RetrieveModel( data );

            var dataLoadedCallback = _.bind(function() {
                var LayoutView = new RetrieveLayoutView({
                    model: this.model
                });

                eeDepart.loadContent( LayoutView, isDeeplink );

                // Set deeplink as false, we no longer need that info
                Backbone.EventsHub.useHub( 'stateComponentHub' ).trigger( 'setStateValue', 'isDeeplink', false );
            }, this );

            /**
             * Promise - based approach for loading data. A model will resolve a
             * promise when the data is ready and the callback will fire rendering
             * the Views.
             */
            this.model.loadAndPrepareData().then( dataLoadedCallback );
        }

Core utilities - Services

define([ 'backboneServices' ],

    function() {
        'use strict';

        return Backbone.Service.extend({

            initialize       : function() {
                this.config = new Backbone.Model();
            },

            services         : {
                'setConfig': 'setConfigCallback', // Set the initial config we
                                                   // get from PHP

                'getConfig': 'getConfigCallback', // Get the whole config object

                'getParam': 'getParamCallback'   // Get one config param
            },

            setConfigCallback: function( configData ) {
                this.config.set( configData );
            },

            getConfigCallback: function() {
                return this.config;
            },

            getParamCallback : function( param ) {
                return this.config.get( param );
            }
        });
    }
);

Backbone.serviceFactory( 'configService', ConfigService); // Register
var myService = Backbone.serviceProvider( 'logtextService' ); // Fetch 

/** 
 * myService now has all above methods available
 */

Resources

define([ 'backbone', 'backboneResources' ],

    function( Backbone ) {
        'use strict';

        var airportsResource = Backbone.Resource.extend({

            fetch: function() {

                this.deferred = $.Deferred();

                if ( !this.done ) {

                    _.defer(function( context ) {
                        var airports = Backbone.serviceProvider( 'configService' )
                                                 .getParam( 'airportList' );

                        context.resource.set( airports );
                        context.done = true;
                        context.deferred.resolve();
                    }, this );
                } else {
                    this.deferred.resolve();
                }

                return this.deferred;
            },

            getAirportCodesWithNames: function() {
                return _.object( this.resource.map(function( model ) {
                    return [ model.get( 'airport_code' ), model.get( 'airport_name' ) ];
                }) );
            }
        });

        Backbone.resourceFactory( 'airportsResource', airportsResource ); // Register
    }
);

Backbone.resourceProvider( 'airportsResource' ).fetch().then( callback ) // Use

Behaviors

define([ 'marionette' ],

    function( Marionette ) {

        'use strict';

        return Marionette.Behavior.extend({

            events       : {
                'change select'           : 'saveByValue',
                'change [type="text"]'    : 'saveByValue',
                'click  [type="radio"]'   : 'saveByValue',
                'click  [type="checkbox"]': 'saveByChecked'
            },

            saveByValue  : function( e ) {

                var canSave = true;

                if ( !_.isEmpty( this.options.allowedTargets ) &&
                    !_.contains( this.options.allowedTargets, e.target.name ) ) {

                    canSave = false;
                }

                if ( canSave ) {
                    this.view.model.set( e.target.name, e.target.value );
                }
            },

            saveByChecked: function( e ) {
                this.view.model.set( e.target.name, e.target.checked );
            }
        });

    });

Components

define([ 'components/loadingScreen/loadingScreenView',
         'components/loadingScreen/loadingScreenModel',
         'text!components/loadingScreen/loadingScreenTemplate.html'
       ],

function( View, Model ) {
    'use strict';
    var loadingScreen = Backbone.Components.Component.extend({
        name             : 'loadingScreenComponent',
        view             : View,
        model            : Model,
        template         : 'text!components/loadingScreen/loadingScreenTemplate.html',

        initialize       : function() {
            var hub = Backbone.EventsHub.registerHub( 'loadingScreen' );
            // we initially set that loading screen is not displayed
            this.displayed = false;
            // and we tell this component what events to listen to
            hub.on( 'loadingScreen:showLoader',  this.showLoadingScreen, this );
            hub.on( 'loadingScreen:hideLoader',  this.hideLoadingScreen, this );

            Backbone.EventsHub.useHub( 'eeDepart' ).on( 'navigate', this.showLoadingScreen, this );
            Backbone.EventsHub.useHub( 'eeDepart' ).on( 'contentLoaded', this.hideLoadingScreen, this );
            Backbone.EventsHub.useHub( 'eeDepart' ).on( 'componentsLoaded', this.componentsLoaded, this );
        },
        componentsLoaded : function() {
            this.create().render();
        },
        showLoadingScreen: function() {
            //...
        },
        hideLoadingScreen: function() {
            // ...
        }
    });

    /**
     * Initialize the component immediately
     */
    new loadingScreen();
});

Two sides of one coin

While here we're mentioning only client side features rest assured that the changes and future plans for Server side processing of eeDepart are equally important for the project long term success.

 

eeDepart Architecture Overview

By jurza

eeDepart Architecture Overview

  • 896