読者です 読者をやめる 読者になる 読者になる

スパイスな人生

素敵な人生をおくるためのスパイスを届けていきたい、そんな想いで仕事をするspice lifeメンバーブログ

既存のRailsアプリケーションにVue.jsを採用した話

Ruby on Rails SPOTLIGHTS

こんにちは、id:ukstudioです。今回は弊社サービスの1つであるSPOTLIGHTSにVue.jsを採用した話をしようと思います。

SPOTLIGHTS自体は一般的なRailsアプリケーションといって問題ない作りになっているので、既存のRailsアプリケーションにどういった形でVue.jsを投入していったかを中心に書いていきます。

Vue.js採用前の状況

SPOTLIGHTS初期のJavaScriptはHTML/CSSと共に外注し納品してもらったものです。 フレームワークといった類はほぼ使っておらずjQueryに頼りきったコードでした。

当時は様々な事情によりこれはこれで妥当な判断だったと思うですが、今後社内でメンテナンスしていくうえで足かせになるであろうというのはなんとなく予想ができていました。jQueryイベントハンドラやDOM操作がひとつのファイルにひたすら連なっているという状況だったためです。

そのため2014年の12月にVue.jsの採用を決定し、少しずつ置き換えていきました。

f:id:ukstudio:20150430151446p:plain

Vue.jsに決定した理由

JavaScriptフレームワークを採用するにあたって、Knockout.jsとVue.jsのどちらかにしようと考えていました。フルスタックフレームワークを除外したのはそのライブラリが廃れた場合の移行や、既存のJavaScriptを少しずつ置き換えるコストを考えた場合にあまり向いていないと判断したためです。またReact.jsも考えましたが、私含めチーム全体のJavaScript力を考えた時に少々チャレンジングすぎるかなと思いこちらも除外しました。

Knockout.jsではなくVue.jsを採用した理由ですが、簡単に言ってしまえば私の独断と偏見です。Knockout.jsは弊社エンジニアのid:asonasが使用経験がありましたがVue.jsの使用経験を持つメンバーが社内にいなかったため、せっかくだし新しいもの触ってみようという気持ちで決めました。また、Vue.jsの採用でネックになりそうなIE対応ですがSPOTLIGHTSではIE8はサポート外だったというのもあります。

Vue.jsのインストールにはRails Assetsを使用

Vue.jsのインストール手段ですがRails Assetsを採用しました。

SPOTLIGHTSは元々JSなどは外部のライブラリも含め同じgitリポジトリ内で管理されていました。その為、本番環境にNode.jsはインストールされておらず今回のためにNode.jsをインストールするのにも少々抵抗がありました。新しい何かを採用する際に同時に新しいものを採用すると混乱しがちだからです。

その点Rais AssetsはGemfileに1行追加するだけでVue.jsをインストールすることができました。

gem 'rails-assets-vue', source: 'https://rails-assets.org'

導入初期はonClickをv-onに置き換え

f:id:ukstudio:20150430151507p:plain

導入としてまずはonClickで制御していた上記画像のようなモーダルの開閉をVue.jsに置き換えました。元々は.openModalというクラスにイベントハンドラをあてていましたが、それを削除しVueのメソッドをv-onで呼ぶようにしました。また同時にモーダルを開閉する処理を行っていたJavaScriptも全てCoffeeScriptに書きなおしました。

f:id:ukstudio:20150430151532p:plain

この時点ではイベントハンドラを置き換えただけなのであまり大きなメリットはありませんでした。コードの見通しは若干よくなりはしましたが、モーダルは至る所で使われていたので影響範囲が広く一部でモーダルが開かない/閉じないなどのバグを発生させてしまったぐらいです。

ですが、私自身まずVue.jsに慣れるという点にフォーカスしていたので置き換えやすいv-onから使いはじめるというのは妥当な判断だったと思います。

データバインディングでVue.jsのメリットを感じる

Vue.jsのメリットが目立つようになってきたのは私達がranunculus(ラナンキュラス)というコードネームで呼んでいるリニューアルへの作業が始まってしばらくした頃です。

SPOTLIGHTSはお花を贈るサービスですので当然贈るお花を決める必要があります。お花を選択するページも今回のリニューアル対象でした。新しいお花の選択ページではクリックしたお花を同画面の下の部分で表示するようになっています。

f:id:ukstudio:20150430151553p:plain

Vue.jsのデータバインディングを使うことでDOM操作を自前で書かなくとも、Vueのメソッド内でdataの値を変更することでプレビューも自動的に変わるようになりました。

# 擬似コードです
new Vue
  data:
    product:
      imagePath: undefined
  methods:
    selectProduct: (e) ->
      @product.imagePath = e.currentTarget.dataset.imagePath
%img(v-attr='src: product.imagePath')

今回のリニューアルではユーザーがなにか操作(大体においてクリック)したら、別の箇所も書き換えるというケースがそれなりにありデータバインディングのお手軽さには非常に助けられました。jQueryでも当然同じことは実現できますが実装の手間は大分軽減されたと感じています。

