「フォームの作り方は慣れてきたけどファイル添付だけ作れない、作ったことない」
「ファイルアップロードは作れるけど2つ以上の添付のやり方がわからない」
「fetchメソッドの引数でURL以外の項目が何を書いて良いのかわかっていない」
本日はそんな方に向けてJavaScriptでのファイルアップロードの作り方を解説します。
この記事では、Requestオブジェクト、Headers、Response、fetch関数、そしてFileListなど、JavaScriptでファイルアップロードを行うための主要な概念と手法に焦点を当てます。どのようにしてこれらの要素を組み合わせ、効果的に利用するかについて解説していきましょう。
ファイルアップロードはユーザーがコンテンツを共有し、ウェブアプリケーションがより対話的でパワフルなものになる際の重要なステップで実装するのが当たり前になりました。
初心者の方もスルーできない内容なので、この機会にJavaScriptを使ったファイルアップロードの基本から実践的な応用までを順を追って練習しましょう。
また動画もあるので必要に応じて活用してください。
JavaScriptを使ったファイル添付〜アップロードの作り方【単体】
まずは基本となる単体のファイル添付についてです。
そもそもフォームにおいてファイル添付はinputタグのtype属性をfileにすることで専用のボタンを画面に表示することができます。
クリックするとMacであればFinder、Windowsであればエクスプローラーが展開されてユーザーが添付したいファイルを選択することができます。
選択したファイルはfilesという配列のプロパティで取得することができます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<!-- <script src="./script.js" defer></script> -->
<script src="./main.js" defer></script>
</head>
<body>
<form name="form" class="form" action="#">
<label for="input">ファイル添付</label>
<input type="file" class="input" />
</form>
</body>
</html>
const input = document.querySelector(".input");
input.addEventListener("change", (e) => {
let files = input.files;
console.log(files);
});
FileListという配列になっていることがわかります。
インデックス番号0番目にオブジェクト形式で選択したファイルが格納されています。
こちら配列になっているのがポイントで、以下のようにすればファイル添付が成功したかを検証することもできます。
const input = document.querySelector(".input");
input.addEventListener("change", (e) => {
let files = input.files;
// ここを変更
if (files.length > 0) {
console.log(files);
} else {
console.log("選択されていません");
}
});
配列の要素が1個以上なければファイル添付がされてないということになりますね。
後で解説しますが複数のファイル添付の際にはインデックス番号が順番に振られてオブジェクト形式で格納されます。
ファイル1個のオブジェクトにはプロパティが用意されていて「ファイル名」「ファイルの種類」「最終更新日」「ファイルサイズ」を確認することができます。
また配列なのでインデックス番号を指定するのを忘れないようにしましょう。
const input = document.querySelector(".input");
input.addEventListener("change", (e) => {
let files = input.files;
if (files.length > 0) {
console.log(files);
// ここを追加
console.log("content-type", files[0].type);
console.log("content-length", files[0].size);
} else {
console.log("選択されていません");
}
});
なんとなくファイル添付の仕組みがわかってきたところで、実務でありがちなアップロードをやってみます。
今回はjsonplaceholderというテスト送信が可能なAPIを使用して、パソコンにあるjsonデータをfetchメソッドで送ってみましょう。
実際にはURLは実際には送信できませんがコピペしてください。
const input = document.querySelector(".input");
input.addEventListener("change", (e) => {
let files = input.files;
if (files.length > 0) {
// ここを変更
let url = "https://jsonplaceholder.typicode.com/posts";
let header = new Headers();
header.append("content-type", files[0].type);
header.append("content-length", files[0].size);
let req = new Request(url, {
body: files[0],
headers: header,
method: "POST",
// CORSエラー回避
mode: "no-cors",
});
fetch(req)
.then((res) => console.log(res.status, res.statusText))
.catch(console.warn);
} else {
console.log("選択されていません");
}
});
アップロードするときのfetchメソッドの引数は以上3点、body(データ)とmethod(命令)とheaders(ヘッダー)です。
それらはRequest()というクラスをインスタンス化するとオブジェクトとして3点をまとめることができます。
例えば、Requestクラスのbodyプロパティにfiles[0]を指定すれば先ほどのオブジェクト形式を選択したことになります。
またmethodプロパティはアップロードなのでPOSTを指定します。
headersプロパティは先ほどコンソールで確認した「ファイルサイズ」「ファイルの種類」にしてみました。
Headers()というクラスをインスタンス化してappendメソッドで好きな項目をヘッダーに含めることができるので、files[0].typeとfiles[0].sizeを指定しています。
最後にmodeというプロパティに”no-cors”を指定しましたが、こちらはCORSエラーの回避するものです。
fetchメソッドでstatusとstatusTextを返すように指定したので上図のような結果になっているかと思います。
特にエラーが表示されていなければOKです。
jsonデータでない画像などを指定するとエラーが表示されますので添付する形式も確認して試してください。
JavaScriptを使ったファイル添付〜アップロードの作り方【複数】
続いて2個以上のファイルを添付したいときの実装方法です。
データの取得は単体と同じでFileListという配列にインデックス番号で分けられたオブジェクト形式で格納されます。
単体のときは0番目しか使わなかったのを1番目、2番目とファイル数に応じてインデックス番号が増えていく感じです。
またHTML側のinputタグにはmultipleという属性を追加しておくことが必要です。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<!-- <script src="./script.js" defer></script> -->
<script src="./main.js" defer></script>
</head>
<body>
<form name="form" class="form" action="#">
<label for="input">ファイル添付</label>
<!-- ここを修正 -->
<input multiple type="file" class="input" />
</form>
</body>
</html>
ついでにお話しておくとacceptという属性もあって、こちらで添付できるファイル形式を限定させることも可能です。
今回もjsonファイルを使用したいのでaccept=”.json”と書いてみます。
ファイル添付する際に画像データなどが選択できないようになっているはずです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<!-- <script src="./script.js" defer></script> -->
<script src="./main.js" defer></script>
</head>
<body>
<form name="form" class="form" action="#">
<label for="input">ファイル添付</label>
<!-- ここを修正 -->
<input accept=".json" multiple type="file" class="input" />
</form>
</body>
</html>
それでは一度2個のjsonファイルを選択して添付してみましょう。
コンソールには以下のように表示されるかと思います。
const input = document.querySelector(".input");
input.addEventListener("change", () => {
let files = input.files;
if (files.length > 0) {
console.log(files);
} else {
console.log("選択されていません");
}
});
単体のときと違ってオブジェクトが2個あるのがわかりますね。
どっちがどっちかというのはnameプロパティにファイル名があるのでnameで判断できます。
アップロード機能については先ほどの単体の時を同じなので以下のように書くことで複数添付の送信が可能です。
1点だけ違うのが複数添付ではヘッダーを書くことができないので、FormDataクラスをインスタンス化してFormDataクラスが自動で生成するヘッダーが使用されることになります。
またRequest()のbodyにはデータ本体(files[0]など)を書くと前章で話しましたが、こちらにはFormDataをインスタンス化した変数を入れます。
const input = document.querySelector(".input");
input.addEventListener("change", () => {
let files = input.files;
if (files.length > 0) {
// ここを追加
let url = "https://jsonplaceholder.typicode.com/posts";
let formData = new FormData();
let req = new Request(url, {
body: formData,
method: "POST",
// CORSエラー回避
mode: "no-cors",
});
fetch(req)
.then((res) => console.log(res.status, res.statusText))
.catch(console.warn);
} else {
console.log("選択されていません");
}
});
今回も上図のような結果になるはずです。
1つでも2つでも大枠の仕組みは同じなので、どちらのパターンも練習しておくことをお勧めします。
またサーバーやメールに実際に送信するにはバックエンドの知識が必要になります。
実務では担当が別であることが多いので初心者の方は本記事の内容をまずは理解することを優先すると良いでしょう。
JavaScriptでアップロードした時のレスポンスについて
ここまでアップロード機能の作り方を解説してきましたが、何かしらデータを投稿したときはレスポンスが返ってきていることを知っていますか?
冷静に考えてみると送信が毎回成功するとは限りませんよね。
何かの理由で正常に送信できなかったとき、ユーザーや開発者が知る必要がありレスポンスというオブジェクトを持ってJavaScriptは知らせてくれます。
例えば先ほどのコードを一部修正してみましょう。
const input = document.querySelector(".input");
input.addEventListener("change", () => {
let files = input.files;
if (files.length > 0) {
let url = "https://jsonplaceholder.typicode.com/posts";
let formData = new FormData();
let req = new Request(url, {
body: formData,
method: "POST",
// CORSエラー回避
mode: "no-cors",
});
fetch(req)
// ここを修正
.then((res) => console.log(res))
.catch(console.warn);
} else {
console.log("選択されていません");
}
});
fetchしたときにres.statusとしていたところをresというオブジェクト全体でコンソールに表示させてみます。
2つのjsonファイルを添付してみると以下のような形になるはずです。
statusとstatusTextというのはResponseオブジェクトの中のプロパティの一部だったことがわかりますね。
このように何かを送信したときにはレスポンスが返ってくることを初心者の方は覚えておきましょう。
特にバックエンドの言語を勉強したことがある方であれば「リクエストとレスポンス」というキーワードを聞いたことがあるはずです。
JavaScriptはマルチな言語でフロントエンドだけでなくサーバーとのやり取りまで対応させることができますので、今後バックエンドをJavaScriptを使うときにレスポンスの仕組みを知っておくと学習が進むでしょう。
とはいえ先ほどやったnew Requestのようにレスポンスにもnew Responseというクラスがあるので大枠は同じような構文を書きます。
// レスポンスの形
// new Response(body, {
// status: 200,
// statusText: "success",
// headers: {
// "Content-Type": "application/json",
// "Content-Length": 100,
// },
// });
先ほどの例題ではなく新しくシンプルなオブジェクトを取り扱うなかでResponseを練習してみます。
オブジェクトをJSON形式にして送信したものを、逆にJSONからオブジェクトに戻すだけのコードを書きます。
オブジェクトをJSON形式にして送信するときはnew FileでFileオブジェクトを使用します。
一方で受け取ったことに対するリアクションをnew Responseでインスタンス化して返します。
const obj = {
id: "0001",
name: "aaa",
age: 20,
};
const json = JSON.stringify(obj);
const file = new File([json], "test.json", { type: "application/json" });
window.addEventListener("DOMContentLoaded", resFn);
async function resFn() {
const response = new Response(file, {
status: 200,
statusText: "success",
headers: {
"Content-Type": file.type,
"Content-Length": file.size,
},
});
console.log(response);
}
Responseオブジェクトはstatusプロパティに受信できたかを特定の数字で表現します。
200とすると成功した意味になります。
数字だとわかりづらいのでstatusTextというプロパティには文字列で何か書くことが多いです。
さらにResponseオブジェクトでもヘッダー情報が必要になり、こちらはファイル添付のときと同じで「ファイルの種類」「ファイルサイズ」などを記載することが一般的です。
コンソールを確認するとインスタンス化したResponseオブジェクトが表示されます。
このプログラムではオブジェクトがJSON形式に変換されて受け取った想定なので、逆にJSON形式からオブジェクトに戻しましょう。
const obj = {
id: "0001",
name: "aaa",
age: 20,
};
const json = JSON.stringify(obj);
const file = new File([json], "test.json", { type: "application/json" });
window.addEventListener("DOMContentLoaded", resFn);
async function resFn() {
const response = new Response(file, {
status: 200,
statusText: "success",
headers: {
"Content-Type": file.type,
"Content-Length": file.size,
},
});
// ここを変更
const data = await response.json();
console.log(data);
}
Responseオブジェクトには受け取ったデータが格納されているのでjson()などメソッドを使って値を取り出すことができます。
本記事ではサーバーを使用せず「自分で送って自分に返す」ようなことをしていますが、実際にファイル添付した画像をプレビューする機能でも「自分で送って自分で返したものをプレビューする」ことをやっています。
新しくHTML、JSを書き直して確認してみましょう。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<script src="./script.js" defer></script>
</head>
<body>
<input type="file" class="input" />
<button><a href="" class="preview">プレビュー</a></button>
</body>
</html>
const input = document.querySelector(".input");
input.addEventListener("change", async (e) => {
const file = e.target.files[0];
const response = new Response(file, {
status: 200,
statusText: "success",
headers: {
"Content-Type": file.type,
"Content-Length": file.size,
},
});
const data = await response.blob();
const url = URL.createObjectURL(data);
const preview = document.querySelector(".preview");
preview.href = url;
});
JavaScriptの前半部分は先ほどと同じくResponseクラスをインスタンス化しているだけです。
続いてResponseオブジェクトを先ほどはJSON形式からオブジェクトに戻すためにjson()を使いました。
今回は画像データということでURLが欲しいので、blob()でBlob形式に変換してからURL.createObjectURL()で画像を表示するURLを生成しています。
あとはHTML側で用意していたaタグのhrefの値に代入すればOKです。
JavaScriptで画像添付の際にリサイズして送信する方法
これまで画像の添付と送信をやってきましたが、画像をリサイズしてから送りたい場面があります。
いろんな画像があり容量の大きいものがあるときはリサイズで小さくしたほうが送りやすいからです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<script src="./script.js" defer></script>
</head>
<body>
<input type="file" class="input" />
<div class="box"></div>
</body>
</html>
前章までのおさらいでinputタグを経由して画像をアップロードして画面に表示するまでは以下のようなコードになります。
const input = document.querySelector('.input');
const box = document.querySelector('.box');
input.addEventListener('change', (e) => {
let image = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
const imgTag = document.createElement('img');
imgTag.src = e.target.result;
};
});
例えば以下の画像は1024×1024サイズの大きめの素材ですので、2.3MBの容量があります。
こちらの画像を400×400までリサイズして容量を減らすことをやってみます。
画像のリサイズはcanvasを使ったやり方です。
const input = document.querySelector('.input');
const box = document.querySelector('.box');
input.addEventListener('change', (e) => {
let image = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
const imgTag = document.createElement('img');
imgTag.src = e.target.result;
// ここから追加
imgTag.onload = (e) => {
// canvasで画像データを加工することでダウンロード、送信するデータを変更できる
const canvas = document.createElement('canvas');
const widthSize = 400;
canvas.width = widthSize;
// サイズ比率
const ratio = widthSize / e.target.width;
canvas.height = e.target.height * ratio;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgTag, 0, 0, canvas.width, canvas.height);
// ファイル形式、画質(100%が基準)
const newUrl = ctx.canvas.toDataURL('images/png', 90);
const newImgTag = document.createElement('img');
newImgTag.src = newUrl;
box.appendChild(newImgTag);
};
};
});
HTML上にimgタグを使ってプレビュー画面を作っていましたがcanvasタグを使うことでサイズの変更ができます。
こちらで再度アップロードしてみると400×400までリサイズされていて容量も1MBを切るように減らすことができました。
ちなみにCSSで表示範囲を小さくすることでプレビュー自体は小さくなりますが、画像の素材自体が小さくなっているわけではないのでCSSではリサイズはできません。
またリサイズした画像を送信するコードは以下のようになります。
現時点では上図のようにbase64形式のURLになっているのでファイルに戻します。
input.addEventListener('change', (e) => {
let image = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
const imgTag = document.createElement('img');
imgTag.src = e.target.result;
imgTag.onload = (e) => {
const canvas = document.createElement('canvas');
const widthSize = 400;
canvas.width = widthSize;
const ratio = widthSize / e.target.width;
canvas.height = e.target.height * ratio;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgTag, 0, 0, canvas.width, canvas.height);
const newUrl = ctx.canvas.toDataURL('images/png', 90);
const newImgTag = document.createElement('img');
newImgTag.src = newUrl;
box.appendChild(newImgTag);
// ここから追加
let converted = convertFile(newUrl);
console.log(converted);
};
};
});
// ここから追加
function convertFile(url) {
let arr = url.split(',');
let mime = arr[0].match(/:(.*?);/)[1];
let data = arr[1];
let dataStr = atob(data);
let dataArr = new Uint8Array(dataStr.length);
let file = new File([dataArr], 'File.png', { type: mime });
return file;
}
base64形式のURLをファイル形式に戻す関数をconvertFileとして別の関数で作りました。
不要な文字をsplitメソッドと正規表現を使ってトリミングした後は、atobメソッドというものを使うことで暗号化されたbase64を復号化することができます。
後はUnit8Arrayで型付き配列に変換したらnew Fileでインスタンス化することでファイル形式になります。
convertFIle関数を実行した後にconsole.logで変換後のデータを表示するようにしているので、ブラウザをリロードして再度アップロードしてコンソールを確認します。
Fileオブジェクトが表示されていることからファイル形式に戻っていることがわかります。
続いてファイル形式に戻したデータを送信する処理をuploadImgという関数にして作ります。
input.addEventListener('change', (e) => {
let image = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
const imgTag = document.createElement('img');
imgTag.src = e.target.result;
imgTag.onload = (e) => {
const canvas = document.createElement('canvas');
const widthSize = 400;
canvas.width = widthSize;
const ratio = widthSize / e.target.width;
canvas.height = e.target.height * ratio;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgTag, 0, 0, canvas.width, canvas.height);
const newUrl = ctx.canvas.toDataURL('images/png', 90);
const newImgTag = document.createElement('img');
newImgTag.src = newUrl;
box.appendChild(newImgTag);
let converted = convertFile(newUrl);
console.log(converted);
// ここから追加
uploadImg(converted);
};
};
});
function convertFile(url) {
let arr = url.split(',');
let mime = arr[0].match(/:(.*?);/)[1];
let data = arr[1];
let dataStr = atob(data);
let dataArr = new Uint8Array(dataStr.length);
let file = new File([dataArr], 'File.png', { type: mime });
return file;
}
// ここから追加
function uploadImg(file) {
let url = 'https://xxxxx';
let payload = new FormData();
payload.append('file', file);
let req = new Request(url, {
method: 'post',
body: payload,
mode: 'no-cors',
});
fetch(req)
.then((res) => console.log(res.status, res.statusText))
.catch(console.warn);
}
FormDataをインスタンス化して送信するためのファイルを先ほど変換済みのFileデータにします。
続いてRequestをインスタンス化してURLとメソッドの指定をする流れは前章の内容と同じになります。
あくまで送信する前にリサイズしておくことがポイントで、プレビュー画面があるとデータが一度URLになっている場合が多く、URLから再度ファイル形式に戻す変換作業が必要になります。
手順は多いですが基本的な仕組みはファイルとリクエストとレスポンスの標準クラスを使って実現できます。
このようにファイル添付を通じてリクエストとレスポンスを一緒に実装できるので、初心者の方も良い題材としてファイル添付の機能を自身のポートフォリオなどで作ってみてはいかがでしょうか。