LoginSignup
1
3

More than 3 years have passed since last update.

Rails 6 + Vue(単一コンポーネント) + vue-router + Vuetify だけどMPA(SPAではない) - Multi SPA

Posted at

構成

ひとつのRailsアプリケーション上に、単一コンポーネントで構成されたSPAを配置します。
まずはひとつのSPAをおくことを目標にしますが、同様に複数のSPAを置くことを意図しています。(よって完全なSPAではありません。またMulti-SPAとなります。)

構成のイメージ:

  • /shared
  • /private
  • /private/episode/:id/schedule
    • ここをSPAにする

Rails

Rails は、MVCなMPAアプリケーションをつくります。
Rails 6 webpacker を利用して vue を用います。webpacker と Vue の導入については説明を省きます。

Layout

SPA 用の layout を設けて、コントローラ(あるいはアクション)で切り替えます。ここでは application_spa とします。

application_spa は後述します。コントローラ#アクション schedule で layout を指定すると下記のようになります。これで episode_controller の schedule だけ application_spa を利用することになりました。

episode_controller.rb
  def schedule
    respond_to do |format|
      format.html { render :schedule, :layout => 'application_spa' }
    end
  end

のちに指定する vue-router と、ここで SPA対象となるRails controller#actionは、同じURLで同じSPAを使う必要があります。(そうでない場合、ひとつのURLで別のものが表示されてしまいます)

Vue 周りの構成

Vue を SFC(単一ファイルコンポーネント)で利用します。SFCなVueからは axios で API を利用します。Vuetify, vue-router, axios については説明を省略します。

この先の手順は Vue のSingle File Components 利用における Vuetify, vue-router, axios と同じです。

用意するもの:
- Vuejs
- vue-router
- Vuetify
- axios

使わないもの:
- Nuxt.js

Rails View

action view

View への routing は、Railsの routes.rb でaction schedule に向けられているとします。それにより schedule.html.haml が用いられるため、layout と schedule.html.haml をSPA用にします。

schedule.html.haml は、content_for を利用して、 layout で使用する javascript_pack_tag と stylesheet_pack_tag を切り替えられるようにします。切り替えられるようにする理由は、複数のSPAを作成する予定のためです(つまりMulti-SPAです)。

view/../schedule.html.haml
- content_for :pack, 'schedules'
#schedules

#schedules の指定は、のちほどJavaScriptで利用します。

layout

layoutは、 view で指定されたcontent_forによってJSファイルを読み込むようにします。stylesheet_pack_tag も指定しないと Vuetify のデザインが当たらないので注意しましょう。

layout/application_spa.html.haml
!!! 5
%html
  %head
    %title= ""
    = csrf_meta_tags
    = yield :meta
    = stylesheet_pack_tag "#{yield :pack}"
    = javascript_pack_tag "#{yield :pack}"
  %body
    = yield

Rails側のViewは以上です。大変シンプルですね。

Vue の呼び出し

javascript_pack_tag が呼び出す schedule.js が起点(エントリーポイント)となります。(よく見かける application.js を呼び出す部分に相当します。)

schedule.js

Rails View で指定した ID #schedules を使います。

packs/schedule.js
// 様々なimport や Vue.use の記述は省略

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '#schedules',  // schedule.html.haml で指定したもの
    vuetify, // import した vuetify 
    router, // import した vue-router
    render: h => h(App)
  }).$mount()
})

app.vue

schedule.js が render: h => h(App) で指定した app.vue は、 router-view を呼びます。記述には pug を用いています。

packs/app.vue
<template lang="pug">
  v-app
    v-app-bar( dark )
      v-toolbar-title Service Name is Unknown
    v-main
      v-container
        v-layout( wrap )
          router-view
</template>

router-view に割り当てられたコンポーネントは、次の router の設定で行っています。

vue router

vue 側の router の設定です。
アプリケーションが / から始まらないため、mode: 'history' を忘れないようにします。
この先、複数のSPAを作成するため、router も複数にします。そのため、 pack 配下に router/ を作成します。 router を schedule.js へ書かずに別ファイルとし packs/router/schedule.js として管理します。

