Rails
AngularJS

APIサーバを Rails、フロントエンドを AngularJS で開発する [その③]

More than 3 years have passed since last update.

前々回の その①:考察編 、前回の その②:環境構築編 の続きで、その③:実装編 です。



  • 実際に Rails と AngularJS のアプリケーションを開発してみました。前にも載せましたが、こいうの。



    • シングルアプリケーション構成で、画面をリロードせずに、データの追加/削除ができます。

      スクリーンショット 2014-08-25 19.06.31.png






  • Rails と AngulaJS の関係は次のような感じ。Rails は API に徹します。画面まわりは AngularJS で。

    スクリーンショット 2014-08-23 0.56.47.png




  • フロントエンドのソースは、Rails と同一ソースツリー内にあり、稼働サーバにも Rails と同時にデプロイされますが(クロスドメイン、クロスオリジンではない)、Rails とは独立した開発環境を持ちます。

    スクリーンショット 2014-08-23 1.36.48.png



最終的なソースは GitHub に載せたので、ページ最後のリンクからご覧ください。


① Rails で Scaffold し CRUD の流れを生成

ゼロから作ってもいいのですが、手を抜くために Scaffold を元に作成します。



  • 下記コマンドを実行。

    $ cd rails_angular_app
    
    $ ./bin/rails g scaffold person name:string age:integer memo:text




  • DBを migrate します。

    $ cd rails_angular_app
    
    $ ./bin/rake db:create(もし作成されていない場合)
    $ ./bin/rake db:migrate



  • ここで一旦、動作確認。

    $ cd rails_angular_app
    
    $ ./bin/rails s



  • ブラウザで http://127.0.0.1:3000/people へアクセスし、次のように表示されればOK。New Person リンクから適当にデータも登録しておきます。

    スクリーンショット 2014-08-24 1.14.28.png




② API用に Rails の Controller を修正

今回の仕様としては、GET /people.json でデータの一覧を、POST /people.json で新規データ作成を、DELETE /people/[id].json でデータの削除をするものとします。

rails_angular_app/app/controllers/people_controller.rb を次のようにします。

Scaffold からの変更点は、コメントで ADD と記載した箇所です。ていうか、ほぼ、いじらず。(もしアクセストークンの連携をする場合、どうやるのかしら。。)


rails_angular_app/app/controllers/people_controller.rb

class PeopleController < ApplicationController

before_action :set_person, only: [:show, :edit, :update, :destroy]

# ADD:認証は今回はスキップ。。
skip_before_action :verify_authenticity_token

# GET /people
# GET /people.json
def index
@people = Person.all
# ADD:一覧で JSON を返す
render json: @people
end

# GET /people/1
# GET /people/1.json
# ADD:今回は使用しないのでコメントアウト
#def show
#end

# GET /people/new
# ADD:今回は使用しないのでコメントアウト
#def new
# @person = Person.new
#end

# GET /people/1/edit
# ADD:今回は使用しないのでコメントアウト
#def edit
#end

# POST /people
# POST /people.json
def create
@person = Person.new(person_params)

respond_to do |format|
if @person.save
format.html { redirect_to @person, notice: 'Person was successfully created.' }
format.json { render :show, status: :created, location: @person }
else
format.html { render :new }
format.json { render json: @person.errors, status: :unprocessable_entity }
end
end
end

# PATCH/PUT /people/1
# PATCH/PUT /people/1.json
# ADD:今回は使用しないのでコメントアウト
#def update
# respond_to do |format|
# if @person.update(person_params)
# format.html { redirect_to @person, notice: 'Person was successfully updated.' }
# format.json { render :show, status: :ok, location: @person }
# else
# format.html { render :edit }
# format.json { render json: @person.errors, status: :unprocessable_entity }
# end
# end
#end

# DELETE /people/1
# DELETE /people/1.json
def destroy
@person.destroy
respond_to do |format|
format.html { redirect_to people_url, notice: 'Person was successfully destroyed.' }
format.json { head :no_content }
end
end

private
# Use callbacks to share common setup or constraints between actions.
def set_person
@person = Person.find(params[:id])
end

# Never trust parameters from the scary internet, only allow the white list through.
def person_params
params.require(:person).permit(:name, :age, :memo)
end
end


とりあえず GETメソッド だけ動作確認。ブラウザで http://127.0.0.1:3000/people にアクセスし、次のように表示されればOK。JSON 形式になってますね^^

