Server Side Rendering

Why it's amazing

  • Can run any jQuery-powered framework on the server.
  • Asynchronous activity is tracked so can-ssr can wait to know when the DOM is ready.
  • Data used to render parts of the page, such as XHR requests can be attached to the rendered output.
  • can-ssr associates CSS imports and correctly returns the page with all progressively-loaded <link> tags needed, preventing FOUC.

Libraries involved

  • can-simple-dom
  • can/util/vdom
  • can-ssr
  • done-autorender

Loading jQuery

What we need

  • A global document that can
    • createElements
    • addEventListeners
    • implements innerHTML

can-simple-dom

Implements minimum DOM apis

var simpleDOM = require("can-simple-dom");
var document = new simpleDOM.Document();

can/util/vdom

  • Sets up a global document
  • Makes addEventListener a noop
  • Uses can/view/parser for innerHTML

Load your app the manual way

var app = require("express")();
var steal = global.steal = require("steal").clone();
global.System = steal.System;
steal.config({
    config: __dirname + "/package.json!npm",
    meta: {
        jquery: {
            format: "global",
            deps: ["can/util/vdom/vdom"]
        }
    }
});

steal.import("main.stache!", "models/appstate", function(tmpl, AppState){

   app.get("/", function(req, res){
       var pathname = url.parse(req.url).pathname;

       var frag = tmpl(new AppState());
       var html = frag.innerHTML;

       req.send(html);
   }); 

});

Problems

  • A document contains more than just the body.
  • Render is synchronous, ajax requests left out.
  • The fragment is leaking event listeners.
  • CSS is not included in the rendered page.

can-ssr/app-map

// app.js
import AppMap from "can-ssr/app-map";
import "can/map/define/";

export default AppMap.extend({
    define: {
        title: "place-my-order"
    }
});

can-ssr/app-map

  • Serves as the view model for your application.
  • New instance is created for every request.
  • Collects promises to know when async activity is complete.
  • Collects data used to render the page such as through XHR requests.

can-ssr/app-map

import Map from "can/map/";
import Component from "can/component/";
import Restaurant from "place-my-order/models/restaurant";
import "can/map/define/";

var ViewModel = Map.extend({
    define: {
        restaurants: {
            get: function(){
                var restaurants = Restaurant.getList();
                this.attr("@root").pageData(restaurants);
                return restaurants;
            }
        }
    }
});

index.stache

<!doctype html>
<html>
    <head>
        <title>Place My Order</title>
    </head>
    <body>
        <can-import from="./app" as="viewModel" />
        <can-import from="place-my-order-assets" />
        <can-import from="loading.component!" />

        {{#if page "home"}}
            <can-import from="home.component!" can-tag="loading">
                <home-page></home-page>
            </can-import>
        {{/if}}
    </body>
</html>

can-import

  • Import stuff into a template
    • can.view.attrs
    • partials
    • css
    • whatever
  • Dynamically import parts of a page
  • Re-export items with "as" attribute

done-autorender

https://github.com/donejs/autorender

Stache plugin

But.

  • Automatically renders into the <head> and <body>
  • Activates live-reload
  • Registers progressively loaded bundles
  • Implements readyAsync

The CSS problem

  • css is imported in each module
  • There is one instance of the app running on the server
  • The request lifecycle is part of can-ssr/app-map
  • How to associate CSS with an individual request?

Trace path of css imports

index.stache -> place-my-order-assets

home.component -> <style>

order/history -> order/history/list -> list.less

parentMap

{
    "home.component": {
        "index.stache"
    },
    "restaurant/list/list": {
        index.stache"
    },
    "models/order": {
        "restaurant/order/new/new",
        "order/history/history"
    }
}

CSS is a side effect

API for side effects

asset-register

asset-register

var register = require("asset-register");

var style = document.createStyle();
...

register("order/history/history.less", "css", function(){
    return style.cloneNode(true);
});

bundle map

{
    "index.stache": {
        "place-my-order-assets": {
            "type": "css",
            "value": function(){ ... }
        }
    },
    "order/history/history": {
        "history.less": {
            "type": "css",
            "value": function() { ... }
        }
    },
    "@global": {
        "inline-cache": {
            "type": "inline-cache",
            "value": { ... }
        }
    }
}

asset-register

<!doctype html>
<html>
    <head>
        <title>Place My Order</title>

        {{asset "css"}}
    </head>
    <body>
        <can-import ....>


        {{asset "inline-cache"}}
    </body>
</html>

can-ssr

https://github.com/canjs/can-ssr

Goals

  • Define all of your routes in your (client-side) app.
    • can.route(":page");
  • Use a catch-all route with your preferred Node web server library.
  • can-serve: Provide a basic server for development.

can-ssr

Create a render function

var render = require("can-ssr")({
    config: __dirname + "/package.json!npm"
});

Request Lifecycle

1. Path passed to render function

// Example:
var renderPromise = render("/restaurants");

Request Lifecycle

2. Create instance of viewModel

// Example:
var startup = steal.startup();

startup.then(function(autorender){
    var viewModel = new autorender.viewModel;
});

Request Lifecycle

3. Get initial route data

// Example:
var routeData = can.route.deparam("/restaurants");

viewModel.attr(routeData);

Request Lifecycle

4. render is called

// Example:
var frag = autorender.render(viewModel);

Request Lifecycle

5. wait for Promises to resolve

// Example:
var readyPromises = viewModel.__readyPromises;

Promise.all(readyPromises).then(function(){
    // frag should have all the elements.
});

Request Lifecycle

6. Recursively check for readyPromises

Request Lifecycle

7. Keep a list of all can-import calls

This is needed to know which rendering assets are part of the request.

Request Lifecycle

8. When all promises resolved, mark viewModel as complete

Request Lifecycle

9. Find assets needed for this request

var imports = viewModel.attr("renderingAssets");

// These are top-level modules such as 
// index.stache, restaurant/list/list

var frag = document.createFragment();

imports.forEach(function(moduleName){
    var bundle = bundleMap[moduleName];

    for(var assetName in bundle) {
        var createElement = bundle[assetName];
        frag.appendChild(createElement());
    }
});

return frag;

Request Lifecycle

10. Get html string

var doc = new document.constructor;
doc.documenteElement.appendChild(renderedFragment);

var html = doc.documentElement.innerHTML;

Request Lifecycle

11. Trigger the "removed" event

traverse(frag, function(el){
    can.trigger(el, "removed");
});

Server Side Rendering

By Matthew Phillips

Server Side Rendering

No FOUC for us

  • 448