AngularJS and Multi Page Applications.

I have been building a HTML and JavaScript front-end for my project Bushido for a few months now and I decided to share my thoughts on the framework I chose for it - AngularJS. It is great to see the dependency injection coming to JavaScript world alongside other features allowing to develop large, reactive applications for the browser. 

When I first looked into JavaScript, back in the beginning of the previous decade it was lacking required tools and frameworks - making it good enough technology for typical websites, but not for rich applications. It certainly was not reactive enough. I don't even want to mention all the cross-browser issues that developers and QA analysts were facing with JavaScript/HTML websites. Times changed though, and majority of these issues are gone, the JavaScript stack came a long way and great frameworks like AngularJS, Ember, React and many more emerged. This article will focus on AngularJS in a context of Single Page Application (SPA) versus Multi Page Application (MPA).

Many people consider AngularJS a Single Page Application framework, but that is a wrong assumption. AngularJS does not enforce developers to create SPAs. It makes it easy to write SPAs, hence people tend to chose this paradigm over MPA, but this may not always be the best choice - especially if you are looking into highly modular, pluggable UI architecture. This article does not aim to convince one being better than another, because every project is different, but it aims to show how I tackled an MPA development with AnguarJS.

When I approached web front-end development for Bushido, I decided to make it an MPA - as I felt this approach will offer the highest level of extensibility and testablity - also from the automated testing point of view. The main gap I faced was the fact, that in a multi page application paradigm, you need to think about the state of the application as a whole, while actually developing completely stand-alone pages, which I started to call modules. To be more precise, this gap touches the model part of the MVC - that has to be shared across the modules. In a typical SPA this problem goes obviously away - since the model is seamlessly available, in an MPA though, when user reloads the page - the model has to be rebuilt and injected to application scope in order to be glued to the module's view. Luckily, AngularJS makes it easy to persist the model on the client-side with either cookies, local storage or session storage.

Anyway, thoughts about how to put together an MVC, AngularJS, AngularUI Router, Multi Page Application paradigm and bridge the model gap evolved into an idea of a skeleton application (some call it a seed project) and this blog post.

You can find the project source code on Github: angularjs-mpa-skeleton source code

Let's start with a directory structure.

Directory Structure

Note the "modules" folder. As this multi-page application consists of 2 pages: signin and home, there are 2 corresponding folders in modules directory. In each of the those - we store files relevant to that page (module). Inside each of the modules we find a root module's view, being a standalone HTML page, controllers, views and components related to that module. It is worth mentioning that each of the modules is also a standalone AngularJS application.

The way the skeleton application works is it starts up with a signin module, and a home module which is loaded upon clicking Sign In button.

var signin = angular.module('signin', ['app', 'ui.router']);

signin.config(function($stateProvider) {

    $stateProvider
        .state('main', {
            name: 'main',
            templateUrl: "/modules/signin/main.html",
            controller: function ($scope, appModel) {
                $scope.signIn = function() {
                    setTimeout(function() {
                        console.log('Mocking a server response for user ' + $scope.userId + ' : ' + $scope.password + ' signin action');
                        appModel.save({ user: { userId: $scope.userId, password: $scope.password }});
                        window.location.href = 'home.html';
                    }, 1000);
                };
            }
        })
});

signin.controller('signinController', ['$state', '$scope', '$cookieStore', 'appConfig', 'appModel', 'appManager', function ($state, $scope, $cookieStore, appConfig, appModel, appManager) {  
    appManager.init(appConfig, $cookieStore, $scope, appModel, true);
    $state.go('main');
}]);

On line 22 we are initializing the application with a "shouldClear" flag being set to true, since we are on sign-in page and we want to reset the state of the application. The "Sign In" button click handler method signIn() implementation saves the model (line 13) and loads up a "home" page.

So let's have a look into this really simplistic home module's controller.

var home = angular.module('home', ['app', 'ui.router']);

home.config(function($stateProvider) {

    $stateProvider
        .state('main', {
            name: 'main',
            templateUrl: "/modules/home/main.html"
        })
});

home.controller('homeController', ['$state', '$scope', '$cookieStore', 'appConfig', 'appModel', 'appManager', function ($state, $scope, $cookieStore, appConfig, appModel, appManager) {  
    appManager.init(appConfig, $cookieStore, $scope, appModel, false);
    $state.go('main');
}]);

On line 13 we are instructing the application to initialize itself with "shouldClear" flag set to false, since we want to fetch the model from the persistence store and attach it to application's scope.

Now a brief look into app.js - containing an application module which is shared by all page modules.

var app = angular.module('app', ['ngCookies']);

app.config(function($cookiesProvider) {  
    $cookiesProvider.defaults.path = '/';
});

app.factory('appConfig', function () {  
    var AppConfig = function () {

    };
    AppConfig.prototype.init = function () {
        return this;
    }
    return new AppConfig();
});

app.factory('appModel', function () {  
    var AppModel = function() {

    };
    AppModel.prototype.init = function (storage) {
        this.storage = storage;
        var data = this.storage.get('data');
        if (data != null) {
            this.save(data);
        }
        return this;
    }
    AppModel.prototype.save = function (data) {
        this.data = data;
        this.storage.put('data', data);
    }
    AppModel.prototype.clear = function () {
        this.data = null;
        this.storage.remove('data');
    }
    return new AppModel();
});

app.factory('appManager', function() {

    return {
        init: function (appConfig, $cookieStore, $scope, appModel, shouldClear) {
            $scope.config = appConfig.init();
            var model = appModel.init($cookieStore);
            if (shouldClear == true) {
                model.clear();
            }
            $scope.model = model;
        }
    }
});

Interesting bit is the fact that the AppModel class gets constructed with storage param. In this project I use $cookieStore, but both $localStorage and $sessionStorage could be used as well. Usage of $cookieStore as a persistance store implies defaults.path setting for $cookiesProvider on line 4. Since all modules reside in different paths on the server, they need to share the cookie setup context - otherwise it won't be possible for them to interact with common cookie. I simply set the cookie path to "/".

The directory structure for this project implies a few tweaks to your web server settings. I use nginx - so the snippets below are nginx specific, but you could apply those basic tweaks on any server

server {  
        listen       80;
        server_name  localhost;

        if ( $request_filename ~ /index.html ) {
            rewrite ^ http://localhost/signin.html permanent;
        }

        location / {
            root   html/app;
            index  index.html;
        }

        location /signin {
            root html/app/modules/signin;
        }

        location /home {
            root html/app/modules/home;
        }
}

First one is a http rewrite for root index.html. In our modular setup - root index.html is empty and I wanted any request hitting it to be redirected to Sign In module. Remaining tweaks are just setting root paths of our modules.

That would be all. Happy coding!

Comments

comments powered by Disqus