Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

What is Progressive Enhancement?

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

server browser JavaScript HTML

Slide 5

Slide 5 text

However, this sucks.

Slide 6

Slide 6 text

UI data data data data API HTML

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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)

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

EGO DEPLETION

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

FastBoot

Slide 16

Slide 16 text

Progressive Enhancement for Ember.js Installs in One Command

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

UI data data data data API data

Slide 22

Slide 22 text

data data data data API data

Slide 23

Slide 23 text

“Server-Side Rendering”

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Other libraries have server-side rendering…

Slide 26

Slide 26 text

FastBoot is server-side rendering for everyone

Slide 27

Slide 27 text

Demo

Slide 28

Slide 28 text

Ease of Use

Slide 29

Slide 29 text

$ ember install ember-cli-fastboot Installation

Slide 30

Slide 30 text

$ ember fastboot Development

Slide 31

Slide 31 text

Deployment

Slide 32

Slide 32 text

Architecture

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Ember App FastBoot Server

Slide 35

Slide 35 text

GET / Ember App FastBoot Server

Slide 36

Slide 36 text

Ember App FastBoot Server App Instance GET /

Slide 37

Slide 37 text

Ember App FastBoot Server App Instance GET /

Slide 38

Slide 38 text

Ember App App Instance API GET /users.json

Slide 39

Slide 39 text

Ember App FastBoot Server App Instance GET / App Instance

Slide 40

Slide 40 text

Ember App FastBoot Server App Instance GET / App Instance

Slide 41

Slide 41 text

Application Instances

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

The Birth & Death of a Request

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Setup

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

A Wild Request Appears

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

/* * 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

Slide 52

Slide 52 text

/* * 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

Slide 53

Slide 53 text

/* * 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

Slide 54

Slide 54 text

/* * 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

Slide 55

Slide 55 text

/** 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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

/* * 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

Slide 59

Slide 59 text

Constraints & Considerations

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Universal JavaScript

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Deployment

Slide 71

Slide 71 text

Ease of Use

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

FastBoot is an Express middleware

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

Want to Help?

Slide 79

Slide 79 text

bit.ly/help-finish-fastboot

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

No content

Slide 83

Slide 83 text

Thank You twitter.com/tomdale

Slide 84

Slide 84 text

Questions? twitter.com/tomdale