冬休みの自由研究として、 angular.js を触り始めましたのでメモです。
こいつが、個人的にはなかなか好みです。
- Dependency Injection がある
- テストの書き方とかのガイドもしっかり揃っている (英語だけど)
- エラー時の手厚いサポート
- RESTful な WebAPIとの親和性
というわけで、今後使っていくために、簡単なアプリを書いてみました。
https://github.com/kawahara/devnull
シンプルな一言メモ的なものです。
まだ、ACLなどを実装しておりませんので、実用なものではありません。
実際に動きを見たい場合は、node.js と mongodb を用意して、
npm install
node app.js
と言った感じで、起動し、 http://localhost:3000 などを見てください。
サーバサイドのアプリは、何を思ったのか、swig が使える設定が入っていますが、
これは最初の開発の試行錯誤の段階で使っていたものです。
実際には、テンプレーティングをいないものです。
サーバサイドのアプリが行っているのは、WebAPI の提供のみです。
今のところ以下のような、WebAPI を提供しています。
- 投稿
- GET /api/posts – 投稿一覧を取得する
- POST /api/posts – 投稿する
- GET /api/posts/:id – 投稿を取得する
- DELETE /api/posts/:id – 投稿を削除する
- 投稿に対するコメント
- GET /api/posts/:postId/reply – 投稿に値するコメントを取得する
- POST /api/posts/:postId/reply – 投稿に対してコメントする
- DELETE /api/posts/:postId/reply/:id – 投稿に対するコメントを削除する
クライアントのアプリは、静的コンテンツである、public/index.html が
angualr.js によって動作しているわけです。コントローラーのコードは、
public/devnull.js に書いています。
resource module を使うと、RESTful な WebAPI を簡単に利用することができます。
HTML側では、angular.js のパッケージに同梱されている angular-resource.min.js を
ロードしておきます。
ではサービスの定義。
var postsServices = angular.module('postsServices', ['ngResource']); postsServices.factory('Post', ['$resource', function($resource) { // $resource オブジェクトを作成, 第2引数はデフォルトのパラメータオブジェクト // 第3引数はアクションを定義したオブジェクト return $resource('api/posts/:id', {}, { // 取得 (配列で取得する) query: {method: 'GET', isArray: true}, // 追加は POST で save: {method: 'POST'}, // 削除は DELETE で remove: {method: 'DELETE'} }); } ]); postsServices.factory('Reply', ['$resource', function($resource) { return $resource('api/posts/:postId/reply/:id', {}, { save: {method: 'POST'}, remove: {method: 'DELETE'} }); } ]);
これらのサービスを、コントローラーで使います。
var devnullApp = angular.module('devnullApp', ['postsServices']); devnullApp.controller('postsCntl', ['$scope','Post','Reply', function($scope, Post, Reply) { // controller // Post や Reply リソースを利用できる } ])
コントローラーで Post.query() を実行すると、 GET api/posts でデータの取得が行われ、
POSTリソースのオブジェクトの配列となって利用できます。
この時、URIは、 api/posts/:id と宣言していますが、id を指定しない場合は、
省略され、 api/posts という形になるようです。
新しく Post を投稿する際は、Post リソースのオブジェクトを新しく作成し、
$save() メソッドを利用します。
var post = new Post({body: "Hello, world."}); post.$save(function(savedObject, handler) { // 保存後の処理 });
IDなどを別途指定する必要がある場合は、リソースのアクションメソッド
の第1引数に渡します。
post.$remove({id: "123"}, function() { // 削除後の処理 });
もちろん、これに対するテストを書いていきます。
まず、公式ドキュメントの通り、karma を利用します。対象のテストや実装が更新されると
自動的にテストを流してくれたりする、素晴らしいツールです。
npm install -g karma karma-jasmine karma-junit-reporter karma-chrome-launcher karma-firefox-launcher karma-script-launcher
config/karma.config.js を用意して、karma を起動します。
コンソールに出たURLを、ブラウザで見ると、テストやスクリプトが更新された際に、
全自動でテストが実行されます。
karma start config/karma.conf.js
INFO [karma]: Karma v0.10.9 server started at http://localhost:9876/
通常、WebAPIを利用したアプリのテストを書くのは面倒に思えますが、
angualr.js では http request 周りを Mock することができる、 $httpBackend を備えています。
これは、特定のリクエストが行われたかを確認したり、
そのレスポンスを自分が指定したものを返すといったことができます。
test/unit/controllersSpec.js に jasmine 用のテストケースを用意しました。
$httpBackend の基本的な使い方としては、
- 予測されるリクエストについて書く
- アクションを実行
- $httpBackend.flush() を実行。この時点でHTTP Request が実際に行われる状態になる。(そもそも、そのようなコールがないような状態だと、テストがコケる。)
という流れになります。
今回のアプリは、ページを開いた際に Post.query() が実行されるようなものになっているので、
GET api/posts がコールされることが予測されます。
なので、
beforeEach(inject(function(_$httpBackend_,$rootScope, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('api/posts') .respond(200, postsData); scope = $rootScope.$new(); ctrl = $controller('postsCntl', {$scope: scope}); }));
のように、api/posts に関しては、200と、予め用意したテストデータを返すようにします。
(これは全てのテストケースについて定義したいので、beforeEach 内に書きます。)
そのうえで、
it('should display post list', function() { expect(scope.posts).toEqualData([]); $httpBackend.flush(); expect(scope.posts).toEqualData(postsData); });
$httpBackend.flush() が実行されたあと、正しくデータが scope にセットされているか。
というのを確かめるというテストが書けます。
it('should display a new post after the submit action', function() { var submitData = { body: "Hello World." }; $httpBackend.expectPOST('api/posts') .respond(201, ""); scope.post = submitData; scope.submit(); $httpBackend.flush(); expect(scope.post).toEqualData(null); expect(scope.posts[0]).toEqualData(submitData); });
投稿のテストも、同様に expectPOST() を使うことで書くことができます。