packs/router/schedule.js
import VueRouter from 'vue-router';
import Schedule from '@/../components/schedule.vue';

const routes = [{
  path: '/episode/:id/schedule', component: Schedule,
  // 増えた path は、Rails 側も対応
}];

export default new VueRouter({
  base: '/private/', 
  mode: 'history',
  routes
});

base: '/private/' を指定して、アプリケーションの階層を /private 配下にしています。指定すると $route.path に含まれません。 router-link で :to を用いる際にも含まれなくなります。

この router/schedule.js は schedule.js から import されています。

axios

SFC 内で axios を用いてAPIにアクセスします。CSRF_TOKENはこちらを参照しました。

.vue の中では、methods で axios を用います。
遷移を伴う一覧を routers-list で記述しました。しかし、同じpathに一致する更新は、APIで取得した値を取り直してくれません。 そこで watch により変更を監視します。 vue-router公式では beforeRouteUpdate も使えると記載があります。

今回はbeforeRouteUpdateを利用しました。

@/../components/schedule.vue
// template は省略
// props や data は省略

methods: {
  getEpisode: function (event) {
    this.axios
      .get('/api/v1/episodes/' + this.$route.params.id + '')
      .then(response => (this.episode = response.data))
      // catch error 省略
  }
},

beforeRouteUpdate(to, from, next){
  this.getEpisode()
  next()
},
// watch を使う場合1
// watch: {
//  $route (to, from) {
//    this.getEpisode()
//  }
// },
//
// watch を使う場合2
// watch: {
//  '$route': 'getEpisode'
// },
// 

mounted () {
  this.getEpisode()
},

Vue側は以上です。これで/private/episode/:id/scheduleを起点としたSPAができました。

まとめ

Rails の layout をSPA用に分け、Railsで完結するアプリケーションと、Vue によるSPAを並存させました。Railsアプリケーション全体からみると、部分的にSPAを提供できます。

複数SPAを目的にした理由

ここでは、SPAを複数置くことを考えました。どの単位に分けると良いのかは、まだ明確な指針を持っていません。そのため、複数SPAを意図した理由についても簡単に記載します。

Vueのroutingで対応ができる範囲で複数のSPAに分けるのは、あまり意味はないだろうと考えています。エントリーポイントが schedule.js になるのかどうかの違いしかないためです。

一方で、起点ごとに異なるコンポーネントを利用する場合や、vue-router の path が遷移で繋がらない場合は、エントリーポイントを分ける意味があると考えました。

今回、構築したRailsアプリケーションのうち、スケジュール管理部分をVueのSPAにしたいと思いました。それだけでひとつのアプリケーションになり得る単位です。
また、同じアプリケーションを Episode 以外のモデルでも利用するため、再利用性の高いアプリケーションにしたいと考えました。そのためSPAによってModelを横断して取り扱えるようにしようと考えました。

今後、schedule 以外のエントリーポイントが増えることを想定しました。schedule 同様に、単独でひとつのアプリケーションになり得る単位です。たとえばログ管理を考えています。

その際に、SPAを統合するイメージが湧かず、別々に切りたいと考えました。Railsが管理する単位では1つのサービスですが、クライアント側に期待する単位では、それぞれ完結しています。たとえばスケジュール管理とログ管理はまったく別の操作性を期待しています。ログとスケジュールを同時に管理したいこともありません。

以上が、複数SPAを考えた理由です。

エントリーポイント間の差分

仮に schedule 以外に log を増やしたいとしましょう。

Rails側の作業はわずかです。
SPAにしたい箇所を、layout application_spa指定することと、log.html.haml view の中で #logs を使うこと、です。

Vue は、packs/schedule.js と同等のエントリーポイントとして packs/logs.js を用意し、 el: '#logs' とします。(もちろん他のelementでも構いません。)
また packs/logs.js は、log 用の router packs/router/logs.js を読み込みます。

axios や app.vue, Vuetify は作業が不要です。

つまり、複数のエントリーポイント間の差分は、対応する router/*.js の path と、利用するコンポーネント群の違いになります。

おわり

以上、Rails の layout をSPA用に分け、エントリーポイントを複数にすることで、Vue によるSPAを複数作成することができました。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3