Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introduction to FastBoot - Global Ember Meetup

tomdale
February 27, 2016

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
Tweet

More Decks by tomdale

Other Decks in Programming

Transcript

  1. 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
  2. Server-Side Rendering • Works if JavaScript fails to load •

    Easier to archive and index • Faster initial load times
  3. 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)
  4. Requirements • No PhantomJS buggy, slow, massive memory consumption •

    No jsdom slow, compatibility quirks • Concurrent
  5. Registry x-image Component user-avatar Component users Route App App Instance

    Container GET /users users Route user-avatar Component App Instance Container
  6. 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
  7. 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
  8. /* * 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
  9. /* * 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
  10. /* * 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
  11. /* * 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
  12. /** 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
  13. 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
  14. 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
  15. /* * 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
  16. Constraints • No jQuery • No DOM • No globals

    • No shared state • No browser- only APIs
  17. 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(); }); } });
  18. 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
  19. // 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'], })); }
  20. 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());
  21. Fast Load SEO Friendly Mobile Friendly Rich Interactions Offline Fast

    Navigation progressive enhancement JavaScript