プログラマブログ

by wacul

menu
  • プログラマ
  • AngularJSでのCORSな通信をpostMessageに置き換える

2014.05.21AngularJSでのCORSな通信をpostMessageに置き換える

あなたは突然AngularJSでCORSでxhrな通信を行いたくなりました。 通信時に独自のヘッダーを付けたいのでpostMessageを使う必要がありましたが、 すでに$http$resourceサービスを使った結構な量のソースコードがあります。 これを修正するのは骨が折れそうな作業です。

しかし、心配は無用です。 このような場合$provide.decoratorが強力な武器となってくれます。 $provide.decoratorを使うことで、すでに書いたソースコードに触れることなく対象のサービスの挙動を変更することができます。

本記事では、まず$provide.decoratorの概要と簡単なチュートリアルについて説明し、 次に$httpBackendを置き換えてpostMessageを使ったxhr通信を行う方法を紹介します。

$provide.decoratorの概要

$provide.decorator はすでに定義されているサービスの生成をフックして新しいサービスに置き換えます。

簡単なチュートリアル

と言ってもこれだけではなんのことかわからないので、まずは簡単な例を見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var app = angular.module('fooMod', []);

// 'hello'という文字列を返すだけのサービスを定義
app.value('helloService', 'hello');

// 'hello world!'という文字列を返すように修正
app.config(function($provide) {
  $provide.decorator('helloService', function($delegate, $log) {
    $log.info('decorated!!');
    return $delegate + ' world!';
  })
});

app.controller('MainCtrl', function($scope, helloService) {
  $scope.hello = helloService;
});

デモ

この例では以下のようなことを行っています。

  • $provideapp(AngularのModule)のconfigメソッドによってDI(dependency injection)される。
  • $provide.decoratorメソッドの第一引数にフックしたいサービスの名前を登録する。
  • $provide.decoratorメソッドの第二引数にサービスを置き換えるための関数を登録する。
    • この関数は$provide.decoratorメソッドによってDIされる。
    • 特に$delagate引数は改変前の元々のサービスがDIされる。

ここでは、Angularのモジュールのvalueメソッドで定義されたサービスを置き換えましたが、 もちろんfactoryproviderメソッドで定義されたサービスを置き換えることもできます。

$httpBackendを置き換えてpostMessageを使ったxhr通信を行う

$httpBackendサービスをフックして、xhrでの通信時にオリジンが違う場合、postMessageを使って通信するようにしてみましょう。 ここでは別オリジンで管理しているサーバーにはproxy.htmlというファイルを置き、 そこを経由してxhr通信を行うことを想定します。 ※サードパーティライブラリとしてjQueryとunderscoreを使用しています。

クライアント側

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
angular.module('foo', []).config(function($provide) {
  var iframe, isReady, isSameOrigin, normalize, uniqueId, waitQueue;

  iframe = null;
  isReady = true;
  uniqueId = (function(i) {
    return function() {
      return i++;
    };
  })(0);
  waitQueue = [];
  isReady = false;
  waitQueue = [];

  // iframeの初期化
  iframe = document.createElement('iframe');
  iframe.src = "http://foo.com/proxy.html";
  iframe.style.display = "none";
  document.body.appendChild(iframe);

  // iframeの読み込み待ち。終わったら通信待ちのものを実行する
  $(iframe).one('load', function() {
    isReady = true;
    return _.each(waitQueue, function(fn) {
      fn();
    });
  });

  isSameOrigin = function(a, b) {
    return normalize(a) === normalize(b);
  };

  normalize = function(url) {
    return url.replace(/^(https?:\/\/[^:\/]+)(:\d+).*?/, function(all, host, port) {
      if (port === ':80' || port === ':443') {
        return host;
      }
      return host + port || '';
    });
  };

  callbacks = [];
  $(window).on('message', function(e) {
    var data = e.data;
    callbacks[data.id](data.status, data.data, data.headers, data.statusText || '');
  });

  $provide.decorator('$httpBackend', function($delegate, $browser) {
    return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
      var exec, id;

      // オリジン判定。同一オリジンの場合は元の$httpBackendを使う
      if (!/^http/.test(url) || isSameOrigin(url, $browser.url())) {
        return $delegate.apply(null, arguments);
      }

      id = uniqueId();
      callbacks[id] = callback;

      // postMessageを使って通信を行います
      exec = function() {
        iframe.contentWindow.postMessage(angular.toJson({
          id: id,
          method: method,
          url: url,
          post: post,
          headers: headers,
          timeout: timeout,
          withCredentials: withCredentials,
          responseType: responseType
        }), '*');
      };

      isReady ? exec() : waitQueue.push(exec);
    };
  });
});

proxy.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!doctype html>
<html>
<head>
  <title>postMessage proxy</title>
</head>
<body>
<script>
if (window.addEventListener) {
  window.on = window.addEventListener;
} else {
  window.on = function (name, fn) {
    window.attachEvent('on' + name, fn);
  };
}

function send(data) {
  parent.postMessage(JSON.stringify(data), '*');
}

window.on('message', function(e) {
  if (e.origin !== 'foo.com') {
    //はじく処理
  }

  var data = JSON.parse(e.data),
      headers = data.headers,
      xhr = new XMLHttpRequest();

  xhr.open(data.method, data.url);
  // add headers
  for (var k in headers) {
    if (Object.prototype.hasOwnProperty.call(headers, k)) {
      xhr.setRequestHeader(k, headers[k]);
    }
  }
  if (data.withCredentials) {
    xhr.withCredentials = data.withCredentials;
  }
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      send({
        id: data.id,
        status: xhr.status,
        data: JSON.parse(xhr.responseText),
        headers: xhr.getAllResponseHeaders(),
        statusText: xhr.statusText
      }, e.origin);
    }
  };
  xhr.send(data.post || null);

});
</script>
</body>
</html>

$httpBackendサービスは$provide.decoratorを使うのに最も適しているサービスの一つと言えます。 AngularJSではインターフェース部分やクエリ文字列の生成等を$http$resource、実際に通信を行う部分を$httpBackendに分離してるので $httpBackendの挙動を変えるだけで全ての通信をpostMessageで行うことができます。 既存のサービスやコントローラー等で$http$resourceを使っている部分を書き換える必要はありません。

まとめ

本記事では$provide.decoratorの概要や使用例を学びました。これは主に

  • サービスのプロパティを変更・追加する。
  • サービスの挙動を変える。

のような目的で使用されます。

  • 元のサービスのソースコードを直接触らなくてもよい。
  • サービスの使っている側のソースコードを変更することなくサービスの挙動を変える事ができる。

といった利点があります。

参考URL

この記事を書いた人kato

加藤です。JavaScriptに興味があります。

waculでは、プログラマを募集しています。

現在はプロダクトとして、課題発見から改善提案まで自動で行うWeb改善プラットフォーム「AIアナリスト」を開発中です。

waculの採用情報へ

ページトップへ