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. View Slide

  2. What is Progressive
    Enhancement?

    View Slide

  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

    View Slide

  4. server browser
    JavaScript
    HTML

    View Slide

  5. However, this sucks.

    View Slide

  6. UI
    data
    data
    data
    data
    API HTML

    View Slide

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

    View Slide

  8. Server-Side Rendering
    • Works if JavaScript fails
    to load
    • Easier to archive and
    index
    • Faster initial load times

    View Slide

  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)

    View Slide

  10. MOBILE-FIRST
    RESPONSIVE-FIRST
    ACCESSIBILITY-FIRST
    CONTENT-FIRST
    SECURITY-FIRST
    OFFLINE-FIRST
    DOCUMENTATION-FIRST
    API-FIRST
    PERFORMANCE-FIRST

    View Slide

  11. EGO DEPLETION

    View Slide

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

    View Slide

  13. View Slide

  14. View Slide

  15. FastBoot

    View Slide

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

    View Slide

  17. View Slide

  18. View Slide

  19. View Slide

  20. View Slide

  21. UI
    data
    data
    data
    data
    API
    data

    View Slide

  22. data
    data
    data
    data
    API
    data

    View Slide

  23. “Server-Side Rendering”

    View Slide

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

    View Slide

  25. Other libraries have
    server-side rendering…

    View Slide

  26. FastBoot is server-side
    rendering for everyone

    View Slide

  27. Demo

    View Slide

  28. Ease of Use

    View Slide

  29. $ ember install ember-cli-fastboot
    Installation

    View Slide

  30. $ ember fastboot
    Development

    View Slide

  31. Deployment

    View Slide

  32. Architecture

    View Slide

  33. Requirements
    • No PhantomJS
    buggy, slow, massive memory consumption
    • No jsdom
    slow, compatibility quirks
    • Concurrent

    View Slide

  34. Ember App
    FastBoot Server

    View Slide

  35. GET /
    Ember App
    FastBoot Server

    View Slide

  36. Ember App
    FastBoot Server
    App Instance
    GET /

    View Slide

  37. Ember App
    FastBoot Server
    App Instance
    GET /

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  41. Application Instances

    View Slide

  42. import Route from "ember-route";
    export default Route.extend({
    model() {
    return this.store.findAll('post');
    }
    });
    exports a class

    View Slide

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

    View Slide

  44. Registry
    x-image
    Component
    user-avatar
    Component
    users
    Route
    App App Instance
    Container
    GET /users
    users
    Route
    user-avatar
    Component
    App Instance
    Container

    View Slide

  45. The Birth & Death
    of a Request

    View Slide

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

    View Slide

  47. Setup

    View Slide

  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

    View Slide

  49. A Wild Request Appears

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  59. Constraints &
    Considerations

    View Slide

  60. Constraints
    • No jQuery
    • No DOM
    • No globals
    • No shared state
    • No browser-
    only APIs

    View Slide

  61. Universal JavaScript

    View Slide

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

    View Slide

  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();
    });
    }
    });

    View Slide

  64. node-fetch.js whatwg-fetch.js
    FastBoot browser
    fetch.js

    View Slide

  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

    View Slide

  66. treeForVendor: function() {
    if (isFastBoot()) {
    return treeForNodeFetch();
    } else {
    return treeForBrowserFetch();
    }
    }

    View Slide

  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'],
    }));
    }

    View Slide

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

    View Slide

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

    View Slide

  70. Deployment

    View Slide

  71. Ease of Use

    View Slide

  72. View Slide

  73. View Slide

  74. IAM Roles
    Instance Profiles
    ElasticBeanstalk Environments
    S3 Buckets
    Bucket Policies
    Autoscaling Groups

    View Slide

  75. Deploy Strategies
    •AWS Elastic Beanstalk
    •Docker
    •Heroku (coming soon)
    •Roll your own!

    View Slide

  76. FastBoot is an Express
    middleware

    View Slide

  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());

    View Slide

  78. Want to Help?

    View Slide

  79. bit.ly/help-finish-fastboot

    View Slide

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

    View Slide

  81. Fast Load
    SEO Friendly
    Mobile Friendly
    Rich Interactions
    Offline
    Fast Navigation
    +

    View Slide

  82. View Slide

  83. Thank You
    twitter.com/tomdale

    View Slide

  84. Questions?
    twitter.com/tomdale

    View Slide