Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

AngularJS × Rails 4 入門:実務で通用するスタック構築チュートリアル

AngularJSの使い始めは、それほど難しくありません。公式ドキュメントは非常に充実しており、チュートリアルも十分にシンプルです。

しかし、複数の技術を組み合わせ始めた途端、話は一気に厄介になります。

素のJavaScriptではなくCoffeeScriptを採用すると、構文の違いに加えてプリコンパイル(事前処理)への配慮も必要になります。これらは単体では些細な問題ですが、そこにRuby on Rails、Jasmine、Karmaまで加わるとどうなるでしょうか。驚くほど手ごわくなります。

本チュートリアルでは、まさにこのスタックを使用します。苦行のためではありません。これは実際の開発現場で目にするタイプの構成だからです。

このチュートリアルでは、Railsには習熟しているものの、AngularJSは必ずしも得意ではない読者を想定しています。

ベースとなるRailsアプリの作成

関わる技術レイヤーが多いため、ここでは最低限の動作しかしないシンプルなアプリケーションを構築します。題材はレストランのCRUD機能——正確には「CR」の部分だけです。「UD」は読者の演習課題として残しておきます ;-)

アプリケーション名はRestauranteurとします。

ここではPostgreSQLとRSpecを使用しますが、DBMSやサーバーサイドのテストフレームワーク自体は重要ではありません。お好みのものを使っても構いません。

初期セットアップ

まずプロジェクトを作成します。

$ rails new restauranteur --database=postgresql --skip-test-unit

Powを使用している場合は、プロジェクトをPowに登録します。

$ ln -s /Users/jasonswett/projects/restauranteur ~/.pow/restauranteur

PostgreSQLのデータベースユーザーを作成します。

$ createuser -P -s -e restauranteur

GemfileにRSpecを追加します。

# Gemfile
gem "rspec-rails", "~> 2.14.0"

RSpecをインストールします。

$ bundle install
$ rails g rspec:install

データベースを作成します。

$ rake db:create

Restaurantモデルの作成

プロジェクトとデータベースが用意できたので、最初のリソースを作成しましょう。Restaurantリソースが持つ属性はname(文字列)のみです。

$ rails generate scaffold restaurant name:string

念のため、レストラン名が重複しないようにしておきましょう。

# db/migrate/[timestamp]_create_restaurants.rb

class CreateRestaurants < ActiveRecord::Migration
  def change
    create_table :restaurants do |t|
      t.string :name

      t.timestamps
    end

    # 以下の行を追加
    add_index :restaurants, :name, unique: true
  end
end

マイグレーションを実行します。

$ rake db:migrate

不正なレストランが作成できないことを検証するスペックを追加しましょう。なお、この時点ではユニーク制約違反でも素のエラーしか返らないことに注意してください。

require 'spec_helper'

describe Restaurant do
  before do
    @restaurant = Restaurant.new(name: "Momofuku")
  end

  subject { @restaurant }

  it { should respond_to(:name) }
  it { should be_valid }

  describe "when name is not present" do
    before { @restaurant.name = " " }
    it { should_not be_valid }
  end

  describe "when name is already taken" do
    before do
      restaurant_with_same_name = @restaurant.dup
      restaurant_with_same_name.name = @restaurant.name.upcase
      restaurant_with_same_name.save
    end

    it { should_not be_valid }
  end
end

以下のバリデーションを追加すれば、スペックが通るようになります。

class Restaurant < ActiveRecord::Base 
  validates :name, presence: true, uniqueness: { case_sensitive: false }
end

これで次のステップに進めます。

AngularJSの導入

いきなりすべてを詰め込むのではなく、まず最もシンプルな「Hello, world」版のAngularJS+Railsアプリケーションを見せてから、その上にレストランのCRUD機能を積み上げていきます。

「Hello, world」ページを特定のRailsリソースに紐付ける理由はありません。そこで、AngularJSのホームページを配信するためのStaticPagesControllerを作成します。

コントローラーの作成

$ rails generate controller static_pages index

現在のルートパスは「Welcome to Rails」ページになっているはずです。これを新しいStaticPagesControllerindexアクションに向けましょう。

# config/routes.rb

Restauranteur::Application.routes.draw do
  # 以下の行を追加
  root 'static_pages#index'
