Help us understand the problem. What is going on with this article?

ReactでブラウザーのStreams APIを使って、ダウンロードプログレスを表示する

Streams APIがブラウザーで使えるようになってからしばらく経つけど、Reactとの相性はどうだ?個人プロジェクトに導入するとき、試行錯誤した結果をここに投稿する。

TL;DR

細かい処理が多くて、抽象化してカスタムなHookができたので、以下のGistからコピペできる
https://gist.github.com/jlkiri/bc0a9bbf5d81c6f8bbe1cfd59a106380

また、その動きが確認できるデモが以下のリンクでアクセスできる(12MBの宇宙の画像をダウンロードする)
https://fetch-stream-hook-demo.jlkiri.now.sh/
(Githubレポジトリ: https://github.com/jlkiri/fetch-stream-hook-demo)

注意点

結論から言うと、MDNに乗っている例をそのまま使えばいいのだが、いくつか気を付けるべき点がある。

レンダリング

普段、Reactでデータを取得する場合、ローディングステート(つまりデータがまだ取得されていない状態)と、取得が終わった状態を分けて表示する。一方で、Streams APIのおかけで取得がチャンクごとに制御できるので、途中結果もすべて表示できる。

しかし、ダウンロードされるチャンクごとにステートの更新をすれば、おそらくCPUの負荷が高くて、ほかに表示されている画面の部分の反応が悪くなる可能性がある。なので、この場合に限って、更新は、Reactに任せずrefを使ってするべき。

以下で、取得したバイト数とファイルのバイト数合計を使って、何パーセントがダウンロードされたかをrefを使って直接表示する。(refの作成などは省略)

fetch(url)
      .then(response => {
        const contentLength = response.headers.get("content-length");

        let loaded = 0;

        const stream = new ReadableStream({
          start(controller) {
            const reader = response.body.getReader();
            return pump();

            function pump() {
              return reader.read().then(({ done, value }) => {
                if (done) {
                  controller.close();
                  return;
                }
                loaded += value.byteLength;

                progressRef.current.textContent = `${Math.round((loaded / contentLength) * 100)}%`;

                controller.enqueue(value);
                return pump();
              });
            }
          }
        });

        return new Response(stream);
      })
      .then(response => response.json())
      .then(data => console.log(data));

ファイルサイズ

ファイルサイズが大きい場合は、ユーザーにダウンロードを止める手段を提供しないといけない。AbortControllerabort()関数を使えば、「止める」ボタンなどが簡単に実装できる。

さらに、navigator.connection.effectiveTypeでユーザーの通信環境がわかるので、取得前に途中プログレスを表示するか、しないかの判断もできる。長くなりそうな場合だけプログレスを表示する選択肢ができる。

その他

サーバーと通信方式によってContent-Lengthヘッダーが欠けている場合も考えられる。たとえば、HTTP/1.1でTransfer-Encoding: chunkedを使うと、Content-Lengthヘッダーがない。その場合、自分で合計のバイト数をあらかじめ確認して定数にするか、処理をあきらめるなどの対策が必要になる。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした