Chapter 10

Web API (1)

OJK
OJK
2021.11.29に更新

JavaScript の Fetch API を使用して、Web API を提供するインターネット上のサービスからデータを取得する方法をここから 3 回に渡って紹介します。Fetch API は素の JavaScript の機能であり、petite-vue とは独立した話なのですが、取得したデータを petite-vue を使って表示します。

雛形コード
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>

  <p>Petite Vue!!</p>

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

PetiteVue.createApp({

}).mount();

API

“API/Application Programming Interface” とは、ざっくりいうと、サービスやシステム、データなどをプログラミングから利用するための取り決め(ルール)のことです。ルールはただの文章なので、実際にはプログラミングで利用できるように、その API 専用のオブジェクトや関数が用意されたり、既存の関数の「引数」として表現されたりします。サービス/システム/データの側にも管理人のようなプログラムが動いていて、そのプログラムと自作のアプリケーションの間のやりとりをオブジェクトや関数を介して行います。

そしてクライアントサイド(ブラウザ側)のウェブアプリケーション開発における API(Web API)といえば、主に次の 2 種類のことを指します。いろんな呼称があるのですが、これは MDN を参考にしたものです(詳しくはこちら)。

  1. ブラウザ API
  2. サードパーティ API

1 のブラウザ API はこれまでずっと使用してきたものです。例えば、document オブジェクトもブラウザ API の実装のひとつです。JavaScript には HTML 文書を操作するためのプログラムが組み込まれていて、document.querySelector メソッドもそのプログラムとやりとりするための API(関数)です。他にも console や window など、ブラウザ + JavaScript で使用するオブジェクトはほぼブラウザ API です(一覧はこちら)。

一方、本チャプターで新しく取り上げる Web API は サードパーティ API です。Google や Twitter など、インターネット上の様々なサービスがその機能やデータなどの “リソース” を提供するために API を提供しており、それらを総称して「サードパーティ API」と呼びます[1]。ウェブアプリケーション開発で API といえば一般的にこちらを指します。

API を提供するサーバのことを アプリケーションサーバ (略してAP サーバ)と呼びます。AP サーバは、ウェブサーバと同じように GET や POST などの HTTP リクエストメソッド を受け付けますが、HTML・CSS・JavaScript を返してくるのではなく、JSON 形式のデータなどのリソースを返してきます。AP サーバもブラウザのアドレスバーから GET リクエストを受け付けますが、通常は JavaScript を使ってリクエストを送ります。

Fetch API

ブラウザ API のひとつである Fetch API は様々な目的で利用されますが、サードパーティ API との通信もその主な用途です。fetch 関数 を使って HTTP リクエストを AP サーバに送り、レスポンス(返答)として様々なリソースを受け取ります[2]

Fetch API は、その fetch 関数と Request オブジェクト、Response オブジェクトをはじめとするいくつかのオブジェクトで構成されます。fetch 関数を利用する流れは次のようになります。await については後ほど取り上げます。

fetch API
Responseオブジェクト = await fetch(Requestオブジェクト);
JavaScriptの通常のオブジェクト = await Responseオブジェクト.json();

先に具体例を見てみましょう。
以下は「{JSON} Placeholder」というサードパティ API(Web API のテスト用サービス)に GET リクエストを送るコードです。

async function fetchData() {
  const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
  const obj = await res.json();
  console.log(obj);
}

fetchData();

このサンプルコードでは fetchData という関数の中で fetch 関数を呼び出していますが、関数名は何でも構いません。関数定義の先頭についている async については後ほど説明します。

fetch 関数の引数をみると、Request オブジェクトなるものではなく、ただの URL(文字列)になっています。次回詳しく説明しますが、単純な GET リクエストの場合はこれで OK です。ちなみに、アプリケーション開発の分野では AP サーバの URL のことを “エンドポイント” といったりもします(分野によって意味の変わる言葉です)。今回はこの URL だけを指定する使い方で最後まで説明します。

fetch 関数の戻り値を受け取っている定数 res のほうは Response オブジェクトです。API サーバの提供するリソースが JSON の場合、Response オブジェクトの json メソッドを呼び出して JavaScript の通常のオブジェクトに変換する必要があります。サードパーティ API が提供するリソースがテキストデータの場合は JSON で返ってくることが多いので、json メソッドで JSON をオブジェクトに変換するところまでを一連の流れとして覚えましょう。

await と async

さて、後回しにしていたキーワード await について説明しましょう。

await の使い方を知るには、JavaScript の非同期処理について理解する必要があります。また、本来は JavaScript で非同期処理を扱うための Promise オブジェクト についても理解する必要があるのですが、本講座では思い切って Promise の解説を割愛します。詳しく知りたい人は こちら を読んでみてください。

