Universal Javascript in Production - Server/Client Rendering

Javascript Aug 16, 2016

This is a transcript of a talk I co-presented for Intuit's Front-end Engineering Conference. This is part two of a two part post on Universal Javascript in Production.

In my previous post, I discussed the journey of QuickBooks Financing and why we decided to switch to Node.js as the backend to our front-end. In this post, I am now going to dive into how we are using Universal Javascript and Node.js to enable server and client side rendering.

Server-side and client-side rendering

One of the fundamental examples of Universal Javascript involves shared templates to enable client and server side rendering. But what exactly does that mean? If you are used to working on a more traditional web stack, this concept may be somewhat harder to visualize because you may be used to specifically implementing one or the other. So let's look at some examples.

Old world

In the “old world”, for your traditional server-side rendered pages, you most likely have your own templating engine that is compatible with your current web framework. So for example, in the Java/Spring world, your typical approach may be to define all of your application routes for your web page, where each route would render a specific JSP/Thymeleaf view. Then when the user goes to a different page of the app, you would go to another application route and render that route’s appropriate page. Nothing new here. Some obvious downfalls of this experience, in recent times, is that it’s not buttery smooth and it doesn’t provide a “native-like” desktop or mobile experience.

In the “old world”, you have your traditional server-side rendered pages. You most likely have your own templating engine that is compatible with your current web framework. So for example, in the Java world, your typical approach may be to define all of your application routes for your web page, where each route would render a specific JSP or Thymeleaf view. Then when the user goes to a different page of the app, you would go to another application route and render that route’s appropriate page. You may even load some dynamic content through AJAX.

Old World

Nothing new here. An obvious downfall of this experience is that it’s not buttery smooth and it doesn’t provide a “native-like” desktop or mobile experience.

Modern world

In the new “modern world” of S.P.A’s, when using a more traditional web stack, you will most likely only create a core set of application routes and views. You will have your core server side views that will load all of your Javascript code and client-side templates, then let your UI framework, like Angular.js, take over on the browser. These templates will usually have completely different syntax/semantics and your UI framework would handle everything from routing, view management, etc.

Modern World

This alleviates the cons of the server-side render approach, but instead you get slower load times and get the well-known “blank white screen” on initial page load. In addition, you would also have to learn how to hybrid the syntax of the two templating languages and make sure the semantics can cross-pollinate.

Enter the new "Universal Javascript" world

However, in the Universal Javascript world, you can now get rid of the cons of both issues by supporting both server and client side rendering while using the same code!
In QuickBooks Financing, what we do is on any request to a particular route, we render that page on the server, serve it on the browser, and then afterwards let the browser take over the rest of the routing and page rendering. If the user refreshes the page at any point of the experience, we will again render that specific page on the server, and then on the handoff to the browser, let the browser take over again.

New World
We are able to accomplish this because Node.js enables us to leverage shareable templates and Universal Javascript enables us to share our javascript expressions/helpers.

Express + Backbone.js + Handlebars

So how did we implement this? There were three things we needed to make this work:

  1. We needed a library/framework to support server-side rendering
  2. We needed a library/framework to support client-side rendering
  3. We needed a template engine that would work for both.

For the first item, we are leveraging a popular web framework called Express to handle our server side rendering on Node.js. One of the major features of Express is that it allows us to define any templating engine we want. We are no longer constrained to the templating engines of whatever web stack we choose. For Java, we had JSP/Thymeleaf; for .NET, we had Razor; etc.

For the second item, when selecting a UI library/framework that could support client-side rendering, we needed to make sure it was IE8 compatible. As a result, our UI library of choice was Backbone.js. The Backbone library also enabled us to define our templating engine of choice. Because both our Node backend and our UI framework allows us to define any templating language we want, it was a perfect, symbiotic relationship.

For the third and last item, we needed a template engine that would work well for both the server and client. As you can tell from our two library/framework choices so far, we are a fan of un-opinionated libraries. So it was a fairly simple choice. We wanted to continue this pattern, thus we decided on Handlebars.

File and code structure

