I've spend some time on prototyping a modular SPA hub that meets the following core requirements:

  • from users perspective - it all looks like one app (seamless navigation, same looks, shared layout, etc.)
  • it consists of independently developed (by different people) modules, BUT these modules may use other modules' presentation elements (on some established level)
  • modules are supposed to be automatically testable (incl. presentation layer)
  • junction points between modules (navigation / integration points) have to be designed for failure -> if a module calls another module that is not present (not deployed / or just doesn't work) it has to gracefully report an error
  • navigation between modules will occur not only on the menu level -> there may be some x-module links in any view

Let's face it - presentation layer-level integration in JavaScript sucks. Or even: presentation layer-level integration sucks, regardless of platform / language. The dependencies you're just about to create are usually nothing more than hard-coded links: neither safe, nor elegant (not mentioning being designed for failure). But unfortunately, sometimes it's a necessity. What can be done about that then?

Option #1 - Intent Model

My first idea was ... quite complex, I admit ;) This is something I've seen a long time ago in Oracle Forms-based app, but it's pretty much the same approach you can observe in modern Android's Intent Model.

  1. Modules publish their features in the shared registry while registering themselves initially - each feature (shared something) consists of a key & routing info (everything that's needed to call / use this something).

  2. The list of feature keys is maintained independently and known to all modules - if a module wants to use some external feature, it has to know its key.

  3. After all modules register themselves (publish their keys in registry) they verify their demands (for features) - by asking the registry whether external features they need are present (because other modules have registered them).

  4. If module's demand for particular feature ...

    • ... is met (key has been registered by someone), routing information is used to reach the feature in run-time
    • ... is not met (key is missing), integration / navigation is blocked / sealed properly - so users don't get nasty error messages.

To make this option even more sexy, you can make routing information really flexible:

  • if caller provides all the required parameters, he just uses a function / follows the route
  • but if called does provide all (or some) input, a suitable pop-up window is generated, so user can fill the gap in runtime

That's basically how it works in Android and it could really work in SPA as well. Yes, it's a bit complex, it requires a common protocol to share routing information, some kind of agreement about what can be shared (and how will integration look alike) plus the list of available keys has to be maintained somehow. But the gain is awesome - although the integration has been put into somehow limiting regime, all the integration is under full control.

Option #2 - Generate JavaScript Routes

But there's another, simpler scenario - implemented already in Play Framework 2.0 - generating JavaScript routes on the server-side, so client-side code refers to JavaScript constructs instead of strings. Here's how it works:

  • First, you publish the JSON content with all the routes (may be parameterized) you want to publish:
public static Result jsRoutes()  
{
    response().setContentType("text/javascript");
    return ok(Routes.javascriptRouter("jsRoutes",
                                      controllers.routes.javascript.Ctrl.op1(),
                                      controllers.routes.javascript.Ctrl.op2(),
                                      controllers.routes.javascript.Ctrl.op3()));
}
  • Then, add a proper route in Play (to the published route info):
GET /jsRoutes  controllers.Application.jsRoutes  
  • Next, you include your routes in the view. Here's the simplest way to do that:
<script type="text/javascript" src="@routes.Application.jsRoutes()"></script>  
  • And now you can use it freely (remember, operation can have parameters as well!) in JavaScript, like that (Ajax call):
jsRoutes.controllers.Ctrl.op1().ajax({  
            success : function(data) {
                // so something here
            },
            error : function(err) {
                // deal with error here             
            }
        });

Obviously, to make it work in multi-module scenario, you need to find a way to split compilation in two phases, so you'll be able to put routing information in first tier of artifacts (shared by all the modules).

This model may be a bit less generic, but OTOH, 75% of all the development is already done, so you can get your results faster.

Summary

UI-level integration is often considered an anti-pattern (especially in web apps), but there are cases where it's pretty much unavoidable. And sadly that was my case. Fortunately it seems that there are at least few ways to do it in a maintainable way:

  • with either build- or run-time validation of routes
  • enhanced with a possibility to cover the integration failures (due to errors or unavailability of service)
  • generic enough to use it in truly modular application
  • while keeping it possible to have whole solution automatically testable