Skip to content

Instantly share code, notes, and snippets.

@KReden

KReden/blog.md Secret

Last active October 3, 2017 04:54
Show Gist options
  • Save KReden/2b00393d0dc654b9ec644f737273eb3f to your computer and use it in GitHub Desktop.
Save KReden/2b00393d0dc654b9ec644f737273eb3f to your computer and use it in GitHub Desktop.

Asset Pipeline and Webpack, living together in perfect harmony: A Vue.js example.

At Outcome Health 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:

  • Maintiance 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 to migrate away from using global variables.
  • Have much greater control over our asset pipeline via webpack configs.

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, but this should apply for most other frameworks as well.

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 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...

IMAGE ALT TEXT HERE

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 unmanagable 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

Lets take a closer look at the phase 0:

  1. 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 https://gist.github.com/73a635f89adfd19788992fdcd022193e

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 https://gist.github.com/2d52adb78b784e6301745d87d8cf9edb

  • 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 for 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... :D

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:

https://gist.github.com/728936e7581fa4bc4d49e123ab17972a

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!

https://gist.github.com/0e97c2e42f08f8abbb9d6a766ae96c9a

Sweet! Now if we look in app/javascript we should see a directory structure like: https://gist.github.com/4ee06ca44495700240d41aacdc361dd5

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:

https://gist.github.com/2a7486b79caac48325e387722cbb22de

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 <%= javascript_pack_tag 'hello_vue' %> and <%= stylesheet_pack_tag 'hello_vue' %> to it. Once that's done, go and launch your dev server and you should see:

hello_vue.png

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:

hello_vue_purple.png

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.

First, 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:

https://gist.github.com/c3455a0d1d0582190640ab99de32babc

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. Lets go ahead and add:

https://gist.github.com/9087c1ac3b72ce61e3269a959ed21bf4

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

hello_vue_purple2.png 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.

Good stuff! But we still have a problem...while we are certianly 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.

2_vue_apps.png

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
  • 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/javascriptsrc/components and place app.vue(updating the import in main.js accordingly) in it. Our new structure looks like:

https://gist.github.com/a602ebcd52e3b43bfa4fb22d1dc1c401

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.

https://gist.github.com/7b11661f87449fa500716c2fd065ce00

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.

https://gist.github.com/bb2d8c4bd3631cd9cfded22604bfde67

For the router we are going to do the same thing. Add app/javascript/src/router/index.js https://gist.github.com/10063ca296b7517bb886697da9f95468

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 https://gist.github.com/108b3ce84607d0402d6cee7aea8d721f

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

requires.png

Oh bother...We will also need to tackle any dependencies like vue-multiselect. Yarn to the rescue! This is what it should look like:

https://gist.github.com/b4a8abdb5e5c6b1243efa17f8a4a98d2

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 so this new main.js is the one place we do all of our setup. By the end we will have something that looks like:

https://gist.github.com/760f912e5595f4e8397bfe7332f37549

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

https://gist.github.com/dfa0761d65ec39e7ae6cabdea22a8f18

Once thats done we add it to main.js like so: https://gist.github.com/31ec10238fe2369d768aa15d273bc64e

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à:

requires.png

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment