こんにちは、@mugi_unoです。
今年は雪がスゴいですね。
暖かい我が家でリモートワークのありたがみを日々噛み締めています。
先日は雪だるまを作りましたが、想像とは違う仕上がりになりました。
娘は「これじゃない!」と不満そうでした。
新しいRails&フロントエンドの環境構築
さて、最近Misocaでは、新規Railsアプリケーションのフロントエンド環境を整える機会がありました。
チームで検討した結果、Turbolinks&Vue.jsを採用しており、
その際に得られた知見を紹介したいと思います。
Turbolinksを使う理由
TurbolinksはGoogleで検索するとサジェストがこんな感じになるほど無効化されがちです。
- 利用するために理解すべき独自の挙動がある
- ゴリゴリにフロントエンドを触っている人からすると
「そこまでやらないで〜〜!」といった気持ちになる
といった具合に避けられがちだと予想していますが、上手く扱うと非同期通信をベースとした軽快に動くUXをお手軽に実現できます。
同様のものをSPAで構築するには、 フロントエンド周りで多くの知識が必要となります。
フロントエンドエンジニアが中心となってメンテナンスをしていけるのであれば問題ありませんが、サーバサイド/フロントエンドの双方を多くのエンジニアが面倒を見るような状況では、最低限のコード量で実現できることが大きいメリットになってきます。
Turbolinksで気をつけるべきこと
特に注意すべきなのが初期化周りです。
一般的にはDOMContentLoaded
イベントやjQuery.ready
などで、適宜必要な初期化スクリプトを実行することが多いかと思いますが、Turbolinksの場合はbodyを差し替えるだけになるため、イベントが発火せずに上手く動作しなくなります。
画面遷移時に適宜documentに対してturbolinks:load
イベントが発火されるため、そちらをハンドリングしてあげることで上手く初期化できるようになります。
このあたりは、TurbolinksのREADMEにも説明があります。
https://github.com/turbolinks/turbolinks#full-list-of-events https://github.com/turbolinks/turbolinks#observing-navigation-events
画面ごとの初期化をどのようにするか?
実際には画面に応じて実行したい初期化処理が異なるケースが多く、都度適切なスクリプトを実行する必要があります。
今回は、RailsのController/Actionを元に、適宜必要な初期化スクリプトを判別してロードするような仕組みとしました。
実際のコード例は以下です。(要点のみ抜粋しています)
application.html.haml
: bodyにControllerとActionを元にしたキーを埋め込む
%body{ 'data-js-initializer': "#{controller.controller_name}_#{controller.action_name}" }
application.js
: headに埋め込み、ページ全体のロード時一度だけ実行する
import Turbolinks from 'turbolinks'; import initialize from './initializers'; Turbolinks.start(); // Turbolinksで遷移した場合の初期化処理 document.addEventListener('turbolinks:load', initialize);
initializers/index.js
: 画面ごとの初期化処理を実行する
import * as initializers from './'; import camelcase from 'camelcase'; export default function () { // bodyに埋め込まれたキーを取得 const initializerName = $('body').data('js-initializer'); // 対応する初期化スクリプトを取得し、存在すれば実行する const initializeScript = initializers[camelcase(initializerName)]; if (initializeScript) { initializeScript(); } }
これにより、たとえば OrdersController#edit
に遷移した場合には、
initializers/ordersEdit.js
がコールされるようになります。
#new
, #edit
などで同じスクリプトとなる場合は、
共通ファイルを1つ作成し、別名でエクスポートすることで対応しています。
export { ordersForm as ordersNew, ordersForm as ordersEdit, } from './ordersForm';
Controller/Actionを元に初期化するのは好みが別れるところもありそうですが、
といったメリットがあるため、今回はこのような形としました。
Vue.jsと組み合わせる
Vue.jsと一緒に利用する場合は、Turbolinksによる画面遷移時のキャッシュとの兼ね合いや、インスタンスの破棄を適切に行えるよう考慮してあげる必要があります。
このあたりについては、Turbolinksリポジトリのwikiに説明と対処用のコード例が記載されており、そちらを参考にすることで実現できます。
VueJs and Turbolinks · turbolinks/turbolinks Wiki · GitHub
内容的には、TurbolinksAdapter
というmixinを作り、turbolinks:visit
のタイミングで1度だけイベントが発火するような仕組みのようです。
TurbolinksAdapter
の適用し忘れが怖い
当然と言えば当然ですが、上記TurbolinksAdapter
は Vue.jsに適用してあげないと正常に動作しません。つまり、TurbolinksAdapter
を適用し忘れて、単純にVue.jsのみをimport/requireしてしまった場合には期待通りの動作とはなりません。
かといって、利用する箇所すべてで意識したくはないですよね。
ローカルパッケージ化しておく
というわけで、Vue.jsをローカルでパッケージ化し、単純にvue
をimport/requireした場合にはTurbolinksAdapter
が適用されたものが得られるようにしました。
このあたりは、以前Vue.jsのバージョンを0.12→2.4にアップグレードした際と同様の手法です。
- 内部にパッケージを作ります。
frontend/private_modules/vue/package.json
{ "name": "vue", "version": "1.0.0", "description": "Vue with turbolinks", "main": "index.js", "dependencies": { "vue": "^2.5.13" } }
frontend/private_modules/vue/index.js
const Vue = require('vue').default; // https://github.com/turbolinks/turbolinks/wiki/VueJs-and-Turbolinks const TurbolinksAdapter = require('./TurbolinksAdapter'); Vue.use(TurbolinksAdapter); module.exports = Vue;
- アプリケーション側からのVue.jsの参照をローカルパッケージに向けます
package.json
(抜粋)
{ "dependencies": { "vue": "file:./frontend/private_modules/vue" } }
これで、Turbolinksでの画面遷移時のVueインスタンスの処理を、開発時に特に意識せずに行えるようになりました。
Turbolinksの外し方
Turbolinksを使うのは少し冒険だな〜と感じる方もいるかもしれませんが、上記構成の良いところとして、「Turbolinksを外そうと思えば、わりと簡単に外せる」という側面があります。
「外すのかよ!」という声が聞こえてきそうですが、出来る限り依存を浅くしておくのはTurbolinksに限らず大事なことだと思ってます。
もし外したい場合には以下の手順を踏むだけです。
application.js
の初期化処理をDOMContentLoaded
のバインドに変える- Turbolinksのimportと
Turbolinks.start();
を消す - package.json上の
vue
の参照をローカルパッケージから、通常のVue.jsに戻す
これで安心して使うことができます。
というわけで、TurbolinksとVue.jsを使った環境構築時の話でした。
興味が湧いた方は、一度試してみてはいかがでしょうか?
Misocaでは、いろんなフレームワークにチャレンジしたいエンジニアを募集中です!