end

Angularのダウンロード

  1. 後ほどテストを正常に動かすために、angular-mocks.jsというファイルが必要になります。Angular公式ドキュメントではほとんど言及されていないようですが、必須のファイルです。
  2. AngularJSチュートリアルでは最新の最先端バージョンが案内されていますが、私の記憶では、最新バージョンのangular.jsangular-mocks.jsの間に互換性の問題がありました。バージョン1.1.5同士なら問題なく動作することを確認しているため、最新安定版ではありませんが、ここではこのバージョンを紹介します。もちろん、時間が経てば互換性の状況は改善していくでしょう。

code.angularjs.orgからangular.jsangular-mocks.jsをダウンロードし、ファイルをapp/assets/javascriptsに移動します。

$ wget https://code.angularjs.org/1.1.5/angular.js \
https://code.angularjs.org/1.1.5/angular-mocks.js
$ mv angular* app/assets/javascripts

アセットパイプラインへの追加

次に、AngularJSファイルを読み込むようアプリケーションに指示し、それに依存する他のファイルより先にロードされることを確認します。(RequireJSなどで依存関係を管理することもできますし、実運用の製品ならそうするでしょうが、このチュートリアルでは技術スタックをできるだけ薄く保ちたいと思います。)

注: AngularとTurbolinksは衝突することがあるため、ここではTurbolinksを無効化します。

// app/assets/javascripts/application.js

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs

// 以下の2行を追加
//= require angular
//= require main

//= require_tree .

レイアウトの設定

ページ内にAngularアプリが存在することを示すng-appng-viewを追加します。また、Turbolinksに関する記述が削除されていることにも注目してください。

  <%= yield %>

Angularコントローラーの作成

まずコントローラー用のディレクトリを作成します。名前は自由につけてかまいません。

$ mkdir -p app/assets/javascripts/angular/controllers

次にコントローラーファイル本体を作成します。ここでは「ホームコントローラー」と呼ぶことにします。Angularの命名規則では、コントローラーのファイル名の末尾にCtrlを付けます。したがってファイル名はapp/assets/javascripts/angular/controllers/HomeCtrl.js.coffeeとなります。

# app/assets/javascripts/angular/controllers/HomeCtrl.js.coffee

@restauranteur.controller 'HomeCtrl', ['$scope', ($scope) ->
  # このコントローラーの中身が空であることに注目してください
]

Angularルートの追加

次に、HomeCtrlを「デフォルトページ」にするためのルーティングディレクティブを追加します。ここではルーティングをapp/assets/javascripts/main.js.coffeeに定義していますが、繰り返しますがファイル名は何でも構いません。

# app/assets/javascripts/main.js.coffee

# この行はHomeCtrlそのものではなく、Angularアプリ全体に関わる記述です。
# アプリケーションの存在をAngularに伝える、いわば宣言のようなものです。
@restauranteur = angular.module('restauranteur', [])

# このルーティングディレクティブは、アプリケーションのデフォルトルートを
# Angularに伝えます。「otherwise」という用語はやや不自然に感じられるかもしれませんが、
# ルートを増やしていくうちに意味が分かってくるはずです。
@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    }) 
])

Angularテンプレートの追加

Angularテンプレートの置き場所も必要です。ここではpublic/templatesに置くことにしました。もちろん、好きな場所に配置できます。

mkdir public/templates

適当な内容でpublic/templates/home.htmlというファイルを作成すれば、ブラウザで確認できるはずです。


This is the home page.

https://restauranteur.dev/(Powを使用していない場合はhttps://localhost:3000/)にアクセスすると、home.htmlの内容が表示されるはずです。

データバインディングの例

これだけだとちょっとだけ興味深いものの、あまり感動はないかもしれません。実際に何かデータを送ってみましょう。app/assets/javascripts/angular/controllers/HomeCtrl.js.coffeeを次のように編集します。

# app/assets/javascripts/angular/controllers/HomeCtrl.js.coffee 

@restauranteur.controller 'HomeCtrl', ['$scope', ($scope) -> 
  $scope.foo = 'bar'
]

これはRailsコントローラーで@foo = "bar"と書くのに近いイメージです。二重中括弧の構文を使えば、テンプレートにfooを差し込めます。

