Migrating Vue.js from Asset Pipeline to Webpack

Vue and Webpack and Rails…Oh My!

Where I work we use a fairly standard Rails stack. That is to say we use Rails + Javascript (Vue.js) + SCSS + HAML to create many of our web platforms. This uses the Asset Pipeline to preprocess, compile, and minify all of our assets.

Historically this has also posed a number of challenges, such as:

  • Maintenance and versioning of 3rd party libraries.
  • Tight coupling to Rails via .erb and .haml files.
  • The need to use a lot of global variables in javascript (think window.var).
  • Inability to use single file components (.vue) to package our components (template, logic, and styles) into a single concern.

With all that in mind, we were fairly estatic when it was announced that Webpack and Yarn support would be coming to Rails 5.1 via the Webpacker gem. With this we would be able to:

  • Use Yarn to manage 3rd party dependencies.
  • Start to migrate to .vue files for our components thanks to Webpacks preprocessors.
  • Start to use ES6 module system and migrate away from using global variables.
  • Have much greater control over our asset pipeline via webpack loaders.

We have just started the journey to migrate, and this will be the first in a series of blog posts detailing our path. With that said, let’s begin!

Note: All of my Javascript examples are going to reference Vue.js since thats what we use.

Asset Pipeline vs Webpack: Round One…FIGHT!

From Rails Guides:

The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages and pre-processors such as CoffeeScript, Sass and ERB. It allows assets in your application to be automatically combined with assets from other gems. For example, jquery-rails includes a copy of jquery.js and enables AJAX features in Rails.

From Webpack concepts

webpack is a module bundler for modern JavaScript applications. When webpack processes your application, it recursively builds a dependency graph that includes every module your application needs, then packages all of those modules into a small number of bundles — often only one — to be loaded by the browser.

Those sound pretty similiar and indeed when it comes to compression and minification they pretty much produce the same results. Where asset pipeline falls short and webpack shines is when it comes to working with new things being introduced in the latest versions of javascript and its frameworks. Top of mind are ES6 modules, the aforementioned single file components, and loaders.

Single File Component

Single file components allow us to take the template, logic, and styles for a given peice of functionality, or component, in our application and combine them into one file. This collocation makes the component more cohesive and maintainable.

Modules allow us to export bits of functionality and import them later on when we want them. In many cases this eleminates the need for global declerations, allowing us to be much more selective about what scope things are loaded in.

Loaders allow you to preprocess files as you load them. This is what allows us to do things like transform a .vue file into html, js, and css static assets.

Plan of attack…

No Bothens where hurt during the making of this blog post.

The first thing we did was make a plan of attack. We sat down and looked at the state of our front end, and realized that this migration would have to happen in phases as it would be way too costly and unmanageable to do all at once. The phases we decided on were:

0. Installation and setup.

  1. Install core libraries and app bootstrapping.
  2. Get JavaScript tests working.
  3. Server configuration.
  4. Core components, app utilities, and installing secondary libraries.
  5. Secondary componants, mixins and filters
  6. Global CSS.
  7. Cleanup of any left over unnecessary things.

The basic idea here was to break up the work into multiple steps that could be further defined, distilled and completed in managable chunks over time by multiple devs. This lets us spread the knowledge as well as minimize impact on implementation and execution of new features on the project.

Phase 0

0. Installation and setup.

You might be thinking “Well duh!” but I like to be thorough. This also allows us sometime in-house to make sure no new dependencies break a currently-functioning app. Things we will need to do:

  1. Install Yarn. If you are using Homebrew run: brew install yarn which will also install node if you don't already have it. Alternativly if you are using nvm to manage node you can run brew install yarn --without-node to skip node.
  2. Install the Webpacker Gem by adding it to your Gemfile and running:

Let’s take a moment and look at what we have done so far. First, we installed Yarn which is a JavaScript package manager. Yarn is pretty cool because it gives us a quick, secure, and reliable way to install and manage JS packages across the app. Check out the docs to read more and familiarize yourself with it.

Second, we installed the Webpacker gem. This installs Webpack and also does a few other things of note. First you will notice that it created a file called config/webpacker.yml. This is what tells webpack how to configure itself. If you take a gander in this file you will see

  • source_path is going to tell Webpack which directory it should start in. Notice that by default, it starts at app/javascript which is different from Rails's typical app/assets/javascripts. This is better to help define what webpack will handle vs asset pipeline.
  • source_entry_path is where Webpack looks for its entry points for the app.
  • public_output_path we will also have a new folder called public/packs. This is where webpack will deposit files once it has finished bundling all the assets together.

Feel free to change any of these values for your specific needs — I just left them default for now. Everything else in the file is pretty standard setup stuff and should be customized accordingly. Also head over to the docs to learn more.

You will also notice that we have a new folder structure under app/javascript/packs. As noted earlier, everything in here Webpack will add to its dependency graph, run through any relvent loaders and plugins and then deposit the results in public/packs. Right now there is just a file called application.js in there but we will add to it shortly.

The last thing I want to point out is a new file called yarn.lock in the root of the project. Think of this file like you do the Gemfile.lock. In fact, it's the same thing for javascript. This is one of the beautiful things about Yarn: it lets us lock our JS dependencies versions just like you would a gem!

