How we migrated from Rails to ember-cli…incrementally

One for our tech customers... We want to port the front end of our large Rails app to ember-cli. We don’t want to do a big bang rewrite. We want to incrementally port features, and release them to production as we go. We want the ported features in ember-cli to run alongside the yet to be ported features that still live in Rails, in what seems to be one seamless application. This is how we are achieving that.

Go to the profile of Aaron Chambers
May 01, 2015
3
0

This article was originally posted on Medium on April 25, 2015.

The goal is to have the Rails app and ember-cli running together seamlessly with zero, or close enough to it, duplication of code/css, until such point as the front end has been completely moved into the ember-cli app.

So, the app I’m currently working with is a pretty beefy Rails app. It has been around for 2 years and has quite a bit of functionality. rake routes shows 346 different routes. Now, maybe not all of them are in use, but you get the gist. It’s a complex application with a lot of functionality.

As well as being a large Rails app, there is some ember functionality on the site, through the use of ember-rails. So, moving this all out into one integrated ember-cli seems like the right thing to do.

The key thing with this piece of work for me is that we migrate this app incrementally. No development for 6 or 12 months with a big bang release at the end. No, “click here to try out our new version” links on the site. The users continue to use the site and are none the wiser to the work we are doing. We port features over, one at a time, and the ember-cli app is integrated into the Rails app, seamlessly.

Initial thoughts might be to use ember-cli-rails. However, I’m not really that interested in that idea. A lot of awesome work is going into ember-cli every day. And I don’t want to rely on another abstraction on top of that to allow me to use it in Rails. I also like the idea of the front end app being a separate repo and project from the API altogether. In short, I think node and ember-cli are awesome, and I think we should use them.

So, I let my brain run loose for a while.

  • How do we do this?
  • How can we pull the ember-cli app into the Rails app, while still allowing the developer to maintain the slick ember-cli workflow?
  • How can we do this with the ember-cli app being a separate app and repo?
  • How do we not duplicate css?
  • How do we only boot the ember app for certain Rails routes?
  • How do we only boot the ember app for certain clients before fully turning the Rails functionality off?
  • How do we accurately reflect what the ember-cli app will look like when served from Rails, while running it on the ember-cli dev server?
  • How do we see what the ember-cli app will look like when served from Rails, in the development environment?
  • How do we actually get Rails to serve the ember-cli app?

So…lots of questions…and lots of answers to find. So I started spiking.

A view from above

The high level idea of what I was aiming for was to allow developers to build an ember app using ember-cli and the great developer workflow it provides. To allow them to port features over from the Rails app to ember-cli. Then to have the Rails server serve the ember-cli app for the ported routes, but continue with the existing Rails functionality for the yet to be ported routes. And eventually to have the front end app be solely an ember app in it’s own repo while the Rails app became solely the API.

For the interim, until the app is fully ported, the plan is to have Rails handle the header and footer as it currently does and then have the ember app anchor on a div in the middle. So, the ember-cli app will literally be the functionality that exists between the header and the footer for the time being.

Styling

One of the main questions in my mind was “how do we handle the css?”. Ideally we don’t want to be duplicating any css. Obviously, we want to be porting over any css specific to the feature that we’re porting. But css that is common to the wider app, and not just a particular feature, really needs to stay in the Rails app.

If we think of how this will work in production, Rails will be pulling the ember app from some location and serving it within the server generated web app. So, the styles will already be available and be applied to the ember app.

But in development, that is a different story. In development, the app will be running on http://localhost:4200 and for the most part we won’t be viewing it within the context of the wider Rails app. The way we have approached this is, when running in development mode, we inject a couple of stylesheet links into the index.html that point to the styles served by the development Rails server.

This does mean that the Rails server also needs to be running when developing the ember-cli app but I think this is a compromise we’re happy to live with.

In order to inject these links, we wrote a small ember-cli addon that uses the contentFor hook to inject the html into the {{content-for ‘head-footer’}} tag.

The addon looks into the app config for a list of stylesheet urls that need to be injected:

// config/environment.js
if (environment === ‘development’) {
  ENV.linksToInject = ['http://some-stylesheet/link.css'];
}

Then the contentFor hook in the addon will inject these links into the index.html:

// ember-cli-style-injector/index.js
contentFor: function(type, config) {
  var links = config.linksToInject;
  if (type === 'head-footer' && links && links.length) {
    var content = [];
    links.forEach(function(link) {
      content.push('<link rel="stylesheet" href="' + link + '">');
    });
    return content.join('\n');
  }
}

Now that we have links to the Rails served css files, our ember-cli app can use them in development. To top it off, we just copied the hard coded html for the header and footer into the index.html file to give the app the accurate look and feel of the existing app.

It is worth noting that index.html file will not actually be used outside of development. This is why we can hardcode the header and footer in to it. See the Deployment section for more details on this.

Root element

