Turbolinks、時々Vue.js

こんにちは、@mugi_unoです。

今年は雪がスゴいですね。
暖かい我が家でリモートワークのありたがみを日々噛み締めています。

先日は雪だるまを作りましたが、想像とは違う仕上がりになりました。

娘は「これじゃない!」と不満そうでした。

f:id:mugi1:20180129173108p:plain

新しいRails&フロントエンドの環境構築

さて、最近Misocaでは、新規Railsアプリケーションのフロントエンド環境を整える機会がありました。

チームで検討した結果、Turbolinks&Vue.jsを採用しており、
その際に得られた知見を紹介したいと思います。

Turbolinksを使う理由

TurbolinksはGoogleで検索するとサジェストがこんな感じになるほど無効化されがちです。

f:id:mugi1:20180129173204p:plain

  • 利用するために理解すべき独自の挙動がある
  • ゴリゴリにフロントエンドを触っている人からすると
    「そこまでやらないで〜〜!」といった気持ちになる

といった具合に避けられがちだと予想していますが、上手く扱うと非同期通信をベースとした軽快に動く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にアップグレードした際と同様の手法です。

tech.misoca.jp

  • 内部にパッケージを作ります。

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では、いろんなフレームワークにチャレンジしたいエンジニアを募集中です!