これでできる! クロスブラウザJavaScript入門

第11回JSONP入門

こんにちは、太田です。今回から、Ajaxと呼ばれるような非同期な通信処理を行うJavaScriptについて解説していきます。今回は特にJSONPについて基礎的な部分を解説します。

JSONとは

JSONについては第9回でも少し触れていますが、改めて解説します。

JSON(JavaScript Object Notation)はJavaScriptから生まれたデータ記述フォーマットで、真偽値、数値、文字列、null値の組み合わせを持ったハッシュか配列かその両方で構成されます。

JSONはそのシンプルさから多くの言語でネイティブにサポートされており、特にウェブ関連ではポピュラーなデータフォーマットです。

JSONのサンプル(配列)
["aaa", "bbb", "ccc"]
JSONのサンプル(ハッシュ)
{"aaa":1, "bbb": 2, "ccc": 3}
JSONのサンプル(ハッシュと配列)
{"num": [1, 2, 3], "abc":["a", "b", "c"]}

JavaScriptとJSONの書式はよく似ていますが、当然ながら同じというわけではありません。JavaScriptでは文字列リテラルを囲うのはシングルクォートでもダブルクォートでも構いませんが、JSONではダブルクォートのみと決められています。また、JSONではプロパティ名にもダブルクォートが必須となっています。JavaScriptにはNaNやundefinedなどの値がありますが、やはりJSONには存在しません。

JSONはそういった曖昧さを排除してデータ記述言語としての精度と、解析のし易さを確保しています。なお、JSONフォーマットの詳細はjson.orgを参照してください。

また、JavaScript自体にはJSONを解析・出力するメソッドがありませんでしたが、ECMA-262 5th editionよりネイティブなJSONサポート(JSON.parse、JSON.stringify)が定義されました。実際にIE 8、Firefox 3.5、Safari 4.0.2、Google Chrome 2、Opera 10.50などでサポートされています。特にJSON.stringifyメソッドを使用すれば手軽にJavaScriptのオブジェクトからJSONを出力することができるので、是非確認してみてください。。

JSONPとは