スクリーンショット 2014-08-24 12.46.13.png


③ クロスオリジン(クロスドメイン)対応

開発中は、Rails 側が 127.0.0.1:3000、AngularJS 側が 127.0.0.1:9000 となるので、AngularJS から Rails へアクセスすとクロスオリジンでエラーとなります(ポートを変更できても、どのみち同じポートではローカルサーバを起動できない。)。

スクリーンショット 2014-08-26 0.32.44.png

そこで、クロスオリジンに簡単に対応できる、rack-cors という gem を導入します。Gemfile を修正し、


rails_angular_app/Gemfile

gem 'rack-cors', :require => 'rack/cors', group: [:development, :test]


bundle install します。

$ bundle install --path vendor/bundle


前回の bundle install 時に rails_angular_app/.bundle/config が作成され、中に --path vendor/bundle が記載してあれば、コマンドラインでは --path vendor/bundle は不要です。


次に rails_angular_app/config/environments/development.rb に下記を追記します。(なので development 環境だけで有効となる。)


rails_angular_app/config/environments/development.rb

config.middleware.insert_before ActionDispatch::Static, Rack::Cors do

allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :delete]
end
end

origins127.0.0.1:9000 としてもいいかと思います。

Railsサーバを再起動すると、GET、POST、DELETEメソッド時に Access-Control-Allow-Origin ヘッダが付与されるようになります。


④ AngularJS から、Rails が解釈できる形で POST できるようにする

app.coffee に config を追加(下ソースの2つ目)し、POSTデータの変換(そのままだと AngularJS からはJSONで送られてしまうので)、および、POST時にHTTPヘッダを追加するようにします。


rails_angular_app/front/app/scripts/app.coffee

"use strict"

angular.module("frontApp", [
"ngSanitize",
"ngRoute"
]).config(["$routeProvider", ($routeProvider) ->
$routeProvider
.when "/",
templateUrl: "views/main.html"
controller: "MainCtrl"
.otherwise
redirectTo: "/"
]).config(["$httpProvider", ($httpProvider) ->

$httpProvider.defaults.transformRequest = (data) ->
return data if data is `undefined`
$.param data

$httpProvider.defaults.headers.post = "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
])



⑤ AngularJS のアプリケーション作成

ここは Rails とか関係なく、普通に作るだけです。今回つくったソースを簡単に説明します。


1. APIハンドリング用のFactory

Controllerに書くのもあれなので、Factoryに分けました。それぞれの API を呼び出すだけです。


rails_angular_app/front/app/scripts/services/api.coffee

"use strict"

angular.module("frontApp")
.factory "Api", ($http) ->

host = "http://127.0.0.1:3000"
#host = ""

getPeople: ->
$http.get(host + "/people")
.success (data, status, headers, config) ->

postPeople: (obj) ->
$http.post(host + "/people.json", obj)
.success (data, status, headers, config) ->

deletePeople: (id) ->
$http.delete(host + "/people/" + id + ".json")
.success (data, status, headers, config) ->



2. View

APIのレスポンス結果を ng-repeat で TABLE へ表示、ng-click に削除ボタンと追加ボタンのメソッドを定義、新規データ追加時のユーザの入力値は、ng-model で変数バインドしています。

デザインでは Twitter Bootstrap の class を利用しています。

削除ボタンの doDelete($index)$index は、ng-repeat の N番目の N が格納されます。

ちなみに ng-controller を書いてないのは、app.coffee で既に View と Controller の対応づけがされている為です。


rails_angular_app/front/app/views/main.html

<div class="container">

<table class="table table-striped">
<thead>
<tr>
<th>名前</th>
<th>年齢</th>
<th>メモ</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="result in results">
<td>{{result.name}}</td>
<td>{{result.age}}</td>
<td>{{result.memo}}</td>
<td align="right"><button class="btn btn-danger" ng-click="doDelete($index)">削除</button></td>
</tr>
<tr>
<td><input type="text" class="form-control" ng-model="new_name"></td>
<td><input type="text" class="form-control" ng-model="new_age"></td>
<td><input type="text" class="form-control" ng-model="new_memo"></td>
<td align="right"><button class="btn btn-primary" ng-click="doPost()">追加</button></td>
</tr>
</tbody>
</table>
</div>


