VueCli::Rails

Build Status

Let's make cool boy Vue even cooler on Rails!

Change Log

Installation

Add this line to your Rails application's Gemfile:

gem 'vue_cli-rails'

And then execute:

$ bundle install

Requirements

  • Ruby >= 2.3
  • Rails >= 4.2
  • Node >= 8.9+
  • Optional: yarn

Features

  • Feel free to use yarn or npm.
  • Single vue_entry rather than confusing stylesheet_pack_tag, javascript_packs_tag and javascript_packs_with_chunks_tag.
  • Get all benefits of @vue/cli.

    • Powered by webpack 4
    • DRY: all-in-one configuration file rather than repeating for webpack, eslint and etc.
    • Out-of-box tooling: Babel, TypeScript, PWA, vue-router, vuex, CSS pre-processors, linter and testing tools.
    • Enhanced alias support in jest.config.js.
  • Run webpack-dev-server together with Rails server with development mode.

  • Just single RAILS_ENV, no more NODE_ENV.

  • Rails way configurations in config/vue.yml.

Getting started

Out-of-box workflow:

  1. Make sure you already installed @vue/cli globally via npm (npm i -g @vue/cli) or yarn (yarn global add @vue/cli)
  2. bundle exec rake vue:create and follow the steps.

    Don NOT select In package.json for "Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?". Some functionalities like alias of jest may not work.

  3. Put your JavaScript files under app/assets/vue/entry_points.

  4. Insert your entry point by vue_entry 'entry_point' in views or render vue: 'entry_point' in controllers.

  5. webpack-dev-server auto starts alongside rails server in dev mode.

  6. Invoke env RAILS_ENV=production bundle exec rake vue:compile to compile assets (you still must manually set RAILS_ENV to production).

More settings are available in config/vue.yml

Usage

Core

Concept: Entry Point and File structure

The root path of your Vue assets is app/assets/vue. This gem will generate several folders. However, app/assets/vue/entry_points is the only one matters.

Webpack sees one JavaScript file as the center of a web page rather than HTML. Thus all styles, images, fonts and other assets are related to a JS files by import 'css/png/svg/woff2/json'. Any .js file under app/assets/vue/entry_points will be a entry-point.

Please ONLY puts your entry-point files under app/assets/vue/entry_points folder with .js extension name.

Be aware, .js.erb and .vue.erb are NOT supported. I will explain the reason in Q&A section.

If you are new to modern front-end development, or more specifically with webpack in this case, please read Q&A section for more information.

Helper vue_entry

vue_entry is like javascript_include_tag and stylesheet_link_tag which generates relative assets links for your entry point. (It's like javascript_packs_with_chunks_tag in Webpacker 4. I will explain why it's different in Q&A.)

You may have interest of path alias in config/vue.yml.

Use render vue: <entry_point> in controllers

Usually you only need <div id="app"></div> and vue_entry 'entry/point' to render a Vue page. You can use render vue: 'entry/point' inside your controller.

This method is simply a wrap of render html: vue_entry('entry_point'), layout: true. So you can pass any arguments supported by render including layout.

For example ```ruby # app/controllers/my_vue_controller class MyVueController < ApplicationController layout 'vue_base' def foo render vue: 'foo/bar' end end ``` ```html My Vue
<%= yield %> ``` ```js // app/assets/vue/entry_points/foo/bar.js import Vue from 'vue'; import Bar from '../views/Bar.vue'; Vue.config.productionTip = false; new Vue({ render: h => h(Bar), }).$mount('#app'); ```

Public Output Path

If the default setting vue_assets does not bother you at all, you can ignore this section.

Actually public_output_path in config/vue.yml is very simple - just a sub path under public directory. You might suffer some problems by changing it without understanding how it works:

  • My regular assets no longer work in dev mode server.
  • I lost all my files in public folder. (Using a VCS would get your ass saved.)
  • Where are my compiled assets for prod under public/assets directory?
