Sharing the Code

Programming stuff that might be useful to others

Ember CLI and Rails in Production

I originally had my Ember app inside Rails and used Rail’s asset pipeline. I recently decided to migrate the Ember app out of Rails and into Ember CLI, which is an Ember-specific build environment. One major advantage of separating them is that deploying changes to the Ember app doesn’t require a restart of the Rails server. Below I outline how to get them working together in production.

Dynamic Image Names

In your templates and JavaScript/CoffeeScript files as long as you refer to images like image_name.png, Ember CLI in production will rename them to the fingerprint versions. But if you use dynamic image names i.e. variableImageName+".png" you will have to figure out the fingerprinted image name yourself. The module that handles fingerprinting – broccoli-asset-rev can generate an asset map if you tell it to. This is a JSON file that maps the non-fingerprinted assets to fingerprinted versions.

ember_app/Brocfile.js:
[sourcecode language=”javascript”]
var app = new EmberApp({
fingerprint: {
generateAssetMap: true
}
});
[/sourcecode]

Next we load the assetMap file using an initializer.

ember_app/app/initializers/assets-map.js:
[sourcecode language=”javascript”]
import Ember from “ember”;

var AssetsMap = Ember.Object.extend({
resolve: function(filePath) {
var path;
if (this.assets != null) {
return “/”+this.assets[filePath];
} else {
return “/”+filePath;
}
}
});

export default initializer = {
name: ‘assetsMap’,
initializer: function(container, application) {
application.deferReadiness();
assetsMap = AssetsMap.create();
promise = new Ember.RSVP.Promise(function(resolve, reject) {
return $.getJSON(“/assets/assetMap.json”, resolve).fail(reject);
});
promise.then(function(data) {
assetsMap.assets = data.assets;
}).then(function() {
application.register(‘misc:assetsMap’, assetsMap, {instantiate: false});
// Currently I’m just injecting it into a view called ‘someView’
application.inject(‘view:someView’, ‘assetsMap’, ‘misc:assetsMap’);
application.advanceReadiness();
});
}
}
[/sourcecode]

And then in our code we can resolve the image name:

[sourcecode language=”javascript”]
var imageURL = this.assetsMap.resolve(‘assets/images/’+imageName+’.png’);
[/sourcecode]

Rails Assets

While I use Rails mostly as an API I have a few server-side rendered pages for things like the reset password page linked from an email. You can either change the prefix for Rails assets paths to something like /rails-assets or have the Ember app generate all the assets.

Changing asset prefix in Rails

Unfortunately “assets” is hard-coded into a number of Ember modules so you have to change the prefix in Rails:

rails_app/config/application.rb:
[sourcecode language=”ruby”]
module Webapp
class Application < Rails::Application config.assets.prefix = "/rails-assets" end end [/sourcecode]

Getting Ember to generate Rail’s assets

Create a symbolic link in rails_app/public/assets/manifest.json pointing to ember_app/dist/assets/manifest.json and disable asset compilation in Rails. Make sure there isn’t a .sprockets-manifest-<hash>.json in rails_app/public/assets. Sprockets v3 has changed the name of the manifest file but it’s still able to pick up the Sprockets v2 manifest.json file. To get Ember to generate a Rails manifest file you need to set generateRailsManifest to true and exclude manifest.json in the fingerprint configuration:

ember_app/Brocfile.js:
[sourcecode language=”javascript”]
var app = new EmberApp({
fingerprint: {
generateAssetMap: true,
generateRailsManifest: true,
exclude: [‘manifest.json’]
}
});
[/sourcecode]

Web Server

While I have my Ember and Rails apps in separate repos, I use Nginx to serve both of them. My Nginx (with Phusion Passenger) config looks like the following:

[sourcecode]
root /path/to/rails-app/current/public;
passenger_enabled on;
passenger_ruby /path/to/ruby;

# Rails assets (you don’t need this section if you have Ember generating Rail’s assets)
location ~ ^/rails-assets/ {
expires 1y;
add_header Cache-Control public;
add_header ETag “”;
break;
}

# Ember app
location ~ (^/$)|(^/index.html$)|(^/assets) {
passenger_enabled off;
root /path/to/ember-app/dist;
expires 1y;
add_header Cache-Control public;
add_header ETag “”;
break;
}
[/sourcecode]

You also need to make sure you are using hash location type. In Ember URLs can be in the form /#/path – ‘hash’ or /path – ‘none’. While the later is generally probably better, in our case it would cause the requests to go to the Rails app rather than Ember.

config/environment.js:
[sourcecode language=”javascript”]
module.exports = function(environment) {

if (environment == ‘production’) {
ENV.locationType = ‘hash’;
}

};
[/sourcecode]

Deployment

I use Capistrano to deploy the Rails app. While there are Ember addons for deployment they currently assume more than a basic server setup. So instead I run a Gulp script on my development machine, using gulp-ssh to pull from the repos and build the Ember app in production mode. If you have Ember generating Rail’s assets you can have the script create the symbolic link to the manifest file and restart the Rail’s server so it picks up the new manifest file.

gulpfile.js:
[sourcecode language=”javascript”]
var fs = require(‘fs’);
var config = require(‘./gulp.json’);
// I don’t add gulp.local.json to the repos
var localConfig = require(‘./gulp.local.json’);
var gulpSSH = require(‘gulp-ssh’)({
sshConfig: {
host: config.host,
username: config.username,
privateKey: fs.readFileSync(config.privateKeyPath),
passphrase: localConfig.passphrase
}
});

gulp.task(‘default’, function(){
gulpSSH.shell([
“cd “+config.projectPath,
“git pull”,
“npm install”,
“bower install”,
“ember build –environment production”
“if [ ! -h “+config.railsProjectPathBase+”/shared/assets/manifest.json ]; then ln -s “+config.projectPathBase+”/dist/assets/manifest.json “+config.railsProjectPathBase+”/shared/assets/manifest.json; fi”
“touch “+config.railsProjectPathBase+”/current/tmp/restart.txt”
]);
});
[/sourcecode]

Comments are closed