📰

4ステップで作って学ぶJSONP

2021/01/01に公開3

名前は知っているし、ライブラリを使って利用したことは有る。
だけど他人に説明できるほど理解できていない JSONP

今年から開発に従事させていただいているアドテク界隈でも割と使われているということで、理解を促進するために複数ステップに分けて自身で作りながら整理いたしました。

そもそもJSOPとは・・・

JSONPとは・・・・

WikiPediaによりますと

JSONP (JSON with padding) とは、scriptタグを使用してクロスドメインな(異なるドメインに存在する)データを取得する仕組みのことである。HTMLのscriptタグ、JavaScript(関数)、JSONを組み合わせて実現される。クロスドメインな通信を実現する方法としては、後にオリジン間リソース共有(英語版) (CORS)も開発され、JSON-Pに代わる選択肢となっている。

とのことです。
文脈から異なるドメイン間でデータを送受信するための仕組みということはわかります。

どんな状況で使われるのか?
これもWikiPediaによりますと・・・

ウェブブラウザなどに実装されている「同一生成元ポリシー」という制約により、ウェブページは通常、自分を生成したドメイン以外のドメインのサーバと通信することはできない。 しかし、HTMLのscriptタグのsrc属性には別ドメインのURLを指定して通信することができるという点を利用することによって別ドメインのサーバからデータを取得することが可能になる。
JSONPでは、通常、上記src属性のレスポンスの内容はjavascript関数呼び出しの形式となるため、src属性に指定するURLにその関数の名前をクエリ文字列の形式で付加する。一般的な方法では、この時に指定する関数名はウェブページ側ですでに定義されているコールバック用の関数の名前になる。
関数名を渡すリクエストパラメータの名前はサーバとクライアント間で事前に取り決めておく必要がある。
例えば(callbackというパラメータ名でparseResponseという関数名を渡す場合)

とのことです。
言いたいことは何となく分かるけど・・・という感じです。(私は)

JSONとはどう違うのか

JSONといえば、データの送受信のための形式(データフォーマット)の一種ですよね。

こんな形の形式ですね。

{
    "message": "This is Json",
    "author": "Bun"
}

フロント側においては、ajax や axios等のライブラリを使ってよく非同期通信でサーバー側からデータを取得するのに利用されていると思います。(XMLHTTPRequest)

XMLHTTPRequestについては、同期通信・非同期通信のどちらでも使うことができるそうですが、MDNのサイトによると、やはり非同期通信で利用されることを想定しているようですね。

XMLHttpRequest は同期及び非同期通信の両方に対応しています。しかし、一般的には性能上の理由により、同期リクエストより非同期リクエストを推奨するべきです。
同期リクエストはプログラムの実行をブロックし、画面を「フリーズ」させたりユーザー操作が反応しない状態にしたりすることがあります。

https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests

しかし、XMLHTTPRequestを利用しても非同期通信ですべてのサーバーと自由にやり取りをできるわけではありません。

CORSという制約があるからです。
(自分で開発しているときに稀によく引っかかってしまいます。)

CORSの拒否のイメージ

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

要するに、とあるWebアプリから全く関係のないWebアプリに自由にアクセスして情報を取得できてしまうと危ないですよね。

なので、やり取りする間で特別な取り決めが無い限り、自由にやり取りはできないようになっているというイメージでしょうか。

もちろん、同一ドメイン間であれば情報のやり取りが可能です。
(ユーザーマイページから、自分の投稿したツイートとか見れますよね。)

http://example.com/user //ここから
http://example.com/article //ここにアクセスして情報取得はできる

ではJSONPはどう違うのか

でも実際問題として、それでは困るケースがあります。

イメージがしやすいものといえば、
GoogleAnalitics でしょうか。

どこのサイトについて、どこのページから、どのように流入があったのか。

Goolge側のサーバーとやり取りをする必要があります。

その他で言えば、例えば郵便番号を送信すれば住所を送り返してくれるWebAPIとかでも利用されていたりします。(この場合やり取りする内容が、外部に漏れても問題のない情報ということも有るからという理由もありそうです。)

<script>タグを利用した異なるドメインの指定

XMLHTTPRequestはご覧の通り、サーバーサイドとの事前の取り決めがない場合は異なるドメインの壁を超えることができませんでした。

