AngularJS　入門 ~ サービス, DI


AngularJS 入門 ~ ルーティング, フィルター　の続きです。

今回はサービスと、DIです。

# サービス

一定処理をサービスとして定義できます。
サービスにすることでテストも容易になります。

## サービスの使い方

実は既にサービスを使っています。

`DescCtrl`を作った時に定義した引数`$routeParams`がサービスです。

```js: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する場合は、サービス名を指定する必要があります。
>
> ```js:
> angular.module('...')
>     .controller('DescCtrl', ['$scope', function (scope) {
>         // サービス名を指定した場合は引数の変数名は何でもよくなる 
>     }]);
> ```


## サービスを定義する

今は`ngInit`で設定している`frameworks`のデータを設定しているので、それをサービスにしてみます。

```js:app/scripts/app.js
angular.module('', [...])
    // サービスの定義
    .factory('frameworksDataSource', function () {

        // ここのスコープは１度しか実行されない
        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 `$`.

```js: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>
```

![kobito.1386293595.300734.png](https://qiita-image-store.s3.amazonaws.com/0/30739/54dc7565-779a-5c96-fce2-dd7de664dea4.png "kobito.1386293595.300734.png")

サービスから取得したデータを表示する事ができました。

### データの取得にDeferredオブジェクトを使用する

データソースがREST API等に移動する事を見越して、Deferredオブジェクト経由でデータを取得するように書き換えてみます。
Angularでは`$q`サービスがそれを提供してます。

```js: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;
        };
    });
```

#### ためしにデータの取得を遅延させる

`$timeout`サービスを使って読み込みを遅くさせてみます。

```js: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();

            // １秒後にデータを返すようにする
            $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>
```
## サービスのテスト

定義したサービスのテストをします。
雛形から少し書き換えが必要な分があるので書き換えます。

```js: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://pivotal.github.io/jasmine/


Gruntで変更を監視していると都度テストを実行してくれます。

![名称未設定-1.png](https://qiita-image-store.s3.amazonaws.com/0/30739/8ae018c0-52d9-d7ec-39da-91289afaec2f.png "名称未設定-1.png")


# DI
既にDIの概念を知っている方は分かっているかと思いますが、今回でもDIを数カ所か使いました。
よくあるDIコンテナと違って、セットアップが全くと言っていいほど要らないので楽ですね。

```app/views/main.js
angular.module('angularSampleApp')
    .controller('MainCtrl', function ($scope, frameworksDataSource) {
        // $scope, frameworksDataSourceがインジェクトされている。
    }
```