LoginSignup
44

More than 5 years have passed since last update.

AngularJSのInterceptorを利用する

Last updated at Posted at 2014-10-12

Interceptor

Interceptorを使用すると、サーバに引き渡される前のリクエストの前処理や、リクエストを行ったアプリケーションコードに引き渡される前のレスポンスの前処理、さらにはグローバルのエラー処理を定義することができるようです。
https://docs.angularjs.org/api/ng/service/$http #Interceptors

Interceptorを利用して、リクエストからレスポンスまでの時間の測定と、レスポンスのエラーハンドリングにトライしてみました。

フォルダ構成
├── angular
│   ├── angular-cookies.js
│   ├── angular-resource.js
│   ├── angular-route.js
│   ├── angular-sanitize.js
│   └── angular.js
├── app
│   ├── controllers.js
│   ├── services.js
│   └── configurations.js ← 新規追加
└── index.html

サンプルソースはAngularJSの開発者ガイドから拝借。
(YahooファイナンスAPIで取得した各国の為替レートを入力した数量・金額に掛けて一覧表示するもの)
https://docs.angularjs.org/guide/concepts

index.html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Try Interceptor</title>
    <script src="angular/angular.js"></script>
    <script src="angular/angular-resource.js"></script>
    <script src="angular/angular-cookies.js"></script>
    <script src="angular/angular-sanitize.js"></script>
    <script src="angular/angular-route.js"></script>
    <script src="app/app.js"></script>
    <script src="app/controllers.js"></script>
    <script src="app/services.js"></script>
  </head>
  <body ng-app="myapp.controllers">
    <h1>AngularJs Sample</h1>
    <div class="container" ng-controller="SampleController">
      <b>Invoice:</b>
      <div>
        Quantity: <input type="number" ng-model="qty" required />
      </div>
      <div>
        Costs: <input type="number" ng-model="cost" required /><br />
        <select ng-model="inCurr">
          <option ng-repeat="c in currencies">{{c}}</option>
        </select>
      </div>
      <div>
        <b>Total:</b>
        <span ng-repeat="c in currencies">
          {{total(c) | currency:c }}
        </span><br/>
        <button class="btn" ng-click="pay()">Pay</button>
      </div>
    </div>
</body>
</html>
controllers.js
angular.module('myapp.controllers', ['myapp.services'])
.controller('SampleController', function($scope, SampleService) {
  $scope.qty = 1;
  $scope.cost = 2;
  $scope.inCurr = 'JPY';
  $scope.currencies = SampleService.currencies;
  $scope.total = function (outCurr) {
    return SampleService.convert($scope.qty * $scope.cost, $scope.inCurr, outCurr);
  };
  $scope.pay = function () {
    SampleService.pay($scope.qty, $scope.incurr);
  };
});
services.js
angular.module('myapp.services', [])
.factory('SampleService', function ($http) {
  var YAHOO_FINANCE_URL_PATTERN = 
    'http://query.yahooapis.com/v1/public/yql?q=select * from '+
      'yahoo.finance.xchange where pair in ("PAIRS")&format=json&'+
        'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
  var currencies = ['USD', 'EUR', 'CNY', 'JPY'];
  var usdToForeignRates = {};
  refresh();
  return {
    currencies: currencies,
    convert: convert,
    refresh: refresh
  };
  function convert(amount, inCurr, outCurr) {
    return amount * usdToForeignRates[outCurr] * 1 / usdToForeignRates[inCurr];
  };

  function refresh() {
    var url = YAHOO_FINANCE_URL_PATTERN.replace('PAIRS', 'USD' + currencies.join('","USD'));
    return $http.jsonp(url).success(
      function(data, status, headers, config) {
        var time = config.responseTimestamp - config.requestTimestamp;
        console.log('The request took ' + (time / 1000) + ' seconds.');
        var newUsdToForeignRates = {};
        angular.forEach(data.query.results.rate, function(rate) {
          var currency = rate.id.substring(3,6);
          newUsdToForeignRates[currency] = window.parseFloat(rate.Rate);
        });
        usdToForeignRates = newUsdToForeignRates;
      }
    );
  };

  function pay(qty, incurr) {
    $http.post("/test", {qty: qty, incurr: incurr});
  }
});

Interceptorの処理はconfigurations.jsで行いました。

configurations.js
angular.module('myapp.configurations', ['myapp.controllers'])
.config(function($httpProvider) {
  $httpProvider.interceptors.push(
    function ($q, $rootScope) {
      return {
        request: function(config) {
          config.requestTimestamp = new Date().getTime();
          return config;
        },
        response: function(response) {
          response.config.responseTimestamp = new Date().getTime();
          return response;
        },
        responseError: function(rejection) {
          if (500 == rejection.status) {
            alert('System Error!');
          }
          return $q.reject(rejection);
        }
      };
    }
  );
});