JSONPはJSON with Paddingの略称です。Paddingは(本来は不要なものの)付け足しという意味です(つまりJSONPはJSONではないので、JSONPはJSONとしてパースできません⁠⁠。多くの場合JSONPはクロスドメインの制限を超えてデータをやり取りするために使用されます。

先ほどのJSONをJSONPらしくしてみると下記のようになります。

JSONPのサンプル
callback( {"num":[1,2,3], "abc":["a","b","c"]} );

{}で囲まれた部分がJSON形式になっており、その周りにcallback( );が追加しました。このcallback()は関数の呼び出しです。

JSONPの理解

JSONPがなぜドメインを超えてデータをやり取りすることができるのか、その仕組みを詳しく解説してみます。

まず、基本的なこととしてJavaScriptにはグローバル変数とローカル変数があります。グローバルになるか、ローカルになるかの条件は次のコードを参照してください。

ローカル変数とグローバル変数
// 宣言なしで変数に代入すると常にグローバル変数に
global1 = 1;
// 関数の外では宣言をしてもグローバル変数に
var global2 = 2;
// グローバルオブジェクトのプロパティもグルーバル変数
window.global3 = 3;
// 関数の外では関数宣言もグローバル関数に
function global4(local1){ // 当然、引数はローカル変数
  // 関数の中の宣言付き変数はローカル変数に
  var local2 = 2;
  var local3 = function(){
    var local4 = 4;
    // varをつけ忘れると常にグローバルに
    global5 = 5;
  };
  var local5 = function(){
    // 当然、local4は参照できない
    alert(typeof local4); // undefined
    alert(global5);       // 5
  }
  local3();
  global6();
}
global4(1);

さらに、グローバル変数はscript要素をまたがって参照することができます(グローバルだから当たり前のことですね⁠⁠。

script要素とグローバル変数
<script>
global1 = 1;
</script>
<script>
alert(global1); // 1
</script>

上記はscript要素に直書きしていますが、jsファイルとして外部ファイルにできます。

script要素とグローバル変数#2
<script src="global1.js">
<!--中身は global1 = 1; とだけ書かれたJSファイル-->
</script>
<script>
alert(global1); // 1
</script>

さらに、外部ファイルは同一ドメインである必要はなく、異なるドメインのjsファイルを読み込むことができます。

script要素とグローバル変数#3
<script src="http://example.com/global1.js">
<!-- global1 = 1; とだけ書かれたJSファイルがあると仮定する -->
</script>
<script>
alert(global1); // 1
</script>

見事、別ドメインのサイトからデータを受け取ることができました。JSONPがドメインを超えてデータをやり取りできる仕組みはこれほど簡単なことなのです。

しかし、既にお気づきの方もいらっしゃると思いますが、これは別ドメインのサイトに対して自サイト上でのJavaScriptの実行権限を与えているということですから、その別ドメインのサイトに悪意があったら(JavaScriptで可能な範囲で)やりたい放題にされてしまうということです。そのため、十分に信用できるサイトに対してのみJSONPを使うようにしなければいけません。

さて、上記方法では静的にjsファイルを読み込んでいるので応用ができません。動的にjsファイルを読み込むようにしてみましょう。やはり方法は簡単で、createElementでscript要素を作ってsrc属性を設定し、document.bodyなどにappendChildするだけです。

動的なscriptの読み込み
function loadJS(src){
  var script = document.createElement('script');
  script.src = src;
  document.body.appendChild(script);
}
loadJS('global1.js');
alert(typeof global1); // undefined

jsファイルは読み込みましたが、global1はundefinedになってしまいました。これは処理の順番が次のようになっているためです。

  1. loadJS('global1.js');
    1. var script = document.createElement('script');
    2. script.src = src;
    3. document.body.appendChild(script);
    4. (global1.jsの読み込み開始)
  2. alert(typeof global1); // undefined
  3. (global1.jsを読み込み中)
  1. (global1.jsの読み込み完了)
  2. global1 = 1;

global1に1が代入されるのはglobal1.jsの読み込み完了後なので、alertの時点ではglobal1は未定義です。

JavaScriptはシングルスレッドなので、上記の処理の順番が前後することは基本的にありません(alertやcomfirmのような例外的な処理で前後することはあります⁠⁠。つまり、jsファイルの読み込みを待たなければいけません。

読み込みを待つ方法としてはscript要素のonloadイベントを使う方法もありますが、残念ながらIEではscript要素にonloadイベントがありません(onreadystatechangeを使う方法もありますがお薦めはできません⁠⁠。そこで使われるのがコールバックです。

コールバックの仕組みも単純です。まず、global2(2);とだけ書かれたjsファイルを用意します。

global2.js
global2(2);

このglobal2.jsを読み込むわけですが、その際に読み込む側でglobal2を定義しておきます。

動的なscriptの読み込み#2
function loadJS(src){
  var script = document.createElement('script');
  script.src = src;
  document.body.appendChild(script);
}
var global2 = function(data){
  alert(data); // 2
};
loadJS('global2.js');

こうすることで、global2.jsが読み込まれた際にglobal2関数が呼び出されます。処理の順番は次のとおりです。

  1. var global2 = function(data) …
  2. loadJS('global2.js');
    1. var script = document.createElement('script');
    2. script.src = src;
    3. document.body.appendChild(script);
    4. (global2.jsの読み込み開始)
  1. (global2.jsの読み込み完了)
  2. global2(2);
    1. alert(data); // 2

さて、このglobal2というグローバル関数はJSONPを呼び出す側で定義しておいて、JSONPとして呼び出される側が実行するわけですが、この名前を呼び出す側で指定できると便利ですね。そこで、JSONP APIを提供する場合はcallback変数の名前をパラメータで指定できるようにするのが一般的です。

動的なscriptの読み込み#3
function loadJS(src){
  var script = document.createElement('script');
  script.src = src;
  document.body.appendChild(script);
}
var jsonp_callback = function(data){
  //
};
loadJS('jsonp.api?callback=jsonp_callback');
var jsonp_callback2 = function(data){
  //
};
loadJS('jsonp.api?callback=jsonp_callback2');

JSONPの活用例

では、JSONPの使い方を見てみましょう。今回は短縮URLを展開するAPIを利用します。まず、次のような短縮URLがあります。

短縮URL
<ul id="tinyurls"> 
<li><a href="http://tinyurl.com/2b9hf8c">http://tinyurl.com/2b9hf8c</a></li>
<li><a href="http://tinyurl.com/242xed7">http://tinyurl.com/242xed7</a></li>
<li><a href="http://tinyurl.com/yas6fwn">http://tinyurl.com/yas6fwn</a></li>
</ul>

このリンクを展開してみましょう。

JSONPの利用例#1
(function(){
var REURL_API = 'http://ss-o.net/api/reurl.json';
var tinyurls = document.getElementById('tinyurls');
var links = tinyurls.getElementsByTagName('a');
var TEXT = ('textContent' in document.body) ?
            'textContent' : 'innerText';
for (var i = 0, len = links.length;i < len; i++) {
(function(i){
  var a = links[i];
  var url = encodeURIComponent(a.href);
  var script = document.createElement('script');
  // callbackの名前をユニークに
  var callbackName = 'callback' + i;
  script.src = REURL_API + '?url=' + url +
               '&callback=' + callbackName;
  // callbackの名前でグローバル関数を定義
  window[callbackName] = function(data){
    a.title = data.url;
    a[TEXT] = data.url;
  };
  document.body.appendChild(script);
})(i);
}
})();

ここで、for文の中に無名関数を作っていますが、この理由については第5回のクロージャ編で説明していますので、よろしければ参照ください。

さて、上記のコードでも動いてはいますが、グローバル関数を何個も作ってしまうのはよろしくありません。そこで、次のようにグローバルオブジェクトを1つ用意して、そのメソッドとしてコールバック関数を定義、JSONP APIからグローバルオブジェクト.コールバック関数名のようにして呼び出す方法がお薦めです。

JSONPの利用例#2
(function(){
window.ReurlAPI = {}; // グローバルオブジェクトを用意
var REURL_API = 'http://ss-o.net/api/reurl.json';
var tinyurls = document.getElementById('tinyurls');
var links = tinyurls.getElementsByTagName('a');
var TEXT = ('textContent' in document.body) ?
            'textContent' : 'innerText';
for (var i = 0, len = links.length;i < len; i++) {
(function(i){
  var a = links[i];
  var url = encodeURIComponent(a.href);
  var script = document.createElement('script');
  // callbackの名前をユニークに
  var callbackName = 'callback' + i;
  script.src = REURL_API + '?url=' + url +
               '&callback=ReurlAPI.' + callbackName;
  // callbackの名前でグローバル関数を定義
  ReurlAPI[callbackName] = function(data){
    a.title = data.url;
    a[TEXT] = data.url;
  };
  document.body.appendChild(script);
})(i);
}
})();

JSONPのセキュリティ

JSONPはJavaScriptの仕様の穴を突いているといっても過言ではないような技術です。そのため、安全に利用するための仕組みなども用意されていません。その分、利用する側、提供する側が十分に注意を払わなければいけません。

なお、将来的にはクロスオリジンXMLHttpRequestやpostMessageなどのセキュリティコントロールがし易いAPIが使えるようになる予定です。

また、JSONPのセキュリティについては[気になる]JSONPの守り方 - @ITに詳しく解説されています。是非こちらも参照してください。

JSONPを利用する場合

前述の通り、JSONPを利用することはそのAPIの提供元にJavaScriptの実行権限を与えることを意味します。信用できないサービスのJSONP APIを利用しないようにしましょう。

また、JSONPの結果を安易にinnerHTMLなどでHTMLとして解釈した場合もクロスサイトスクリプティングのリスクがあります。なるべくなら上記のサンプルのようにtextContent(IE以外)かinnerText(IE用)を使用するか、createTextNodeでテキストノードとして処理するようにしましょう。

加えて、上記のAPIのようなケースでは取得できるデータがURLであることを期待していますが、実はjavascript:で始まるブックマークレット形式のデータや、データスキームなどのデータが帰ってくることもありえます。それらをリンクのhrefなどに設定してしまうのも避けたほうがよいでしょう。

JSONPを提供する場合

JSONPには特定のサイトからのリクエストに対してのみデータを返すように制御する仕組みなどはありません。JSONPで提供するデータはあらゆるドメインから利用可能になります。当然のことですが機密情報をJSONPのデータに含めてはいけません。

また、これも当然のことですがJSONPとして返すデータに外部からの入力されたデータを含めるのであれば適切なエスケープが必要となります。具体的にはデータ部分が正しいJSON形式となるように注意するとよいでしょう。

さらに、JSONPのコールバック関数に指定できる文字列を自由に設定できるようにしてしまうとその部分にJavaScriptやHTMLを書くことができてしまうので、使用できる文字列は制限したほうがよいでしょう(例えば[a-zA-Z0-9_.\[\]]など⁠⁠。

ちなみにJSONPを提供する場合、そのContent-Typeはtext/javascript; charset=utf-8で返すのが良いでしょう。JSONのContent-Typeはapplication/json; charset=utf-8ですが、JSONPの場合実は単なるJavaScriptであることは前述のとおりです。

まとめ

今回はJSONPの基礎を詳しく解説してみました。JSONPが実は単なるJavaScriptファイルの読み込みに過ぎない実に簡単な仕組みであることは理解頂けたのではないかと思います。

次回はXMLHttpRequestを中心に非同期処理を掘り下げていく予定です。

おすすめ記事

記事・ニュース一覧