その壁を超えることができる手段が幾つかあります。

そのうちの一つが<script>タグのsrc属性での異なるドメインの指定です。
分かりやすいのがライブラリ等をCDNで利用する際ですね。

Jqueryとかvue.jsをかる~く使いたい時に、以下のようにHTMLに記載してCDNを利用したことがございませんか?

<!- vue.jsをCDNで読み込み ->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

これで異なるドメインのjsファイルを読み込んであたかもローカルに配置しているように利用することができます。

scriptタグのsrc属性は違うドメインのURLを指定できます。つまり、外部のjsファイルを読み込んで実行することができる。JSONPはこの仕組を利用します。

ここから実際に実装して試してみます。

Step1:静的にjsファイルの単純読み込み

まずは、ドメインAからドメインBのjsファイルを読み込むところを実装してみていきましょう。

ローカル環境を汚したくないので、docker-composeを使って楽にNode.jsでWebサーバーを立ててみます。

ファイル構成は以下のような形です。

.
├── docker-compose.yml
├── html
│   └── jsonp1.html //このHTMLはブラウザに展開してドメインAを再現
└── src
    ├── app.js
    ├── package-lock.json
    ├── package.json
    └── public
        └── js
            └── jsonp1.js // ドメインBのこのjsファイルを読み込む

docker-compose.ymlは以下のように記載します

version: '3'

services:
  app:
    image: node:15.4.0 
    container_name: jsonp_practice
    tty: true
    volumes:
      - ./src:/src
    working_dir: '/src'
    ports:
      - '8080:3000'
    command: /bin/bash -c "npm install && node app.js"

src/app.jsは以下のように記載します。
expressを使って http://localhost:8080 にサーバーを立てます。

// express モジュールのインスタンス作成
const express = require('express');
const app = express();
// パス指定用モジュール
const path = require('path');

app.listen(3000, () => {
  console.log('Running at Port 3000...');
});

// 静的ファイルのルーティング
app.use(express.static(path.join(__dirname, 'public')));

// その他のリクエストに対する404エラー
app.use((req, res) => {
  res.sendStatus(404);
});

public配下を公開しています。

ここで public/js/jsonp1.jsを以下のように記載します。

alert('外部ドメインからの読み込み');

これでブラウザから http://localhost:8080/js/jsonp1.js にアクセスすると以下のように先程記載したソースコードを見ることができます。

外部js ファイルの読み込みイメージ

HTMLを作成する

次に、 html/jsop1.html を以下のように記載します。

<!DOCTYPE html>
<html>
    <head>
        <title>JSONPのテスト</title>
    </head>
    <body>
        <h1>スクリプト読み込み</h1>
    </body>
    <script src="http://localhost:8080/js/jsonp1.js"></script>
</html>

先程公開したウェブサーバーのjsファイルを読み込むようにしています。

そこで、このHTMLを ブラウザ(私はGoogle Chromeを使っています。)で開いてみてください。

すると、以下のように無事に alertが実行されています。

alert文実行イメージ

これでscriptタグのsrc属性で異なるドメインのjsファイルを読み込むことができました。

クロスドメインのjs読み込みのイメージ

Step2:グローバルな変数を受け取る

次は異なるドメインのjsファイルから情報を受け取ってみます。
次のようにデータを追加します。

.
├── docker-compose.yml
├── html
│   ├── jsonp1.html
│   └── jsonp2.html //追加
└── src
    ├── app.js
    ├── package-lock.json
    ├── package.json
    └── public
        └── js
            ├── jsonp1.js
            └── jsonp2.js //追加
// jsonp2.js
// グローバル変数を定義
var message = {
    author: "bun",
    scope: "global"
}
<!DOCTYPE html>
<html>
    <head>
        <title>グローバル変数を受け取る</title>
        <script src="http://localhost:8080/js/jsonp2.js"></script>
    </head>
    <body>
        <h1>スクリプト読み込み</h1>
    </body>
</html>

<script>
alert(`Author:${message.author},変数のスコープ${message.scope}`);
</script>

追加した、jsonp2.htmlをまたブラウザで読み込んでみます。

alert2のイメージ

これもイメージしやすいですね。

実際の実装的には以下のような形と変わりません。

