2022年4月14日

フェッチ

JavaScriptはサーバーにネットワークリクエストを送信し、必要なときにいつでも新しい情報をロードできます。

たとえば、ネットワークリクエストを使用して以下を行うことができます。

  • 注文を送信する
  • ユーザー情報をロードする
  • サーバーから最新の更新を受信する
  • など

…そして、これらすべてはページをリロードすることなく実行できます!

JavaScriptからのネットワークリクエストには、「AJAX」(Asynchronous JavaScript And XMLの略)という包括的な用語があります。ただし、XMLを使用する必要はありません。この用語は古い時代に由来するため、その単語が含まれています。この用語をすでに聞いたことがあるかもしれません。

ネットワークリクエストを送信してサーバーから情報を取得するには、複数の方法があります。

fetch()メソッドは最新かつ多用途であるため、まずはそれから始めます。古いブラウザではサポートされていません(ポリフィルできます)が、最新のブラウザでは非常によくサポートされています。

基本的な構文は次のとおりです。

let promise = fetch(url, [options])
  • url – アクセスするURL。
  • options – オプションのパラメータ:メソッド、ヘッダーなど。

optionsがない場合、これは単純なGETリクエストであり、urlの内容をダウンロードします。

ブラウザはすぐにリクエストを開始し、呼び出し元のコードが結果を取得するために使用するpromiseを返します。

レスポンスの取得は、通常2段階のプロセスです。

まず、fetchによって返されたpromiseは、サーバーがヘッダーで応答するとすぐに、組み込みのResponseクラスのオブジェクトで解決されます。

この段階では、HTTPステータスを確認して成功したかどうかを確認し、ヘッダーを確認できますが、まだ本文はありません。

ネットワークの問題やサイトが存在しないなど、fetchがHTTPリクエストを実行できなかった場合、promiseは拒否されます。404や500などの異常なHTTPステータスはエラーを引き起こしません。

レスポンスプロパティでHTTPステータスを確認できます

  • status – HTTPステータスコード、例:200。
  • ok – ブール値、HTTPステータスコードが200〜299の場合はtrue

例えば

let response = await fetch(url);

if (response.ok) { // if HTTP-status is 200-299
  // get the response body (the method explained below)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

次に、レスポンス本文を取得するには、追加のメソッド呼び出しを使用する必要があります。

Responseは、さまざまな形式で本文にアクセスするための、複数のpromiseベースのメソッドを提供します。

  • response.text() – レスポンスを読み取り、テキストとして返す
  • response.json() – レスポンスをJSONとして解析する
  • response.formData() – レスポンスをFormDataオブジェクトとして返す(次の章で説明)
  • response.blob() – レスポンスをBlob(タイプ付きのバイナリデータ)として返す
  • response.arrayBuffer() – レスポンスをArrayBuffer(バイナリデータの低レベル表現)として返す
  • さらに、response.bodyReadableStreamオブジェクトであり、本文をチャンクごとに読み取ることができます。後ほど例を示します。

たとえば、GitHubから最新のコミットを含むJSONオブジェクトを取得してみましょう。

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // read response body and parse as JSON

alert(commits[0].author.login);

または、awaitを使用せずに、純粋なpromise構文を使用しても同じです。

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));

レスポンステキストを取得するには、.json()の代わりにawait response.text()を使用します。

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

let text = await response.text(); // read response body as text

alert(text.slice(0, 80) + '...');

バイナリ形式での読み取りの例として、“fetch”仕様のロゴ画像をフェッチして表示してみましょう(Blobの操作の詳細については、Blobの章を参照)。

let response = await fetch('/article/fetch/logo-fetch.svg');

let blob = await response.blob(); // download as Blob object

// create <img> for it
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);

// show it
img.src = URL.createObjectURL(blob);