TL, DR - DO NOT name it as any path used by anything else - It's being used in: - Rails dev server will forward all request under `/#public_output_path` to `webpack-dev-server`; - Webpack will put all compiled assets under `public/#public_output_path`. Unfortunately, it always remove the entire folder before compiling. - Alternative ways - For dev proxy problem: to set different values for `development` and `production` mode in `config/vue.yml`. - For deleting folder when set it to `assets` for prod: run `rake vue:compile[with_rails_assets]` to invoke `rake compile:assets` as well.

Summary

If you still feel confusing, please create a new project and select copy demo code.

I will explain what happens in Explanation by Demo.

Available Settings

General settings file is config/vue.yml

  • manifest_output

Where to put manifest.json which required by Rails production mode. You can set it in development mode for inspection.

All entry-points will be compiled into assets files. Rails needs manifest.json to know what are the files and will serve all its JS/CSS tags.

  • package_manager

Pretty straightforward, which package manager will be used. Valid value: npm or yarn. It does NOT support pnpm or other package managers. You can find the reason in Q&A.

  • public_output_path

Because it is very important I put it in core section.

  • launch_dev_service (NOT available for production mode)

rails server will launch it when starting by default vue-cli-service serve. It will be invoked by npx vue-cli-service serve or yarn exec vue-cli-service serve depends on your package_manager.

  • camelCase settings will be used in vue.config.js

Please see available options.

  • alias

    It's basically resolve/alias for Webpack. However, you don't have to config this settings in .eslintrc.js and jest.config.js again and again. @vue/cli will pass the settings to eslint via its plugin. The configuration for jest will be generated and passed to jest.config.js through vue.rails.js.

Customize your webpack configurations in vue.config.js

Feel free to update vue.config.js by yourself. There are some lines of boiler-plate code to adapt compression-webpack-plugin and webpack-bundle-analyzer.

Rake Tasks

  • vue:create

Install required packages and configurations. You should run this task to get @vue/cli initializing your project.

