Webuilder240

非SPAでRailsとVueをうまく共存させる

2021-08-22 01:04:31 +0900

Vue JavaScript Rails

想定読者

  • VueとRailsを「SPAではない」「MPA」で使っている
    • 理由は既存の「資産を生かしたい」だったり、フロントエンドエンジニアの不在だったり色々ある。
  • 要件的にはすごい複雑なUXを実現するというよりは部分的にチャットの機能があったりで、ページの中でVueを部分的に使わないとつらい、複雑なUXがあるくらいの規模感。
  • Railsの思想に共感はするけど、DOM操作をするのつらい、Vueとかは部分的に使いたい。

という人々におすすめできそうなVueとRailsの共存方法について記載します。
サンプルのコードをこちらにアップしておきました。
こちらのサンプルを見ながらこの記事を読んでいただけると幸いです。




ディレクトリ構成

なんとなくのディレクトリ構成です。
特に肝になるのはcontrollersでこのディレクトリ以下に、そのページで必要なVueの実装だったりを書いていくようにしていきます。
controllersという名前にしているのですが、特に制約はないです。
好きな名前でいいと思います。案としてはcontrollersのほかに、ここはpagesみたいな名前でもいいかもしれません。

3905ab58b46b30ef86ca35fcbfced843.png 12.9 KB

Railsとの連携について

RailsのContorllerとVueを密結合にしたVueインスタンスを用意する

具体的には、Vueインスタンスと関連させる、「HTMLクラス」がついたHTMLを用意しておきます。
<!DOCTYPE html>
<html>
  <head>
    <title>RailsVueIntegrate</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <div id="vue-notification">
      <button type="button" @click="increment()"> Increment </button>
      <button type="button" @click="emitted()"> Emitted </button>

      <p v-text="counter"></p>
      <p v-text="message"></p>
    </div>
    <div id="vue-<%= controller.controller_name %>-<%= controller.action_name %>">
      <%= yield %>
    </div>
  </body>
</html>
ここでは。「vue-{コントローラー名}-{アクション名}」という命名規則で作っています。

関連するJSについてはこんな感じになるかと思います。(例: users_index_controller.js)
import Vue from 'vue/dist/vue.esm.js';
import TurbolinksAdapter from 'vue-turbolinks'
import User from "../components/User"

Vue.use(TurbolinksAdapter)

document.addEventListener('turbolinks:load', () => {
  const element = document.getElementById("vue-users-index") 
  if (element) {
    const app = new Vue({
      el: element,
      components: {
        User
      },
      data: () => { 
        return {
          message: "Hello World Users Index",
        }
      },
    })
  }
})

こうすることで、RailsのController、Actionに関連するVueインスタンスを作るためのルールがある程度設定することができました。
サンプルでは、application_controllerという「どのRailsのAction」の場合でも動いてほしいものをここに書くようにしています。

異なるVueインスタンス間でのイベントのやり取りについて

MPAだし、複数のVueインスタンスで何かデータのやり取りをしたいこともあるかもしれません。
Vuexでもいいのですが、ちょっとオーバーエンジニアリングな気もしなくもないので、
EventEmitterを使って簡単に実現できる仕組みを作ってみました。

あまり多用するとカオスになってしまうのですが、MPAですし、連携するVueインスタンスも2つか多くても3つでしょうし、あまり問題にはなってないです。

emitterの実装についてはこんな感じになりました。
// lib/emitter.js
const EventEmitter = require('events')
const eventEmitter = new EventEmitter()
export default eventEmitter

emitterを利用する(発信側)はこんな感じで
// application_controller.js 
import Vue from 'vue/dist/vue.esm.js';
import TurbolinksAdapter from 'vue-turbolinks'
import emitter from "../lib/emitter"

Vue.use(TurbolinksAdapter)

document.addEventListener('turbolinks:load', () => {
  const element = document.getElementById("vue-notification") 
  if (element) {
    const app = new Vue({
      el: element,
      data: () =>{
        return {
          message: "Hello World Notification Component",
          counter: 0
        }
      },
      mounted() {
        emitter.emit('notification.mounted', "hello")
      },
      methods: {
        increment() {
          this.counter+=1
        },
        emitted() {
          emitter.emit('notification.emitted', "hello")
        }
      },
    })
  }
})
emitterのイベントを受け取る側はこんな感じ
// users_index.controller.js 
import Vue from 'vue/dist/vue.esm.js';
import TurbolinksAdapter from 'vue-turbolinks'
import User from "../components/User"
import emitter from "../lib/emitter"

Vue.use(TurbolinksAdapter)

document.addEventListener('turbolinks:load', () => {
  const element = document.getElementById("vue-users-index") 
  if (element) {
    const app = new Vue({
      el: element,
      components: {
        User
      },
      data: () => { 
        return {
          message: "Hello World Users Index",
        }
      },
      mounted() {
        emitter.on("notification.emitted", (params) => {
          console.log(`emitted: ${params}`)
        })
      },
      beforeDestroy() {
        emitter.removeAllListeners()
      }
    })
  }
})

TurboLinksとの共存について

TurbolinksとVueを共存させる方法はもう確立していて、
公式でも「vue-turbolinks」ってライブラリ使って対処してねということでWebpackerでVueをインストールした際に生成されるコメントにも記載されています。



beforeDestroyでEmitterのリスナーを掃除する

EmitterのリスナーはTurboLinksを使っていなければ、ページのリロードが発生するので
特に気にすることはあまりないのですが、
Turbolinksを利用している場合はページの切り替わりは発生していないので、
ちゃんとbeforeDestroyのライフサイクルで掃除してあげる必要があり、掃除しないと1回イベントをemitしたときに複数回処理されるということになりかねません。


まとめ

  • 今回は個人でプロジェクト始めるにはこんなゆるふわな感じでいいのではないか?というご提案でした。
    • 別にRailsに限らずうまいこと使えそうですよね。
  • 今後Rails7でimport-mapsでよりカジュアルにJavaScriptが使える未来が来るかもしれません。この時に、Hotwireという選択もありですが、ほかの選択肢としてVueをカジュアルに使うというのは折衷案としてアリなのかなぁと思っています。

関連しそうなブログ