Rails
AngularJS

rails × AngularJSでシングルページアプリケーションを作るTips

More than 1 year has passed since last update.

メリークリスマスですね、今回でTECH::CAMP Advent Calendar 2015は最後になりますが、ラストを飾るに相応しくない渋めの内容になります。(笑)

Webアプリケーションの新しいアーキテクチャの一つにSPA(Single Page Application)というものがあります。一言で言えばアプリケーション内のリンクを辿っていくときにページ全体を読み込むのではなく必要な所だけを読み込んでいくようなもののことです。

今回は、僕がそんなSPAをAngularJSとrailsを組み合わせて作った時に悩んだ2つのポイントについて書きたいと思います。
まだまだぺーぺーなのでお手柔らかに、肩の力を抜いてお読み下さいませ。
なお、定石的な実装や手順については既に良記事がいくつかありますので説明しません。

railsは4.2、AngularJSは1.4系

前提

以下のような記事を読めるアプリケーションを想定します。

  • ナビゲーションエリアとコンテンツエリアに分かれている
  • トップページ/homeのコンテンツエリア内の各記事をクリックすると、記事の詳細ページ/posts/:idへ遷移する
  • ページ遷移時にナビゲーション部分は再読み込みされず、コンテンツエリア内だけが読み込まれる


・トップページ
名称未設定-2____150___CMYK_GPU_プレビュー_.jpg

・コンテンツクリック後(コンテンツエリアのみ描画)
名称未設定-2____91_05___CMYK_GPU_プレビュー_.jpg

その1【ng-viewをどこに置くか】

悩んだ点その1です。実際にググってもサイトによって違う部分でした。ここでは2つのパターンを紹介します。

パターン1

以下のようなルーティングにします。上3行はrailsのAPI用のルーティングです。

routes.rb
...

namespace :api, defaults: {format: 'json'} do
  resources :posts, only: [:index, :show]
end

root 'top#index'
get '/home'      => 'top#index'
get '/posts/:id' => 'top#index'

そして、top#indexはlayouts/application.htmlに飛ばしてしまいます

top_controller.rb
class TopController < ApplicationController

  def index
    render "layouts/application"
  end

end

まじかよって感じですが、application.htmlはこのようにします

application.html.slim
...

body ng-app="sampleApp"

  = render "layouts/navigation"

  div(ng-view)

普段からrailsで普通のウェブアプリケーションを作っている人は、application.htmlに<%= yield %> がないのに気づいたかと思います。
つまり、yieldの代わりにng-viewを入れているのですね。AngularJSでは、ng-viewをいれたところに、ngRouteモジュールの$routeProviderサービスで設定するangularのテンプレート及びコントローラーが埋め込まれていきます。これにより、layouts/navigationの部分は初回アクセス時のみ読み込み、後は遷移したURLに従って必要なテンプレートを読み込むだけでよいというSPAの要件が実現します。

次に、angularのルーティングも載せておきます。最近はより強力な機能を持つui-routerもありますが、こちらのngRouteの方が簡単です。これによって、前述のng-viewの場所にリクエストされたURLに応じてテンプレートとコントローラーを埋め込んでいきます。

app/assets/javascripts/app.js.coffee.erb
app = angular.module('App', ['ngResource', 'ngRoute'])

...

app.config ($routeProvider, $locationProvider) ->
  $locationProvider.html5Mode true
  $routeProvider.when '/', redirectTo: '/home'
  $routeProvider.when '/home', templateUrl: "<%= asset_path 'home/top.html' %>", controller: 'TopController'
  $routeProvider.when '/posts/:post_id', templateUrl: "<%= asset_path 'posts/show.html' %>", controller: 'PostController'
  $routeProvider.otherwise redirectTo: '/home'

パターン2

基本的にはパターン1と変わりません。同じようにルーティングから見ていきましょう。

routes.rb
...

namespace :api, defaults: {format: 'json'} do
  resources :posts, only: [:index, :show]
end

root 'top#index'
get '/home'      => 'top#index'
get '/posts/:id' => 'top#index'

これはパターン1と全く同じですね。次にtop#indexです。

top_controller.rb
class TopController < ApplicationController

  def index
  end

end

パターン1ではlayouts/applicationに飛ばしていましたが、今回は特に記述していないのでテンプレートはtop/index.htmlです。したがって、layouts/application.htmlにはng-viewを置かず<%= yield %>が通常通りあります。

top/index.html.slim
div(ng-view)
layouts/application.html.slim
body ng-app="sampleApp"

  = render "layouts/navigation"

  = yield

angularのルーティングは同じなので省略します。

見比べていかがでしょうか。パターン2の方がパターン1に比べて少し回りくどく感じる方もいるかもしれません。

どっちがいいの?

僕は最初、パターン1の方で書いていました。「railsがビューまで担当していた部分を、angularで置き換える」というのがシンプルに美しく表現できていると思ったからです。
ただ、開発していくうちに管理画面のようなページはrailsでビュー作っちゃっても良いんじゃないかと思うように。すると、パターン1では柔軟性に欠けることに気づきパターン2にしました。
実際、パターン2のものの方が多そうです。上記の他にメリットがあるようでしたらご教授願います。

その2【ブラウザバックでのリロード問題】

rails + AngularJSでSPA開発を進めていくうちに、ある問題に気づきました。
ブラウザバックで初回アクセスページに戻るとページ全体がリロードされてしまう
作っているアプリケーションがナビゲーション部分で動画を再生するようなものであったため、この問題は見過ごすわけにはいきませんでした。(そうでなくても、ブラウザバックした時にサーバーにページ全体をリクエストして読み込みに時間がかかるのでは、SPA化したのがもったいないですよね)

結論:railsのturbolinksを消しましょう

僕は全然気づかなかったのですが、AngularJSとturbolinksは多くの点で機能が被っており相性が悪いとのこと。海外のエンジニアブログなんか読むと良く議論がなされるテーマのようですが、「共存も可能だが、極力使わない方が良い」という声が多く見受けられました。

というわけで以下のステップでturbolinksを消します。

  1. Gemfileから、gem 'turbolinks'を削除
  2. app/assets/javascripts/application.jsから//= require turbolinksを削除
  3. application.htmlから2つある"data-turbolinks-track" => trueの記述を削除(下記)
application.html.slim
  // before
   = stylesheet_link_tag "application", media: "all", 'data-turbolinks-track' => true
   = javascript_include_tag "application", 'data-turbolinks-track' => true
  // after
  = stylesheet_link_tag "application", media: "all"
  = javascript_include_tag "application"

これで、どれだけブラウザバックをしてもリンクをたどるのと同じようにページ遷移できるようになりました。


以上です。読んでいただきありがとうございます。
AngularJSは2.0で大きな変更があったり、そもそもJSのフレームワーク戦争真っ只中だったりしてぐらついてる感が否めませんが、簡単なSPAを作るには良い感じがするので今後も勉強していきたいと思います。

それでは 良いお年を!