AngularJSとRails4を組み合わせる

https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4

1 comment | 1 point | by WazanovaNews 3年以上前


Jshiike 3年以上前 | ▲upvoteする | link

Railsアプリのホスティングサービスを提供しているShelly Cloudがブログで、フロントでAngularJS、バックエンドでRails 4を採用するときの留意点について、まとめています。使用しているコードサンプルはこちらになります。

1) Building a JSON API in Rails

RailsでAPIをいちから作ってみる。angularjs-rails-resourceのようなライブラリを使うときは詳細が違ってくるが、考え方は概ね同じ。

Routing

namespace :api, defaults: {format: :json} do
  resources :task_lists, only: [:index] do
    resources :tasks, only: [:index, :create, :update, :destroy]
  end
end

いたって標準的な手法。全てのリストをtask_lists#indexから取得し、特定のリストのためのtask listingをtasks#indexから入手、create/update/destroyで必要なアクションをとる。format: :jsonを使うのが手軽なデフォルト。rake routesを実行すると、下記のようなアウトプットになる。

GET    /api/task_lists/:task_list_id/tasks(.:format)     api/tasks#index {:format=>:json}
POST   /api/task_lists/:task_list_id/tasks(.:format)     api/tasks#create {:format=>:json}
PATCH  /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#update {:format=>:json}
PUT    /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#update {:format=>:json}
DELETE /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#destroy {:format=>:json}
GET    /api/task_lists(.:format)                         api/task_lists#index {:format=>:json}

更新のアクションとしてPATCHとPUTの二つがあるが、PATCHはRails 4.0で追加でサポートされるよになった。詳細はこちらを参照されたし。

Request parameters

Rails 4では、マスアサイメント(フィールドの一括設定)の保護の仕方も変更になっている。モデルでパラメータをホワイト/ブラックリスト化するのではなく、requirepermitメソッドを使って、コントローラで処理しなくてはいけなくなった。create/updateのアクション両方で使えるよりは、helperメソッドを作るほうが自分も好きだ。

def safe_params
  params.require(:task).permit(:description, :priority, :completed)
end

この定義で、取りうるアクションの実装は下記のようになる。

def create
  task = task_list.tasks.create!(safe_params)
  render json: task, status: 201
end
def update
  task.update_attributes(safe_params)
  render nothing: true, status: 204
end

Generating JSON

JSONのアウトプットを返すのは、render json: objectを書くくらいシンプルであるべき。active_model_serializers gemを使えば、処理がすごくシンプルになる。オブジェクトをJSONにする際には、常に適切なserializerが利用される。今回のto doリストアプリの場合は、taskのarrayをこのようになる。

render json: TaskList.find(params[:id]).tasks

gemをインストールした後に、AngularJSで使いやすいように正確にフォーマットするには設定が必要で、下記のコードをconfig/initializers/active_model_serializers.rbに追加する。

ActiveSupport.on_load(:active_model_serializers) do
  # Disable for all serializers (except ArraySerializer)
  ActiveModel::Serializer.root = false
  # Disable for ArraySerializer
  ActiveModel::ArraySerializer.root = false
end

設定に従って、serializerはこのように定義する。

# app/serializers/task_serializer.rb
class TaskSerializer 

そして、アウトプットは下記のとおりとなる。

[
 {'id' => 123,
  'description' => 'Send newsletter',
  'priority' => 2,
  'due_date' => '2013-09-10',
  'completed' => true},
 {'id' => 124,
  'description' => 'Prepare presentation',
  'priority' => 1,
  'due_date' => '2013-09-17',
  'completed' => false}
]

Testing

RSpecを使ったサンプルテストのコードは下記のようになる。

describe Api::TasksController do
  it "should be able to create a new record" do
    post :create, task_list_id: task_list.id,
      task: {description: "New task"}, format: :json
    response.should be_success
    JSON.parse(response.body).should == {'id' => 123, ...}
  end
end

format: :jsonがあることで、パラメータがJSONになることが担保される。このようなテストを書いていると、レスポンスをパースするhelperメソッドを定義したくなるかもしれない。その場合は、下記のコードをspec_helper.rbに加えるとよい。

module JsonApiHelpers
  def json_response
    @json_response ||= JSON.parse(response.body)
  end
end
RSpec.configure do |config|
  config.include JsonApiHelpers, type: :controller
end

下記の替わりにこのコードがあることで、

JSON.parse(response.body).should == {...}

このように書くことができる。

json_response.should == {...}

このほうがコードが多少きれいになるし、アウトプットに複数のassertionを設定しても、レスポンスは一度だけしかparseされない。

2) Building AngularJS application

Including AngularJS files

一番手っ取り早いのは、AngularJSのタグを含んだJavaScriptを直接レイアウトに置く方法。この記事を書いている時点での最新は1.0.8で、下記の二行をapp/views/layouts/application.html.slimに加えるとよい。