3. Controller


  • 先程の Factory を DI

  • 初期表示時に、変数の初期化と、一覧データの取得

  • 追加ボタン、削除ボタンが押された時の処理

を書きます。

POSTデータ(bodyの中身)は、JSON形式で用意します。key は、Rails の場合は モデル名[カラム名] とする必要があるみたいです。(少しハマった。Rails 標準の New 画面での POST リクエストを解析しました..)

新規データ追加時/削除時は、それぞれ $scope.results.push $scope.results.splice で、画面にも反映しています。こういう時は AngularJS は楽ですね。

一応、then で、非同期に API の結果を反映するようにしてあります。


rails_angular_app/front/app/scripts/controllers/main.coffee

"use strict"

angular.module("frontApp")
.controller "MainCtrl", ["$scope", "Api", ($scope, Api) ->

clearInput = ->
$scope.new_name = ""
$scope.new_age = ""
$scope.new_memo = ""

clearInput()

Api.getPeople().then (res) ->
$scope.results = res.data

$scope.doPost = ->

obj =
"person[name]": $scope.new_name
"person[age]": $scope.new_age
"person[memo]": $scope.new_memo

Api.postPeople(obj).then (res) ->
$scope.results.push res.data
clearInput()

$scope.doDelete = (index) ->
Api.deletePeople($scope.results[index].id).then (res) ->
$scope.results.splice index, 1
]



4. index.html

yo で作成されたものほぼそのままで、追加した JavaScript ファイル(api.js)の読み込みだけを追記します。


rails_angular_app/front/app/index.html

<!-- build:js({.tmp,app}) scripts/scripts.js -->

<script src="scripts/app.js"></script>
<script src="scripts/controllers/main.js"></script>
<script src="scripts/services/api.js"></script>
<!-- endbuild -->


⑥ フロントエンドのソースを Rails 側へ展開

開発中は不要なのですが、稼働サーバにソースをあげたり、同一ドメイン(ポート)での動作確認をするために、フロントエンドのソースを Rials の public ディレクトリへ展開する設定をします。

(minify や concat なども、ここで対応されます。)

これは簡単で、Gruntのタスクで、dist の値を一つ書き換えるだけです。


rails_angular_app/front/Gruntfile.js



yeoman: {
// configurable paths
app: require('./bower.json').appPath || 'app',
//dist: 'dist'
dist: '../public'
},


試しに展開してみます。まずは、APIの向き先ドメインを空(=自ホスト)へ変更し、


rails_angular_app/front/app/scripts/services/api.coffee

"use strict"

angular.module("frontApp")
.factory "Api", ($http) ->

#host = "http://127.0.0.1:3000"
host = ""

getPeople: ->
$http.get(host + "/people")
.success (data, status, headers, config) ->


grunt build を実行します。

$ cd rails_angular_app/front

$ grunt build -f

ブラウザで、http://127.0.0.1:3000 へアクセスしてみます。

スクリーンショット 2014-08-26 1.16.40.png

OKですね^^ あとは production 環境にデプロイすれば、きっと動くはず!


おわりに



  • 今回のソースは GitHub に置きました。大したものではないですが、ご自由にご利用ください。


    • https://github.com/hkusu/Rails_AngularJS_app


    • お手元で再現する場合は、git clone した後、次のようにすればいいかと思います(試してないのでたぶん)。

      $ cd rails_angular_app
      
      $ bundle install --path vendor/bundle
      $ ./bin/rake db:create
      $ ./bin/rake db:migrate
      $ cd front
      $ npm install
      $ bower install



    • Rails の public ディレクトリもコミットしておいたつもりですが、もし無ければ、次のようにしてフロントエンドのソースを public フォルダへ展開してください。

      $ cd rails_angular_app/front
      
      $ grunt build



      • アプリケーションを動かすには、次のように Rails のローカルサーバを起動した後、ブラウザで http://127.0.0.1:3000 へアクセスしてください。

        $ cd rails_angular_app
        
        $ ./bin/rails s







  • 時間は空いてしまうと思いますが、今回のようなテストアプリケーションでなく、実際のモバイルアプリケーションの運用で使えるよう、続編を書く予定です。


    • Onsen UI (CSSフレームワーク) で、モバイルアプリのUIを作成

    • Grape で API を作成

    • マスタの管理画面を Active Admin で作成

    • セキュリティ対応

    • Heroku もしくは EC2(AWS)へのデプロイ