Now that we’ve selected our server side web framework, our client side library, and our templating engine, how do we manage our code? How do we share our templates between the two environments to enable server and client rendering?

Folder Structure

What we did was create a dedicated app-server folder that contains everything that needs to be handled by our Node server, like HTTP routing, server side rendering, etc.

We also have a dedicated static folder that contains all the assets required by the browser, e.g. UI framework code, stylesheets, etc. So when it’s time to go live in production, we simply deploy app-server to our Node instances and deploy our client folder to our CDN or our static asset directory on NGINX.

The server is unaware of what exists on the client, and the client is unaware of what exists on the server. We want to continue a good design practice of separation of concerns. So where do we put our shared templates?

Common Directory

We re-mediate this issue by creating a “common” directory that contains all the files and assets that need to be shared between the two. Both servers have a reference to this folder and we simply deploy it on both our Node server and our CDN.

How do we implement our view rendering?

Now that we've established that our shared templates exist within the common directory, how are we using it to implement our view rendering? Let's look at an example.

Shared Template

We'll first establish a shared template. Let's say we had a sign in template, that exists within the /common/shared/account directory.

<div class="sign-in-container">
    <div class="header">
        <h1><img src="{{imagesPath}}/icons/lock_harmony.png"/>SECURE SIGN IN</h1>
        <ul>
            <li>Sign in and we can retrieve most of what we need to know about your business</li>
            <li>Return to view or accept updated offers at any time</li>
        </ul>
        <h4>Please sign in to your Intuit Account to begin.</h4>
    </div>
    <div class="sign-in-widget-container">
        <div class="sign-in-widget-header">
            <img src="{{imagesPath}}/logos/qbf_logo_harmony_small.png"/>
        </div>
        <div id="intuit-sign-in-widget"></div>
    </div>
    <div class="footer">
        <a href="/legal" target="_blank" trackingaction="sign_in_legal">Terms of Service</a>
    </div>
</div>

Server rendering

How do we render this view on the server? Well, it’s super easy to render a view against a template, because it’s all already built into Express and ready to go.

On our express config, we reference both the common and app-server view folders.

// Serve views from the node server's client dir & from common dir
// viewsPath is defined in the environment configs

server.set("views", [viewsPath, "./common/views"]);

Then when the request for a particular page comes in, we simply render the page requested.

// accountRouter.js
var router = express.Router(),
    accountController = require("../controllers/account");

router.get("/sign-in", accountController.index);
// accountController.js
var uiConstants = require("../../../common/constants/ui");

function index(req, res) {
    var options =  _.cloneDeep(uiConstants);

   res.render("shared/account/sign-in", {
        title: "QuickBooks Financing | Sign In",
        options: options
    });
}

As you can see, we are rendering the "sign-in" template view. With the express config we set up, we can find this file through the /common/views directory.

To summarize: because any new request to our app first goes through our NGINX proxy and then passes through immediately to our Express server, if the request is for a page, we simply render the view as is. The view can exist in either the common or app-server folder because we set up Express to reference both directories.

Client rendering

How do we render a view on the client using the same template? We render the template on the client by using Backbone views. Nothing crazy or special, we're doing essentially what is exactly specified in the documentation.

define([
    "underscore",
    "configs/appData",
    "./modal",
    "utils/intuit/account",
    "hbs!common/views/shared/account/sign-in"
], function(_, appData, ModalView, account, template) {

    // ModalView inherits from BaseLayoutView
    var SignInModal = ModalView.extend({

        template: template,

        ...
    });

    return SignInModal;
});

So here's an example of a view widget in our app. As you can see we defined the same view template that was referenced in the server, except in this case, it's used to render a sign-in modal on the browser. The view will compile the template and render it, and then inject it into the DOM.

Client + Server rendering

Now's the fun part .Given that I just described how to render a view on the server and on the client using the same template, how can we use these implementation semantics to enable client + server side rendering to create a fast, smooth user experience?

Remember how I mentioned we were using Backbone views to render our templates on the client? Well, we're not just using vanilla Backbone views. What we’ve done is built a special Backbone view specifically to fit our system, akin to Rendr by Airbnb, but done to our desired specifications.