<script>
	var message = {
	    author: "bun",
	    scope: "global"
	}
</scipt>
	
<script>
	alert(`Author:${message.author},変数のスコープ${message.scope}`);
</script>

グローバルに定義した変数なので、同一ページ内であれば<script>タグが異なっても読み取ることが可能です。

Step3:少し改良してみる

今度はちょっと改良してみます。

今度はただ単にデータを受け取ってalertで表示するのではなく、画面に表示してみます。

またファイルを増やします。

.
├── docker-compose.yml
├── html
│   ├── jsonp1.html
│   ├── jsonp2.html
│   └── jsonp3.html //追加
└── src
    ├── app.js
    ├── package-lock.json
    ├── package.json
    └── public
        └── js
            ├── jsonp1.js
            ├── jsonp2.js
            └── jsonp3.js //追加

jsop3.html

<!DOCTYPE html>
<html>
  <head>
    <title>データを受け取って、画面に反映する</title>
  </head>
  <body>
    <h1>ここの下に読み込んだデータを表示する</h1>
    <p>
        作った人: <span id="author"></span>
    </p>
    <p>
        メッセージ: <span id="message"></span>
    </p>
  </body>
</html>

<script>
  function appendScriptTag(src) {
    let script = document.createElement('script');
    script.src = src;
    document.body.appendChild(script);
  }
  const callBack = function (data) {
    const message = data.message;
    const author = data.author;
    document.getElementById('message').innerText = message;
    document.getElementById('author').innerText = author;
  };

  appendScriptTag('http://localhost:8080/js/jsonp3.js');
</script>

ちょっと今までよりわかりにくいですが、

appendScriptTag でscriptタグを直書きするのではなく、動的に作成するように変更しています。(動的に作成することで、任意のタイミングでscriptタグを生成できるように準備します。Step4で具体例をお見せします。)

また、callBackという変数 に関数を突っ込んでいます。

サーバー側に配置しているjsファイルで以下のようにcallBackの関数を呼び出すようにします。

// jsonp3.js
var obj = {
    "message": "コールバック関数から呼ばれます",
    "author": "bun"
}

callBack(obj);

このようにコールバック関数を利用したのは、動的に追加したscriptタグのjsファイルの読み込みが終わってから、処理を実行してほしいためです。

コールバック関数について、よくわからない場合は、以下の記事を参考にされたらよいかと思います。(私の記事ではありません。わかりやすかったです。)

https://sbfl.net/blog/2019/02/08/javascript-callback-func/
https://qiita.com/nekoneko-wanwan/items/f6979f687246ba089a35

この jsonp3.htmlをブラウザで読み込むことで以下のように、データをドメインBから受け取って画面に表示しています。
(正確にはサーバーサイドでデータを定義して、HTMLに記載しているコールバック関数を呼び出してもらっています)

js読み込みイメージ

Step4:動的なデータの受け渡し

ここまでで、scriptタグのsrc属性には静的なjsファイルの読み込みを行ってきました。

今度はクライアント側からサーバーサイドに何らかの情報を渡して、動的にサーバーサイドから返却される情報を変更してみます。
(ajaxでもaxiosでも何からのパラメーターを送付して、目的となるデータをサーバーから返却してもらいますよね。)

流れとしては以下のような形になります。

  1. クライアント側で、コールバック関数を定義しておく
  2. 任意のタイミングで、scriptタグを生成してサーバー側にアクセス。その時、定義しているコールバック関数の名前をサーバー側に教えてあげる。
  3. サーバー側でパラメーターを受け取り、パラメーターに応じて返却する情報を変更(実務ではここがDBからの情報取得処理になったりすることを想定)
  4. サーバー側はapplication/javascript形式で返却。
  5. 受け取ったブラウザ側でその関数を実行

動的やり取りのイメージ

これだけだとわかりにくいので、実装をしていってみましょう。

今までほぼ機能を使っていない expressに働いてもらいます。

またファイルを追加します。

.
├── docker-compose.yml
├── html
│   ├── jsonp1.html
│   ├── jsonp2.html
│   ├── jsonp3.html
│   └── jsonp4.html //追加
└── src
    ├── app.js //ここを変更します
    ├── package-lock.json
    ├── package.json
    └── public
        └── js
            ├── jsonp1.js
            ├── jsonp2.js
            └── jsonp3.js

