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. What is Progressive
    Enhancement?

    View full-size slide

  2. 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 full-size slide

  3. server browser
    JavaScript
    HTML

    View full-size slide

  4. However, this sucks.

    View full-size slide

  5. UI
    data
    data
    data
    data
    API HTML

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. 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 full-size slide

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

    View full-size slide

  10. EGO DEPLETION

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  13. UI
    data
    data
    data
    data
    API
    data

    View full-size slide

  14. data
    data
    data
    data
    API
    data

    View full-size slide

  15. “Server-Side Rendering”

    View full-size slide

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

    View full-size slide

  17. Other libraries have
    server-side rendering…

    View full-size slide

  18. FastBoot is server-side
    rendering for everyone

    View full-size slide

  19. $ ember install ember-cli-fastboot
    Installation

    View full-size slide

  20. $ ember fastboot
    Development

    View full-size slide

  21. Architecture

    View full-size slide

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

    View full-size slide

  23. Ember App
    FastBoot Server

    View full-size slide

  24. GET /
    Ember App
    FastBoot Server

    View full-size slide

  25. Ember App
    FastBoot Server
    App Instance
    GET /

    View full-size slide

  26. Ember App
    FastBoot Server
    App Instance
    GET /

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  30. Application Instances

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  34. The Birth & Death
    of a Request

    View full-size slide

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

    View full-size slide

  36. 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 full-size slide

  37. A Wild Request Appears

    View full-size slide

  38. 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 full-size slide

  39. /*
    * 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 full-size slide

  40. /*
    * 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 full-size slide

  41. /*
    * 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 full-size slide

  42. /*
    * 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 full-size slide

  43. /**
    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 full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. /*
    * 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 full-size slide

  47. Constraints &
    Considerations

    View full-size slide

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

    View full-size slide

  49. Universal JavaScript

    View full-size slide

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

    View full-size slide

  51. 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 full-size slide

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

    View full-size slide

  53. 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 full-size slide

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

    View full-size slide

  55. // 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  60. FastBoot is an Express
    middleware

    View full-size slide

  61. 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 full-size slide

  62. Want to Help?

    View full-size slide

  63. bit.ly/help-finish-fastboot

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  66. Thank You
    twitter.com/tomdale

    View full-size slide

  67. Questions?
    twitter.com/tomdale

    View full-size slide