Chapter 11

Web API (2)

OJK
OJK
2021.11.29に更新

Web API の 2 回目は Request オブジェクトの詳細です。前回は AP サーバの URL だけを指定するシンプルな GET リクエストでしたが、今回はクエリー付きの GET リクエスト、そして POST リクエストの例を見ていきます。また、Fetch API によるローカルファイルの読み込みについても簡単に触れます。

雛形コード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>petite-vue入門</title>
  <style>
    [v-cloak] { display: none; }
  </style>
</head>
<body v-cloak @mounted="init">

  <p v-if="data">{{data}}</p>

  <script src="https://unpkg.com/petite-vue"></script>
  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

PetiteVue.createApp({
  async init() { },
  data: false
}).mount();

Request オブジェクト

前チャプターでは、「{JSON} Placeholder」というテスト用のサードパーティ API を利用していました。fetch 関数の引数には AP サーバの URL を指定しています。

JS
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const res = await fetch(url);
const obj = await res.json();
this.data = obj;

一方、fetch 関数の構文は次のようであるとも紹介しました。この構文では、fetch 関数の引数は文字列ではなく Request オブジェクトになっています。

fetch関数
Responseオブジェクト = await fetch(Requestオブジェクト)

本来、Request オブジェクトはコンストラクタ Request() を使って次のように生成されます。

Requestオブジェクトの生成
Requestオブジェクト = new Request('リソースのURL', { オプションオブジェクト })

「コンストラクタ」とはオブジェクトを生成する特別な関数で、オブジェクトと同名の関数です。『文系大学生のための JavaScript 入門』では Date オブジェクトを const date = new Date(); として生成して利用しましたが、それと同じです。

ところで、fetch 関数は引数に Request オブジェクトしか受け取らないので、次のように Request コントラクトと同じ引数を直接渡しても動作するように作られています。

fetch関数の引数をオブジェクト指定
fetch('リソースのURL', { オプションオブジェクト })

「リソースの URL」は必須ですが、「オプションオブジェクト」はその全ての項目(プロパティ)が既定値でよければ省略可能です。単純な GET リクエストの場合、全てが既定値でよいので省略され、リソースの URL だけが残ります。前チャプターのサンプルコードで fetch 関数の引数に URL の文字列しか指定しなかったのはそのためです。

Request オプジェクトの代表的なプロパティを紹介します。

プロパティ 意味
method HTTP リクエストメソッド(GET や POST など)
headers HTTP リクエストヘッダー(Headers オブジェクト)
body POST メソッドなどに付ける本文

method プロパティでリクエストメソッドを選択します。本講座では GET と POST しか扱いませんので、'GET'もしくは'POST'という文字列を指定します。この method プロパティの既定値は 'GET' です。

headers プロパティは、サードパーティ API が要求する特別な HTTP リクエストヘッダーがあるときに指定します。HTTP リクエストに付帯する一般的なヘッダー情報はここで指定する必要はありません。headers プロパティの既定値は「なし」です。必要な場合は Headers オブジェクトをコンストラクトで生成して値を設定するのですが、これも Request オブジェクトと同じで、単なるオブジェクトで代替できます。

body プロパティは、POST リクエストを使用する際の本文(メッセージボディ)です。HTML では form 要素を使って送られるフォームデータがこれにあたりますが、JavaScript から POST リクエストを送る場合は body プロパティの値(オブジェクト)として本文を指定します。既定値は「なし」です。

既定値がそれぞれ 'GET'/なし/なし なので、特別な HTTP リクエストヘッダーを指定する必要のない単純な GET リクエストの場合はオプションオブジェクトが不要となり、Request オブジェクトは実質「リソースの URL」のみとなります。一方、POST メソッドを使用する場合には、method プロパティの明示的な指定('POST')が必要になり、場合によっては body プロパティも指定することになります。

クエリ付きの GET リクエスト

サードパーティ API を利用する際、GET リクエストにパラメータ(付加情報)を付けるよう指示されていることがあります。GET リクエストのパラメータは “クエリ/query” と呼ばれ、URL の後ろに「?」を置き、そのあとに「クエリ=値」と続けます。クエリと値のセットは「&」で繋いで複数送ることができます。

例えば、「apikey」というクエリに「AABBCC」という値を、「id」というクエリに「ojk」という値を付けて、サードパーティ API「https://httpbin.org/get」に GET リクエストを送信したいときは次のようになります。

GETリクエスト with クエリ
fetch('https://httpbin.org/get?apikey=AABBCC&id=ojk')

クエリも値も URL から連続した単なる文字列で、引用符に囲む必要もありません。ただ、こうして文字列にベタ書きしてしまうと扱いづらいので、実際には定数などにクエリ値を一旦設定してから URL の文字列を生成することになるでしょう。

JS
PetiteVue.createApp({
  async init() {
    const apikey = 'AABBCC';
    const url = `https://httpbin.org/get?apikey=${apikey}&id=${this.id}`;
    const res = await fetch(url);
    const obj = await res.json();
    this.data = obj;
    console.log(JSON.stringify(obj, null, 2));
  },
  data: false,
  id: 'ojk'
}).mount();