setTimeout(() => { // hide after three seconds
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);
重要

本文を読み取るメソッドは1つだけ選択できます。

response.text()でレスポンスをすでに取得している場合、本文の内容はすでに処理されているため、response.json()は機能しません。

let text = await response.text(); // response body consumed
let parsed = await response.json(); // fails (already consumed)

レスポンスヘッダー

レスポンスヘッダーは、response.headersのMapのようなヘッダーオブジェクトで利用できます。

これは正確にはMapではありませんが、名前で個々のヘッダーを取得したり、それらを反復処理したりするための同様のメソッドがあります。

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// get one header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// iterate over all headers
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

リクエストヘッダー

fetchでリクエストヘッダーを設定するには、headersオプションを使用できます。送信ヘッダーを含むオブジェクトがあり、次のようになります。

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

…ただし、設定できない禁止されたHTTPヘッダーのリストがあります。

  • Accept-CharsetAccept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • CookieCookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Proxy-*
  • Sec-*

これらのヘッダーは、適切で安全なHTTPを確保するため、ブラウザによってのみ制御されます。

POSTリクエスト

POSTリクエスト、または別のメソッドのリクエストを行うには、fetchオプションを使用する必要があります。

  • method – HTTPメソッド、例:POST
  • body – リクエスト本文、次のいずれか
    • 文字列(例:JSONエンコード)
    • FormDataオブジェクト、データをmultipart/form-dataとして送信する
    • Blob/BufferSource、バイナリデータを送信する
    • URLSearchParams、データをx-www-form-urlencodedエンコーディングで送信する(まれに使用される)

ほとんどの場合、JSON形式が使用されます。

たとえば、このコードはuserオブジェクトをJSONとして送信します。

let user = {
  name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

リクエストbodyが文字列の場合、Content-Typeヘッダーはデフォルトでtext/plain;charset=UTF-8に設定されることに注意してください。

ただし、JSONを送信するため、headersオプションを使用して、JSONエンコードデータの正しいContent-Typeであるapplication/jsonを送信します。

画像の送信

BlobまたはBufferSourceオブジェクトを使用して、fetchでバイナリデータを送信することもできます。

この例では、マウスを上に移動することで描画できる<canvas>があります。「送信」ボタンをクリックすると、画像がサーバーに送信されます。

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Submit" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
      let response = await fetch('/article/fetch/post/image', {
        method: 'POST',
        body: blob
      });

      // the server responds with confirmation and the image size
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

Blobオブジェクトには組み込みのタイプ(ここではtoBlobによって生成されたimage/png)があるため、ここではContent-Typeヘッダーを手動で設定しないことに注意してください。Blobオブジェクトの場合、そのタイプはContent-Typeの値になります。

submit()関数は、async/awaitなしで次のように書き直すことができます。

function submit() {
  canvasElem.toBlob(function(blob) {
    fetch('/article/fetch/post/image', {
      method: 'POST',
      body: blob
    })
      .then(response => response.json())
      .then(result => alert(JSON.stringify(result, null, 2)))
  }, 'image/png');
}

まとめ

典型的なfetchリクエストは、2つのawait呼び出しで構成されます。

let response = await fetch(url, options); // resolves with response headers
let result = await response.json(); // read body as json

または、awaitなしで

fetch(url, options)
  .then(response => response.json())
  .then(result => /* process result */)

レスポンスプロパティ

  • response.status – レスポンスのHTTPコード
  • response.ok – ステータスが200〜299の場合はtrue
  • response.headers – HTTPヘッダーを含むMapのようなオブジェクト

レスポンス本文を取得するメソッド

  • response.text() – レスポンスをテキストとして返す
  • response.json() – レスポンスをJSONオブジェクトとして解析する
  • response.formData() – レスポンスをFormDataオブジェクトとして返す(multipart/form-dataエンコーディング、次の章を参照)
  • response.blob() – レスポンスをBlob(タイプ付きのバイナリデータ)として返す
  • response.arrayBuffer() – レスポンスをArrayBuffer(低レベルのバイナリデータ)として返す

これまでのFetchオプション

  • method – HTTPメソッド
  • headers – リクエストヘッダーを含むオブジェクト(すべてのヘッダーが許可されるわけではない)
  • body – 送信するデータ(リクエスト本文)をstringFormDataBufferSourceBlob、またはUrlSearchParamsオブジェクトとして

次の章では、fetchのより多くのオプションとユースケースについて説明します。

タスク

GitHubログインの配列を取得し、GitHubからユーザーをフェッチして、GitHubユーザーの配列を返す非同期関数getUsers(names)を作成します。

指定されたUSERNAMEのユーザー情報を含むGitHub URLは、https://api.github.com/users/USERNAMEです。

サンドボックスにテスト例があります。

重要な詳細

  1. ユーザーごとに1つのfetchリクエストが必要です。
  2. リクエストは互いに待機するべきではありません。できるだけ早くデータが到着するようにします。
  3. リクエストが失敗した場合、またはそのようなユーザーが存在しない場合、関数は結果の配列でnullを返す必要があります。

テスト付きのサンドボックスを開く。

ユーザーをフェッチするには、fetch('https://api.github.com/users/USERNAME')が必要です。

レスポンスのステータスが200の場合、.json()を呼び出してJSオブジェクトを読み取ります。

それ以外の場合、fetchが失敗した場合、またはレスポンスのステータスが200以外の場合、結果の配列でnullを返します。

コードは次のとおりです。

async function getUsers(names) {
  let jobs = [];

  for(let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      successResponse => {
        if (successResponse.status != 200) {
          return null;
        } else {
          return successResponse.json();
        }
      },
      failResponse => {
        return null;
      }
    );
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
}

.then呼び出しはfetchに直接アタッチされるため、レスポンスを受信したときに他のフェッチを待機せずに、すぐに.json()の読み取りを開始することに注意してください。

await Promise.all(names.map(name => fetch(...)))を使用し、結果に対して.json()を呼び出した場合、すべてのフェッチが応答するまで待機します。.json()を各fetchに直接追加することにより、個々のフェッチが互いに待機することなく、データのJSONとしての読み取りを開始するようにします。

これは、主にasync/awaitを使用する場合でも、低レベルのPromise APIが依然として役立つ方法の例です。

テスト付きのソリューションをサンドボックスで開く。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHubのissueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code>タグを使用します。数行の場合は、<pre>タグで囲みます。10行を超える場合は、サンドボックス(plnkrjsbincodepen…)を使用します。