非同期処理

まず、非同期処理について説明していきます。

JavaScript のコードは、通常は上から下に順番に実行されていき、前の処理が完了してからでないと次の処理には進みません。関数呼び出しでは実行されるコードの記述場所は飛びますが[3]、関数が呼び出される順番は守られます。

同期処理
console.log('Hello');
func();
console.log('World!!');

function func() {
  console.log('☆');
}

// → コンソールには「Hello☆World!!」と表示される

このように、前の処理の完了を待って次の処理が行われることを「同期処理」といいます。JavaScript にかぎらず、大抵のプログラミング言語は同期処理が通常です。

この同期処理の順番が入れ替わったら困りそうなものですが、実際には順番を飛ばしたいときはあります。処理の完了に時間がかかる場合です。

同期処理したくない例
console.log('Hello');
func();
console.log('World!!');

function func() {
  /* ここに時間のかかる処理が入る */
  console.log('☆');
}

// → Hello……(かなり待つ)………☆World!!

上記の例の場合、関数 func の処理が終わるまで「World!!」の文字はコンソールに出力されません。こういった遅れがアプリケーションで生じるとフリーズした状態になってしまいます。例えば、データを PC にダウンロードする処理などは、ダウンロードの完了を待つ間にも他の処理をしたくなると思います。

JavaScript では、関数単位で非同期処理をさせることが可能です。
以下のサンプルコードはイメージです。実際には関数を非同期処理させるための記述が必要です。

非同期処理(イメージ)
console.log('Hello');
asyncFunc();  // 完了を待たない
console.log('World!!');

// 非同期関数(だと想定する)
function asyncFunc() {
  /* ここに時間のかかる処理が入る */
  console.log('☆');
}

// → Hello World!!……(かなり待つ)………☆

このように時間のかかる処理を含む関数を非同期にすれば、先に「Hello World!!」まで表示させてから、遅れて「☆」が表示されるようにできます。

どうすれば非同期処理をする関数を定義できるのか…ですが、本講座は文系大学生向きなので、その部分については触れません(Promise が出てくるので)。非同期関数は誰か賢い人が用意してくれるので、私たちはそれを使用するだけにしましょう。

fetch 関数は非同期処理

さて、察しのとおり、本チャプターの主役である fetch 関数は非同期関数です。というのも、ネットワーク越しにリソースを取得するのに時間がかかることがあるからです。Response オブジェクトの json メソッドも非同期関数です。リソースを取得して変換しているあいだフリーズするようなアプリケーションはちょっと嫌ですしね。

しかし、話はそう簡単ではありません。fetch 関数はレスポンスを受け取ります。そして、大抵の場合、そのレスポンスにはリソースが含まれていて、それをアプリケーションで利用したいわけです。例えば、以下のコードを実行するとどうなるでしょうか。

fetchは非同期関数
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const res = fetch(url);  // 完了を待たない
const obj = res.json();  // ここでエラー
console.log(obj);

json メソッドを呼んだところでエラーになります。ブラウザによって表記は代わりますが「res.json は関数じゃないよ」というメッセージが表示されます。定数 res が json メソッドを持っていないときにこういうエラーメッセージになります。この時点では、定数 res はまだ Response オブジェクトではないからです。

それも当然の話で、const res = fetch(url); は非同期に処理されるので、fetch 関数の処理が完了する前、つまり定数 res に Response オブジェクトが代入される前に次の処理 const obj = res.json(); が実行されてしまうからです。ちゃんと Response オブジェクトを受け取ってから次の処理に進みたいなら、fetch 関数の処理が完了するのを待つ必要があります。

なんだか議論が往復しているようですが、非同期に処理をしてくれる非同期関数をわざわざ同期的に実行したい(処理の完了を待ちたい)というケースがある…ということです。

await で非同期関数の完了を待つ

ここでやっと await が登場します。
fetch 関数の呼び出しの直前に await を付けると非同期関数を同期的に実行してくれます。json メソッドも非同期関数なので await が必要です。

awaitのみ(まだ不十分)
const res = await fetch(url);
const obj = await res.json();
console.log(obj);

ただし、「まだ不十分」とあるように、これだけではエラーになります。
await には対となるキーワード async があり、「await は、async の付いた関数定義の中でしか使えない」というルールがあるからです。さきほどのサンプルコードをちょっと強引に関数に包んでみましょう。

関数で包んで先頭にasyncを付ける
async function fetchData() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  const res = await fetch(url);
  const obj = await res.json();
  console.log(obj);
}

