XMLHttpRequest でファイルをアップロードする方法
この記事では XMLHttpRequest と FormData を使って、ファイルをアップロードする方法を説明します。
また Bootstrap を使って次のようなアップロードフォームを作成する方法も紹介します。
Fetch API などでもファイルアップロードは可能ですが、現時点では fetch() にはアップロードの進捗状況のチェックイベントがありません。 このため進捗状況のチェックをしたいときは fetch() は使えません。この点で XMLHttpRequest を使う方法は重宝します。
XMLHttpRequest によるファイルアップロードの概要
XMLHttpRequest を使うと JavaScript から、非同期でサーバーと通信を行うことが可能になります。
非同期処理というのは、処理を開始したら、その場で処理の終了を待たずに、なんらかの方法で後で処理が終了したことの通知を受けるような仕組みです。
ファイルのアップロードは、ネットワークの状況や、アップロードするファイルの数やサイズによって、 どのくらい時間がわかりません。このため、ファイルのアップロード処理は、同期処理で行うより、非同期で行う方が望ましいです。
file 型の input 要素でファイルを選択する
ファイルを選択するには、 file 型の input 要素を使います。
<form id="f1">
<input type="file" id="file1" name="file1">
</form>
file 型の input 要素に change イベントを設定すると、 ファイルの選択状態が変わった時に、イベントハンドラが呼び出されます。
このときに、input の files プロパティに、選択されたファイルの情報が設定された、 File オブジェクトの配列が設定されます。
const f = document.getElementById('file1');
f.addEventListener('change', (evt) => {
const fileIn = evt.target;
// fileIn.files[0]
// fileIn.files[1]
// ...
});
ひとつだけファイルを選択した場合は、files[0] がそのファイルに対する File オブジェクトです。
このファイルオブジェクトは JavaScript で取り置いておけば、インプット要素で選択するファイルを変更しても、取り置いたファイルオブジェクトは有効です。
アップロードファイルは FormData に詰め込む
アップロードするファイルの File オブジェクトを取得したら、それを FormData オブジェクトにセットします。
もしお行儀よく、インプット要素が form 要素の子要素として作成されていれば、FormData の作成は簡単です。
フォーム要素を FormData に渡した上で FormData オブジェクトを作成すれば、自動的に FormData オブジェクトにセットされます。
const form = document.getElementById('f1');
const fd = new FormData(form);
「FormData の使い方」 も参考にしてください。
また、手動で FormData オブジェクトにファイルオブジェクトを設定しても構いません。
このときはインプット要素でファイルを選択した結果作成された File オブジェクトを、JavaScript でどこかに保存しておいて後から FormData オブジェクトにセットするか、 あるいはアップロード用のデータを Blob として作成してそれを FormData にセットすることができます。
アップロードの進行状況は upload プロパティへイベント設定で
アップロードの進行状況を確認するにはどうしたらよいでしょうか?
アップロードの状況は、XMLHttpRequest オブジェクトに紐付く XMLHttpRequestUpload オブジェクトで取得できます。
XMLHttpRequestUpload オブジェクトは、XMLHttpRequest オブジェクトの upload プロパティにセットされています。このオブジェクトにイベントリスナーを追加することで、アップロードの開始、進行状況のチェック、エラー発生、アップロード終了などのイベントを処理することができます。
xhr.open("POST", "/test1");
...
// アップロード関連イベント
xhr.upload.addEventListener('loadstart', (evt) => {
// アップロード開始
});
xhr.upload.addEventListener('progress', (evt) => {
// アップロード進行パーセント
let percent = (evt.loaded / evt.total * 100).toFixed(1);
console.log(`++ xhr.upload: progress ${percent}%`);
});
xhr.upload.addEventListener('abort', (evt) => {
// アップロード中断
console.log('++ xhr.upload: abort (Upload aborted)');
});
xhr.upload.addEventListener('error', (evt) => {
// アップロードエラー
console.log('++ xhr.upload: error (Upload failed)');
});
xhr.upload.addEventListener('load', (evt) => {
// アップロード正常終了
console.log('++ xhr.upload: load (Upload Completed Successfully)');
});
xhr.upload.addEventListener('timeout', (evt) => {
// アップロードタイムアウト
console.log('++ xhr.upload: timeout');
});
xhr.upload.addEventListener('loadend', (evt) => {
// アップロード終了 (エラー・正常終了両方)
console.log('++ xhr.upload: loadend (Upload Finished)');
});
xhr.send(fd);
以上のように、一連のイベントをセットした後で、XHR オブジェクトの send() メソッドで、 FormData オブジェクトを POST すればアップロードできます。
全体のコードサンプルはこの後の例をみてください。
XMLHttpRequest による基本的なファイルアップロード・サンプル
概要は上で説明した通りです。ここではいくつか具体的なサンプルを紹介します。
【サーバー側】アップロードファイルを受け取るサーバー側のコードサンプル (Node+Express)
まず、アップロードされたファイルを受け取る側の、サーバー側のコード・サンプルです。Node.js 上の Express を使っています。
アップロードのテストをしやすいように、受け取ったファイルは直ちに削除しています。
const express = require('express');
const multipart = require('connect-multiparty');
const app = express();
const path = require('path');
const fs = require('fs');
app.use(multipart());
app.use(express.static(path.join(__dirname, './public_html')));
app.post('/test1', (req, res) => {
try {
console.log('/test1');
console.log(req.headers['content-type']);
console.log('* body');
console.log(req.body);
console.log('* files');
console.log(req.files);
if (req.files) {
let i = 1;
while (true) {
let f = req.files[`file${i++}`];
if (!f) {
break;
}
console.log(f);
fs.unlink(f.path, (err) => {
if (err) {
return;
}
console.log('*** The file was deleted.');
});
}
}
const obj = {
message: 'Hello from server!'
};
res.status(200).json(obj);
} catch (e) {
console.log('*** catch');
console.log(e);
res.status(500).json(e);
}
});
app.listen(3001, () => console.log('Listeninig on port 3001...'));
これを server.js として、 Node で実行します。
% node server.js
この下で作成する test1.html、test1.js、test2.html、・・・などのファイルは、 public_html という名前のフォルダを作って、その中に作成してください。
ファイルとフォルダの配置は次のように想定しています。
+ node_modules + public_html - test1.html - test1.js - test2.html - test2.js - test3.html - test3.js - package-lock.json - package.json - server.js
特に Express でないとアップロードがうまくいかない、ということはありませんので、サーバー側のコードは好きな物を使ってください。
単一のファイルをアップロードするサンプル
ここではシンプルなファイル選択インプット要素と、アップロードボタンと、クリアボタンを作ります。
を押すとファイルの選択が解除されます。 を押すとファイルがアップロードされます。
test1.html として次を作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Test foo</title>
<script src="test1.js"></script>
</head>
<body>
<form id="f1">
<input type="file" id="file1" name="file1">
</form>
<div>
<button type="button" id="clearButton">Clear</button>
<button type="button" id="b1">Upload</button>
</div>
</body>
</html>
test1.js として次を作成します。
ここで document.getElementById の代わりとして、$ という文字を使っています。jQuery の $ ではありませんので注意してください。
'use strict';
const $ = (id) => document.getElementById(id);
window.addEventListener('load', () => {
$('b1').addEventListener('click', (evt) => {
evt.preventDefault();
if (!$('file1').value) {
console.log('No file is selected.');
return;
}
const fd = new FormData($('f1'));
const xhr = new XMLHttpRequest();
xhr.open("POST", "/test1");
// Basic Events
xhr.addEventListener('load', (evt) => {
console.log('** xhr: load');
let response = JSON.parse(xhr.responseText);
console.log(response);
});
xhr.addEventListener('progress', (evt) => {
console.log('** xhr: progress');
});
xhr.addEventListener('error', (evt) => {
console.log('** xhr: error');
});
// Upload Events
xhr.upload.addEventListener('loadstart', (evt) => {
console.log('++ xhr.upload: loadstart');
});
xhr.upload.addEventListener('progress', (evt) => {
let percent = (evt.loaded / evt.total * 100).toFixed(1);
console.log(`++ xhr.upload: progress ${percent}%`);
});
xhr.upload.addEventListener('abort', (evt) => {
console.log('++ xhr.upload: abort (Upload aborted)');
});
xhr.upload.addEventListener('error', (evt) => {
console.log('++ xhr.upload: error (Upload failed)');
});
xhr.upload.addEventListener('load', (evt) => {
console.log('++ xhr.upload: load (Upload Completed Successfully)');
});
xhr.upload.addEventListener('timeout', (evt) => {
console.log('++ xhr.upload: timeout');
});
xhr.upload.addEventListener('loadend', (evt) => {
console.log('++ xhr.upload: loadend (Upload Finished)');
});
xhr.send(fd);
});
$('clearButton').addEventListener('click', (evt) => {
console.log('** clear button clicked');
evt.preventDefault();
const file1 = $('file1');
file1.value = file1.defaultValue;
file1.dispatchEvent(new Event('change'));
});
});
ファイル・インプット要素の選択を解除するには、インプット要素の value に defaultValue をセットします。
処理の成否は表に表示しないので、F12 でブラウザのデバッガを起動して、コンソールで確認してください。
ファイルサイズの大きなファイルをアップロードすれば、アップロードの進行状況が表示されるはずです。
ファイルサイズが小さいといきなり 100% 完了となりますので、テストのとき気をつけてください。
複数のファイルをアップロードするサンプル
次の例では複数のファイルを選択して、一度にアップロードします。
test2.html として次を作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Test foo</title>
<script src="test2.js"></script>
</head>
<body>
<form id="f1">
<input type="file" id="file1" name="file1" multiple>
</form>
<div>
<button type="button" id="clearButton">Clear</button>
<button type="button" id="b1">Upload</button>
</div>
<ul id="fileList">
</ul>
</body>
</html>
test2.js として次を作成します。
'use strict';
const $ = (id) => document.getElementById(id);
const selected_files = [];
window.addEventListener('load', () => {
$('b1').addEventListener('click', (evt) => {
evt.preventDefault();
if (selected_files.length == 0) {
console.log('No file is selected.');
return;
}
const fd = new FormData();
for (let i = 0; i < selected_files.length; i++) {
const f = selected_files[i];
fd.append(`file${(i+1)}`, f, f.name);
}
const xhr = new XMLHttpRequest();
xhr.open("POST", "/test1");
// Basic Events
xhr.addEventListener('load', (evt) => {
console.log('** xhr: load');
let response = JSON.parse(xhr.responseText);
console.log(response);
});
xhr.addEventListener('progress', (evt) => {
console.log('** xhr: progress');
});
xhr.addEventListener('error', (evt) => {
console.log('** xhr: error');
});
// Upload Events
xhr.upload.addEventListener('loadstart', (evt) => {
console.log('++ xhr.upload: loadstart');
});
xhr.upload.addEventListener('progress', (evt) => {
let percent = (evt.loaded / evt.total * 100).toFixed(1);
console.log(`++ xhr.upload: progress ${percent}%`);
});
xhr.upload.addEventListener('abort', (evt) => {
console.log('++ xhr.upload: abort (Upload aborted)');
});
xhr.upload.addEventListener('error', (evt) => {
console.log('++ xhr.upload: error (Upload failed)');
});
xhr.upload.addEventListener('load', (evt) => {
console.log('++ xhr.upload: load (Upload Completed Successfully)');
});
xhr.upload.addEventListener('timeout', (evt) => {
console.log('++ xhr.upload: timeout');
});
xhr.upload.addEventListener('loadend', (evt) => {
console.log('++ xhr.upload: loadend (Upload Finished)');
});
xhr.send(fd);
});
$('file1').addEventListener('change', (evt) => {
const fileIn = evt.target;
if (!fileIn.value) {
return;
}
for(let f of fileIn.files) {
selected_files.push(f);
}
updateFileList();
fileIn.value = fileIn.defaultValue;
});
$('clearButton').addEventListener('click', (evt) => {
evt.preventDefault();
console.log('** clear button clicked');
selected_files.length = 0;
updateFileList();
});
function updateFileList() {
const fl = $('fileList');
fl.innerHTML = ''; // remove all children
for(let f of selected_files){
const fn = f.name;
const li = document.createElement('li');
li.innerHTML = fn;
fl.appendChild(li);
}
}
});
ここではファイルインプット入力フィールドで拾ってきた File オブジェクトを、selected_files という配列に取り置いています。 アップロードボタンを押した時に、そこから File オブジェクトをとってきて、FormData に詰め込んでアップロードしています。
Bootstrap を使って UI を整えたファイルアップロードフォームの例
上の二つのサンプルでは見栄えが悪いので、Bootstrap を利用してアップロードフォームを作成します。
さらにアップロードの進行状況をプログレスバーで表示し、直感的に完了した割合がわかるようにします。
Bootstrap には custom-file、custom-file-input などの、 カスタム CSS クラスが既に用意されています。これを利用するだけできれいな入力フォームになります。
test3.html として次を作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Test foo</title>
<script src="test3.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<form id="f1">
<div class="input-group mt-2">
<div class="input-group-prepend">
<span class="input-group-text" id="">File 1</span>
</div>
<div class="custom-file">
<input type="file" id="file1" name="file1"
lass="custom-file-input">
<label for="file1" id="file1Label"
class="custom-file-label"></label>
</div>
<div class="input-group-append">
<button type="button" id="clearButton" class="btn btn-secondary">Clear</button>
</div>
</div>
</form>
<div id="pbc1" style="visibility: hidden;" class="progress mt-1">
<div id="pb1" class="progress-bar bg-success" role="progressbar"></div>
</div>
<p class="mt-2">
<button type="button" id="b1" class="btn btn-primary">Upload</button>
</p>
</div>
</body>
</html>
test3.js として次を作成します。
上二つのサンプルとこのサンプルでは、見た目には随分違いますが、JavaScript のコードとしてはほとんど変わりありません。
'use strict';
const $ = (id) => document.getElementById(id);
const pb = (p) => {
const pb1 = $('pb1');
const pbc1 = $('pbc1');
if (p < 0) {
pbc1.style.visibility = 'hidden';
pb1.style.width = '0%';
} else {
pbc1.style.visibility = 'visible';
pb1.style.width = `${p}%`
}
};
window.addEventListener('load', () => {
$('b1').addEventListener('click', (evt) => {
evt.preventDefault();
if (!$('file1').value) {
console.log('No file is selected.');
return;
}
const fd = new FormData($('f1'));
const xhr = new XMLHttpRequest();
xhr.open("POST", "/test1");
// Basic Events
xhr.addEventListener('load', (evt) => {
console.log('** xhr: load');
let response = JSON.parse(xhr.responseText);
console.log(response);
});
xhr.addEventListener('progress', (evt) => {
console.log('** xhr: progress');
console.log(evt);
});
xhr.addEventListener('error', (evt) => {
console.log('** xhr: error');
console.log(evt);
});
// Upload Events
xhr.upload.addEventListener('loadstart', (evt) => {
console.log('++ xhr.upload: loadstart');
});
xhr.upload.addEventListener('progress', (evt) => {
let percent = (evt.loaded / evt.total * 100).toFixed(1);
console.log(`++ xhr.upload: progress ${percent}%`);
pb(percent);
});
xhr.upload.addEventListener('abort', (evt) => {
console.log('++ xhr.upload: abort (Upload aborted)');
});
xhr.upload.addEventListener('error', (evt) => {
console.log('++ xhr.upload: error (Upload failed)');
});
xhr.upload.addEventListener('load', (evt) => {
console.log('++ xhr.upload: load (Upload Completed Successfully)');
});
xhr.upload.addEventListener('timeout', (evt) => {
console.log('++ xhr.upload: timeout');
});
xhr.upload.addEventListener('loadend', (evt) => {
console.log('++ xhr.upload: loadend (Upload Finished)');
setTimeout(() => pb(-1), 3E3);
});
pb(0);
xhr.send(fd);
});
$("file1").addEventListener("change", (evt) => {
console.log(evt);
const label = $('file1Label');
if (evt.target.files.length == 0) {
label.innerHTML = '';
return;
}
const filename = evt.target.files[0].name;
label.innerHTML = filename;
});
$('clearButton').addEventListener('click', (evt) => {
console.log('** clear button clicked');
evt.preventDefault();
const file1 = $('file1');
file1.value = file1.defaultValue;
file1.dispatchEvent(new Event('change'));
});
});
プログレスバーは、幅 width のパーセントを設定するだけです。
以上、ここでは XMLHttpRequest と FormData を使って、非同期でファイルをアップロードする方法を紹介しました。