Value of "foo": {{foo}}  

今度こそ本格的に

簡単なHello Worldアプリはすでに完成しました。本格的なCRUDアプリケーションを作るのも、それほど難しくはありません。

データベースへのシード

データベースにあらかじめレストランがいくつか入っていれば、CRUD操作もぐっと実感的になります。以下のシードファイルを使ってください。

# db/seeds.rb

Restaurant.create([
  { name: "The French Laundry" },
  { name: "Chez Panisse" },
  { name: "Bouchon" },
  { name: "Noma" },
  { name: "Taco Bell" },
])
rake db:seed

レストラン一覧ページの作成

まずレストラン用のテンプレートフォルダを作成します。

mkdir public/templates/restaurants

最初に作るのは一覧ページのテンプレートです。

[index](/#)

  * {{ restaurant.name }}

それぞれの記述が何を意味するのかは、すぐ後に説明します。まずはコントローラーを作成しましょう。

# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee

@restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) ->
  $scope.restaurants = []
  $http.get('./restaurants.json').success((data) ->
    $scope.restaurants = data
  )
]

最後に、ルーティング設定を調整します。

# app/assets/javascripts/main.js.coffee

@restauranteur = angular.module('restauranteur', [])

@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    when('/restaurants', {
      templateUrl: '../templates/restaurants/index.html',
      controller: 'RestaurantIndexCtrl'
    }).
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    })
])

これで、ついにURI /#/restaurantsにアクセスしてレストランの一覧を表示できるようになりました。先に進む前に、テストを1つ追加しておきましょう。

最初のテストを追加する

JavaScriptテスト用フォルダを追加します。

mkdir spec/javascripts

テストを書きます。

# spec/javascripts/controllers_spec.js.coffee

describe "Restauranteur controllers", ->
  beforeEach module("restauranteur")

  describe "RestaurantIndexCtrl", ->
    it "should set restaurants to an empty array", inject(($controller) ->
      scope = {}
      ctrl = $controller("RestaurantIndexCtrl",
        $scope: scope
      )
      expect(scope.restaurants.length).toBe 0
    )

Karmaの設定ファイルを追加します。

// spec/javascripts/restauranteur.conf.js

module.exports = function(config) {
  config.set({
    basePath: '../..',

    frameworks: ['jasmine'],

    autoWatch: true,

    preprocessors: {
      '**/*.coffee': 'coffee'
    }, 

    files: [
      'app/assets/javascripts/angular.js',
      'app/assets/javascripts/angular-mocks.js',
      'app/assets/javascripts/main.js.coffee',
      'app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee',
      'app/assets/javascripts/angular/*',
      'spec/javascripts/*_spec.js.coffee'
    ]  
  });
};

Karmaをインストールしてサーバーを起動します。

sudo npm install -g karma
sudo npm install -g karma-ng-scenario
karma start spec/javascripts/restauranteur.conf.js

https://localhost:9876/にアクセスすると、テストが実行され成功するはずです。失敗する様子を見たい場合は、expect(scope.restaurants.length).toBe 0expect(scope.restaurants.length).toBe 1に変更して、もう一度テストを実行してみてください。

今追加したテストの実質的な意味については疑問の余地があるかもしれません。しかし、ここでの狙いは「Angularのコードをテストハーネスに組み込む手間」を省いてあげることです。CoffeeScriptプリプロセッサの扱いやangular-mocks.jsの読み込みなど、まったく自明ではなく、私自身も何時間も頭を悩ませてようやく解決できたポイントがあります。

レストランページの拡張

ここで、レストラン一覧テンプレートに一時的な変更を加えてみます。

  * {{restaurant.name}} ({{restaurant.id}})

/#/restaurantsを再表示すると、レストランのIDが表示されていないことに気づくでしょう。なぜ空なのでしょうか?

Rails 4でスキャフォールドを生成すると、いくつかの.jbuilderファイルが作られます。

