Introduction to FastBoot - Global Ember Meetup

9bf3a766e037b9d5a4da0a6f9d0f4f68?s=47 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.

9bf3a766e037b9d5a4da0a6f9d0f4f68?s=128

tomdale

February 27, 2016
Tweet

Transcript

  1. None
  2. What is Progressive Enhancement?

  3. 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
  4. server browser JavaScript HTML

  5. However, this sucks.

  6. UI data data data data API HTML

  7. Coupling business logic, authentication & data access to the UI

    sucks. API UI
  8. Server-Side Rendering • Works if JavaScript fails to load •

    Easier to archive and index • Faster initial load times
  9. 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)
  10. MOBILE-FIRST RESPONSIVE-FIRST ACCESSIBILITY-FIRST CONTENT-FIRST SECURITY-FIRST OFFLINE-FIRST DOCUMENTATION-FIRST API-FIRST PERFORMANCE-FIRST

  11. EGO DEPLETION

  12. The best way to make someone do something is to

    make it free
  13. None
  14. None
  15. FastBoot

  16. Progressive Enhancement for Ember.js Installs in One Command

  17. None
  18. None
  19. None
  20. None
  21. UI data data data data API data

  22. data data data data API data

  23. “Server-Side Rendering”

  24. Server-Side… • Rendering • Routing • Model Fetching • Serialization

    • Logging • Authentication
  25. Other libraries have server-side rendering…

  26. FastBoot is server-side rendering for everyone

  27. Demo

  28. Ease of Use

  29. $ ember install ember-cli-fastboot Installation

  30. $ ember fastboot Development

  31. Deployment

  32. Architecture

  33. Requirements • No PhantomJS buggy, slow, massive memory consumption •

    No jsdom slow, compatibility quirks • Concurrent
  34. Ember App FastBoot Server

  35. GET / Ember App FastBoot Server

  36. Ember App FastBoot Server App Instance GET /

  37. Ember App FastBoot Server App Instance GET /

  38. Ember App App Instance API GET /users.json

  39. Ember App FastBoot Server App Instance GET / App Instance

  40. Ember App FastBoot Server App Instance GET / App Instance

  41. Application Instances

  42. import Route from "ember-route"; export default Route.extend({ model() { return

    this.store.findAll('post'); } }); exports a class
  43. import Component from "ember-component"; export default Ember.Component.extend({ tagName: 'nav' });

    exports a class
  44. Registry x-image Component user-avatar Component users Route App App Instance

    Container GET /users users Route user-avatar Component App Instance Container
  45. The Birth & Death of a Request

  46. HIGHLY TECHNICAL WARNING This material will not be on the

    test.
  47. Setup

  48. 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
  49. A Wild Request Appears

  50. 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
  51. /* * 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
  52. /* * 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
  53. /* * 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
  54. /* * 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
  55. /** 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
  56. 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
  57. 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
  58. /* * 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
  59. Constraints & Considerations

  60. Constraints • No jQuery • No DOM • No globals

    • No shared state • No browser- only APIs
  61. Universal JavaScript

  62. ember-network github.com/tomdale/ember-network

  63. 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(); }); } });
  64. node-fetch.js whatwg-fetch.js FastBoot browser fetch.js

  65. 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
  66. treeForVendor: function() { if (isFastBoot()) { return treeForNodeFetch(); } else

    { return treeForBrowserFetch(); } }
  67. // 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'], })); }
  68. (function() { define('ember-network/fetch', [ 'exports' ], function(self) { self['default'] =

    FastBoot.require('node-fetch'); }); })();
  69. (function() { define('ember-network/fetch', [ 'exports' ], function(self) { self['default'] =

    FastBoot.require('node-fetch'); }); })();
  70. Deployment

  71. Ease of Use

  72. None
  73. None
  74. IAM Roles Instance Profiles ElasticBeanstalk Environments S3 Buckets Bucket Policies

    Autoscaling Groups
  75. Deploy Strategies •AWS Elastic Beanstalk •Docker •Heroku (coming soon) •Roll

    your own!
  76. FastBoot is an Express middleware

  77. 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());
  78. Want to Help?

  79. bit.ly/help-finish-fastboot

  80. Fast Load SEO Friendly Mobile Friendly Rich Interactions Offline Fast

    Navigation progressive enhancement JavaScript
  81. Fast Load SEO Friendly Mobile Friendly Rich Interactions Offline Fast

    Navigation +
  82. None
  83. Thank You twitter.com/tomdale

  84. Questions? twitter.com/tomdale