ページごとにVueインスタンスを作り肥大化を抑える

SPOTLIGHTSはRailsアプリケーションですのでJavaScriptのファイル群は全てSprocketsによってapplication.jsに統合されます。よってVue.js採用時点でページごとにJavaScriptを読み分ける仕組みはありませんでした。

最初のうちはひとつのVueインスタンスに全てのコードを詰め込んでいました。想像に難くないと思いますが当然肥大化します。Vue.jsにはmixinの仕組みがあるのでファイル自体は分割できるのですがインスタンスの肥大化自体は避けられません。さすがに厳しいと感じたので対応することにしました。

案としてはふたつありました。ひとつは各ページごとにJavaScriptファイルを用意し個別に読み込ませること。Sprocketsはapplication.rbの指定で個別にファイルを用意することができるので不可能ではありませんでしたが、既存ページに個別に読み込む処理を記述していくのは大変だなと思い却下しました。

採用した案はもうひとつのapplication.js上で読み込む処理を切り替えることでした。具体的に言ってしまえばbodyタグにそのページ専用のdata attributesを持たせその値を見て分岐させます。

%body(data-controller-name="#{controller.controller_name}" data-action-name="#{controller.action_name}")
switch controller_name
  when 'plans'
    switch action_name
      when 'new'
        # PlansController#newの処理
  else
    # それ以外のページの処理

この方法の利点は既存のページの影響を最小限に少しずつ置き換えていくことができることです。あるページに関する実装や修正が発生したときに、それらとあわせてそのページ専用のVueインスタンスに置き換えていきました。

switch文の複雑さを抱えてしまいましたが、Vueインスタンスはページごとに分割されたおかげで肥大化を抑えることができました。

コンポーネントの利用

Vueインスタンスをページごとに作るようにしたといってもページによってはそれなりの複雑さになることがありました。そのためコンポーネントを用いて更に分割することにしました。

上記画像はお花のお届け日を選択するカレンダーですが、ここですることは大まかに3つあります。

  • 選択された日付に応じてカレンダーの描画を変更する
  • 選択された日付に応じてプレビューの描画を変更する
  • 選択された日付をPOST用のhidden fieldの値に入れる

この3つのうち最初の1つをコンポーネントとして実装しました。このカレンダーは選択されたお届け日に対して、お花の購入締切日の計算やカレンダーの各要素のクラスの割り当てなどそれなりに複雑な処理が必要でした。その部分をコンポーネントとすることで、日付選択のUIに関する部分だけど1つのまとまりにできます。

f:id:ukstudio:20150430151619p:plain

Vue.component('delivery-calendar',
  methods:
    selectDeliveryDate: (e) ->
      # カレンダーの描画処理
      $emit('selectDeliveryDate', delivery_date: e.currentTarget.dataset.deliveryDate)
)

new Vue
  methods:
    fillFormAndPreview: (e) ->
      # 選択日時のプレビュー
      # POST用hidden fieldの値更新
%delivery-calendar(data-v-events='selectDeliveryDate: fillFormAndPreview')

このコンポーネントは選択された日付を$emitを使ってイベントと一緒に通知しますが、それをどう処理するかに関しては関与しません。イベントを受け取る側では選択に関する細々とした処理がコンポーネント内に隠蔽されるため、その日付をどう処理するかにだけ集中すればよくコードの見通しもよくなります。

f:id:ukstudio:20150430151637p:plain

また、こうすることでカレンダーを別のページでも使えるようになりました。上の画像は左右それぞれ別のページですが、同じコンポーネントを使っています。またそれぞれ選択された日付に応じて行うべき処理が若干違うのですが、それはイベントを受け取った側で処理するだけなのでコンポーネント側では気にする必要がありません。

テンプレートはscriptタグを使用

コンポーネントで使用するテンプレートはscriptタグで定義するようにしました。なぜかというとカレンダーのHTMLなどは元々Hamlで書かれていたのでそれらの資産をそのまま流用したかったのと、チームのエンジニアがHamlに馴染んでいるためこの方式ならば抵抗感もないだろうという判断のためです。

%script(type='text/x-template')
  .delivery-calendar
    省略
%delivery-calendar

コンポーネントのタグとセットで必ずscriptタグも必要になってしまいますが、Railsのpartialの仕組みを使えばあまり問題になりません。

まとめ

さて、いかがだったでしょうか。既にプロダクションとして投入しているRailsアプリケーションにVue.jsを採用した事例として参考になれば幸いです。

現時点で課題はいくつか残っており、例えばVueインスタンスを振り分けるためのswitch文がそれなりにでかくちょっと無視できない感じになってきました。この問題に関してはなんらかのルーティングライブラリの採用などを検討しています。

とはいえ基本的にはチームからの評判もよく、ここ2ヶ月の進捗を見ている限りではVue.js採用は概ね成功といえる状況です。今後もよりよいサービスを作れるよう色々挑戦していきたいと思っています。

長々とありがとうございました。