Phase 1

Install Core Libraries — Pt. 1

1. Install Core libraries and app bootstrapping.

When I say Install Core libraries, I mean any JS library that is required for your application to function properly. Since we are a Vue app for us that means:

  1. Vue
  2. Vuex
  3. VueRouter

You might be saying to yourself, “There is no way that’s all the JS libraries you’re using in your app…” and you would be correct! We certainly use jQuery, lodash, and others that we are going to keep in the Asset Pipeline for now. Remember, baby steps… 😃

Now, let’s get to it. First, we are going to install our framework via the rails webpacker command. Webpack comes with several integrations for React, Angular, Vue and Elm. Since I am using Vue I ran:

From the documentation this command

… will add Vue and required libraries using yarn plus any changes to the configuration files. An example component will also be added to your project in app/javascript so that you can experiment with Vue right away.

After that run yarn to get the rest of what we need.

Tip: If you are unsure what the name of a package might be you can always head over to Yarn’s package search to look stuff up!

Sweet! Now if we look in app/javascript we should see a directory structure like:

I am going to skip over application.js and app.vue right now and focus on hello_vue.js because it has things in it that are going to be important to us.

App bootstrapping — Pt. 1

If you crack open hello_vue.js you will be greated by:

That first chunk at the top is a great way to test out that things are working properly. First, pop open wherever you are loading head (ours is in a file aptly named head) and add

to it. Once that's done, go and launch your dev server and you should see:

appended to the bottom of your app. Opening up app.vue and adding a background color, like purple, to the styles and refreshing the page should yield:

Dope! Webpack is working properly! Now, all thats happening here is that we are creating, mounting, and appending a new Vue app to the end of the page. As the comment says though, we wont be able to target elements in our already existing app. This isn’t very useful to us. Reading further though we see that there is a different way to do things; a way that lets us interact with our current app! Let’s check it out.

Lets just get rid of that top portion — we don’t need it. Actually, we will only want to keep the lines starting at import Vue from 'vue/dist/vue.esm' to the bottom of the file. When you are done, things should look like:

Now if we load up our app we should see…nothing. Hello Vue is gone and checking the console we see an error [Vue warn]: Cannot find element: #hello. This makes sense, since we are no longer appending the app to the end of the document. Go ahead and add:

on the landing page of your app and refresh. Oh look:

A quick note here: Remember that we added <%= javascript_pack_tag 'hello_vue' %> to the head of the project. At this point you will want to make sure this loads AFTER asset pipeline so we can find the apps elements.

For us that meant placing that line in our footer under = javascript_include_tag “application-foot” which is where we load in all our asset pipeline stuff.

Good stuff! But we still have a problem…while we are certainly targeting an element in our application to mount the Vue app too, we now have two apps running. If you have the Vue browser tools you can open it up to see the evidence of this.

To fix this we need to do a couple of things.

App bootstrapping — Pt. 2

First lets do some cleanup and renaming.

  • In app/javascript/packs we can get rid of application.js. We wont need it.
  • Rename hello_vue.js to main.js (Remember to change the javascript_pack_tag and stylesheet_pack_tag too!). This is going to become the new place we will bootstrap the current app.
  • Head back to where you placed the hello div and remove it.

Finally lets make a new folder app/javascript/src/components and place app.vue(updating the import in main.js accordingly) in it. Our new structure looks like:

Now lets tackle the next step: migrating our actual app! Open up whatever file you do this in currently (for us it’s app/assets/javascripts/main.js.erb). Take the part where you bootstrap the new app (Vue: new Vue({})) and move it to our new main.js.

Next lets migrate over the store and router. Add app/javascript/src/store/index.js. Using the module system we are going to export a function that builds our store.

For the router we are going to do the same thing. Add app/javascript/src/router/index.js

Notice the global window.app. This is a hold over from the way we used to do things. By making sure the asset pipeline files are loaded first I don't have to make huge changes to the current architecture of the app to migrate this over yet.

In app/javascript/packs/main.js import the router and store

Almost done, just a few things left to do. Next, we are going to want to clean up the requires for Vue and pals.

Oh bother…We will also need to tackle any dependencies like vue-multiselect. Yarn + imports to the rescue!

yarn add vue-multiselect

This is what it should look like:

We also declare some global components, filters, and mixins in main.js.erb and run some other setup tasks that I am going to move over too making this new main.js the one place we do all of our setup. By the end we will have something that looks like:

And finally lets change app.vue. I'll rename it to example.vue and edit its contents like so:

Once that’s done we add it to main.js like so:

And this will prove that the old world and the new world are working together. I added a property to the store called exampleText = "Asset Pipeline + Webpack == Win!" which we are accessing from this component.

Reload the app and voilà:

And that’s pretty much all there is to it. Now we have an app that is running both Asset Pipeline and Webpack. We are serving up the core bits of the app via Webpack and letting AP handle the rest and have a .vue templated component rendering inside the app. This means that moving forward, work can begin on the rest of the phases.

You’re probably wondering…what about tests? We are still working on that part and will have something out about it soon. In the mean time check out the testing docs.

There is also still a lot of work to get everything else up and running to stay tuned as we detail a journey to this new frontier!