LoginSignup
51
55

More than 5 years have passed since last update.

AngularJS 入門 ~ サービス, DI

Last updated at Posted at 2013-12-06

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

今回はサービスと、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>

kobito.1386293595.300734.png

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

データの取得に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;
        };
    });

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

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

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

名称未設定-1.png

DI

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
55