2021年6月22日

Fetch: ダウンロードの進捗

fetchメソッドでは、*ダウンロード*の進捗状況を追跡できます。

注意: 現在、fetchで*アップロード*の進捗状況を追跡する方法はありません。その目的には、XMLHttpRequestを使用してください。後ほど説明します。

ダウンロードの進捗状況を追跡するには、response.bodyプロパティを使用できます。これはReadableStream、つまり、ボディをチャンクごとに提供する特別なオブジェクトです。Readable Streamについては、Streams API仕様で説明されています。

response.text()response.json()などのメソッドとは異なり、response.bodyは読み取りプロセスを完全に制御できるため、任意の時点で消費量をカウントできます。

response.bodyからレスポンスを読み取るコードの概要を以下に示します。

// instead of response.json() and other methods
const reader = response.body.getReader();

// infinite loop while the body is downloading
while(true) {
  // done is true for the last chunk
  // value is Uint8Array of the chunk bytes
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

  console.log(`Received ${value.length} bytes`)
}

await reader.read()呼び出しの結果は、2つのプロパティを持つオブジェクトです。

  • done – 読み取りが完了した場合はtrue、そうでない場合はfalse
  • value – バイトの型付き配列: Uint8Array
注意してください

Streams APIでは、for await..ofループを使用したReadableStreamの非同期反復についても説明していますが、まだ広くサポートされていません(ブラウザの問題を参照)。そのため、whileループを使用しています。

ローディングが完了するまで、つまりdonetrueになるまで、ループ内でレスポンスチャンクを受信します。

進捗状況を記録するには、受信したすべてのフラグメントvalueの長さをカウンターに追加する必要があります。

レスポンスを取得し、コンソールに進捗状況を記録する完全な動作例を以下に示します。詳細な説明は後述します。

// Step 1: start the fetch and obtain a reader
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');

const reader = response.body.getReader();

// Step 2: get total length
const contentLength = +response.headers.get('Content-Length');

// Step 3: read the data
let receivedLength = 0; // received that many bytes at the moment
let chunks = []; // array of received binary chunks (comprises the body)
while(true) {
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

  chunks.push(value);
  receivedLength += value.length;

  console.log(`Received ${receivedLength} of ${contentLength}`)
}

// Step 4: concatenate chunks into single Uint8Array
let chunksAll = new Uint8Array(receivedLength); // (4.1)
let position = 0;
for(let chunk of chunks) {
  chunksAll.set(chunk, position); // (4.2)
  position += chunk.length;
}

// Step 5: decode into a string
let result = new TextDecoder("utf-8").decode(chunksAll);

// We're done!
let commits = JSON.parse(result);
alert(commits[0].author.login);

それを段階的に説明しましょう

  1. 通常どおりfetchを実行しますが、response.json()を呼び出す代わりに、ストリームリーダーresponse.body.getReader()を取得します。

    同じレスポンスを読み取るために、これらの両方のメソッドを使用することはできません。リーダーを使用するか、レスポンスメソッドを使用して結果を取得します。

  2. 読み取る前に、Content-Lengthヘッダーから完全なレスポンスの長さを把握できます。

    クロスオリジンリクエストには存在しない場合があります(Fetch: クロスオリジンリクエストの章を参照)。また、技術的にはサーバーはそれを設定する必要はありません.しかし、通常は設定されています。

  3. 完了するまでawait reader.read()を呼び出します.

    レスポンスチャンクを配列chunksに収集します。これは重要です。レスポンスが消費された後、response.json()または他の方法を使用して「再読み取り」することはできません(試してみるとエラーが発生します)。

  4. 最後に、Uint8Arrayバイトチャンクの配列であるchunksがあります。これらを1つの結果に結合する必要があります。残念ながら、これらを連結する単一のメソッドはないため、それを行うためのコードがいくつかあります。

    1. 結合された長さを持つ同じ型の配列であるchunksAll = new Uint8Array(receivedLength)を作成します。
    2. 次に、.set(chunk, position)メソッドを使用して、各chunkを順番にコピーします。
  5. chunksAllに結果があります。ただし、これは文字列ではなくバイト配列です。

    文字列を作成するには、これらのバイトを解釈する必要があります。組み込みのTextDecoderはまさにそれを行います。その後、必要に応じてJSON.parseできます.

    文字列ではなくバイナリコンテンツが必要な場合はどうでしょうか?それはさらに簡単です。手順4と5を、すべてのチャンクからBlobを作成する1行に置き換えます。

    let blob = new Blob(chunks);

最後に、結果(文字列またはブロブ、どちらでも便利な方)と、その過程での進捗状況の追跡があります。

もう一度注意してください。これは*アップロード*の進捗状況(現在はfetchでは不可能)ではなく、*ダウンロード*の進捗状況のみです。

また、サイズが不明な場合は、ループ内でreceivedLengthを確認し、特定の制限に達したらループを中断する必要があります。そうすることで、chunksがメモリをオーバーフローさせることはありません。

チュートリアルマップ

コメント

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