HTMLを以下のようにします。

<!DOCTYPE html>
<html>
  <head>
    <title>データを受け取って、画面に反映する</title>
  </head>
  <body>
    <h1>ボタンを押したタイミングで画面に反映する</h1>
    <button id="button">ボタン</button>
    <p>作った人: <span id="author"></span></p>
    <p>メッセージ: <span id="message"></span></p>
    <ul id="list"></ul>
  </body>
</html>

<script>
  // スクリプトタグを追加する関数(任意のタイミングで呼び出し)
  function appendScriptTag(src) {
    let script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    document.body.appendChild(script);
  }
  // callバック関数。サーバーからデータを受け取って実行したい処理を書く
  const callBack = function (jsonData) {
    const message = jsonData.message;
    document.getElementById('message').innerText = message;
    const author = jsonData.author;
    document.getElementById('author').innerText = author;
    const list = jsonData.list;
    const ulTag = document.getElementById('list');
    for (let i = 0; i < list.length; i++) {
      let liTag = document.createElement('li');
      liTag.innerText = list[i];
      ulTag.appendChild(liTag);
    }
  };
  // クリックされたタイミングでscriptタグを追加。URLパラメーターで実行してほしいコールバック関数を教える。
  window.onload = function () {
    const btn = document.getElementById('button');
    btn.addEventListener('click', function () {
      appendScriptTag('http://localhost:8080/?callback=callBack&pattern=a');
    });
  };
</script>

サーバーサイドは以下のように実装します。

// app.js
// express モジュールのインスタンス作成
const { response } = require('express');
const express = require('express');
const app = express();
// パス指定用モジュール
const path = require('path');

app.listen(3000, () => {
  console.log('Running at Port 3000...');
});

app.get('/', (req, res) => {
  res.contentType('application/json');
  const query = req.query;
  const pattern = query.pattern;
  const callbackName = query.callback;
  // 送信された情報によって返却される情報を変更
  let obj = {};
  if (pattern == 'a') {
    obj = {
      author: 'bun_patterna',
      message: 'パターンAです',
      list: [
        '実際は',
        'パラメーターでDBから取得する値を',
        '変えるイメージです。',
      ],
    };
  } else {
    obj = {
      author: 'bun_patternb',
      message: 'パターンBです',
      list: ['パラメーターを動的に', '変更する'],
    };
  }
  // 今回は実装を勉強するために以下のようにしていますが、res.jsonp(obj)で大丈夫です
  json = JSON.stringify(obj);
  res.send(`${callbackName}(${json})`);
  //こんな感じのjsコードとして返されます
  // callback({author: ・・・}) というjsコードを返却
});

そしてHTMLをブラウザで開きます。

ボタン押下前
ボタン押下前のイメージ

ボタン押下後
ボタン押下後のイメージ

こんな形で任意のタイミングで、異なるドメインのサーバーからデータを取得することができました!

まとめ

以上JSONPを理解するために4ステップで実装してきました。

今回はあまり説明することができていませんが、

JSONPによるやり取りは信頼できるサーバーのみにするべきです。

実装を見てわかるように、サーバーから任意の命令を実行することができるからです。

この辺りについては、いくつも参考記事がありますので、是非ご参照ください。

https://blog.ohgaki.net/stop-using-jsonp

次回以降で、アドテクにおけるJSONPの利用のされ方等について私が学習した内容を記載したいと思います。

なお、今回利用したコードはこちらのリポジトリに上げております。

Discussion

ichi53ichi53

api作成に伴いJSONPについていろんな記事を読んでましたが一番わかりやすかったです!
ありがとうございます!

bun913bun913

@ichi53 さん
まさか記事を褒めていただけるとは思ってなかったので、嬉しいです!
ありがとうございます😭

ockeghemockeghem

CORSという制約があるからです。

制約ということであれば、CORSではなく、同一オリジンポリシー(Same Origin Policy)ですね。元々CORSがない時代に、同一オリジンポリシーの枠内でAPIを呼び出すために考案された方式がJSONPです。CORSは別オリジンのAPIを呼び出すための仕様なので、「CORSがあればJSONPは要らないよね」となります。