railsエンジニアの西辻です。今回の記事では、Railsプロジェクトで一部の画面のみをVue.jsを用いてSPA化するにあたって、その際に得た知見などを共有できたらと思います。Overview大きく以下の流れで書いていきます。実装を進めていく中での気づきスマホ対応の方針決め最後にMotivationまず、なぜRailsプロジェクトで一部の画面のみをSPA化する必要があったかの背景を説明したいと思います。今年の5月からtoB向けの管理ツールを新規開発したのですが、その際にjQueryだとコードの見通しが悪いのでVue.jsを積極的に利用していこうという話があり、チームメンバーでVue.jsを学習しながら開発を進めていました。管理ツール自体は無事リリースでき、稼働はしているのですがVue.jsへの知見が少なかったのとリリース日までの開発のリソース上、管理ツールのメイン機能であるメッセンジャーの大部分がRailsとVue.jsの微妙な連携で成り立っており、UXがよくない状態でした。ローカル環境にはなりますが、実際のメッセンジャー画面が以下のスクリーンショットになります。メッセンジャーは大きく3つのセクションに分かれています。List上に表示されます。あるメール系のアプリの動きにかなり近いです。Summaryに対応しているユーザ情報が表示されます。メッセンジャーは具体的に以下の部分でUXに問題がありました。Listを選ぶ度にブラウザリロードされてしまい、ユーザの操作に連続性が保てない。上記の問題も絡んで、スマホ対応がCSSのみで完結しない状態で、スマホ対応が困難な状況。Listのページングで進んでもTopic選択時にブラウザリロードしてしまい、ユーザが操作している位置を見失う。メッセージ送信時にブラウザリロードされてしまう。して並べてみると、ブラウザリロードされてしまうことがほとんどの問題の原因となっていることが分かります。上記の問題を解決するために、中途半端にRailsのviewとVue.jsが連携している部分をRailsのview利用をやめて、Vue.jsでSPA化する必要があるという考えに至りました。自分自身、Vue.jsへの知識がそんなになく部分的なSPAが実現可能かという点について検証をしてみないと分からない状態でした。考え込んでも埒が明かないので小さめのプログラムを組み、実現可能かの検証を数日行っていました。検証のポイントとしては、以下の項目をあげていました。Summary)間でイベントによる連携ができるかどうか将来的にスマホ対応することができる設計ができるかどうかソースコードの見通しが悪くならないかどうかURLとして特定のTopicを表現できるかどうかRailsの資産を活かした形でVue.jsを利用できるかどうか最終的にはSPA化する判断に至ったのですが、当初の考えではRailsのviewをいい感じに再利用してできないかを模索していました。templateに対してVue.jsを導入しており、考え的にその派生でいけないかという印象を強く持っていました。engineとしてはhamlを利用しており、特定の要素に対してVue.jsを動作させるような実装が簡易的でRailsの恩恵を最大限受けながら開発スピードを速めて進めることができました。側が以下のようにRails側の変数とVue.jsのsyntaxが入り混じりながら記述するスタイルです。"#{t('application.name')}にエージェントを招待します。招待したいエージェントのメールアドレスを入力して送信ボタンを押してください。'agents/index'javascript_pack_tagで呼び出されるVue.js側のコードが以下です。今回対応しようとしているメッセンジャーのような機能に対しては向いてないようでした。templateで頑張って実装したのですが、ブラウザリロードがどうしても発生してしまうため、この方法では実現したいことができないと判断しました。templateメインに考えてしまうとUX向上が実現ができない考えに至りました。Summary)間でデータのやりとりは問題なくできるかという疑問でした。これができないことにはブラウザリロードを防ぐことができないと考えたためです。Eventsを読んで実際にコードで動かしてみて確認し問題ないことが分かりました。Summaryを子コンポーネントとして扱うことにしました。将来的にスマホ対応することができる設計ができるかどうか今回ブラウザリロードを防ぐだけが目的ではなく将来的に3つのセクションをスマホ対応させる必要があります。GmailアプリなどのメールアプリでよくあるUIで、メールを選んで詳細に行って戻ったりが実現イメージに近いです。順に遷移していけるのが理想系になります。この理想系が実現可能かどうかも重要なポイントだったので公式ドキュメントを読みながらコードを書いて検証を進めました。Componentsの存在を知りました。Componentsを動かしてみながら、これを利用して3つのセクション毎にコンポーネントを作成したら、綺麗に作れるんじゃないかという感じがしてきました。Routesのドキュメントを見る限り、コンポーネントになっていればうまくスマホの遷移にも対応できる感じがしていました。ソースコードの見通しが悪くならないかどうかhamlに直接Vue.jsのbindingなどを書いていくと正直見づらい感じがしていました。今回メッセンジャーを改修するにあたり、大幅にVue.jsのコードが増えることが予想されたので、ソースコードの見通しが悪くなり今後のメンテナンス性が落ちてしまうのではないかという懸念がありました。Componentsが解決してくれました。hamlとjsファイルが分かれているのはjQueryの時と似た状況でファイル間のスイッチがどうしても必要でした。Componentsは一見、前時代的な記述に見えましたが実際templateとjsが同じファイル内にあるというのは実装する上でやりやすかったです。URLとして特定のTopicを表現できるかどうか管理ツールなのでブラウザリロードを防ぐ手法を取った場合に、特定のTopicで問題が発生した際にURLとしてアクセスできないと問題の調査が困難になることが予想されました。Vue.jsメインになったとしてもRailsのルーティング同様/topics/:topic_id/messagesのような形でアクセスできることが必須でした。Routerを利用することで解決できる目処がありました。実装を進めていく中での気づき一通り小さなプログラムコードで検証が終わり、プロダクトコードに組み込むイメージが大体できあがってきました。Summaryの3つのコンポーネントを作成することにし、Rails側はapiでデータを返し、Vue.jsはそれを受け取り表示を行う形にすることに決めました。して文章に起こすとよくある当たり前の構成になりました、ただ既存のコード量が多いので足掻きたい気持ちがありこの思想までなかなかたどり着けなかったです。viewは全て捨てることになるのでこの時点で、「あのコード全部apiに寄せるのか・・・」という感情が湧き上がって来ました。チームメンバーの誰もが最終的にメッセンジャー部分は全てapi化することが必須だという空気がありましたが、今回の調査・検証でやり切ったら綺麗な世界ができる確信が持てたので気合い入れてapiへ実装し直していきました。ここからは、実際に実装していく中で気づいたことなどを記述していきます。Highlightが効いて良かったです。Highlightが効くプラグインも便利でした。(ex-Jade)実際の開発の進め方について今回はスマホ対応まであるのでレスポンシブなデザインの部分はデザイナーの稲井さんにやっていただきました。Componentsを利用してVue.js化をどんどん進めていく感じになりました。Components化し、それに伴いRails側で必要なapiを実装していきました。Rails側でのapi実装についてまずは、apiからデータを受け取れるようにしないとVue.js側の実装はやりづらかったのでapiを作っていくことにしました。apiはjsonapiの形式を他のプロジェクトで導入しており、クライアントサイドの実装がやりやすい印象を持っていたので今回もjsonapi形式でapiを作成していくことにしました。これに合わせてVue.js側で利用できるjsonapiをデシリアライズするライブラリが必要になりました。今回は以下のライブラリを利用することにしました。SeyZ/jsonapi-serializerこのライブラリには複数のリレーションが紐づく場合にデシリアライズに失敗するバグがあり、更新が止まっている状態です。有志の方が上記のバグを直しているのでそれを自分たちのGithubにforkしてそれをyarnから利用する形式にしました。jsonapi自体の仕様自体はほとんど固まっているのでパフォーマンス向上などはあると思いますがデシリアライザーとして利用する分にはforkで十分と判断しました。サーバーサイド、クライアントサイドでjsonapi形式に則った形で連携できることがわかったので実装を進めていきます。controllerの例をあげます。ここでのポイントがいくつかあるので書いていきます。Vue.jsメインでの実装になるにあたって、Rails側に乗っている画像リソースに対してどのようにアクセスするかの問題がありました。これはjsonapi形式におけるmetaを利用して解決しています。tagとなったコンテンツをVue.js側でv-htmlを利用して扱うといい感じでした。Vue.js側のtemplateで以下のように呼び出します。div(v-html="meta.calender_icon")この方法があるので複雑なhtmlをhelperで生成していたものはhelpersで加工済みのhtmlをそのままapiで返却してVue.js側のtemplateでv-htmlディレクティブを利用した実装が楽にできました。今回の管理ツールではRails側でdecoratorを利用していたのでjsonapiのserializerでdecoratorを呼び出す手法を取ると再実装コストを大幅に抑えられました。具体的なコードで示すと以下になります。decoratorは既存のRailsのviewでも利用できるので再利用性がかなり高まった気がします。endendserializerではobject.decorateで任意のdecoratorが呼び出せるので、その性質を利用しています。endend上記のような感じで一通り方針が固まったので、あとはひたすらapiを作成していきました。Componentsの実装についてapiが完成したのでVue.js側の実装を次に進めていきます。以下がエントリーポイント的な部分のindex.jsになります。router周りは、みやっち(宮永)に手伝ってもらったのでそのうちきっといい解説ブログが書かれることでしょう。素直にこのpathの時はこのコンポーネントを利用すると書かれているので見やすいですね。Routerのrootに当たる部分に向けないといけないので注意が必要です。Routerのrootに当たるのでRails側でtopics#indexに向かうように変更を加えています。endindex.jsで$mount('#topics-index')としているので実際のindex.hamlでは以下のように%global-navbarとするとコンポーネントを呼び出すことができます。'topics/index'非常にシンプルになりました。GlobalNavbar.vueの中身を見ていきます。実際のコードを載せると長くなるので必要な部分のみ抜き取っています。}</script>GlobalNavbarの中で更にTopicListを呼び出しています。UserSummaryも呼び出していく形を取っています。TopicListは以下のようになっており、MessageListとUserSummaryに対して親コンポーネントになるようになっています。子コンポーネントであるmessage-listとuser-summaryに対しては:isTopicLoadingのような形で値を渡せます。子コンポーネント側はさらにwatchで監視を行い親コンポーネントからの変化を受け取るという感じになります。@createMessageEvent=receiveMessageEventという形で子コンポーネントから発火されたイベントを親コンポーネントであるTopicListで受け取ることができます。)</template>具体的に子コンポーネントでの親コンポーネントの値の受け取り方と子コンポーネントから親コンポーネントへのイベント通知についてみていきます。}</script>親コンポーネントからの値の受け取りはpropsを利用しています。同時にwatchを利用して監視を行い、値が変化したら子コンポーネントにも伝達されます。message)で親コンポーネントに値を渡しています。上記のケースだとメッセージ送信時にTopicListに送信されたメッセージを通知して、最新のメッセージが表示されるようになっています。今回の管理画面ではBootstrap4を利用しており、一部動きをつけるためにjQueryを入れていましたがBootstrap-Vueというライブラリを利用するとVue.jsとBootstrapの連携が可能になります。自分たちのユースケースだとModalsやTooltipsを利用しています。スマホ対応の方針決めスマホ対応ですが、PCとスマホのどちらにも対応するためにroutingを適切に行う必要がありました。考えて最終的に以下のようになりました。最終的な調整はみやっちがやってくれましたが、実現したいSPAの形に収めることができ素晴らしかったです。最後に長々と書きましたが、今回のメッセンジャー改修についての知見をまとめてみました。して振り返ると色々考えながら実装を進める必要があり、自分自身初めてのSPA実装となったので調査、検証の連続でなかなか面白い体験でした。実際完成したものを触ってみると非常に使い勝手があがっており、利用者からも良いフィードバックを得られているので、やってよかったという思いです。いえ、まだまだ改善の余地はあるので今後も更に精進していきたいです。