AngularJS入門(2) 〜ルーティング, フィルター〜 の続きです。
今回はサービスと、DIです。
サービス
一定処理をサービスとして定義できます。 サービスにすることでテストも容易になります。
サービスの使い方
実は既にサービスを使っています。
DescCtrl
を作った時に定義した引数$routeParams
がサービスです。
// app/scripts/main.js
angular.module('...')
.controller('DescCtrl', function ($scope, $routeParams) {
$scope.framework = {
name: $routeParams.name
};
});
サービスを使いたい時は、定義のクロージャの引数の変数名を サービス名と同じ にするだけでよいです。
AngularJSのサービス => http://docs.angularjs.org/api/ng#service
補足 Angularは変数名からサービスを特定しているので、minifyされると動作しなくなってしまいます。 minifyする場合は、サービス名を指定する必要があります。
angular.module('...') .controller('DescCtrl', ['$scope', function (scope) { // サービス名を指定した場合は引数の変数名は何でもよくなる }]);
サービスを定義する
今はngInit
でframeworks
のデータを設定しているので、それをサービスにしてみます。
// app/scripts/app.js
angular.module('', [...])
// サービスの定義
.factory('frameworksDataSource', function () {
// ここのスコープは1度しか実行されない
var frameworks = ['Backbone.js', 'Ember.js', 'Knockout.js'];
// ここで return したオブジェクトがサービスになる
return function () {
return frameworks;
};
});
補足 Angularのサービスに
$
が付いているので付けたくなりますが、衝突するので付けない方がよいです。$ Prefix Naming Convention
You can create your own services, and in fact we will do exactly that in step 11. As a naming convention, angular’s built-in services, Scope methods and a few other Angular APIs have a
$
prefix in front of the name. The$
prefix is there to namespace Angular-provided services. To prevent collisions it’s best to avoid naming your services and models anything that begins with a$
.
// app/scripts/main.js
/* jshint indent: 4, newcap: false */
angular.module('angularSampleApp')
// サービスを取得する事にする
.controller('MainCtrl', function ($scope, frameworksDataSource) {
$scope.world = 'Angular';
// サービス経由でデータを取得して設定する
$scope.frameworks = frameworksDataSource();
$scope.addFramework = function (text) {
$scope.frameworks.push(text);
$scope.world = '';
};
})
<!-- app/views/main.html -->
<p>
<input type="text" ng-model="world">
<span ng-click="addFramework(world)" class="btn btn-primary">Add</span>
</p>
<p>Hello {{ world }}!</p>
<div> <!-- ngInitは削除 -->
<h3>{{ frameworks.length }} frameworks</h3>
<input type="text" ng-model="searchText" />
<ul>
<li ng-repeat="f in frameworks | reverse | filter:searchText">
<a ng-href="/#/frameworks/{{f}}">{{f|uppercase}}</a>
</li>
</ul>
</div>
サービスから取得したデータを表示する事ができました。
データの取得にDeferredオブジェクトを使用する
データソースがREST API等に移動する事を見越して、Deferredオブジェクト経由でデータを取得するように書き換えてみます。
Angularでは$q
サービスがそれを提供してます。
// app/scripts/app.js
angular.module('', [...])
// サービスの定義
.factory('frameworksDataSource', function ($q) { // サービスから他のサービスを使う
var frameworks = ['Backbone.js', 'Ember.js', 'Knockout.js'];
return function () {
var defer = $q.defer();
defer.resolve(frameworks);
return defer.promise;
};
});
// app/scripts/main.js
/* jshint indent: 4, newcap: false */
angular.module('angularSampleApp')
.controller('MainCtrl', function ($scope, frameworksDataSource) {
$scope.world = 'Angular';
// サービス経由でデータを取得して設定する
var promise = frameworksDataSource();
promise.then(function (frameworks) {
$scope.frameworks = frameworks;
});
$scope.addFramework = function (text) {
$scope.frameworks.push(text);
$scope.world = '';
};
})
ためしにデータの取得を遅延させる
$timeout
サービスを使って読み込みを遅くさせてみます。
// app/scripts/app.js
/* jshint indent: 4 */
'use strict';
angular.module('angularSampleApp', [
'ngCookies', 'ngResource', 'ngSanitize', 'ngRoute'
])
.factory('frameworksDataSource', function ($q, $timeout) { // $timeoutサービス
var frameworks = ['Backbone.js', 'Ember.js', 'Knockout.js'];
return function () {
var defer = $q.defer();
// 1秒後にデータを返すようにする
$timeout(function () {
defer.resolve(frameworks);
}, 1000);
return defer.promise;
};
})
;
少し手抜きですが、ngHide
ngShow
あたりを使うと簡単にそれっぽい感を演出できます笑
<!-- app/views/main.html -->
<p>
<input type="text" ng-model="world">
<span ng-click="addFramework(world)" class="btn btn-primary">Add</span>
</p>
<p>Hello {{ world }}!</p>
<div>
<h3>{{ frameworks.length }} frameworks</h3>
<input type="text" ng-model="searchText" />
<span ng-hide="frameworks">読み込み中...</span> <!-- 追加 -->
<ul>
<li ng-repeat="f in frameworks | reverse | filter:searchText">
<a ng-href="/#/frameworks/{{f}}">{{f|uppercase}}</a>
</li>
</ul>
</div>
サービスのテスト
定義したサービスのテストをします。 雛形から少し書き換えが必要な分があるので書き換えます。
// Gruntfile.js
...
watch: {
js: {
files: ['{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js'],
tasks: ['newer:jshint:all']
},
jsTest: {
files: ['test/spec/**/*.js'], // ここを書き換える(35行目あたり)
tasks: ['newer:jshint:test', 'karma']
},
...
// test/spec/controllers/main.js
'use strict';
describe('Controller: MainCtrl', function () {
// load the controller's module
beforeEach(module('angularSampleApp'));
var MainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope: scope
});
}));
// 不要になったテストを削除
// it('should attach a list of awesomeThings to the scope', function () {
// expect(scope.awesomeThings.length).toBe(3);
// });
});
テストを書きます。
// test/spec/app.js
'use strict';
/* jshint indent: 4 */
describe('angularSampleAppTest', function () {
// 自分のモジュールを読み込む
beforeEach(module('angularSampleApp'));
describe('FrameworksDataSourceTest', function () {
// テストに必要なサービスを準備
var ds, timeout;
beforeEach(function () {
// サービスはクロージャーにインジェクトして、それらを保持して使います。
inject(function (frameworksDataSource, $timeout) {
ds = frameworksDataSource;
timeout = $timeout;
});
});
// テスト
it('service test', function () {
var data = null;
ds().then(function (frameworks) {
data = frameworks;
});
// 非同期処理なのでデータは空
expect(data).toBeNull();
// 内部的にはタイマーを使ってるので、タイマーを進めるとデータが取得できる
timeout.flush();
expect(data).toEqual(['Backbone.js', 'Ember.js', 'Knockout.js']);
});
});
});
このテストフレームワークはJasmineなので、詳しいテストの書き方は公式を参照してください。 Jasmine 公式 => http://jasmine.github.io/
Gruntで変更を監視していると都度テストを実行してくれます。
DI
既にDIの概念を知っている方は分かっているかと思いますが、今回でもDIを数カ所か使いました。 よくあるDIコンテナと違って、セットアップが全くと言っていいほど要らないので楽ですね。
// app/views/main.js
angular.module('angularSampleApp')
.controller('MainCtrl', function ($scope, frameworksDataSource) {
// $scope, frameworksDataSourceがインジェクトされている。
}