Server-side rendering for Ember.js apps
JavaScript HTML
Latest commit ee4bd3b Feb 10, 2017 @danmcclain danmcclain committed on GitHub Merge pull request #269 from ember-fastboot/remove-npm-install-from-w…
…atch

Only start app once, call fastboot.reload

README.md

Ember FastBoot

npm version Build Status

An Ember CLI addon that allows you to render and serve Ember.js apps on the server. Using FastBoot, you can serve rendered HTML to browsers and other clients without requiring them to download JavaScript assets.

Currently, the set of Ember applications supported is extremely limited. As we fix more issues, we expect that set to grow rapidly. See Known Limitations below for a full-list.

The bottom line is that you should not (yet) expect to install this add-on in your production app and have FastBoot work.

Introduction Video

Introduction to Ember FastBoot

Installation

FastBoot requires Ember 2.3 or higher.

From within your Ember CLI application, run the following command:

ember install ember-cli-fastboot

Running

  • ember fastboot --serve-assets
  • Visit your app at http://localhost:3000.

You may be shocked to learn that minified code runs faster in Node than non-minified code, so you will probably want to run the production environment build for anything "serious."

ember fastboot --environment production

You can also specify the port (default is 3000):

ember fastboot --port 8088

See ember help fastboot for more.

Using Node/npm Dependencies

Whitelisting Packages

When your app is running in FastBoot, it may need to use Node packages to replace features that are available only in the browser.

For security reasons, your Ember app running in FastBoot can only access packages that you have explicitly whitelisted.

To allow your app to require a package, add it to the fastbootDependencies array in your app's package.json:

{
  "name": "my-sweet-app",
  "version": "0.4.2",
  "devDependencies": {
    // ...
  },
  "dependencies": {
    // ...
  },
  "fastbootDependencies": [
    "rsvp",
    "path"
  ]
}

The fastbootDependencies in the above example means the only node modules your Ember app can use are rsvp and path.

If the package you are using is not built-in to Node, you must also specify the package and a version in the package.json dependencies hash. Built-in modules (path, fs, etc.) only need to be added to fastbootDependencies.

Using Dependencies

From your Ember.js app, you can run FastBoot.require() to require a package. This is identical to the CommonJS require except it checks all requests against the whitelist first.

let path = FastBoot.require('path');
let filePath = path.join('tmp', session.getID());

If you attempt to require a package that is not in the whitelist, FastBoot will raise an exception.

Note that the FastBoot global is only available when running in FastBoot mode. You should either guard against its presence or only use it in FastBoot-only initializers.

FastBoot Service

FastBoot registers the fastboot service. This service allows you to check if you are running within FastBoot by checking fastboot.isFastBoot. There is also a request object under fastboot.request which exposes details about the current request being handled by FastBoot

Delaying the server response

By default, FastBoot waits for the beforeModel, model, and afterModel hooks to resolve before sending the response back to the client. If you have asynchrony that runs outside of those contexts, your response may not reflect the state that you want. To solve this, the fastboot service has deferRendering function that accepts a promise. It will chain all promises passed to it, and the FastBoot server will wait until all of these promises resolve before sending the response to the client. These promises must be chained before the rendering is complete after the model hooks. For example, if a component that is rendered into the page makes an async call for data, registering a promise to be resolved in its init hook would allow the component to defer the rendering of the page.

Cookies

You can access cookies for the current request via fastboot.request in the fastboot service.

export default Ember.Route.extend({
  fastboot: Ember.inject.service(),

  model() {
    let authToken = this.get('fastboot.request.cookies.auth');
    // ...
  }
});

The service's cookies property is an object containing the request's cookies as key/value pairs.

Headers

You can access the headers for the current request via fastboot.request in the fastboot service. The headers object implements part of the Fetch API's Headers class, the functions available are has, get, and getAll.

export default Ember.Route.extend({
  fastboot: Ember.inject.service(),

  model() {
    let headers = this.get('fastboot.request.headers');
    let xRequestHeader = headers.get('X-Request');
    // ...
  }
});

Host

You can access the host of the request that the current FastBoot server is responding to via fastboot.request in the fastboot service. The host property will return the host (example.com or localhost:3000).

export default Ember.Route.extend({
  fastboot: Ember.inject.service(),

  model() {
    let host = this.get('fastboot.request.host');
    // ...
  }
});

To retrieve the host of the current request, you must specify a list of hosts that you expect in your config/environment.js:

