メリークリスマスですね、今回でTECH::CAMP Advent Calendar 2015は最後になりますが、ラストを飾るに相応しくない渋めの内容になります。(笑)
Webアプリケーションの新しいアーキテクチャの一つにSPA(Single Page Application)というものがあります。一言で言えばアプリケーション内のリンクを辿っていくときにページ全体を読み込むのではなく必要な所だけを読み込んでいくようなもののことです。
今回は、僕がそんなSPAをAngularJSとrailsを組み合わせて作った時に悩んだ2つのポイントについて書きたいと思います。
まだまだぺーぺーなのでお手柔らかに、肩の力を抜いてお読み下さいませ。
なお、定石的な実装や手順については既に良記事がいくつかありますので説明しません。
railsは4.2、AngularJSは1.4系
##前提
以下のような記事を読めるアプリケーションを想定します。
- ナビゲーションエリアとコンテンツエリアに分かれている
- トップページ
/home
のコンテンツエリア内の各記事をクリックすると、記事の詳細ページ/posts/:id
へ遷移する - ページ遷移時にナビゲーション部分は再読み込みされず、コンテンツエリア内だけが読み込まれる
##その1【ng-viewをどこに置くか】
悩んだ点その1です。実際にググってもサイトによって違う部分でした。ここでは2つのパターンを紹介します。
###パターン1
以下のようなルーティングにします。上3行はrailsのAPI用のルーティングです。
...
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に飛ばしてしまいます
class TopController < ApplicationController
def index
render "layouts/application"
end
end
まじかよって感じですが、application.htmlはこのようにします
...
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 = 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と変わりません。同じようにルーティングから見ていきましょう。
...
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です。
class TopController < ApplicationController
def index
end
end
パターン1ではlayouts/applicationに飛ばしていましたが、今回は特に記述していないのでテンプレートはtop/index.htmlです。したがって、layouts/application.htmlにはng-viewを置かず<%= yield %>が通常通りあります。
div(ng-view)
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を消します。
- Gemfileから、
gem 'turbolinks'
を削除 - app/assets/javascripts/application.jsから
//= require turbolinks
を削除 - application.htmlから2つある
"data-turbolinks-track" => true
の記述を削除(下記)
// 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を作るには良い感じがするので今後も勉強していきたいと思います。
それでは 良いお年を!