fetchData(); // 関数呼び出しを忘れずに

これでやっと取得したリソースがコンソールに表示されます。

なお、await によって処理が同期される範囲は async の付いた関数内だけです。async な関数の外の世界は、await な関数の完了を待たずに進みます。以下のコードを実行すると、関数 func の完了を待たずに console.log('World!!'); が実行されることがわかります。

同期処理になる範囲
async function fetchData() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  const res = await fetch(url);
  const obj = await res.json();
  console.log(obj);
}

console.log('Hello');   // 1番目に完了
fetchData(); // 3番目に完了
console.log('World!!'); // 2番目に完了

// → Hello World!! の後に取得したリソースが表示される

そもそも「async」という英単語は「非同期の」という意味ですね。関数定義に async を付けるということは、その関数自体が非同期関数になるということです。これで、async 関数内は同期的に進むけども、async 関数の外は非同期的に進んでくれるので、fetch() や json() を await で止めてもアプリケーション全体がフリーズするということはありません。

petite-vue から fetch する

それでは petite-vue で fetch 関数を使ってみましょう。
petite-vue ではメソッド(関数)を定義して処理を呼び出しますので、そのメソッド名の前に async を付ければ OK です。

HTML
<button @click="fetchData">読込</button>
<p v-if="data">{{data}}</p>
JS
PetiteVue.createApp({
  // データプロパティ
  data: false, // 初期値はこうしておく

  // メソッド
  async fetchData() {
    const url = 'https://jsonplaceholder.typicode.com/todos/2';
    const res = await fetch(url);
    const obj = await res.json();
    this.data = obj;
  }
}).mount();

[読込]ボタンを押すと fetch 関数を呼び出し、通常のオブジェクトに変換した上で、データプロパティ data にリソースを渡します。data の初期値は false にしておき、リソースが取得されるまでは v-if を使って描画しないようにしています。この例では問題ありませんが、data のプロパティを参照するコードがあるとエラーになるので、v-if で防ぐことを習慣化しておきます。
以下のように代替の文言を付けるのもよいでしょう。

HTML
<button @click="fetchData">読込</button>
<p v-if="data">{{data}}</p>
<p v-else>データはありません</p>

もう少し大きなリソースを取得してみましょう。Random User Generator というテスト用のサードパーティ API を使用します。今度は @mounted を使ってアプリケーション起動時にリソースを取得するようにしてみました。

HTML
<body v-cloak @mounted="init">
  <p v-if="data">{{data}}</p>
</body>
JS
PetiteVue.createApp({
  // アプリ起動時に行う処理
  async init() {
    const url = 'https://randomuser.me/api/'; // 変更
    const res = await fetch(url);
    const obj = await res.json();
    this.data = obj;
  },

  // データプロパティ
  data: false
}).mount();

以下のように表示されたかと思います。Random User Generator ですからデータはランダムに生成されます(ので具体的な内容は皆さんと下図とでは異なります)。

さっきよりもかなり長くなりました。これを目視で読み取るのは辛いので、オブジェクトを JSON(文字列)の形に整形してコンソールに表示して確認します。メソッド init の最後に以下のように記述してください。

JSON.stringifyで整形表示
console.log(JSON.stringify(this.data, null, 2));
// 構文:JSON.stringify(オブジェクト, null, インデント数)

JSON.stringify メソッドは JavaScript のオブジェクトを JSON 形式の文字列に変換する用途で使われますが(参考)、第 2・第 3 引数を与えることで以下のように整形した文字列を出力してくれるので、大きなオブジェクトをコンソールで閲覧するときに便利です。

ブラウザによって表記は異なりますが、構成は同じだと思います。以下は Chrome の例です(長いので途中を省略しています)。