module.exports = function(environment) {
  var ENV = {
    modulePrefix: 'host',
    environment: environment,
    baseURL: '/',
    locationType: 'auto',
    EmberENV: {
      // ...
    },
    APP: {
      // ...
    },

    fastboot: {
      hostWhitelist: ['example.com', 'subdomain.example.com', /^localhost:\d+$/]
    }
  };
  // ...
};

The hostWhitelist can be a string or RegExp to match multiple hosts. Care should be taken when using a RegExp, as the host function relies on the Host HTTP header, which can be forged. You could potentially allow a malicious request if your RegExp is too permissive when using the host when making subsequent requests.

Retrieving host will error on 2 conditions:

  1. you do not have a hostWhitelist defined
  2. the Host header does not match an entry in your hostWhitelist

Query Parameters

You can access query parameters for the current request via fastboot.request in the fastboot service.

export default Ember.Route.extend({
  fastboot: Ember.inject.service(),

  model() {
    let authToken = this.get('fastboot.request.queryParams.auth');
    // ...
  }
});

The service's queryParams property is an object containing the request's query parameters as key/value pairs.

Path

You can access the path (/ or /some-path) of the request that the current FastBoot server is responding to via fastboot.request in the fastboot service.

export default Ember.Route.extend({
  fastboot: Ember.inject.service(),

  model() {
    let path = this.get('fastboot.request.path');
    // ...
  }
});

Protocol

You can access the protocol (http or https) of the request that the current FastBoot server is responding to via fastboot.request in the fastboot service.

export default Ember.Route.extend({
  fastboot: Ember.inject.service(),

  model() {
    let protocol = this.get('fastboot.request.protocol');
    // ...
  }
});

The Shoebox

You can pass application state from the FastBoot rendered application to the browser rendered application using a feature called the "Shoebox". This allows you to leverage server API calls made by the FastBoot rendered application on the browser rendered application. Thus preventing you from duplicating work that the FastBoot application is performing. This should result in a performance benefit for your browser application, as it does not need to issue server API calls whose results are available from the Shoebox.

The contents of the Shoebox are written to the HTML as strings within <script> tags by the server rendered application, which are then consumed by the browser rendered application.

This looks like:

.
.
<script type="fastboot/shoebox" id="shoebox-main-store">
{"data":[{"attributes":{"name":"AEC Professionals"},"id":106,"type":"audience"},
{"attributes":{"name":"Components"},"id":111,"type":"audience"},
{"attributes":{"name":"Emerging Professionals"},"id":116,"type":"audience"},
{"attributes":{"name":"Independent Voters"},"id":2801,"type":"audience"},
{"attributes":{"name":"Staff"},"id":141,"type":"audience"},
{"attributes":{"name":"Students"},"id":146,"type":"audience"}]}
</script>
.
.

You can add items into the shoebox with shoebox.put, and you can retrieve items from the shoebox using shoebox.retrieve. In the example below we use an object, shoeboxStore, that acts as our store of objects that reside in the shoebox. We can then add/remove items from the shoeboxStore in the FastBoot rendered application as we see fit. Then in the browser rendered application, it will grab the shoeboxStore from the shoebox and retrieve the record necessary for rendering this route.

export default Ember.Route.extend({
  fastboot: Ember.inject.service(),

  model(params) {
    let shoebox = this.get('fastboot.shoebox');
    let shoeboxStore = shoebox.retrieve('my-store');

    if (this.get('fastboot.isFastBoot')) {
      return this.store.findRecord('post', params.post_id).then(post => {
        if (!shoeboxStore) {
          shoeboxStore = {};
          shoebox.put('my-store', shoeboxStore);
        }
        shoeboxStore[post.id] = post.toJSON();
      });
    } else {
      return shoeboxStore && shoeboxStore[params.post_id];
    }
  }
});

Disabling incompatible dependencies

There are two places where the inclusion of incompatible JavaScript libraries could occur:

  1. app.import in the application's ember-cli-build.js
  2. app.import in an addon's included hook

ember-cli-fastboot sets the EMBER_CLI_FASTBOOT environment variable when it is building the FastBoot version of the application. You can use this to prevent the inclusion of the library at build time:

if (!process.env.EMBER_CLI_FASTBOOT) {
  // This will only be included in the browser build
  app.import('some/jquery.plugin.js')
}

Known Limitations

While FastBoot is under active development, there are several major restrictions you should be aware of. Only the most brave should even consider deploying this to production.