もちろん、petite-vue のデータプロパティも使えます。ユーザにフォーム入力してもらった値を v-model でデータプロパティに取得し、その値をクエリとしてサードパーティ API に GET リクエストするようなアプリケーションが簡単に作れます。

また、URLSearchParams オブジェクト を仲介する方法もあります。クエリとして日本語を渡すときには URLSearchParams を使った方法でないとうまくいかない場合もあるようなので、こちらを定石として覚えておいてもよいと思います。

URLSearchParams
const query = new URLSearchParams({
  apikey: 'AABBCC',
  id: this.id,
});

const url = 'https://httpbin.org/get?' + query;
const res = await fetch(url);

URLSearchParams コンストラクタの引数に与えたオブジェクトにて、クエリと値のペアを設定します。こうして生成された URLSearchParams オブジェクトは URL の文字列に加算演算子で追加できます。

クエリが正しく送られているか確認してみましょう。実は、今回利用している「httpbin.org」というサードパティ API は、送信したリクエストをそのままレスポンスとして送り返してくれるサービスです。コンソールログを確認すると、下図のようなリソースが表示されているかと思います(図は途中で省略しています)。

先頭の "args" プロパティの中身が、こちらから送信したクエリです。ここに表示されていれば正しく送れているということです。

HTTP リクエストヘッダーを付ける

サードパーティ API によっては HTTP リクエストヘッダーに付加情報を付けるよう指示される場合があります。Request オブジェクトの headers プロパティに指定することで追加できます。

fetch 関数の引数にオブジェクトで直接指定する場合は以下のようになります。指定されるヘッダー名にはハイフンが含まれることも多いのですが、その場合はプロパティ名を引用符で囲います。

HTTPリクエストヘッダー
PetiteVue.createApp({
  async init() {
    const url = 'https://httpbin.org/get';
    const res = await fetch(url, {
      headers: {
        Authentication: 'ojk',
        'Content-Type': 'application/json'
      }
    });

    const obj = await res.json();
    console.log(JSON.stringify(obj, null, 2));
  }
}).mount();

このようにして指定した HTTP リクエストヘッダーの情報も httpbin のレスポンスに含まれて戻ってきます。"headers" プロパティの部分ですが、最初から設定されているヘッダーがたくさんあるので探し出してください。

事前に Headers オブジェクトを生成しておくこともできます。

Headersオブジェクトの生成
const headers = new Headers({
  Authentication: 'ojk',
  'Content-Type': 'application/json',
});

const res = await fetch(url, { headers: headers });

POST リクエスト

POST リクエストでは画像データが送れるなど GET と若干の違いはありますが、できることはほぼ同じです。POST と GET の使い分けは、主にリソースに対する操作の違いになります。GET は文字どおりリソースを取得する処理に使われ、POST はリソースを追加する処理に使われることが多いです(そして PUT は更新、DELETE は削除です)。

どの HTTP リクエストメソッドを使用するかはサードパーティ API から指定されるので、私たちはどのリクエストでも送れるようになっておけば OK です。本講座では GET と POST しか扱いませんが、POST の方法がわかれば PUT と DELETE は method プロパティが異なるだけです。

サードパーティ API として、再び {JSON} Placeholder を使いましょう。POST リクエストによってリソース(以下では TODO リスト)を追加することができます。Request オブジェクトのプロパティはすでに紹介していますから、コードを読み取ってみてください。

POSTリクエスト
PetiteVue.createApp({
  async init() {
    const url = 'https://jsonplaceholder.typicode.com/todos';

    // メッセージボディをオブジェクトで用意
    const msgBody = {
      userId: 1,
      title: 'fetch関数をマスターする',
      completed: false
    };

    // POSTリクエスト
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-type': 'application/json',
      },
      body: JSON.stringify(msgBody) // JSON化
    });

    const obj = await res.json();
    console.log(JSON.stringify(obj, null, 2));
  }
}).mount();

追加されたデータはそのままレスポンスとして返ってきます。元の TODO リストが 200 件登録されているので、id が 201 番目になっています。

元の TODO リストは同じエンドポイントに対して GET リクエストを送れば取得できるので確認してみてください。ただし、このテスト用 API では、POST で TODO を登録してもサーバ側に記録されませんので、POST した後に GET してもオリジナルの 200 件しか表示されません。

サンプルコードの説明ですが、まず、Request オブジェクトの methods プロパティに 'POST' を指定すれば POST リクエストになります。また、この Web API では、POST のときはリクエストヘッダーにて Content-type に 'application/json' を設定する必要があるので追加しています。

POST する TODO データを本文(メッセージボディ)に記述して送ります。上のサンプルコードでは定数 msgBody にオブジェクトの形で用意してから、Request オブジェクトの body プロパティに指定しています。body プロパティには文字列しか指定できないので、オブジェクトや配列は JSON.stringify メソッドで JSON 化する必要があります。

