ikerhurtado.com
You're in
Iker Hurtado's pro blog
Developer | Entrepreneur | Passionate
I'm a software engineer specialized in simulation, graphics and GPU programming. My target platforms nowadays are Linux, Android and the Web client.    

Notes on SPA Architecture (for NOMAD/Encyclopedia web client)

25 Oct 2016   |   iker hurtado  
Share on Twitter Share on Google+ Share on Facebook

Firstly, it's worthy to note that this project has some particular features that condition architectural and technical decisions:

  • Search and visualization oriented application
  • Low user data input
  • It has graphical-interactive views -3D representations included
  • Long term and well-funded project

The above, the state of technology (browsers ecosystem) and the knowledge I've gained this time led me to the next decisions related to the application architecture and assets organization:

  • Not to use an global app framework: This project doesn't fit perfectly in either mainstream apps oriented frameworks (AngularJS, Backbone.js, etc) or pure graphical-interactive oriented frameworks (Three.js, Pixi.js). So the best option is to reuse and adapt the patterns and best practices that fit the special project features.
  • Use of modules to organize and get uncoupled and maintainable code. Try to create small subsystems that are not interdependent. Dependencies make code hard to maintain and test.
  • Distinguish definition code from initialization code. Reusable components should be defined without actually being instantiated/activated.
  • Use of the Model-View pattern. (avoid the concept of Controller)

    Models represent all of the state/data in the application (no state/data stored in the DOM).

    Views observe model changes (event system), reflect the content of the models. Views implementation based in uncoupled UI components using a programmatic approach (internal templating).

  • Use of global event system. Implement a system for inter-module communications based in events to reduce the coupling (pub/sub pattern).
  • Simple routing. The application will have the ability to associate app/views states with URLs.

I'll try to follow the above guidelines as much as possible.

My two sources of knowledge on this topic are:
- the web book Single page apps in depth
- the book SPA Design and Architecture.

The best part of them is this great introduction to the subject:

1. Modern web applications: an overview - Single page apps in depth

From here, I will go deeper with the previous points.

Modules and Code organization

I will use the module pattern as first level of code organization and to get uncoupled and maintainable code. The module management systems will be CommonJS and the implementation tool Webpack.

It's worthy to distinguish file modules (as referred by the above module pattern) from app modules. These can be composed of file modules (forming a package) and code a well defined functionality (app activity).

I extract these paragraphs from the indispensable 3. Getting to maintainable - Single page apps in depth with guidelines about the app modules (called packages bellow), their organization and initialization.

Models and other reusable code (shared views/visual components) probably belong in a common package. This is the core of your application on which the rest of the application builds. Treat this like a 3rd party library in the sense that it is a separate package that you need to require() in your other modules. Try to keep the common package stateless. Other packages instantiate things based on it, but the common package doesn't have stateful code itself.

Beyond your core/common package, what are the smallest pieces that make sense? There is probably one for each "primary" activity in your application. To speed up loading your app, you want to make each activity a package that can be loaded independently after the common package has loaded (so that the initial loading time of the application does not increase as the number of packages increases). If your setup is complex, you probably want a single mechanism that takes care of calling the right initializer.

The directory structure is a more subjective issue, this can be a way to do it: Building an application out of packages - singlepageappbook.com

Definition versus Initialization

I extract a very good explanatory paragraph:

Isolate the state initialization/instantiation code in each package by moving it into one place: the index.js for that particular package (or, if there is a lot of setup, in a separate file - but in one place only). Export a single function initialize() that accepts setup parameters and sets up the whole module. This allows you to load a package without altering the global state. Each package is like a "mini-app": it should hide its details (non-reusable views, behavior and models).

...

What you should do instead is have two parts, one responsible for definition, and the other performing the initialization for this particular use case.

I'll use a code file called init.js that instances and initializes the package/module components. In addition this can set relations between components (composition, hierarchy, event handlers). This creates dependencies between the components but they remain at package level.

Views architecture and implementation

