構成
ひとつの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 を利用することになりました。
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です)。
- content_for :pack, 'schedules'
#schedules
#schedules
の指定は、のちほどJavaScriptで利用します。
layout
layoutは、 view で指定されたcontent_forによってJSファイルを読み込むようにします。stylesheet_pack_tag も指定しないと Vuetify のデザインが当たらないので注意しましょう。
!!! 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 を使います。
// 様々な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 を用いています。
<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 として管理します。
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
を利用しました。
// 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を複数作成することができました。