RailsにVue.jsを小さく導入する

先月開催されたフロントエンドもくもく会 - 新年LT大会というイベントで 「RailsにVue.jsを小さく導入する」というLTを発表したが、あえなく時間切れになってしまった。

LTはTurbolinksはどんなもので、敬遠されがちという説明で終わってしまったので、 続きをブログに書くことにした。

前提

どんなRailsアプリケーションにVue.jsを導入したかというと

  • Rails 5.0で作られ、現在Rails 5.1を使っている
  • Turbolinksを使っている
  • jQueryに依存している
  • APIやjbuilderでJSONを返してはくれない

RailsをAPIに専念させ、フロントエンドをSPAで実装するには時既に遅しである。

Vue.jsのインストール

まずはWebpackerでVue.jsを導入することにする。導入方法はwebpackerのREADMEを参照すればよいので割愛する。

webpackのエントリーポイントはapp/javascript/packs/main.jsとした。

レールに乗っかれるのはここまで。あとはご自由にという感じである。

基本

Vue.jsはコンパイラ付きの完全版を使い、Turbolinksと共存させたいので、vue-turbolinksを使う。turbolinks:loadイベントでVueコンポーネントをマウントする。コンポーネントのマウント先はViewテンプレートに記述したIDになる。

/ RailsのViewテンプレート.slim
#mount-point
/* app/javascript/packs/main.js */
import Vue from 'vue/dist/vue.esm';
import ComponentA from 'components/ComponentA.vue'
import TurbolinksAdapter from 'vue-turbolinks';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if (document.getElementById('mount-point')) {
    new Vue({
      name: 'SampleComponent',
      el: '#mount-point',
      template: '<ComponentA/>'
      components: {
        ComponentA
      },
    });
  }
});

div#mount-pointComponentAがマウントされる。これで任意のViewテンプレート内にVue.jsを導入できる。小さく導入できた。

応用

コンパイラを含むVue.jsの完全版を利用しているので、DOMテンプレートを利用できる。 以下のようにRailsのコントローラーから渡された変数をVue.jsのコンポーネントに受け渡すことができる。

/ RailsのViewテンプレート.slim
#mount-point
  component-a([email protected])
  component-b
/* app/javascript/packs/main.js */
import Vue from 'vue/dist/vue.esm';
import ComponentA from 'components/ComponentA.vue'
import ComponentB from 'components/ComponentB.vue'
import TurbolinksAdapter from 'vue-turbolinks';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if (document.getElementById('mount-point')) {
    new Vue({
      name: 'SampleComponent',
      el: '#mount-point',
      components: {
        ComponentA,
        ComponentB
      },
    });
  }
});

RailsということでActionView::FormHelperも使える。こんなこともできるが、これは横着である。

/ RailsのViewテンプレート.slim
= form_with model: Item.new do |f|
  .form-group#mount-point
    = f.text_field :name,
                   class: 'form-control',
                   'v-model.trim' 'nameValue',
                   'v-on:blur': 'checkValue'
/* app/javascript/packs/main.js */
import Vue from 'vue/dist/vue.esm';
import TurbolinksAdapter from 'vue-turbolinks';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if (document.getElementById('mount-point')) {
    new Vue({
      name: 'SampleComponent',
      el: '#mount-point',
      data: function() {
        return {
          nameValue: ''
        }
      },
      methods: {
        checkValue: function() {
          if (this.nameValue.length < 3) {
            // 何かしらの処理
          }
        }
      }
    });
  }
});

さらにbodyタグにマウントポイントを設定すればよいのでは?と試したら動作するが、既存のjQueryプラグインやChartkick(Ruby版)が動作しなくなってしまったので断念した。順次Vueに移行していけば解決する見通しはある。

注意点

上記の方法を使えばページ内にVueコンポーネントを展開できる。例では一つだが、必要に応じて何個もVueコンポーネントを展開できる。

ただし、それぞれのコンポーネント間で状態を共有したい場合はVuexを使う必要がある。

また、vue-turbolinksはソースコードが短いので一読をすすめるが、その動作はページを移動するごとにVueコンポーネントは破棄している。よって、ページをまたいで状態を維持したい場合、自分はvue-persistedstateで状態を永続化と復元をしている。

やはりTurbolinksなのでSPAのような動きをしているが、違うのである。

その他にVue.jsはDOMをテンプレートとして利用できるが、特有の注意事項を理解した上で利用するとよい。

まとめ

  • Turbolinksは共存できる。
  • RailsのViewとVueが混在していてカオスであるが、受け入れればなんとかなる。
  • これを5分のLTで説明するのは無理だ。
一覧へ戻る