No didInsertElement

Since didInsertElement hooks are designed to let your component directly manipulate the DOM, and that doesn't make sense on the server where there is no DOM, we do not invoke either didInsertElement or willInsertElement hooks. The only component lifecycle hooks called in FastBoot are init, didReceiveAttrs, didUpdateAttrs and willDestroy.

No jQuery

Running most of jQuery requires a full DOM. Most of jQuery will just not be supported when running in FastBoot mode. One exception is network code for fetching models, which we intended to support, but doesn't work at present.

Prototype extensions

Prototype extensions do not currently work across node "realms." Fastboot applications operate in two realms, a normal node environment and a virtual machine. Passing objects that originated from the normal realm will not contain the extension methods inside of the sandbox environment. For this reason, it's encouraged to disable prototype extensions.

Double build times and no incremental builds

Due to limitations in Ember CLI, builds take twice as long to generate the second set of FastBoot assets. This also means incremental builds with live reload don't work either. This aims to be resolved by FastBoot 1.0. In the mean time, we introduce a short-circuit evironment flag to not do a FastBoot build:

FASTBOOT_DISABLED=true ember build

This is useful to keep your existing workflow while in development, while still being able to deploy FastBoot. This flag will be removed in FastBoot 1.0.

Troubleshooting

Because your app is now running in Node.js, not the browser, you'll need a new set of tools to diagnose problems when things go wrong. Here are some tips and tricks we use for debugging our own apps.

Verbose Logging

Enable verbose logging by running the FastBoot server with the following environment variables set:

DEBUG=ember-cli-fastboot:* ember fastboot

PRs adding or improving logging facilities are very welcome.

Developer Tools

You can get a debugging environment similar to the Chrome developer tools running with a FastBoot app, although it's not (yet) as easy as in the browser.

First, install the Node Inspector:

npm install node-inspector -g

Make sure you install a recent release; in our experience, older versions will segfault when used in conjunction with Contextify, which FastBoot uses for sandboxing.

Next, start the inspector server. We found the experience too slow to be usable until we discovered the --no-preload flag, which waits to fetch the source code for a given file until it's actually needed.

node-inspector --no-preload

Once the debug server is running, you'll want to start up the FastBoot server with Node in debug mode. One thing about debug mode: it makes everything much slower. Since the ember fastboot command does a full build when launched, this becomes agonizingly slow in debug mode.

Avoid the slowness by manually running the build in normal mode, then running FastBoot in debug mode without doing a build:

ember build && node --debug-brk ./node_modules/.bin/ember fastboot --no-build

This does a full rebuild and then starts the FastBoot server in debug mode. Note that the --debug-brk flag will cause your app to start paused to give you a chance to open the debugger.

Once you see the output debugger listening on port 5858, visit http://127.0.0.1:8080/debug?port=5858 in your browser. Once it loads, click the "Resume script execution" button (it has a ▶︎ icon) to let FastBoot continue loading.

Assuming your app loads without an exception, after a few seconds you will see a message that FastBoot is listening on port 3000. Once you see that, you can open a connection; any exceptions should be logged in the console, and you can use the tools you'd expect such as console.log, debugger statements, etc.

Note Regarding Node 6.4 and above

If you're using Node 6.4 and above, there's currently an open issue involving node-inspector. A fix has been committed, and an updated version just needs to be published to NPM. Until an updated version of node-inspector is published to NPM, you can clone node-inspector locally, and call the appropriate script. For example, instead of calling node-inspector, call [path to node-inspector]/bin/inspector.js

Incompatibility With node --inspect

Node 6.3 is the first version to offer support for the --inspect flag. This is intended to replace usage of tools like node-inspector. However the --inspect flag isn't suitable for debugging FastBoot apps because Node does not support debugging of code in a vm module context (see this issue), which is a module that FastBoot utilizes.

Because of this, it's recommended to use either node-inspector, or Node's out-of-process debugging utility, until a version of Node is released that resolves the above-mentioned issue.

Tests

Run the automated tests by running npm test.

Note that the integration tests create new Ember applications via ember new and thus have to run an npm install, which can take several minutes, particularly on slow connections.

To speed up test runs you can run npm run test:precook to "precook" a node_modules directory that will be reused across test runs.

Debugging Integration Tests

Run the tests with the DEBUG environment variable set to fastboot-test to see verbose debugging output.

DEBUG=fastboot-test npm test