まず、一番上の階層は results と info というプロパティを持ったオブジェクトであることがわかります。info プロパティのほうは内容を見るかぎりリソースではなさそうです。一方の results プロパティには gender や name といったプロパティ名が見えます。どうやらこのオブジェクトがリソースのようです。注意したいのは、result プロパティの値がオブジェクトではなく配列であることです。[ で始まっているのを見落とさないようにしてください。

ということで、このリソースの本体は「results プロパティ → 配列 → 0 番目のオブジェクト」であることがわかりました。このように、サードパーティ API を利用するときには、必要とするデータを発掘する作業が最初に必要になります。

データプロパティ data に渡すデータを以下のように変更します。これで、リソースの本体にあたるオブジェクトが data に入りました。なぜ obj.results[0] となるかわからない人は、ここで立ち止まってよく考えてみてください。

JS
async init() {
  const url = 'https://randomuser.me/api/';
  const res = await fetch(url);
  const obj = await res.json();
  this.data = obj.results[0];  // リソースの本体部分を抽出する
  console.log(JSON.stringify(this.data, null, 2));
}

上記のコードは、this.data = await res.json().results[0] とは書けません。関数呼び出しに「.」を続けてプロパティにアクセスすることは問題ないのですが、json メソッドも非同期関数なので、res.json() の結果(通常のオブジェクト)が返ってくるまでちゃんと await してからでないと result プロパティを参照してはいけないのです。そのため、await なコードは行を独立させてローカル定数に結果を受け取り、それからそのプロパティにアクセスするようにします。

リソース本体だけにしても相変わらずプロパティは多いですが、このあたりで table 要素に表示させてみましょう。

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

<!-- 略 -->

<table v-if="data">
  <tr v-for="(val, prop) in data">
    <td>{{prop}}</td>
    <td>{{val}}</td>
  </tr>
</table>

以下のようになりますね(データの内容はランダムに変わります)。

各プロパティの下がさらにオブジェクトになっているものがありますが、ここまでわかれば欲しいデータに辿り着けるできるのではないかと思います。例えば、電話番号は data.phone でアクセスできますね。ログイン名は data.login.username です。

試しに、氏名・性別・メールアドレス・国・写真 URL の情報を取り出して、ランダムユーザのプロフィールを作成してみましょう。

雛形を用意しましたので、これを埋める形で作ってみてください。性別はゲッター(アクセサプロパティ)を使用することにします。

HTML
  <table v-if="data">
    <tr>
      <td rowspan="3"><img></td>
      <td>氏名</td>
      <td></td>
    </tr>
    <tr>
      <td>居住国</td>
      <td></td>
    </tr>
    <tr>
      <td>メール</td>
      <td></td>
    </tr>
  </table>
JS
PetiteVue.createApp({
  // 初期化処理
  async init() {
    const url = 'https://randomuser.me/api/';
    const res = await fetch(url);
    const obj = await res.json();
    this.data = obj.results[0];
    console.log(this.data);
  },

  // データプロパティ
  data: false,

  // アクセサプロパティ
  get gender() {
    // genderが 'male' なら「男」、そうでなければ「女」
    // this.data がリソース取得後であることを if文 で
  }
}).mount();
コードの例

答え合わせをしてみてください。

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Random User Profile</title>
  <style>
    [v-cloak] {
      display: none;
    }

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

<body v-cloak @mounted="init">

  <table v-if="data">
    <tr>
      <td rowspan="3">
        <img :src="data.picture.large">
      </td>
      <td>氏名</td>
      <td>
        {{data.name.first}} {{data.name.last}}({{gender}})
      </td>
    </tr>
    <tr>
      <td>居住</td>
      <td>{{data.location.country}}</td>
    </tr>
    <tr>
      <td>メール</td>
      <td>{{data.email}}</td>
    </tr>
  </table>

  <script src="https://unpkg.com/petite-vue"></script>
  <script src="script.js"></script>
</body>

</html>
script.js
'use strict';

PetiteVue.createApp({
  // 初期化処理
  async init() {
    const url = 'https://randomuser.me/api/'; // 変更
    const res = await fetch(url);
    const obj = await res.json();
    this.data = obj.results[0];
    console.log(this.data);
  },

  // データプロパティ
  data: false,

  // アクセサプロパティ
  get gender() {
    if (this.data) {
      return this.data.gender === 'female' ? '女' : '男';
    }
  },
}).mount();

なお、Random User Generator では性別が男/女のみなので三項演算子で記述していますが、「その他」がある場合はこれでは NG ですね。

脚注
  1. ブラウザ API は標準化団体(W3C/WHATWG )が仕様を定めているのに対して、Google や Twitter の API は(当然ですが)自社で勝手に仕様を決めています。そのため、標準団体外の第三者による(=サードパーティ)という言葉が付けられているのだと思います。 ↩︎

  2. 本チャプターでは「リソース=データ」という文脈で説明していますが、リソース(資源)が必ずしもデータとは限りません。例えば、モータ付きの IoT デバイス上で動いている AP サーバに HTTP リクエストを送り、モータを動作させるだけの場合もあります(その際もレスポンスは必ず戻ってきますが)。これもまたリソースへのアクセスのひとつです。 ↩︎

  3. コード上の見た目の話です。実際に実行されるのはコンピュータのメモリ上に格納された命令ですが、メモリ上の命令は、そもそも書いた順番に関係なく飛び飛びに配置されています。 ↩︎