progress要素を使ったロードの状態の管理

HTML, JavaScript

HTML には、プログレスバー向けに progress 要素が存在し、タスクの進行状況を管理できる。

HTML LS より progress 要素の説明
Can I use
Polyfill

ロードの種類

ローディングアニメーションには、目的地が決まっている Determinate (確定型)と、
同じ動きを繰り返し行う Indeterminate (不確定型)の 2 種類が存在する。
このうちプログレスバーは、Determinateに属するもの。
クルクル回るスピナーなどは Indeterminate にあたる。

非progress要素

progress 要素を使わず、divなどでプログレスバーを表現する場合。

html

<div id="progresslabel" class="opacity0">ロードの進捗状況</div>
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-labelledby="progresslabel" ></div>

上記のように、progressbarロールと aria-value の値を設定する。
(その他、ロード中の場所への aria-busy の設定なども必要だが、割愛する)

progress要素

progress要素。

html

<label for="progressbar">ロードの進捗状況</label>
<progress id="progressbar" max="100" value="10">10%</progress>

上記のように、最大値の max と現在値の value を設定する。minはない。
この要素ではブラウザベンダーの提供するプログレスバーが表示され、value の値に応じてゲージが増加する。

ブラウザベンダーが提供するデフォルトのウィジェットが表示される

子要素には value と同じ値を入れることが推奨される。
そうすることで、レガシーUA にとっても認識可能な値となるため。

CSSでの装飾

プログレスバーは Form 系の要素と同じく、appearance などで装飾される。
これを CSS で上書きできるけど、結構カオス。
主にバー本体とゲージの色を設定できるが、ブラウザ間で統一されていない。

特に Chrome と Firefox では barvalue の解釈が異なっている。
さらに、IE ではゲージ色の変更に、background-colorではなく color を用いている。

css

#progress[value] {
  appearance: none;
  background-color: #fff;
  border: 1px solid #eee;
  border-radius: 2px;
  color: #63cf0c; /* IE */
  height: 10px;
}
/* chrome */
#progress[value]::-webkit-progress-bar {
  background-color: #fff;
}
#progress[value]::-webkit-progress-value {
  background-color: #63cf0c;
}
/* firefox */
#progress[value]::-moz-progress-bar {
  background-color: #63cf0c;
}

The HTML5 progress Element | CSS-Tricks

valueの省略

progress 要素は通常 Determinate だが、 value属性を省略すると、Indeterminate としても使用できる。
ただ、iOS Safari では空表示と同じ扱いになる。

html

<label for="progressbar">いつ終わるかわからないやつ</label>
<progress id="progressbar">Loading</progress>

progress要素の使用

Fetch API と Firebase Cloud Storage、2 つの場面で試す。

Fetch APIを使う時

Fetch API には XMLHttpRequest のような progress イベントがないけど、IE を捨てれば Streams API で近いものを再現できる。

有無を言わさず 2GB の dat ファイルを読み込むサーバーがあり、これを Fetch API で取得するケース。
content-lengthを後で使用するので、レスポンスヘッダーに追加する。

node

const express = require('express');
const fs = require('fs');

const app = express();

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "http://localhost:8080");
  res.header("Access-Control-Expose-Headers", "content-length");
  next();
});

app.listen(3000);

app.get('/test', function(req, res, next) {
  const data = fs.readFileSync('./dummy.dat');
  res.send(data);
});

Fetch API は、body に ReadableStream を返す。
この ReadableStream には read メソッドがあり、donevalue の Promise を返す。
その中の value はバイナリデータとして、Uint8Array 型で格納される。

読み込んだバイト数は value.byteLength で取得できる。
最終的にはバイト数が response.headers.get('content-length') と同じになるので、そこから全体の割合を算出し、progress 要素の value に代入する。

readメソッドの done は false を返すが、読み込みが完了した時に true になる。
要はこれが true になるまで、処理を再帰的に呼び出し続けるだけ。

Fetch での Stream を用いたプログレス取得とキャンセル | blog.jxck.io
JavaScript の Streams API で細切れのデータを読み書きする - Subterranean Flower Blog

ts

function fetchItem() {
  fetch('http://localhost:3000/test').then(res => {
    if (!res.ok || !res.body || !res.headers.get('content-length')) return;

    // 合計バイト数
    const total = res.headers.get('content-length') as string;

    // 読み込みバイト数の初期状態
    let loaded = 0;

    const reader = res.body.getReader();

    function readChunk(readerResult: ReadableStreamReadResult<Uint8Array>) {
      if (readerResult.done) {
        completeFetchItem(); // ← doneがtrueになった時の処理
        return;
      }
      loaded += readerResult.value.byteLength;
      setProgressVal(Math.round((loaded / parseInt(total)) * 100));
      reader.read().then(readChunk);
    }
    reader.read().then(readChunk);
  });
}

// progress要素をいじる処理
function setProgressVal(percentage: number) {
  progressbar.setAttribute('value', String(percentage));
  progressLabel.innerText = `${percentage}%`;
}

2GB の巨大サイズなので、じわじわ読み込んでいく。

progress要素で読み込み状況を表示している様子

長い場合は "%" を表示しておくことで、待ち時間への不安を軽減できる。
ユースケースによっては、中止するボタンがあったりする。

progress要素の読み込み状況を表示中にキャンセルしている様子

Firebase Cloud Storageにアップロードする時

Firebase Cloud Storage では、state_changed から snapshot を取得できる。
その中に bytesTransferred (転送バイト数)と totalBytes(全体のバイト数)があるので、Fetch の例と同じように割合を出すだけ。

ウェブでファイルをアップロードする - Firebase

ts

const firebaseStorage = firebase.storage();

function uploadPict(dataUrl: string, fileName: string) {
  const storageRef = firebaseStorage.ref(fileName);
  const task = storageRef.putString(dataUrl, 'data_url');
  task.on(firebase.storage.TaskEvent.STATE_CHANGED, snapshot => {
    setProgressVal((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
  });
}

Firebase Cloud Storageにアップロードしている。読み込みアニメーションがぎこちない

先ほどの大容量ファイルと違って、数 MB 程度のファイルで常に監視すると、
上動画のようにぎこちない動きになってしまう。
その場合はある程度読み込ませたところでアニメーションを止めたり、段階的に動かしたりしてごまかす。

Firebase Cloud Storageにアップロードしている

まとめ

  • Determinate(確定的)と Indeterminate(不確定)な Loading アニメーションがある
  • Determinate なプログレスバーには progress 要素が使える
  • CSS いじりは辛い
  • 読み込んだバイト数 / 全体バイト数 * 100 で割合を出して value 属性に入れていく
  • Fetch API では Streams API から読み込み情報を得られる
  • Firebase Cloud Storage は observer から読み込み情報を得られる