エラー処理

ここまで触れずにきましたが、HTTP リクエストには通信エラーがつきものですので、実際のアプリケーション開発では Response オブジェクトの検証が必須です。Response オブジェクトには以下の情報が含まれます。

プロパティ 意味
ok ステータスコードが 200 番台のときに true
status ステータスコード
statusText ステータスコードに対応するメッセージ

HTTP 通信が成功するとステータスコードは 200 番台になり、ok プロパティは true になります。ステータスコードの一覧は こちら を見てください。検証を加えたコードは例えば以下のようになります。

JS
const url = 'https://httpbin.org/status/404';
const res = await fetch(url);
if (res.ok) {
  console.log(res);
  /* 本来はJSONからオブジェクトに変換する処理がここに入る */
  // const obj = await res.json();
  // this.data = obj;
} else {
  console.error(`${res.status} : ${res.statusText}`);
}

エンドポイント「httpbin.org/status」では、上記サンプルコードのように、返してくるステータスコードを自由に設定できます(サンプルでは 404 になっています)。ステータスコードの一覧を見ながら、いろいろとステータスコードを試してみてください。ただし、このエンドポイントは 200 番台を指定してもリソースが JSON で返ってこないため、res.json() を実行するとエラーになります。Response オブジェクト res を直接 console.log して結果を確認ください。

なお、AP サーバの URL が間違えているときは fetch 関数を呼び出したところでエラーとなり、if (res.ok) まで到達しないようです。

ファイルからデータを取得する

本チャプターの最後に、Web API の話ではありませんが、fetch 関数を使ったファイルの読み込みについて触れておきたいと思います。

次のチャプターで説明しますが、実はセキュリティの関係で、ブラウザから直接利用できるサードパーティ API はあまり多くありません。サードパーティ API を利用するには、サーバサイドでのプログラミングが必要になります。本講座が扱うのはクライアントサイド(ブラウザだけで動かせる範囲)ですから、擬似的に Web API を使ったアプリケーションのモックアップを作る方法として、仮のリソースをファイルで用意して読み込むことを考えます。

ただし、ファイルを読み込むためにはアプリケーションがウェブサーバ上で動作している必要があります。Visual Studio Code の Live Server 拡張機能はローカルでウェブサーバを動かす仕組みなので条件を満たします。

ファイル読み込みの方法は簡単で、fetch 関数の「リソースの URL」にファイルへのパス(経路)を指定するだけです。それ以降の処理は Web API と同じです。パスは、ドキュメントルートからの絶対パス指定となります。本講座では index.html と script.js が同じフォルダにある状態だと思いますので、「ドキュメントルート = index.html が置いてある位置」になります。その場合、これまで使ってきた相対パスの先頭に '/' を付けるだけで絶対パスになります。

以下は、index.html の置いてあるフォルダに resource というフォルダを作成し、その下に data.json という名前で JSON 形式のファイルを設置した場合です。

HTML
<style>
table, td {
  border: solid black 1px;
  border-collapse: collapse;
  padding: 5px 10px;
}
</style>

<!-- 略 -->

<table v-if="data">
  <tr>
    <th>名前</th><th>タイプ</th><th>重さ</th><th>特性</th>
  </tr>
  <tr v-for="pkmn in data">
    <td>{{pkmn.name}}</td>
    <td>{{pkmn.type}}</td>
    <td style="text-align: right">{{pkmn.weight}}kg</td>
    <td>{{pkmn.ability}}</td>
  </tr>
</table>
JS
PetiteVue.createApp({
  async init() {
    const path = '/resource/data.json'; // 絶対パス指定

    // URLがパスになっただけで、以下の処理は同じ
    const res = await fetch(path);
    const obj = await res.json();
    this.data = obj.list;
  },
  data: false,
}).mount();
data.json
{
  "list": [
    {
      "name": "ガーディ",
      "type": "ほのお",
      "weight": 19.0,
      "ability": "もらいび"
    },
    {
      "name": "コイキング",
      "type": "みず",
      "weight": 10.0,
      "ability": "すいすい"
    },
    {
      "name": "コダック",
      "type": "みず",
      "weight": 19.6,
      "ability": "しめつけ"
    },
    {
      "name": "サンダース",
      "type": "でんき",
      "weight": 24.5,
      "ability": "ちくでん"
    },
    {
      "name": "ピカチュウ",
      "type": "でんき",
      "weight": 6.0,
      "ability": "せいでんき"
    },
    {
      "name": "ヒトカゲ",
      "type": "ほのお",
      "weight": 8.5,
      "ability": "もうか"
    },
    {
      "name": "ブースター",
      "type": "ほのお",
      "weight": 25.0,
      "ability": "もらいび"
    },
    {
      "name": "ミュウ",
      "type": "エスパー",
      "weight": 4.0,
      "ability": "シンクロ"
    }
  ]
}

結果は チャプター 7 で作成したテーブルと同じです。