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
- 460