The tasks of the view layer:

  • Mapping data to HTML (aka: rendering a template)
  • Updating views in response to change events (model data changes)
  • Binding behavior to HTML via event handlers. When the user interacts with the view HTML, a way to trigger behavior (code).

The view layer implementation is expected to provide a standard mechanism or convention to perform these tasks.

I read a very interesting extract about the two possible implementation approaches:

There two general modern single page app (view layer) approaches that start from a difference of view in what is primary: markup or code.

If markup is primary, then one needs to start with a fairly intricate templating system that is capable of generating the metadata necessary to implement the functionality. You still need to translate the templating language into view objects in the background in order to display views and make sure that data is updated. This hides some of the work from the user at the cost of added complexity.

If code is primary, then we accept a bit more verbosity in exchange for a simpler overall implementation.

But isn't writing code in the view bad? No - views aren't just a string of HTML generate (that's the template). In single page apps, views have longer lifecycles and really, the initialization is just the first step in interacting with the user. A generic component that has both presentation and behavior is nicer than one that only works in a specific environment / specific global state. You can then instantiate that component with your specific data from whatever code you use to initialize your state.

The more rich and interactive the app is, the more suitable the programmatic way is.

Decisions

I'll try to build uncoupled UI components using programmatic approach because of the level of interactivity required.

I will use the ECMAScript 6 templating system (Template literals): This way I avoid adding integration complexity and external dependency.

The dependencies will be standards as possible: HTML5 tags and JavaScript browser APIs (DOM, canvas, WebGL) to do structure to the componentes. In addition, established libraries could be used (Three.js, D3.js) if appropriate. The aesthetics (style) of the component will be uncoupled (external styles via external CSSs).

Implementation details

An extract about the differences between reusable and not reusable views:

Almost every view in your app should be instantiable without depending on any other view. You should identify views that you want to reuse, and move those into a global app-specific module. If the views are not intended to be reused, then they should not be exposed outside of the activity. Reusable views should ideally be documented in an interactive catalog.

An extract about the implementation of the listening to DOM events:

Listening to DOM events is all about the lifecycle of our view. We need to make sure that we attach the DOM listeners when the element containing the view is inserted into the DOM and removed when the element is removed. In essence, this requires that we delay event registration and make sure it each handler is attached (but only once), even if the view is updated and some elements within the view are discarded (along with their event handlers).

...

The gist of the render process is that views go through a number of states: 1. Not instantiated, 2. Instantiated and rendered to buffer, 3. Attached to the DOM, 4. Destroyed. Event bindings need to keep track of the view's state to ensure that the events are bound when a DOM element exists (since the only way to bind events is to have a DOM element for it).

Some code here: Implementing event bindings

Model-backed views depends on models both at programming time and at runtime. I extract this:

In this approach, models are the starting point: you instantiate models, which are then bound to/passed to views. The view instances then attach themselves into the DOM, and render their content by passing the model data into a template. To illustrate with code:

var model = new Todo({ title: 'foo', done: false }),
view = new TodoView(model);
The idea being that you have models which are bound to views in code.

A good resource on the topic:
5. What's in a View? A look at the alternatives - Single page apps in depth

A good example showing a UI component implementation:
JavaScript Objects & Building A JavaScript Component – Part 1 - Call Me Nick (Part 2).

The model layer

Some model layer concepts:

A data source (or backend proxy / API) is a convenient code API responsible for reading data from the backend.

Models: store data, emit events when data changes, can be serialized and persisted.

Collection: contains items, emits events when items are added/removed, has a defined item order. An interesting extract about it:

If you think that views should contain their own behavior/logic, then you probably want collections that are aware of models. This is because collections contain models for the purpose of rendering; it makes sense to be able to access models (e.g. via their ID) and tailor some of the functionality for this purpose.

Cache

I start with a good definition:

A data cache is used in managing the lifecycle of models, and in saving, updating and deleting the data represented in models. Models may become outdated, they may become unused and they may be preloaded in order to make subsequent data access faster. The difference between a collection and a cache is that the cache is not in any particular order, and the cache represents all the models that the client-side code has loaded and retained.

Data cache: caches models by id, allowing for faster retrieval and preventing duplicate instances of the same model.

The cache system for this application complies with:

- Holds two types of models: models (inmutable) coming from back-end and models created on the client that don't need to be persisted.

- Doesn't need persistence management.

So, the implementation is less complex.

Global Event System and Routing

The communication between app modules (packages) will be carried out through global event system. The objects/models/views can both emit events and get subscribed to other event emitter.

The advantage of this system is decoupling, since none of the objects/models/views need to know about each other: they just know about the global event-emitter and how to handle a particular event. (Very good explanation of this in the section 6.2 of the book SPA Design and Architecture).

It needs not be the same among highly coupled components inside a app module. It can be valid to use the regular event subscription (set directly a listener) in order to keep simplicity.

The routing system (page navigation in a single-page environment) is very related to the global event system.

We want that some views are associated to an app state, and that state is represented by an URL (using the browser URL fragment ability). The global event system facilitates the changes of state on the app and it has to be coordinated with the routing system.

The implementation of an app state/view reachable by an URL will be: first, define an URL fragment path to that view, second, implement an system event that perform the transition to that state/view, and third, the path action will release the event. So, When an app component intends to invoke a new state/view it only has to set the document.location to the fragment URL of the target state/view.

Example. This line of code defines the URL fragment path and associates it to the global event release (of course, the hard part -the event listener implementation- doesn't appear):

Router.add('search', search => PubSub.publish('show-search', search));

The next sections give some ideas about the Global Event System and the routing system implementation:

The pub/sub pattern

I extract some paragraphs from the section 6.2 of the book SPA Design and Architecture:

Modules encapsulate our logic and provide individual units of work. Although this helps decouple and privatize our code, we still need a way for modules to communicate with each other. (...) a design pattern called pub/sub, which allows one module to broadcast messages to other modules.

(...) pub/sub is a pattern that helps keep the modules of your code base decoupled. It can be a powerful and flexible tool, but it’s not without its disadvantages.

Pros: It promotes a loose coupling of your modules through notifications posted to a message broker, instead of having to maintain direct dependencies. As with using APIs, pub/sub is easy to implement. Notification topical messages can be broadcast to many subscribers at once. Different parts of the application can elect whether to pay attention to published messages.

Cons: Notifications flow in only one direction. No acknowledgement or response is sent back to the publisher. Topics are simple text strings. You must rely on a naming convention to ensure that they’re routed to the correct recipient. It’s harder to track the flow of messages through the system while debugging. In your code, you must ensure that the subscriber is available and listening before the notification is published, or the topic won’t be heard.

Routing

It's the feature that allows the page navigation in a single-page environment.

I extract some paragraphs from the section 4.2 of the book SPA Design and Architecture:

(...) when I talk about navigation, I’m talking about managing the state of the SPA ’s views, data, and business transactions as the user navigates. The router manages the application’s state by assuming control of the browser’s navigation, allowing developers to directly map changes in the URL to client-side functionality. (...) Notice that at no time does the router interact with the server. All routing is done in the browser.

The fragment identifier method will be used to enable the router to provide server-less navigation. The fragment identifier is any arbitrary string of text at the end of the URL that appears after the hash identifier symbol (#). Some paragraphs explaining this mechanism:

Browsers treat this part differently from the rest of the URL . When a new fragment identifier is added to the URL , the browser doesn’t attempt to interact with the server. The addition does, however, become a new entry in the browser’s history. This is important because all entries in the browser’s history, even those generated from the fragment identifier, can be navigated to via normal means, such as the address bar and the navigation buttons. This action also adds a new entry in the browser’s history.

The 'location' object contains an API that allows you to access the browser’s URL information. In an SPA , routers take advantage of the location object to programmatically gain access to the current URL , including the fragment identifier. The router does this to listen for changes in the fragment identifier portion of the URL , via the window’s 'onhashchange' event.

POST A COMMENT: