Gemify Your Client-Side App for Rails
Lawson Kurtz, Former Senior Developer
Article Categories:
Posted on
Need a client-side app in a Rails project? Build it separately and include it as a gem!
If you've ever worked with a Rails app before, you've already worked with gemfiied assets.
gem "jquery-rails"
is a staple of nearly every Gemfile. But while gemification of assets is traditionally restricted to common libraries like jQuery, there's no reason you can't use it for your own custom assets. In fact there are several compelling reasons to do so when you need to include a more complex client-side application into Rails.
What exactly is a gemified asset?
A gemified asset is an asset that is included into a Rails application via a gem. Gems make dependency inclusion and versioning incredibly easy. By wrapping assets in a gem, they instantly benefit from this simple dependency management.
Gemifying an Client-Side App
Make your app.
First, you create your client-side app in isolation, with it's own unique repository, and using whatever build/test systems fit the needs of that particular app. Keeping this client-side app separate from the Rails app means we aren't coerced into using the asset pipeline in weird ways just to be able to use modern front-end development tooling. You just build the app exactly how you want to.
Add a gemspec
.
Once the client-side app project is established, you'll need to add a gemspec
file. This is essentially a gem's equivalent of a JavaScript project's package.json
. In fact you likely already have a package.json
in your project already, and can choose to pull information from that file directly into the gemspec
file if you'd like. (I've included an example of how to do this in the lib/gem/version.rb
snippet a bit further down this page.
# your-project.gemspec
require File.expand_path('../lib/gem/version', __FILE__)
Gem::Specification.new do |spec|
spec.name = "your-project"
spec.version = YourProject::Rails::VERSION
spec.authors = ["Jane Doe"]
spec.email = ["jane.doe@viget.com"]
spec.summary = "I'm a client-side application!"
spec.description = "I'm a better description of this client-side application!"
spec.homepage = "https://github.com/you/your-project"
spec.license = "UNLICENSED"
spec.files = Dir["{lib}/**/*"]
spec.require_path = "lib"
spec.add_dependency "railties", "~> 3.1"
end
Add Rails engine registration.
Next you need to add two simple ruby files. The first will register your gem as a Rails engine, which is necessary for Rails to be able to include assets from your gem into the asset pipeline. Registration is as simple as creating a subclass of ::Rails::Engine
.
# lib/gem/engine.rb
require File.expand_path('../version', __FILE__)
module YourProject
module Rails
class Engine < ::Rails::Engine
end
end
end
In addition to registering your gem as an engine, you'll also need to define a gem version. This will be used by Rails when versioning your assets through the asset pipeline. To reduce duplication, I'd suggest pulling the version directly from package.json
so you don't ever have to update your version in two places.
# lib/gem/version.rb
require 'json'
package_info = JSON.parse(File.read(File.expand_path('../../../package.json', __FILE__)))
module YourProject
module Rails
VERSION = package_info["version"]
end
end
Update your build.
Rails will look for assets from registered engines (like your gem) at three different paths: app/assets
, lib/assets
, and vendor/assets
. Update your build to place production built assets into one of these locations. (We prefer to put our custom assets in lib/assets
as we use the app
directory exclusively for client-side app related source code. Following Rails asset pipeline convention, JS files should be dropped into lib/assets/javascripts
, and CSS files should be dropped into lib/assets/stylesheets
.
The method for moving built versions of your assets into these directories will be entirely dependent on you build system. We prefer simple builds with Make, so we simply define an additional task for clearing out the old assets and replacing them with copies from our standard build output directory.
GEM_ASSETS := ./lib/assets
gem:
rm -rf $(GEM_ASSETS)/{javascripts,stylesheets}/*
cp $(OUT)/*.js $(GEM_ASSETS)/javascripts/
cp $(OUT)/*.css $(GEM_ASSETS)/stylesheets/
Build and push the gem.
With your gemspec
, Ruby files, and built assets in place, it's time to build the gem.
gem build your-project.gemspec
You may find it useful to establish a dedicated npm script for performing all the tasks necessary to build new versions of the gem. Ours looks like:
"scripts": {
// ...
"gemify": "make all && gem build your-project.gemspec"
}
Once the gem is built, you can push it to RubyGems (or another gem repository of your choice), or simply commit it to source and push the whole project to GitHub if you'd like to serve it from there.
Including Your Gemified Asset in Rails
This is the easy part! Simply add your gem to your Gemfile
.
# If your gem is hosted by RubyGems:
gem "your-project"
# If you'd like to pull in your gem from a public GitHub repository:
gem "your-project", github: "you/your-project"
# If you'd like to pull in your gem from a private GitHub repository:
# (First make a user that only has read access to the private repo, and grab an OAuth token to use in the url below.)
gem "your-project", git: "https://abcdefghijklmnopqrstuvwxyz:x-oauth-basic@github.com/you/your-project.git"
# If you'd like to pull in your gem from a local directory (useful for testing):
gem "your-project", path: "/path/to/your-project"
Then add your specific assets to the manifests in application.js
and/or application.css.scss
.
/* application.js */
//= require jquery
//= require your-project
/* application.css.scss */
/*
*= require jquery.ui.slider
*= require your-project
*/
Voila! You now have access to your gemified assets in Rails.
Updating Gemified Assets
From here, you can push new releases of your asset anytime you want to update the client-side app.
To push a new release, make whatever changes you'd like, increment the version number in package.json
, run your gem build command (e.g. npm run gemify
), and push it to your gem repository.
On the Rails side, you can choose to include a specific version of the gem in your Gemfile, or you could update to the latest available version via bundle update --source your-project
. Since Rails asset versioning is tied to your gem version, you never have to worry about cache invalidation. You're done.
Pros & Cons
So why would you want to gemify an asset in the first place? And what are the caveats?
Pros
The huge advantage of gemification for client-side apps is that separation allows them to be treated as proper applications, not just side-kicks of a Rails application. This means your client-side app can have its own repo, with its own contributors, working with its own (better) build tools, protected by its own test suite, and with its own, isolated deployment process. In most cases, the concern addressed by the client-side app is completely distinct from the main concerns of your Rails app, so it makes technical sense to isolate these concerns from one another.
While there are many ways to separate a client-side app from a Rails app, gemification is probably the easist to manage because of how easy gems make deployment and versioning. I dare you to find an easier, more reliable, and more flexible way to include external assets into Rails.
Cons
Joining separate systems is inherently complex. If your needs are simple, or are well met by the asset pipeline, splitting your asset into a separate gem probably won't be worth the squeeze.
Sharing code also becomes more complicated when you separate concerns. Some extra though is required about what belongs where, and how best to share common logic like Sass color values, etc.
Finally, with separate build systems in place for various assets, it's possible to include multiple versions of a shared library that otherwise should have only been loaded once. If this is a concern, consult your build tool documentation about how to construct builds to rely on shared, external libraries.
Summary
So there you have it. We've enjoyed including client-side apps into larger Rails applications via gemification. Hopefully you will too! Let us know what you think in the comments below.