We created a view called the BaseLayoutView. This view will be inherited by all of our client side views and manages everything from garbage collection, managing client/server side rendering, and others.

Through this view we have also extended it to create something called a BasePageView, which is responsible for all things related to a “page” in our app. We use this in conjunction with our client-side UI router so that when the UI router sees a navigation change, it will render the next appropriate BasePageView.

To quickly summarize, we have two files:

  1. BaseLayoutView.js - inherited by all views
  2. BasePageView.js - represents a page

Piecing it all together

Let's walk through a flow and see how all of these are pieced together.

User requests the page

Say our user goes to our home page with the first request as www.quickbooksfinancing.com/home. The express server will interpret that request and render the initial home page view on our app server, and then hand it off to the browser to render.

Within this server-side rendered page, we have defined how to load in our SPA experience. We load in our javascript code, initialize our UI router, and then let the client take over. Our Backbone router will interpret the current route, and “attempt” to render the appropriate Backbone page view. So if the route leads to the home page, the home Backbone view will try to render if it hasn't already been done by the server.

Remember that all of our pages inherit from BasePageView.js and this is where the magic happens.

// BasePageView.js
var BasePageView = BaseLayoutView.extend({

    initialize: function (options) {
        // This will be set to true if it has not been rendered from the server
        this.isTemplateRendered = false;
        ...
        this.render();
    },

    render: function() {
        this.renderTemplate();
        ...
    },

    renderTemplate: function() {
        if (this.isEmptyEl()) {    
            this.isTemplateRendered = true;
            this.setElement(this.template(this.viewOptions));
        }
    }
    ...
});
// HomePageView.js
var HomePageView = BasePageView.extend({

    el: "#page-home",

    name: "home",

    events: {
        "click sup": "scrollToFootnotes"
    }

    ...
});

Because every page inherits from BasePageView.js, what it’ll do is check to see if the current page was already rendered through the server (which in this case, it will be). If it has already been pre-rendered by the server, the view will simply attach itself to the existing DOM node and then set up any event listeners or render any subviews, then the app will be functional to the user.

User navigates within the app

Now say our user goes through the app and clicks on a button, which is supposed to take you to another part of the flow, i.e. a new page.

When the user clicks that button, we initiate a route change, and for the sake of example, let’s say we want to now go to the interview page. We leverage the Browser’s history.pushstate to append a subroute to the browser’s url string. When this happens, our UI router will recognize this change, destroy the current page, and attempt to render the next appropriate page. Remember, this is all done on the client.

When the router tries to render the appropriate page, it will be a page that is a subclass of BasePageView.js. BasePageView.js will again check to see whether the page has already been rendered by the server, it’ll see an empty DOM, so it’ll use its provided template to render the page instead.

Bringing it all together

Now let’s say that the user decides to refresh this interview page. What will happen?

What’ll happen is that the request will be sent to NGINX, and then to our Express server again. Because of the history.pushstate change we made earlier, the request made to our app will request for the interview page, not the home page! For this page, we can use the same exact template used when it was rendered client-side. Our server will construct the interview page, send it back to the browser, the browser will render it, download all the javascript and stylesheets, and then our SPA will take over again!

That’s it!

BMO Dance
All of this has enabled us to remove the common blank white screen of single page applications, which in turn allows us to create a better user experience by improving our perceived page load.

  • Want to use the view on the server?
  • Add a new express route endpoint and render the view.
  • Change your mind?
  • Create a BasePageView and define a UI route.
  • Want both?
  • Then set up both!
  • IE8 and IE9 don’t support history.pushstate?
  • Then default to hash fragments and let the client side router do the heavy lifting.

We are using a combination of the Browser’s history.pushstate, the client side router, the server side routing, our custom Backbone page view, and shared templates to enable client/server side rendering.

The true beauty of this system is that you can build a view once and it can be used by either the server or client; all you need to do is set up the appropriate configuration.

But this is only one example of how we are using Universal Javascript to better the user experience. In the next post, I will go over how we can also leverage Universal Javascript to improve developer productivity.

Tags