ファイルアップロードの進捗率をJavaScriptで取る方法

最近のサムネイルは全部ChatGPTさんに作ってもらってます

この記事を三行にまとめると

EventSourceを使って進捗の続きを取得する
ぶっちゃけsetIntervalでもいいと思います
AIにお願いすれば良い感じにコードを書いてくれるはず
ファイルをアップロードするとき、JavaScriptで進捗状況などを出したいことがあると思います。XMLHttpRequestのonprogressを使えば一応それ的なことは実現できるのですが、onprogressはローカルからウェブサーバーへのアップロードまでしか追うことができないので、ウェブサーバーからさらに外部のサーバーへアップロードする場合、そこの進捗は取れません。

そこでかなり強引なやり方にはなってしまうのですが、XMLHttpRequestのuploadが終わったら今度はEventSourceを使って進捗の続きを取得する方法を試してみたいと思います。



XMLHttpRequestのonprogress

まずはXMLHttpRequestのonprogressで進捗を取ってみます。

xhr = new XMLHttpRequest();
xhr.timeout = 0;
xhr.open('post', 'アップロードするURL', true);

// 現在の進捗率を取得
xhr.upload.onprogress = function(e) {
  if(e.lengthComputable) {
    percent = Math.floor((e.loaded / e.total) * 50);
    console.log('現在の進捗率は:' + percent + '%');
  }
}

// アップロード完了
xhr.onload = function(e) {}

// データをPOST
formData = new FormData(document.querySelector('form'));
xhr.send(formData);

いろいろ省略して書いたすごいざっくりとしたコードですが、大事なのはxhr.upload.onprogressの部分です。ここで進捗率を取ります。ファイルのアップロードが始まるとここにe.loadedやe.totalで現在の送信データ量と総データ量が取れるので、これをパーセントを出すことができます。

しかし先ほども言ったようにここで取れるのはローカルからウェブサーバーへのアップロードまで。そこで処理が完結するならここだけで良いのですが、今回はそこからさらに別の場所にアップロードするのでpercentは最大で50%になるようにしています。



EventSourceで残りの50%を取得

XMLHttpRequestにはアップロードが完了したときに発動するonloadendというイベントがあるので、onprogressの処理が完了した後はこの中で残りの50%〜100%の進捗を取得します。

xhr.upload.onloadend = function(e) {
  source = new EventSource('進捗状況取得URL');
  source.onmessage = function(e) {
    $data = JSON.parse(e.data);

    percent = $data.percent;
    console.log('現在の進捗率は:' + percent + '%');
	
    if(percent >= 100) {
      source.close();
    }
  }
}

こんな感じです。ちょっと語弊があるかもしれませんが、EventSourceは簡単に言えばsetIntervalで定期的にURLにアクセスするのと似たような動きを実現できるものです。setIntervalのように一定の間隔でリクエスト先のURLからデータを受け取ってくれます。じゃあsetIntervalでええんやないのって言いたくもなりますが……まあ、ぶっちゃけsetIntervalでもいいと思います。

ちなみにEventSourceは接続を止めないと延々とデータを受信し続けるので、setIntervalにおけるclearIntervalのような感じで進捗が100になったらclose()で接続を終了する必要があります。じゃあやっぱりsetIntervalでええんやないのって言いたくもなりますが……まあ、ぶっちゃk(以下略)

一応EventSourceとsetIntervalの主な違いについてChatGPTさんにたずねたところ、このような回答をいただきました。

■ EventSource(SSE)
リクエスト:最初の一回だけ
サーバーの負荷:低い
リアルタイム性:ほぼリアルタイム
実装難易度:サーバー側にSSE対応が必要

■ setInterval(ポーリング)
リクエスト:間隔ごとに発生
サーバーの負荷:高い
リアルタイム性:ポーリング間隔に依存
実装難易度:簡単

つまり実装が簡単なのはsetIntervalの方だけどインターバルごとにリクエストが発生するので、サーバーの負荷とかを考慮すると無難なのはEventSourceみたいですね。今回のようなファイルアップロードの進捗とかリアルタイムチャットのような機能を実装するときはEventSourceの方が適しているって感じです。

実装難易度のところにSSE対応が必要と書いてありますが、そんなに難しいことはないです。ようはJSONデータを適切にJavaScript側に送れれば良いので、例えばこんな感じでOKです。

// JavaScript
xhr.upload.onloadend = function(e) {
  source = new EventSource('/progress.php');
  source.onmessage = function(e) {
    $data = JSON.parse(e.data);

    percent = $data.percent;
    console.log('現在の進捗率は:' + percent + '%');
	
    if(percent >= 100) {
      source.close();
    }
  }
}

// progress.php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
echo 'data: '.json_encode(['percent' => 80])."\n\n";
ob_flush();
flush();
exit;

すごいざっくりと書いちゃいましたけど、これでコンソール画面に「現在の進捗率は80%」と出ます。今は進捗率を直で書いてますけど、ここにファイルの転送状況を算出する処理を入れ込めば少しずつ進捗率が増えていきます。例えばAWSのS3へのアップロードならMultiPart Uploadという機能を使えば現在の転送状況を取得しながらアップロードできるので、DBやElastiCacheなどに書き込んでそれをprogress.phpで読み取ればOKです。

今回はEventSourceの紹介がメインなのでMultipart Uploadについては割愛しますが、AIに「AWSのS3にマルチアップロードでファイルをアップし、その進捗状況をElastiCacheに保存してSSE側で取得したい」みたいにお願いすれば良い感じにコードを書いてくれるはずです。お試しあれ。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください