プログレッシブファイルアップロード:UXと技術ガイド
ドラッグ&ドロップ、進捗バー、スマートなエラーハンドリングで、ユーザー体験を高めるプログレッシブなファイルアップロードの構築方法を解説します。
プログレッシブなファイルアップロードはすべての段階で即座で信頼できるフィードバックをユーザーに提供する:ファイルをドロップした瞬間の選択確認、アップロード中のリアルなETAを伴うスムーズな進捗バー、失敗時の具体的な再試行オプション、完了後の明確な成功状態と次のアクション。技術的な要素はHTML5の DataTransfer APIによるドラッグアンドドロップ、再開可能なチャンク分割アップロード、バイト精度の進捗のためのReadableStreamを使ったfetchストリーミング、IndexedDBによるタブリロードを生き残る状態管理だ。適切に実装すれば、5GBのファイルをアップロードするユーザーはアプリがフリーズしているか疑わない。
「プログレッシブ」が意味すること
プログレッシブにはこのコンテキストで2つの意味がある。1つ目:プログレッシブエンハンスメント。アップロードが2012年のブラウザでも<input type="file">のPOSTとして機能し、JavaScriptが利用可能な場合にドラッグアンドドロップ、チャンキング、リトライが追加される。2つ目:プログレッシブディスクロージャー。UIは必要なときのみ複雑さを表示—アップロード中はパーセンテージを表示し、失敗時にのみリトライの詳細を表示する。両方の意味が同じ原則を指す:ユーザーが行き詰まることなく、情報なしに待たせない。
避けるべき失敗モードは「スピナー地獄」—進捗、ETA、問題が発生しているかどうかの表示がない一般的な読み込みアニメーション。ユーザーは信頼できないアップロードをキャンセルする。
ブラウザと戦わないドラッグアンドドロップ
HTML5のドラッグアンドドロップAPIはバグで有名だ。許容できるようにするいくつかのルール:
const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragging');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dragging');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragging');
handleFiles([...e.dataTransfer.files]);
});
dragoverでpreventDefaultを呼び出さないとドロップターゲットがドロップを受け付けない。ChromeとFirefoxでディレクトリコンテンツを再帰的にキャプチャするための唯一の方法として、フォルダを受け入れる必要がある場合はe.dataTransfer.filesでなくe.dataTransfer.itemsとwebkitGetAsEntry()を使う。
フォールバックも使いやすくする:スタイル付きの<input type="file" multiple>をラップする表示可能な<label>はキーボードとスクリーンリーダーナビゲーションを含む100%のユーザーに機能する。
ユーザーが信じる進捗の表示
進捗バーが3つの理由で跳び回る:不均一なチャンクサイズ、TCPスロースタート、ネットワークスタックでのバッファリング。2秒の後追い平均でスムーズにする:
const samples = [];
function recordSample(bytes) {
const now = performance.now();
samples.push({ time: now, bytes });
while (samples.length > 1 && now - samples[0].time > 2000) samples.shift();
}
function throughput() {
if (samples.length < 2) return 0;
const delta = samples[samples.length - 1];
const base = samples[0];
return (delta.bytes - base.bytes) / ((delta.time - base.time) / 1000);
}
ETAを(totalBytes - uploadedBytes) / throughput()として計算し、表示を少なくとも5秒にクランプし、「約2分」という人間的な言葉でフォーマットする。パーセンテージとバイトカウンターの両方を表示する(「2.1GBのうち340MB」)—何かがおかしいと感じたとき、ユーザーは2つを照合する。
実行可能な回復のあるエラー状態
一般的な「アップロードに失敗しました」メッセージは信頼を破壊する。失敗を5つのバケツに分類し、それぞれを明確に表示する:
- ネットワーク切断(オフラインイベント、TCPリセット):自動リトライで「再接続中...」
- サーバー5xx:手動リトライボタンで「サーバーエラー、5秒後に再試行」
- サーバー4xx(413大きすぎる、415不正なタイプ):ファイル置き換えで「ファイル拒否:大きすぎます」
- 認証期限切れ(401、403):「セッション期限切れ、続けるにはサインイン」
- クライアントクラッシュ(JSエラー、ブラウザがタブを終了):リロード時のIndexedDBからの回復
メッセージと解決する1つのアクションを組み合わせる。ユーザーがオフラインの場合、navigator.onLineとonlineイベントで監視したオンライン/オフライン状態を表示する。
Fetchストリームによるバイト追跡
XMLHttpRequest.upload.onprogressはアップロード進捗を追跡する従来の方法だが、HTTP/3では不安定でsendバッファにキューされたバイトを見逃す。モダンなアプローチはReadableStreamでバイトが生成される際にカウントする:
function trackedStream(blob, onBytes) {
let sent = 0;
return new ReadableStream({
async pull(controller) {
const reader = blob.stream().getReader();
while (true) {
const { done, value } = await reader.read();
if (done) { controller.close(); return; }
sent += value.byteLength;
onBytes(sent);
controller.enqueue(value);
}
}
});
}
duplex: 'half'付きでfetchのbodyとしてストリームを渡す。Safariのリクエストストリームサポートは17.4でリリースされた。それ以前はXMLHttpRequestにフォールバックする。
一時停止、再開、キャンセル
1分以上かかるものには一時停止ボタンが期待される。チャンク分割アップロードで一時停止は「新しいチャンクのディスパッチを停止する」だけで、再開は作業キューが残ったところから続く。キャンセルはAbortControllerを使う:
const ctrl = new AbortController();
cancelButton.onclick = () => ctrl.abort();
await fetch(url, { method: 'PUT', body: blob, signal: ctrl.signal });
中断時にクリーンアップ:サーバーのアップロードセッションをDELETEしてストレージのリークを防ぎ、IndexedDBエントリをクリアし、初期状態に戻る。一時停止は状態を保持し、キャンセルは破棄する。この区別をUIで明確にする。
HexaTransferのアップローダーはこのプログレッシブパターン—プレーンフォームフォールバック、ドラッグアンドドロップ強化、fetchストリーミング、IndexedDB対応の再開、具体的なエラー回復—を使用している。詳細は https://hexatransfer.com で。無料、アカウント不要、最大10GB。
エンドツーエンド暗号化で大容量ファイルを安全に送信
エンドツーエンド暗号化で最大10GBのファイルを無料で転送。アカウント不要。ファイルはアップロード前にブラウザで暗号化されるため、他の誰にも読まれません。
ファイルを送信