progress要素を使ったロードの状態の管理
HTML, JavaScriptHTML には、プログレスバー向けに 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 では bar
と value
の解釈が異なっている。
さらに、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
メソッドがあり、done
と value
の 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 の巨大サイズなので、じわじわ読み込んでいく。
長い場合は "%" を表示しておくことで、待ち時間への不安を軽減できる。
ユースケースによっては、中止するボタンがあったりする。
Firebase Cloud Storageにアップロードする時
Firebase Cloud Storage では、state_changed
から snapshot を取得できる。
その中に bytesTransferred
(転送バイト数)と totalBytes
(全体のバイト数)があるので、Fetch の例と同じように割合を出すだけ。
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);
});
}
先ほどの大容量ファイルと違って、数 MB 程度のファイルで常に監視すると、
上動画のようにぎこちない動きになってしまう。
その場合はある程度読み込ませたところでアニメーションを止めたり、段階的に動かしたりしてごまかす。
まとめ
- Determinate(確定的)と Indeterminate(不確定)な Loading アニメーションがある
- Determinate なプログレスバーには progress 要素が使える
- CSS いじりは辛い
- 読み込んだバイト数 / 全体バイト数 * 100 で割合を出して value 属性に入れていく
- Fetch API では Streams API から読み込み情報を得られる
- Firebase Cloud Storage は observer から読み込み情報を得られる