As the ember-cli app will be served from somewhere within the existing Rails application, between the header and the footer, it cannot boot itself on the <body> tag of the document as it does by default. Therefore we needed to specify that it anchor itself on some other arbitrary tag.

// config/environment.js
APP: {
  rootElement: '#ember-app-container'
}

We then needed to add this id to a div in the index.html, somewhere between the mock header and footer, and then add it to the same place in the real layout in the Rails app. This way, the ember app will anchor itself correctly in both development and production.

Serving the ember app, from Rails

The big question here was, how are we going to serve the ember-cli app from Rails. And how would we conditionally do this depending on which features had been ported and which hadn’t.

The high level idea was to be that for routes that have been ported, Rails would retrieve the urls that point to the ember-cli built assets (css and js) and then merge them into the Rails layout. When that layout was rendered in the browser, it would pull the ember assets down and boot the ember app on the rootElement mentioned above.

That was the idea. But how were we going to go about it? How would Rails know what the ember assets were and where they lived? In order to answer these questions, we need to digress slightly to look at how we would deploy the ember app.

Deployment

In short, we are using ember-cli-deploy to deploy the ember-cli app. That was a no brainer. The interesting part comes when you look at what we were deploying.

The idea here is that the ember-cli app is firstly built, resulting, essentially, in a bunch of assets (css, js, images etc) and an index.html file, which is used to boot the app.

The assets would then be pushed to S3/Cloudfront where they will be accessed by the Rails server.

The index.html file, however, is then parsed and all the interesting stuff inside is turned into a config like JSON string and pushed to Redis. This JSON config, with all the links to the assets, is what Rails will retrieve when it’s time to serve the ember app.

The first cut of this JSON config looks something like this:

{
  "base": [{ href: "/" }],
  "link": [{ href: "http://some-url/app.css", rel: "stylesheet"}],
  "meta": [{ content: "config", name: "myapp/config/environment"}],
  "script": [{ src: "http://some-url/app.js"}]
}

These are the parts of the index.html file that seemed important to us. At this stage I don’t think we’ve missed anything.

Serving the ember app, from Rails…again

Aaaaand, we’re back.

So, what is this all going to look like from the Rails perspective? Well, let’s have a look.

First of all, we wanted Rails to default to the existing functionality, while still being able to request that it serve the ember app instead. We use a query parameter for this. If we append ?enable_ember=true to any Rails url, Rails will render an ember specific layout. But we only want to do this for routes that have actually been ported to the ember app. So we added a method to the ApplicationController called render_ember_if_requestedwhich looks like this:

// app/controllers/application_controller.rb
def render_ember_if_requested(&block)
  if ember_requested?
    @ember_config = ember_config_adapter.config
    render "ember/index", layout: "ember_application"
  else
    block.call if block_given?
  end
end
def ember_requested?
  !!params[:enable_ember]
end

This can be used in any controller action that we have ported to the ember app:

// app/controllers/posts_controller.rb

def index
  authorize! :index, :posts

  render_ember_if_requested
end

So, if we append the ?enable_ember=true query param when going to this route, the ember layout will be served instead of the existing Rails layout.

The @ember_config instance variable is the JSON config data retrieved from Redis, which is merged into the erb layout, adding the link and script tags needed to pull the ember assets into the page:

// app/views/ember_application.html.erb
<% if @ember_config && @ember_config.include?("link") %>
  <% @ember_config["link"].each do |config| %>
    <%= stylesheet_link_tag "#{config['href']}", media: "all" %>
  <% end %>
<% end %>

This is all fine and dandy for production, but what happens if we want to see the ember app, served from the Rails app, in the development environment? We don’t want to have to deploy the ember-cli app to Redis and S3 just to look at it in development.

When in development, Rails generates it’s own version of the JSON config, pointing to the assets served by the local ember-cli server.

Let’s do this

So, everything I’ve mentioned above I’ve spiked out and confirmed works. I’m sure there will be more questions arising as we dive deeper in to this but for the time being I’m pretty confident that this approach will allow us to start developing a separate ember-cli app, independently of the Rails app, yet still have Rails include and serve the resulting ember app. I think we will be able to get the best of both worlds. The worlds were we can make the most of the amazing developer workflow ember-cli provides while still serving the existing app from Rails until such time that all the features have been ported.

Most importantly, and most exciting to me is the fact I think we can do this all incrementally, with out any big bang rewrite and without the user being any the wiser.

If anyone else is wondering about how best to approach incrementally moving from Rails to ember-cli please get in touch as I’d love to chat about your experiences, thoughts or suggestions.

Go to the profile of Aaron Chambers

Aaron Chambers

Developer, Strange Studios

I've been working in technology for the past 15 years. I love to keep abreast of the latest frameworks and languages that help me build better applications, faster. I'm a massive fan of, and contributor to, EmberJS and it's community and ecosystem. I love where the web is heading and I do everything I can to ride the waves and make it a better place and help others to do the same.

No comments yet.