Share
Embed
Introduction to FastBoot - Global Ember Meetup
Slides from my talk at the Global Ember Meetup. A video of this presentation is available at https://vimeo.com/157688134.
tomdale
February 27, 2016
Transcript
84 slides
-
If you make your core tasks dependent on JavaScript, some
of your potential users will inevitably be left out in the cold. But if you start by building on a classic server/ client model, and then enhance with JavaScript, you can have your cake and eat it too. Jeremy Keith -
Server-Side Rendering • Works if JavaScript fails to load •
Easier to archive and index • Faster initial load times -
Benefits of JavaScript • Works offline • No page reloads
(great for e.g. music players) • Fast (once loaded) • Rich interaction • Access to device features (camera, storage, GPS) -
The best way to make someone do something is to
make it free -
Server-Side… • Rendering • Routing • Model Fetching • Serialization
• Logging • Authentication -
Requirements • No PhantomJS buggy, slow, massive memory consumption •
No jsdom slow, compatibility quirks • Concurrent -
import Route from "ember-route"; export default Route.extend({ model() { return
this.store.findAll('post'); } }); exports a class -
Registry x-image Component user-avatar Component users Route App App Instance
Container GET /users users Route user-avatar Component App Instance Container -
function EmberApp(options) { var distPath = options.distPath; var appFilePath =
options.appFile; var vendorFilePath = options.vendorFile; var moduleWhitelist = options.moduleWhitelist; debug("app created; app=%s; vendor=%s", appFilePath, vendorFilePath); moduleWhitelist.forEach(function(whitelistedModule) { debug("module whitelisted; module=%s", whitelistedModule); }); // Create the sandbox, giving it the resolver to resolve once the app // has booted. var sandboxRequire = buildWhitelistedRequire(moduleWhitelist, distPath); var sandbox = createSandbox({ najax: najax, FastBoot: { require: sandboxRequire } }); sourceMapSupport.install(Error); sandbox.run('sourceMapSupport.install(Error);'); var appFile = fs.readFileSync(appFilePath, 'utf8'); var vendorFile = fs.readFileSync(vendorFilePath, 'utf8'); sandbox.run(vendorFile, vendorFilePath); debug("vendor file evaluated"); sandbox.run(appFile, appFilePath); debug("app file evaluated"); var AppFactory = sandbox.require('~fastboot/app-factory'); this._app = AppFactory['default'](); } ember-fastboot-server/lib/server.js -
FastBootServer.prototype.middleware = function() { return function(req, res, next) { var
path = req.url; var server = this; debug("handling url; url=%s", path); var startTime = Date.now(); this.app.visit(path, { request: req, response: res }) .then(success, failure) .finally(function() { debug("finished handling; url=%s", path); }); function success(result) { server.handleSuccess(res, path, result, startTime); } function failure(error) { server.handleFailure(res, path, error, startTime); } }.bind(this); }; ember-fastboot-server/lib/server.js -
/* * Called by an HTTP server to render the
app at a specific URL. */ EmberApp.prototype.visit = function(path, options) { var req = options.request; var res = options.response; var bootOptions = buildBootOptions(); var doc = bootOptions.document; var rootElement = bootOptions.rootElement; return this.buildApp() .then(registerFastBootInfo(req, res)) .then(function(instance) { return instance.boot(bootOptions); }) .then(function(instance) { return instance.visit(path, bootOptions); }) .then(serializeHTML(doc, rootElement)); }; ember-fastboot-server/lib/ember-app.js -
/* * Builds a new FastBootInfo instance with the request
and response and injects * it into the application instance. */ function registerFastBootInfo(req, res) { return function(instance) { var info = new FastBootInfo(req, res); info.register(instance); return instance; }; } ember-fastboot-server/lib/ember-app.js -
/* * A class that encapsulates information about the *
current HTTP request from FastBoot. This is injected * on to the FastBoot service. */ function FastBootInfo(request, response) { this.request = request; this.response = response; this.cookies = this.extractCookies(request); this.headers = request.headers; } FastBootInfo.prototype.extractCookies = function(request) { // If cookie-parser middleware has already parsed the cookies, // just use that. if (request.cookies) { return request.cookies; } // Otherwise, try to parse the cookies ourselves, if they exist. var cookies = request.get('cookie'); if (cookies) { return cookie.parse(cookies); } // Return an empty object instead of undefined if no cookies are present. return {}; }; /* * Registers this FastBootInfo instance in the registry of an Ember * ApplicationInstance. It is configured to be injected into the FastBoot * service, ensuring it is available inside instance initializers. */ FastBootInfo.prototype.register = function(instance) { instance.register('info:-fastboot', this, { instantiate: false }); instance.inject('service:fastboot', '_fastbootInfo', 'info:-fastboot'); }; ember-fastboot-server/lib/ember-app.js -
/* * Called by an HTTP server to render the
app at a specific URL. */ EmberApp.prototype.visit = function(path, options) { var req = options.request; var res = options.response; var bootOptions = buildBootOptions(); var doc = bootOptions.document; var rootElement = bootOptions.rootElement; return this.buildApp() .then(registerFastBootInfo(req, res)) .then(function(instance) { return instance.boot(bootOptions); }) .then(function(instance) { return instance.visit(path, bootOptions); }) .then(serializeHTML(doc, rootElement)); }; ember-fastboot-server/lib/ember-app.js -
/** The `ApplicationInstance` encapsulates all of the stateful aspects of
a running `Application`. At a high-level, we break application boot into two distinct phases: * Definition time, where all of the classes, templates, and other dependencies are loaded (typically in the browser). * Run time, where we begin executing the application once everything has loaded. Definition time can be expensive and only needs to happen once since it is an idempotent operation. For example, between test runs and FastBoot requests, the application stays the same. It is only the state that we want to reset. That state is what the `ApplicationInstance` manages: it is responsible for creating the container that contains all application state, and disposing of it once the particular test run or FastBoot request has finished. @public @class Ember.ApplicationInstance @extends Ember.EngineInstance */ ember.js/packages/ember-application/lib/system/application-instance.js -
registry.register('renderer:-dom', { create() { return new Renderer(new DOMHelper(options.document), { destinedForDOM:
options.isInteractive }); } }); ember.js/packages/ember-application/lib/system/application-instance.js -
visit(url) { this.setupRouter(); let router = get(this, 'router'); let handleResolve
= () => { // Resolve only after rendering is complete return new RSVP.Promise((resolve) => { // TODO: why is this necessary? Shouldn't 'actions' queue be enough? // Also, aren't proimses supposed to be async anyway? run.next(null, resolve, this); }); }; let handleReject = (error) => { if (error.error) { throw error.error; } else if (error.name === 'TransitionAborted' && router.router.activeTransition) { return router.router.activeTransition.then(handleResolve, handleReject); } else if (error.name === 'TransitionAborted') { throw new Error(error.message); } else { throw error; } }; // Keeps the location adapter's internal URL in-sync get(router, 'location').setURL(url); return router.handleURL(url).then(handleResolve, handleReject); } ember.js/packages/ember-application/lib/system/application-instance.js -
/* * After the ApplicationInstance has finished rendering, serializes the
* resulting DOM element into HTML to be transmitted back to the user agent. */ function serializeHTML(doc, rootElement) { return function(instance) { var head; if (doc.head) { head = HTMLSerializer.serialize(doc.head); } try { return { url: instance.getURL(), // TODO: use this to determine whether to 200 or redirect title: doc.title, head: head, body: HTMLSerializer.serialize(rootElement) }; } finally { instance.destroy(); } }; } ember-fastboot-server/lib/ember-app.js -
Constraints • No jQuery • No DOM • No globals
• No shared state • No browser- only APIs -
import Route from "ember-route"; import fetch from "ember-network/fetch"; export default
Route.extend({ model() { return fetch('https://api.github.com/users/tomdale/events') .then(function(response) { return response.json(); }); } }); -
Ember App $ ember build $ ember fastboot:build dist fastboot-dist
├── assets │ ├── github-fastboot-example-7d524d7383b17148.js │ ├── github-fastboot-example-d41d8cd98f00b204.css │ ├── vendor-24184402e30c77a3019fd6ec3d6540f1.js │ └── vendor-d41d8cd98f00b204e9800998ecf8427e.css ├── crossdomain.xml ├── index.html └── robots.txt ├── assets │ ├── assetMap.json │ ├── github-fastboot-example-859699d51.js │ ├── github-fastboot-example-d41d8cd98.css │ ├── vendor-7f3f12757332674b088504899cc4d436.js │ └── vendor-d41d8cd98f00b204e9800998ecf8427e.css ├── crossdomain.xml ├── index.html ├── node_modules │ └── node-fetch ├── package.json └── robots.txt -
treeForVendor: function() { if (isFastBoot()) { return treeForNodeFetch(); } else
{ return treeForBrowserFetch(); } } -
// Returns a shim file from the assets directory and
renames it to the // normalized `fetch.js`. That shim file calls `FastBoot.require`, which allows // you to require node modules (in this case `node-fetch`) in FastBoot mode. function treeForNodeFetch() { return normalizeFileName(funnel(path.join(__dirname, './assets'), { files: ['fastboot-fetch.js'], })); } -
(function() { define('ember-network/fetch', [ 'exports' ], function(self) { self['default'] =
FastBoot.require('node-fetch'); }); })(); -
(function() { define('ember-network/fetch', [ 'exports' ], function(self) { self['default'] =
FastBoot.require('node-fetch'); }); })(); -
var FastBootServer = require('ember-fastboot-server'); var express = require('express'); var server
= new FastBootServer({ distPath: 'fastboot-dist' }); var app = express(); if (commandOptions.serveAssets) { app.get('/', server.middleware()); app.use(express.static(assetsPath)); } app.get('/*', server.middleware()); -
Fast Load SEO Friendly Mobile Friendly Rich Interactions Offline Fast
Navigation progressive enhancement JavaScript