長文注意。
angularjsについて今更ながらに触り始めて色々と感動したので纏めておく。
angularjsがどういったフレームワークかは公式のチュートリアルを眺めてたらぼんやりと把握できると思うので今回その辺の話はあまり触れない。
http://angularjs.org/
angularjsのAPIについては公式のドキュメント含めて様々なメディアやブログに取り上げられているが、導入から体系的に語られてるものはあまり無い印象だったので、僕のブログでは導入から具体的な目的に沿った実装方法を紹介していこうと思う。
ちなみに自分のangularjsへの理解も触り始めて一週間程度なのでだいぶ甘い。
angularjsを一週間やってみた感想
最初の2日くらいがだいぶつらい。
飲み込みが早い人ならすぐに使いこなすのかもしれないが、angularjsはdirective, controller, filter, resource, serviceなどのキーワードが沢山出てきて、そして本当にどれも重要だったりする。
とにかく、公式のドキュメントを読みまくって手を動かすしか無い。
あとこの辺とか読んだ。英語よくわからないけどまぁなんとなく読んだ。
AngularJS in 60 Minutes
3日目くらいから感動の連続だった。
ほんとこれすごいとおもった(小並感)
Rails4にangularjsを導入する
sampleのrailsアプリのRails.root/app
以下はこんな感じだと想定する。
.
├── assets
│ ├── javascripts
│ │ ├── application.js
│ │ ├── widgets.js
│ └── stylesheets
│ ├── application.css
├── controllers
│ ├── application_controller.rb
│ ├── widgets_controller.rb
├── models
│ ├── widget.rb
└── views
├── layouts
│ └── application.html.haml
└── widgets
└── index.html.haml
まずはGemfileに
gem 'angular-rails-engine'
を追記して、bundle install
をして、application.jsに
//= require angular/angular
//= require angular/angular-resource
を追記する。
あと僕の環境ではangularjsをproduction環境で動かす際にconfig/environments/production.rb
に
config.assets.js_compressor = :uglifier
を設定しないとエラーがでて動かなかった。
取り敢えずHello World
angularjsのスコープを設定する
angularjsは自身がviewのどの部分を管理するかを知る必要がる。
逆に言うと、サイトの大部分はjQueryやbackboneなどで構築してるけど部分的にangularjsを導入してみたい、などの要件にもスコープの設定ができるがゆえに簡単に対応することが出来きる。
まずはapp/views/layouts/application.html.haml
に、
!!!
%html{'ng-app' => 'sampleApp'}
sampleApp
というネームスペースをhtmlタグに宣言する。
次にapp/views/widgets/index.html.haml
に以下の内容を記述する。
%div{"ng-controller" => "WidgetsController"}
{{ hello }}
divに"ng-controller"
という属性でネームスペースを宣言することで、このdiv以下の要素はangularjsの管理下にある事をangularjs自身は知ることが出来る。
"ng-controller"
以下のviewではjavascript expressionが自在に呼び出せるようになる。
上記のviewでは{{ … }}
で囲まれた部分がexpressionとして評価されhelloという変数の内容をhtml上に表示することが出来る。
viewで宣言したスコープとjsを紐付ける
"ng-controller"
以下のhello
に値を結びつける処理は以下になる。
//./app/assets/javascripts/widgets.js
var sampleApp = angular.module('sampleApp', []);
sampleApp.controller('WidgetsController', function($scope){
$scope.hello = 'Hello World';
})
まず一行目の
var sampleApp = angular.module('sampleApp', []);
によって%html{'ng-app' => 'sampleApp'}
で宣言したモジュールを作成する。
3行目以下の処理によりviewで宣言したhelloに値を代入する。
sampleApp.controller('WidgetsController', function($scope){
$scope.hello = 'Hello World';
})
次にsampleApp.controller
でviewで宣言したcontrollerである'WidgetsController'
を生成し、第二引数のfunctionにわたってくる$scope
という変数を経由してview(html)側のscopeにアクセスでき、$scope
を経由して変更された値は「リアルタイム」でviewに反映される。
$scopeで管理できるものは変数だけでなくfunctionでもいけるので例えば、
// view
%div{"ng-controller" => "WidgetsController"}
{{ hello('soplana') }}
//./app/assets/javascripts/widgets.js
sampleApp.controller('WidgetsController', function($scope){
$scope.hello = function(name){
'Hello '+name+'!!';
}
})
のような事も可能だ。
directiveを使ってみる
まだ僕自身directiveって何だよ感があるんだけど、現段階の理解としては「DOMに対する操作、あるいはテンプレートそのもの」を指すと考えている。
directiveは「DOMに対する操作、あるいはテンプレートそのもの」をDOM属性や要素名によって表現する。
いくつか例をあげてみる。
ng-clickを使う
$('#hoge').on('click', function(){})
みたいな事がしたい場合
// view
%div{'ng-click' => 'click()'}
hoge
//./app/assets/javascripts/widgets.js
sampleApp.controller('WidgetsController', function($scope){
$scope.click = function(){}
})
ng-hideを使う
$('#hoge').hide()
みたいな事がしたい場合
// view
%div{'ng-hide' => 'true'}
hoge
ng-repeatを使う
要素を繰り返し出力したい
// view
%ul
%li{'ng-repeat'=>'widget in widgets'}
{{ widget.name }}
//./app/assets/javascripts/widgets.js
sampleApp.controller('WidgetsController', function($scope){
$scope.widgets = [
{name: 'widget1'},
{name: 'widget2'},
{name: 'widget3'}
]
})
// 出力結果
<ul>
<li>widget1</li>
<li>widget2</li>
<li>widget3</li>
</ul>
この他にも非常に強力で便利なdirectiveが沢山容易されている。
また、自分でcustom directiveを作成して使うことも可能だ。
ここまでで一旦まとめ
基本的にはdirectiveを使って、controllerに定義したclickイベントを呼び出したり、DOMを操作したりする使い方が多そうだ。
そうなるとcontrollerがだんだんfatになっていく問題が浮上するが、それについてはいくつか対処法があり
- そもそも細かい単位でcontrollerを作っていく
- DIを利用する
- custom directiveを使う
などが考えられる(他にもあったら or 違うだろ!みたいなのがあったら教えてください)。
DIの話も面白いので次回まとめます。
angularjsでajax
やっとか…疲れた…。
railsのrouting
以下のような何の変哲もないroutingを想定する。
GET /widgets(.:format) widgets#index
POST /widgets/:id(.:format) widgets#update
DELETE /widgets/:id(.:format) widgets#destroy
※本来はPUTでupdateされるべきだが、PUTが出てくると内容が濃くなりすぎるのでPOSTに変更させてもらう
処理フロー
まず画面はこんな感じだ
画面にはwidgetが7つ並んでおり、それぞれに「この機能を使う」「この機能を削除する」というアクションボタンが用意されている。
この画面でやりたいことは以下になる。
- htmlのロードが完了したら、ajaxによりwidget一覧を取得する
- widget一覧を画面に出力する
- 「この機能を使う」をユーザがクリックした場合
- ajaxでPOSTリクエストを出しwidgetモデルを使用可能状態にupdateする
- ボタンを「この機能を削除する」に変更する
さて、これをjQueryで実装することを想像すると簡単な処理とは言えど、ロジックとDOM操作が入り組んだ保守性の悪いコードが容易に想像できる。(綺麗に書ける人も当然いるだろうけども)
view書いちゃう
まずはview
%div{"ng-controller" => "WidgetsController"}
.row
.col-lg-12
%h4.page-header
%i.fa.fa-gavel.fa-fw
機能の追加・削除
.row
.col-lg-4{'ng-repeat'=>"widget in widgets"}
.panel.panel-default.widget
.panel-heading
%span.glyphicon.glyphicon-user
{{ widget.label }}
%button.btn.btn-info
この機能を使う
%button.btn.btn-warning
この機能を削除する
.panel-body
{{ widget.description }}
viewはdirectiveの章で扱ったng-repeat
の例とさほど変わらない。
すこしhamlの要素が増えただけだ。
これによりcontrollerでwidgets
という変数に値を入れれば、繰り返し処理が行われhtmlに出力してくれることになる。
次にボタンをクリックした時の挙動のことを想像してみよう。
一連のフローで必要になるdirectiveは、
- ajaxリクエストを出すイベント発火用の
ng-click
- showとhideをtoggleさせる為の
ng-show
とng-hide
が必要になるっぽいので追加しちゃう。
%div{"ng-controller" => "WidgetsController"}
.row
.col-lg-12
%h4.page-header
%i.fa.fa-gavel.fa-fw>
機能の追加・削除
.row
.col-lg-4{'ng-repeat'=>"widget in widgets"}
.panel.panel-default.widget
.panel-heading
%span.glyphicon.glyphicon-user>
{{ widget.label }}
%button.btn.btn-info{'ng-click'=>'widget.save()', 'ng-hide'=>'widget.active'}
この機能を使う
%button.btn.btn-warning{'ng-click'=>'widget.remove()', 'ng-show'=>'widget.active'}
この機能を削除する
.panel-body
{{ widget.description }}
ng-show
とng-hide
はwidgetが持っているactive
というbool値を渡す事でtoggleを実現できそうだ。
js書いちゃう
// ./app/assets/javascripts/widgets.js
var sampleApp = angular.module('sampleApp', ['ngResource']);
sampleApp.factory('Widget', function($resource){
var Widget = $resource('/widgets/:id.json', {id: '@id'});
return Widget;
});
sampleApp.controller('WidgetsController', function($scope, Widget){
$scope.widgets = Widget.query();
})
はい、三行目あたりからfactory
やら$resource
やら良くわからないのが出てきましたね。
この辺がcontrollerをfatにしない為の仕組みの一つであるDIなんだけど、今回はそこは掘り下げて話さない。
次回頑張る。
まずvar Widget = $resource('/widgets/:id.json', {id: '@id'});
この行によってWidgetクラスが生成される。
誤解を恐れずにいうなら、angularjsにおけるajax通信部分はrailsで考えるとmodelに相当するものだと思った。
$resource
を使って生成されたオブジェクトには以下のメソッドが追加される。
Widget.get(); // 特定のデータを取得する : GET
Widget.save(); // 特定のデータを更新する : POST
Widget.query(); // 複数件のデータを取得する : GET
Widget.remove(); // 特定のデータを削除する : DELETE
Widget.delete(); // 特定のデータを削除する : DELETE
これらは$resource
の第一引数に渡したURLにRESTfulにアクセスできるメソッドだ。
また上記のメソッドは、Widgetクラスのインスタンスにもコピーされる。
インスタンスからこれらのメソッドにアクセスする場合は、prefixとして$がつく。(widget.$save()
, widget.$remove()
みたいに)
一度話はそれるがRailsでWidgetモデルをupdate and deleteすることを考えてみよう。
以下のようになるはずだ。
widget = Widget.find(2)
widget.active = true
widget.save #=> updateされる
widget = Widget.find(2)
widget.delete #=> deleteされる
考えてみればfind
によってMysqlなりmongoなりにコネクションを貼りデータを取得してオブジェクトを操作して結果をまたDBに通知している。
angularjsの$resource
が行うこともこれと何ら変わりない。
ただ問い合わせる先がDBでなくWebアプリケーションであるだけだ。
$resourceについて考えてみる
sampleApp.factory('Widget', function($resource){
var Widget = $resource('/widgets/:id.json', {id: '@id'});
return Widget;
});
ここはWidgetクラスの宣言に過ぎない。
$resource
の第二引数に渡す{id: '@id'}
は第一引数の:id
に対応しており、@
をつけて宣言することでWidgetクラスのインスタンス.idから取得せよ、という宣言になる。
id
を渡さない場合は:id
部分が無視されて、'/widgets.json'
に対するアクセスになってくれる。
インスタンスがwidget.id
のように値を持っているpropertyを持つ場合は/widgets/5.json
のようなURLを自動生成してくれる。
sampleApp.controller('WidgetsController', function($scope, Widget){
$scope.widgets = Widget.query();
})
ではこれはどうか。
上記で話した通りWidget.query()
は$resource
が付与するメソッドであり、複数件のデータを取得する場合に用いられる。
この例だと、二行目は/widgets.json
にGETでリクエストを発行するので対応するRailsアクションで
render json: @widgets
など複数件のwidgetをrenderするようにしておく。
callbackは書かない
$scope.widgets = Widget.query();
まぁ書いてもいいんだけど、上記のように書いてもqueryメソッドが裏側でajax通信を非同期で始めても完了したかどうかはプログラマは知らなくても良い。
この時点でまず空の参照をquery()が返し、$scope.widgets
に入れる。
後のことはcontrollerの仕事ではなく$scope
に処理を委譲するのだ。
無事レスポンスが返ってくる事で$scope
がdirectiveに通知しテンプレートに反映する。
改めて処理フローを考える
さて、ここまでの事を踏まえて改めてviewのコードをみると、なんとなく分かるはずだ。
.col-lg-4{'ng-repeat'=>"widget in widgets"}
.panel.panel-default.widget
.panel-heading
%span.glyphicon.glyphicon-user>
{{ widget.label }}
%button.btn.btn-info{'ng-click'=>'widget.$save()', 'ng-hide'=>'widget.active'}
この機能を使う
%button.btn.btn-warning{'ng-click'=>'widget.$remove()', 'ng-show'=>'widget.active'}
この機能を削除する
.panel-body
{{ widget.description }}
注目すべき点は以下の二行になる。
%button.btn.btn-info{'ng-click'=>'widget.$save()', 'ng-hide'=>'widget.active'}
この機能を使う
%button.btn.btn-warning{'ng-click'=>'widget.$remove()', 'ng-show'=>'widget.active'}
この機能を削除する
ng-click
により呼び出されるwidget.$save()
は、ng-repeat
によりeachやfor文のように繰り返されている、Widgetクラスのインスタンスから呼び出しているメソッドだ。
もっと具体的にいうとRailsでrenderしたjsonデータの一件一件がココに入ってくる。
つまりこれらのインスタンスはwidget.id
というプロパティを持つことが保証されているので、
var Widget = $resource('/widgets/:id.json', {id: '@id'});
この宣言により'/widget/5.json'
というURLが、インスタンスが持つpropertyから組み立てられPOSTリクエストが発生することになる。
さらにレスポンスが返ってくるとこのインスタンスは自動でアップデートされる。
さらにさらに$scopeがそのアップデートを検知して、ng-show
およびng-hide
のbool値を更新し、要素を隠したり出したりしてくれる。
ちなみにRailsでは、対応するアクションでそれぞれ
render json: @widget
と更新されたwidgetオブジェクトのjsonを返すようにするのを忘れてはならない。
とにかく処理の記述量が少なく済む
これだけの内容ならほんの数行のjavascriptを書くだけで実装できてしまった。
DIなどの仕組みによりコードの再利用性も高く、可読性も高い。
ちょっと思いの外長文になりすぎて疲れたのでこの辺で今回は勘弁しといてやるか…。
最後に、railsのprecompileによりjsがMinifyされるとangluarjsがうまく動作しない場合があるので、その対策としてcontrollerの宣言には以下の様なシンタックスも容易されている。
本番運用を考えて最初からこっちで書いておくといいかもしれない。
sampleApp.controller('WidgetsController', ['$scope','Widget', function($scope, Widget){
$scope.widgets = Widget.query();
}])
次回はDIについてもう少し話したい。