What it does for you

  1. Select package manager: Y=yarn, N=npm
  - Directly use npm if yarn has not been installed.
  - Prefer yarn by default unless detect `package-lock.json`
  1. Auto install @vue/cli globally with your package manager.
  2. Invoke vue create to initialize Vue project.

    When detected existing package.json

    • Y - Yes: Fully overwrite
    • N - No: Skip
    • A - Auto: You won't loss anything (old_config.merge(new_config))
    • K - Keep: Keep all your settings already have (new_config.merge(old_config))
  3. Install js-yaml and webpack-assets-manifest

  4. Deleting Vue demo code under src folder

  5. Copy demo code to under app folder and update config/routes.rb

  6. Copy vue.rails.js and vue.config.js

  - Do not change `vue.rails.js`! This rake task will always restore `vue.rails.js` back.
  - Yes you can update `vue.config.js`. Just make sure you know what are you won't break the configuration. You can chance `config/vue.yml` instead.
  1. Generate config/vue.yml
  - The convention is: `camelCase` for regular `vue.config.js`, `snake_case` for special usage.
  - You can find a full list of [Vue CLI config options below](#valid-vue-cli-config-options).
  - All available options [here](#available-options)

BE AWARE: the default option for config/vue.yml is Y (to replace existing file), otherwise your package manager change won't be saved. All your files won't be overwritten silently except vue.rails.js.

  • vue:compile

Compile Vue assets. Please specify RAILS_ENV=production to compile assets for production.

Optional argument: [with_rails_assets] to invoke rake compile:assets after it finished.

However, you can invoke vue-cli-service build (if vue-cli-service installed globally, or you can use npx vue-cli-service build or yarn exec vue-cli-service build) with RAILS_ENV=production to build assets.

A good practice is to use cross-env to pass RAILS_ENV=production. So cross-env RAILS_ENV=production vue-cli-service build will work on any platform and shell.

  • vue:json_config

Converts config/vue.yml to JSON to be used by vue.rails.js.

vue.rails.js prefers parsing config/vue.yml with js-yaml. So this is just in case. You may suffer performance issue when your Rails app grow big.

  • vue:support[formats]

Adds template or style language support. Vue ships with supporting pug, slm, sass, less and stylus out-of-box. How ever, you still have to install some loaders manually if you did not select that language with vue:create.

You can add multiple languages at the same time: rake vue:support[pug,stylus]

  • vue:node_env

Adds cross-env and npm-run-all to your devDependencies in package.json, and adds dev, prod, serve and rails-s to scripts as well.

It enables you to start rails dev server alongside webpack-dev-server without pain, and compile production assets.

  # to start `rails s` and `webpack-dev-server` together
  npm run dev
  # or
  yarn dev

  # same as `/usr/bin/env RAILS_ENV=production bundle exec vue:compile`
  npm run prod
  # or
  yarn prod

You can update scripts/rails-s and/or scripts/prod if you need to more stuff:

  {
    "scripts": {
      "rails-s": "cross-env NO_WEBPACK_DEV_SERVER=1 rails s -b 0.0.0.0",
      "prod": "cross-env RAILS_ENV=production bundle exec rake vue:compile[with_rails_assets]"
    }
  }
  • vue:inspect

    Alias of vue inspect, npx vue-cli-service inspect or yarn exec vue-cli-service inspect. Display the webpack configuration file.

You may need to invoke rake with bundle exec. Rails 5 and above supports new rails rake:task flavor.

Migrate from Webpacker

It's very easy to migrate from Webpacker.

  1. Install this gem and bundle install
  2. Install @vue/cli globally then follow the instructions of rake vue:create;
  3. Edit config/vue.yml, set default/entry_path to source_path (by default app/javascript) joins source_entry_path (by default packs);
  4. Change all javascript_packs_with_chunks_tag to vue_entry;
  5. Fix all nonsense xxxx_packs_tag;
  6. If you mind public_output_path and manifest_output you can change them to follow Webpacker values; > I strongly not recommend to put manifest_output.json under public folder;
  7. Update vue.config.js if you have any customized webpack configurations; > You can inspect how webpack settings at anytime
  8. Directly rails s to start dev server; > You can get rid of bin/webpack-dev-server and bin/webpack now. However, still recommend rake vue:node_dev and run yarn dev so it will kill webpack-dev-server properly when your Rails dev server stopped.
  9. Call env RAILS_ENV=production rake vue:compile[with_rails_assets] instead of env RAILS_ENV=production rake assets:precompile to compile all assets for production.
  10. Delete unused Webpacker files
    • bin/webpack-dev-server
    • bin/webpack
    • config/webpack
    • config/webpacker.yml

Strongly recommend to backup your codebase before the migration.

Enjoy Hot Module Replacement now!

Valid Vue CLI config Options

You can check the full list on Vue CLI official website.

  • Special

    • publicPath - see public_output_path
    • outputDir - see public_output_path
    • configureWebpack - vue.rails.js will generate it. entry, output and resolve/alias are heavily required by this gem. So you must manually update it in vue.config.js very carefully.
      Demo

    Changes to vue.config.js

    const {
      manifest,
      pickUpSettings,
      // isProd,
    -  // getSettings,
    +  getSettings,
    } = railsConfig;
    
    + const { configureWebpack: { entry, output, resolve, module: cwModule } } = getSettings('configureWebpack');
    
    module.exports = {
      ...pickUpSettings`
        outputDir
        publicPath
        devServer
    -    configureWebpack
    
        filenameHashing
        lintOnSave
        runtimeCompiler
        transpileDependencies
        productionSourceMap
        crossorigin
        css
        parallel
        pwa
        pluginOptions
      `,
    +  configureWebpack: {
    +    entry,
    +    output,
    +    resolve,
    +    module: cwModule,
    +  },
      chainWebpack: (config) => {
    

  • Supported

    • [x] filenameHashing
    • [x] lintOnSave
    • [x] runtimeCompiler
    • [x] transpileDependencies
    • [x] productionSourceMap
    • [x] crossorigin
    • [x] css
    • [x] devServer
    • [x] parallel
    • [x] pwa
    • [x] pluginOptions
  • Unsupported

    • [ ] baseUrl - Deprecated
    • [ ] assetsDir - ignored
    • [ ] indexPath - N/A
    • [ ] pages - N/A
    • [ ] integrity - N/A
    • [ ] chainWebpack - directly edit vue.config.js

Trouble Shooting & Known Issues

  • My dev server can't find assets

    Sometimes your webpack-dev-server might still be running while Rails dev server had terminated. For example, you had executed exit! in pry.

    Usually webpack-dev-server should be killed when next time you start rails server. However, for some reason it could fail and the new webpack-dev-server will listen to another port. Then you must manually kill them:

    lsof -i:3080 -sTCP:LISTEN -Pn
    kill -9 <pid>
    

    Alternatively, you can run rake vue:node_dev and always start your dev server with:

    npm run dev
    # or
    yarn dev
    

    I know it is not Rails-way at all. I don't want to waste time to just get it worked properly in Rails way - you are already using Node, why it bothers you?

  • My API does not work with CSRF token

    Because Vue does not have opinion of Ajax (or JSON API) preference, you must implement what jquery-ujs does by yourself. There is an example code in vanilla JS with querySelector whish should work for IE8+:

    // fetch API
    async (url, data) => {
      const $csrfParam = document.querySelector('meta[name="csrf-param"]');
      const $csrfToken = document.querySelector('meta[name="csrf-token"]');
      const csrfParam = ($csrfParam && $csrfParam.getAttribute('content')) || undefined;
      const csrfToken = ($csrfToken && $csrfToken.getAttribute('content')) || undefined;
    
      try {
        const response = await fetch(url, {
          method: 'POST',
          headers: {
            'Content-type': 'application/json',
            'X-CSRF-Token': csrfToken,
          },
          body: JSON.stringify({
            ...data,
            [csrfParam]: csrfToken,
          }),
        });
        return response.json();
      } catch (e) {
        // handle failed case
      }
    }
    

    Alternatively you can turn off CSRF token and set SameSite cookie if all your clients no longer use IE. Modern browsers can handle SameSite flag to prevent CSRF attacks.

  • Mocha tests not working

    This is an known issue.

  • TypeScript can not find my aliases

    This is an known issue. TS is still using tsconfig.json rather than a .js or .ts file. You must manually update it for now. I will try to find a way out.

  • My yarn test:.../npm run test:... not working properly

    The test requires setting RAILS_ENV=test. You can invoke rake vue:test[unit] rake vue:test[e2e] instead.

  • Got errors like command "..." does not exist for rake vue:lint/test

    This rake task simply invokes vue-cli-service test:.... Those commands will be generated by some vue plugins. It won't be available unless you got correct plugin installed.

Q & A ## Q&A ### Can I get rid of `js-yaml` and `webpack-assets-manifest` Only `webpack-assets-manifest` is a required dependency. It will be used to generate `manifest.json` which required for both dev and prod. `vue.rails.js` uses `js-yaml` for parsing `config/vue.yml`. It will fallback to `rake vue:json_config` if `js-yaml` not been installed. However, when your Rails app grow bigger, you will very likely find rake tasks start slower and slower. ### Can I use YAML for template inside .vue Short answer I don't know and I don't recommend. There are several HAML packages but all are too old. JS world suggests [pug](https://pugjs.org). You can also use [slm](https://github.com/slm-lang/slm) if you prefer [Slim](http://slim-lang.com/). Both are quite similar to CSS selector syntax, which means you don't really need to spend time to learn. Just `rake vue:support[pug,slm]` and try them out: `