= javascript_include_tag "//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"
= javascript_include_tag "//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular-resource.min.js"

もちろんファイルをダウンロードして、app/assets/javascripts/に置くこともできる。renameの影響で、asset pipelineがAngularJSのコードを壊してしまうかもしれないが、それを避けるには下記のコードをconfig/environments/production.rbに置くとよい。

config.assets.js_compressor = Uglifier.new(mangle: false)

このおかげで、JavaScriptファイルを連結/圧縮する際に名称がおかしくなることがない。詳細は、こちらの"A Note on Minification"を参照されたし。

Structuring the AngularJS code

AngularJSアプリは、メインアプリのmodule、controller、directive、serviceで構成される。app/assets/javascripts/に置いておけば、asset pipelineがまとめて対応してくれる。最終的にどこに置くかは各人の好みだが、自分の場合は、application.jsに、まず依存する外部のもの(jQuery, AngularJS)をリスト。次にメインアプリのmoduleを含むファイル、そして最後にrequire_treedirectiveを置いた。

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require lib/angular.min
//= require lib/angular-resource.min
//= require todoApp
//= require_tree .

これを前提に、メインアプリのmoduleはtodoApp.js.coffeeで定義され、下記のようになる。

todoApp = angular.module('todoApp', ['ngResource'])

残りのファイルはサブディレクトリに置いた。AngularJSアプリの標準的なelementは、controllersdirectivesservicesに。その他の依存関係のファイルは、libに。

Defining the service

Rails APIはAngularJSアプリから、ngResource moduleを介してアクセスできる。controllerのリソースディレクトリを使う替わりに、serviceを定義するのがよい。この手法で、Rails modelでやるときと同じように、データにアクセスするときに細かいことを抽象化できる。下記の事例が、taskにアクセスする基本的なserviceをCoffeeScriptで書いたもの。

angular.module('todoApp').factory 'Task', ($resource) ->
  class Task
    constructor: (taskListId) ->
      @service = $resource('/api/task_lists/:task_list_id/tasks/:id',
        {task_list_id: taskListId, id: '@id'})
    create: (attrs) ->
      new @service(task: attrs).$save (task) ->
        attrs.id = task.id
      attrs
    all: ->
      @service.query()

例えば、特定のリストから全てのtaskの一覧を取得するには、

$scope.tasks = Task(taskListId).all()

Making it work with CSRF protection

Railsでは書くページのhead sectionに組み込むトークンで、CSRFに対応できる。AngularJSで有効にするには、全てのAPIリクエストでトークンを使う必要がある。下記のコードをメインアプリのファイル(自分の場合は、todoApp.js.coffee)に置く。

todoApp.config ($httpProvider) ->
  authToken = $("meta[name=\"csrf-token\"]").attr("content")
  $httpProvider.defaults.headers.common["X-CSRF-TOKEN"] = authToken

Making it work with turbolinks

turbolinkはRails 4でデフォルト採用されたが、複数のページで異なるシングルページアプリをサポートしなくてはいけない場合は、AngularJSアプリで問題を起こすかもしれない。回避策としては、下記のコードをメインのアプリのファイルに置くこと。

$(document).on 'page:load', ->
  $('[ng-app]').each ->
    module = $(this).attr('ng-app')
    angular.bootstrap(this, [module])

これのおかげで、turbolinkがfetch & replaceをする度に、AngularJSアプリを適切に初期化される。

Making updates using the PATCH method

冒頭に紹介したPATCHメソッドは、ngResourceではデフォルトでサポートされていないが、簡単にセットできる。メインアプリのファイルに下記のコードを置くこと。

defaults = $http.defaults.headers
defaults.patch = defaults.patch || {}
defaults.patch['Content-Type'] = 'application/json'

これで、PATCHリクエストはapplication/jsonコンテンツタイプになる。そして、リソースの定義を修正することで、更新の際のデフォルトのアクションとして設定できる。

$resource('/api/task_lists/:task_list_id/tasks/:id',
  {task_list_id: taskListId, id: '@id'},
  {update: {method: 'PATCH'}})

Testing

AngularJSアプリの単体テストのコンポーネントは、DI (dependency injection)が考慮されていてよくできている。Karmaを使ったテストインフラのセットはAngularJSの公式チュートリを参照されたし。Jasmineangular-mocksも簡単に使うことができた。

Debugging

テストが失敗したら、ブラウザを立ち上げて手動で調べてみるのがよい。StackoverflowでのMisko Heveryの回答が非常に役にたった。ブラウザでAngularJSアプリの内部を確認するのはそれほど難しいことではない。jQueryでelementを把握するだけのこと。例えば、taskDescription elementのコンテクストにおいてスコープにアクセスするには、

$("#taskDescription").scope()

そこからcontrollerのステートを追うことができる。Chrome extensionのAngularJS Batarangも役に立つ。


[2013] ワザノバTop100アクセスランキング


#angularjs #rails #コーディング

Back