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

第11回 JSONP入門

この記事を読むのに必要な時間:およそ 5 分

こんにちは,太田です。今回から,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');

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/

コメント

コメントの記入