Rails + Webpacker + Vue.js

20th May 2017 – 1874 words

Ruby on Rails ships with it’s version 5.1 with “webpack and yarn” support. Reading the associated PRs (#1 #2, the wrapper is relatively thin. Most of the work is done by Webpacker, which can also be used with older versions of Rails.

For one of our products, we decided to revamp our report & chart UI (kanaleo.de - our tool for applicant surveys). Having a little bit of experience with Vue.js (which is fantastic by the way, but not the main topic of this post), we decided to run with a single page app. To leverage all of the cool features (Single Page Components, EcmaScript Next) and many other third party libraries (Highcharts, D3, Leaflet, etc.), we also felt the urge for a better build system. Using the asset pipeline with wrapper gems (or Rails-assets.org for that matter) works mostly good, but often has caveats, especially when building SPAs.

Having Webpack integrated into the Rails app and build process sounded particularly interesting.

Webpacker usage

The Webpacker Gem is currently in flux, so it is recommended to follow git master at the moment.

  • Webpacker uses Yarn package manager under the hood, which should be exist on your system before. There are packages for all kinds of systems, but in last resort you can just npm install -g yarn.
  • Install Gem gem 'webpacker', github: 'rails/webpacker'
  • Run generator ./bin/rails webpacker:install - that will generate a bunch of files, mainly a package.json, yarn.lock, and a config/webpack/ directory.
  • Depending, if you are planning on using Vue, React, Elm or Angular 2, run the respective generator: ./bin/rails webpacker:install:vue
  • if you are using Rails < 5, then the commands could require rake instead of rails

In our case, Webpack generated:

  • a app/javascripts/packs/hello_vue.js this is the entry point for Webpack, comparable with the manifest application.js before. All source files in app/javascripts/packs/* will be generated into public/packs/*.
  • your app.vue, which should live under app/javascripts/APP_NAME/*.{vue,js}

Unfortunately, the build process of Webpack is not integrated into a Rails server process, This is why you need another process running in development, which can either be:

  • ./bin/webpack-dev-server a dev server with hot reloading that will occupy port 8080
  • ./bin/webpack-watcher a simple file watcher without hot reloading

As we are normally working on many apps at the same time and in various network settings, we decided for the second option. If you are doing the same, you need to disable the server in config/webpack/development.server.yml, otherwise your JavaScript packs will not load!

Now you can link your JavaScript inside rails, like

<%= javascript_pack_tag 'app' %>
<%= stylesheet_pack_tag 'app' %>

Browser compatibility Polyfills / IE11

Developing with Chrome and Firefox worked without any major issues; Source Maps worked like a charm and can tell us the correct location of a Runtime error.

But, we also need that app to run on older Browsers, especially Internet Explorer 11. After trying a lot of things we came to this solution for us.

Modified our .babelrc and installed yarn add babel-plugin-transform-runtime.

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": "> 1%",
        "IE": 11,
        "uglify": true
      },
      "useBuiltIns": true
    }]
  ],
  "plugins": [
    "transform-object-rest-spread",
    "transform-es2015-arrow-functions",
    "transform-runtime"
  ]
}

For Vuex, we also needed another polyfill, Promise, which we fixed by adding yarn add es6-promise and requiring that in our pack-file (e.g. my_app.js):

require('es6-promise').polyfill();

Another very specific problem was with Vue-strap, as Webpack didn’t transform the imported files from node_modules (Issue here wffrance/vue-strap/issues/69).

Production/Build gotchas

You need to have Yarn and a recent NodeJS installed on the system that builds the assets.

If you having a Capistrano-like deployment (current/shared/repo - we are using a Dresden-Weekly Ansible version) with symlinking and Git archive export, you need to make sure:

  • Export all the new files: yarn.lock, package.json, .babelrc, .postcssrc.yml
  • Symlink the public/packs folder between deployments

Apart from that, there are no other build steps - Webpacker hooks into the assets:precompile task and generate the packs.

Appendix: Getting Started with vue

As we developed our app, we added more and more dependencies, such as Vue-Router, VuexStore, or Moment-js. Just use yarn add vue-router etc. to add dependencies without having to modify package.json. Your app then needs to import (or require) the relevant parts

Here a stripped down index.js from our app:

# app/javascripts/my_app/index.js
import VueRouter from 'vue-router';
import * as Vue from 'vue/dist/vue.common.js';

// Some Highcharts imports + Config
import VueHighcharts from 'vue-highcharts';
import Highcharts from 'highcharts';
import more from 'highcharts/highcharts-more'
import highchartsMore from 'highcharts/highcharts-more';
import solidGauge from 'highcharts/modules/solid-gauge';
highchartsMore(Highcharts);
solidGauge(Highcharts);
Vue.use(VueHighcharts, { Highcharts });

// adding our store and router
import store from 'my_app/store/index';
import router from 'my_app/router.js';

// attach and start the app, if there is a #app element

import App from 'my_app/component/App.vue';

document.addEventListener('DOMContentLoaded', () => {
  var i, element;
  var elements = document.querySelectorAll('#app')
  for (i = 0; i < elements.length; i++) {
		// you might want to pass some initial state, like currentUser, given params etc via data attributes
    const user = JSON.parse(element.dataset.currentUser)
    store.commit('addUser', user)
    var app = new Vue({
      el: element,
      template: '<App/>',
      router: router,
      components: { App },
      store: store,
    })
  }
})

Also, some examples for a router.js and store.js:

// app/javascripts/my_app/router.js

import VueRouter from 'vue-router';

import IndexRoute from 'report/routes/IndexRoute'
import DetailsPage from 'report/routes/DetailsPage';

const routes = [
  { path: '/', redirect: { path: '/mix/index' }},
  { path: '/start/index', component: IndexRoute },
  { path: '/:someParam/', component: DetailsPage, props: true }
]

const router =  new VueRouter({
  routes // short for routes: routes
})
export default router;
import * as Vue from 'vue/dist/vue.common.js';
import Vuex from 'vuex/dist/vuex.esm';
Vue.use(Vuex)

export const store = new Vuex.Store({
  state: {
    // initial state
    filter: {
      location: null,
      query: null,
      radius: 30,
    },
    currentUser: null
  },
  mutations: {
    addUser(state, user) {
      // gotcha: you need to copy over deep objects when assigning into store
      // we ran into this issue, as our view was not updated when we changed
      // store attributes, because we are passing modified nested filter objects around
      //  you can use the {...OBJECT} es7 operator or array.concat for copying.
      state.currentUser = {...user}
    },
  }
})

One of Vue’s core feature are the single file components, that a really dig. You can also use Pug (template language like Slim/HAML) or SCSS directly:

<template lang='pug'>
  div
    .row
      .col-sm-6
        h3 Hello World
      .col-sm-6
        span 
</template>

<script>
export default {
  data() {
    value: 1
  }
}
</script>

<style lang='scss' scoped>
$color: #123456;
</style>

Appendix II: Why I like Vue

Why not React or Angular2? I particularly like:

  • Stability - Upgrade from 1 to 2 was mostly straight forward (Looking at you Angular or React version 0.X)
  • Third party libs:
    • important packages also maintained by the core team. This guarantees a set of dependencies, that work good with each other. (Vuex Store, Vue-router, Vue Dev Tools for Chrome, Vue-CLI for stand-a-lone projects)
  • Single File Components - Putting local styles, templates and script is not a novel idea - React does similar. But Vue is using this on HTML-level, so there is no workaround syntax to piggyback HTML/CSS over JavaScript (class vs React’s className)
  • Flexibility
    • Scope: We use Vue both for a small snippet of JavaScript on one app, and for a full blown component/router based app
    • Usage: You can use Vue almost 1:1 like React; mostly stateless functional components with a Flow architecture and immutable a store pattern. You can also use Vue like a drop-in-replacement for Angular 1. Some “Controller” with state and methods.
  • Ease-of-learn, esp. when coming from non-functional background. I feel, that the many used functional patterns of React can be overwhelming for programmers with a OO background. Having to explain reducers, thunk, saga, immutability, currying to colleagues can be a bit overwhelming. This is of course subjective, but for me Vue feels very easy too learn. All the bits and pieces fit together nicely and made me an immersive learning possible.

In conclusion, I feel Vue is the best of both Angular (1) and React together as one nicely build package. If you haven’t, try it :)