$ ls -1 app/views/restaurants/*.jbuilder
app/views/restaurants/index.json.jbuilder
app/views/restaurants/show.json.jbuilder

app/views/restaurants/index.json.jbuilderを開くと、次のようになっています。

# app/views/restaurants/index.json.jbuilder

json.array!(@restaurants) do |restaurant|
  json.extract! restaurant, :name
  json.url restaurant_url(restaurant, format: :json)
end

ご覧のとおり、:nameは含まれていますが:idがありません。追加しましょう。

# app/views/restaurants/index.json.jbuilder

json.array!(@restaurants) do |restaurant|
  json.extract! restaurant, :id, :name
  json.url restaurant_url(restaurant, format: :json)
end

ファイルを保存して/#/restaurantsを再読み込みすれば、IDが表示されるはずです。

次に、テンプレートを元の状態に戻します。

[index](/#)

  * {{ restaurant.name }}

ところで、どこかの時点でお気づきかもしれませんが、私たちはviewRestaurant()というものを参照していながら、実際には何も定義していませんでした。ここで定義しましょう。

# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee

@restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) ->
  $scope.restaurants = []
  $http.get('./restaurants.json').success((data) ->
    $scope.restaurants = data
  )

  # 以下の行を追加
  $scope.viewRestaurant = (id) ->
    $location.url "/restaurants/#{id}"
]

Railsの規約ではresource_name/:idは「show」ページにマッピングされますので、ここでも同じようにします。showテンプレート、ルート、コントローラーを作成しましょう。

# {{restaurant.name}}
# app/assets/javascripts/main.js.coffee

@restauranteur = angular.module('restauranteur', [])

@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    when('/restaurants', {
      templateUrl: '../templates/restaurants/index.html',
      controller: 'RestaurantIndexCtrl'
    }).
    when('/restaurants/:id', {
      templateUrl: '../templates/restaurants/show.html',
      controller: 'RestaurantShowCtrl'
    }).
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    })
])
# app/assets/javascripts/angular/controllers/RestaurantShowCtrl.js.coffee

@restauranteur.controller 'RestaurantShowCtrl', ['$scope', '$http', '$routeParams', ($scope, $http, $routeParams) ->
  $http.get("./restaurants/#{$routeParams.id}.json").success((data) ->
    $scope.restaurant = data
  )
]

/#/restaurantsを再読み込みしてレストランをクリックすると、そのレストランの詳細ページに移動するはずです。やりましたね!

今回はここまで

特に印象的な結果は得られなかったかもしれませんが、AngularJSをRails 4に組み込む手間を少しでも省けたなら幸いです。次のステップとしては、CRUD機能をよりDRYにできるngResourceを調べてみることをおすすめします。

さらに学びたい方へ

Adam Andersonによる素晴らしい記事もぜひチェックしてください。彼の「Bootstrapping an AngularJS app in Rails 4.0」シリーズは、私がAngularJSとRailsを始めるきっかけとなりました。彼のチュートリアルを一通りやってみるのも良いでしょう。ただし、このチュートリアルとの違いは、私は細部まで徹底的に手取り足取り説明することを心がけており、途中でつまずく可能性を最小限に抑えているという点です。

  1. Android MessagesをPCで使おう!Web版の初期設定と知って得する便利機能5選

    Googleは巨大なプラットフォームであり、世界有数の検索エンジンであることは言うまでもありません。しかし、その実力は検索だけにとどまりません。日常生活をより快適にしてくれる多彩なサービスを数多く提供しています。そんなGoogleが、Android Messagesアプリのアップデートをリリースしました。これにより、任意のWebブラウザからSMSメッセージを送受信できるようになり、AndroidユーザーはPCやMacから手軽にメッセージのやり取りが可能になりました。 利用に必要なのは、最新版にアップデートしたAndroid Messagesアプリだけ。以下では、Web版Android Mess

  2. Xen 仮想化の開始

    Zで綴られるZenは、8時間保持した後、雪の中でおしっこをしているときに頭頂部にある典型的な感覚です. Xen は X で綴られており、KVM と同様に、企業市場を対象としていますが、家庭でもビジネスでもセットアップを検討できるオープンソースの仮想化テクノロジです。 KVM に関するいくつかのチュートリアルがあり、導入記事、ストレージとネットワーク (ブリッジ ネットワークを含む) のセットアップと構成の方法、および VirtualBox との競合の解決方法をカバーしています。ここで、Xen についてもう少し学びましょう。同様に、仮想マシンをインストールして実行するために必要な最初の手順、