Interceptorの定義は$httpProviderがもつ配列interceptorsにfactoryをpushするだけで行えます。

interceptorsにpushするfactoryに定義できるfunctionは以下の4つ。

  • request : サーバに引き渡される前のリクエストの前処理を定義
  • response : リクエストを行ったアプリケーションコードに引き渡される前のレスポンスの前処理
  • requestError : リクエストでエラーが起きた際の処理を定義
  • responseError : レスポンスでエラーが起きた際の処理を定義

(see https://docs.angularjs.org/api/ng/service/$http #Interceptors)

本稿ではリクエストからレスポンスまでの時間の測定と、レスポンスのエラーハンドリングを行うため、requestresponseresponseErrorを定義しました。

requestresponseでは処理時の時間を算出するように、
responseErrorはHTTPステータスコード500 の場合に処理をするようにしています。

実行してみる

リクエストからレスポンスまでにかかる時間はコンソールに出力されるはずなので確認してみると、
console
と表示されていることがわかります。

エラーハンドリングのテストをする

エラーハンドリングの確認はサーバーサイドを書いてもいいのですが、面倒なのでテストで確認してみます。

基本的なテストの書き方は以下を参照してください。
Jasmineのみを使用したAngularJSのテスト

AngularJSではバックエンドの振る舞いをMock化する$httpBackendを提供しています。
https://code.angularjs.org/1.2.26/docs/api/ngMock/service/$httpBackend
これを利用してSampleServicepayメソッドで行っている$http.postのサーバーサイドをMock化しました。
※SampleService読み込み時に$http.jsonpでyahooapiにアクセスしてしまうのでこれもMock化しています。

sampleServiceSpec.js
'use strict';

describe('Service: SampleService', function () {

  beforeEach(module('myapp.services'));

  var $httpBackend,
      SampleService ;

  var YAHOO_FINANCE_URL_PATTERN = 
    'http://query.yahooapis.com/v1/public/yql?q=select * from '+
      'yahoo.finance.xchange where pair in ("PAIRS")&format=json&'+
        'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';

  var alert_msg;

  beforeEach(inject(function($injector) {

    // Set up the mock http service responses
    $httpBackend = $injector.get('$httpBackend');

    var currencies = ['USD', 'EUR', 'CNY', 'JPY'];
    var url = YAHOO_FINANCE_URL_PATTERN.replace('PAIRS', 'USD' + currencies.join('","USD'));
    $httpBackend.when('JSONP', url).respond(
      {query: {results: {rate: [
        {id: 'HOGE', rate: 1.0}
      ]}}}
    );

    alert_msg = '_defalut_';
    spyOn(window, 'alert').and.callFake(function(msg) {
      alert_msg = msg;
    });

    SampleService = $injector.get('SampleService');
  }));

  it('should success, when pay() is called', function() {
    expect(alert_msg).toEqual('_defalut_');
    var data = {qty: 1, cost: 1000};
    $httpBackend.when('POST', '/test', data).respond(200, '');
    SampleService.pay(1, 1000).success(function() {
       expect(true).toBe(true);
     }).error(function() {
       expect(false).toBe(true);
     });
    $httpBackend.flush();
    expect(alert_msg).toEqual('_defalut_');
  });

  it('should fail, when pay() is called', function() {
    expect(alert_msg).toEqual('_defalut_');
    var data = {qty: 1, cost: 1000};
    $httpBackend.when('POST', '/test', data).respond(400, '');

    SampleService.pay(1, 1000).success(function() {
      expect(false).toBe(true);
    }).error(function() {
      expect(true).toBe(true);
    });
    $httpBackend.flush();
    expect(alert_msg).toEqual('_defalut_');
  });

  it('should fail and emit alert message for status code 500, when pay() is called', function() {
    expect(alert_msg).toEqual('_defalut_');
    var data = {qty: 1, cost: 1000};
    $httpBackend.when('POST', '/test', data).respond(500, '');

    SampleService.pay(1, 1000).success(function() {
      expect(false).toBe(true);
    }).error(function() {
      expect(true).toBe(true);
    });
    $httpBackend.flush();
    expect(alert_msg).toEqual('System Error!');
  });
});

テストケースは3つ。

  1. ステータスコード200の正常系
  2. ステータスコード400の異常系
  3. ステータスコード500の異常系

alertメッセージの中身を確認しています。

参考

Interceptors in AngularJS and Useful Examples
AngularJS を本気でつかうための tips

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
44