「ファイル添付を実装するときに登場するFileやBlobが何かを知らないで作っている」
「画像アップロードでなぜか複雑なコードが登場する理由を知らずにコピペしている」
「ネットで調べると人によって書き方が違うので、どれを参考にすれば良いか混乱したことがある」
本日はそんな方に向けて、FileとBlobが何をするためのものかを解説してみたいと思います。
Web開発の世界では、ユーザーが画像をアップロードする機会が増えています。
JavaScriptを使用して画像アップロード機能を実装する際に遭遇する代表的な要素として、FileとBlobが使用されます。
これらのオブジェクトはユーザーが選択したファイルを扱ったり処理したりする上で不可欠なものです。
今回はJavaScriptで画像アップロードを実装する上で欠かせないFileとBlobに焦点を当て、それぞれの役割や違いを探ります。
さらに、実際のコーディングにおいてどのようにこれらのオブジェクトを活用するかを具体的な例を交えながら解説していきます。
JavaScriptで登場するBlobとFileの違い
まず知って欲しいのが私たちが使っている文字や画像などは「人間が理解できるデータ」になっているだけであって、そもそもはバイナリーデータという「コンピュータが理解できるデータ」が元になっています。
つまり画像をアップロードする機能を実装する場合にはコンピュータが理解できるデータに直す作業が必要になります。
そこで登場するのがBlobオブジェクトです。
Blobは「バイナリーラージオブジェクト」の略称でブラウザのメモリに存在するバイナリーデータを扱うためのクラスです。
クラスなのでインスタンス化して使用します。
let blob = new Blob(配列オブジェクト、MIMEタイプ);
第一引数には配列、オブジェクト形式になっているデータを入れることでBlobが作られます。
とはいえ普段やるような配列ではなく ArrayBuffer
、TypedArray
、DataView
といったBlobデータを作成するときに必要な配列のことです。
ざっくり言うとデータの設計図のようなものに該当すると思っていてください。
第二引数にはMIMEタイプと言って、こちらはデータの種類になります。
パソコン作業でエクセル、PDF、画像などいろんな拡張子があるのと同じで、Blobを作成するときも「どんな形式で作るか?」を指定する必要があります。
こちらの方がイメージしやすいでしょう。
そしてBlobと同じようなものでFileというクラスもあります。
BlobとFileは一緒に使用することもあるので「どっちが何をしているんだ?」と感じられた方も多いかもしれません。
実はFileはBlobを継承したクラスです、つまりFileも元はBlobだったわけです。
Webサイト側からユーザーのパソコンの中のデータにアクセスする役割です。
Fileもインスタンス化するのですがBlobと違って「ファイル名」を引数で指定します。
let file = new File(配列オブジェクト、ファイル名、MIMEタイプ);
コンソールログにBlobとFileを出力して違いを確認しておきましょう。
今回はhelloという文字が書かれたテキストデータをBlobとFileのそれぞれで作成してみます。
function createBlobFile() {
//作成する配列バッファーのサイズをバイト単位で指定。setInt8を実行する数。
let arrBfr = new ArrayBuffer(5);
// 配列バッファーは直接操作できないためデータビューを使用
let dataV = new DataView(arrBfr);
// テキストに書く文字(hello)を作成
dataV.setInt8(0, 104); // h
dataV.setInt8(1, 101); // e
dataV.setInt8(2, 108); // l
dataV.setInt8(3, 108); // l
dataV.setInt8(4, 111); // 0
// 配列、配列バッファなど繰り返し処理のできるデータを指定してBlobデータを作成(ファイルサイズと種類が返される)
let blob = new Blob([arrBfr], { type: "text/plain" });
console.log(blob);
// ファイル生成だとファイル名、種類、サイズ、最終更新日まで含まれる
let file = new File([arrBfr], "test.txt", { type: "text/plain" });
console.log(file);
}
createBlobFile();
同じテキストデータを2つ作ったわけですが、Blobで作成した場合よりFileで作成した場合の方がプロパティの種類が多いですね。
BlobはシンプルにサイズとMIMEタイプだけです。
Fileはファイル名や最終更新日など含まれていて、BlobにあるサイズとMIMEタイプもあります。
このことからも先ほどお話したFileはBlobを継承したクラスであることがわかります。
Blobの上位互換がFileとでも言えるかもしれませんね。
BlobとFileについて初心者の方でも最低限はこれくらいまでは理解しておいて欲しいです。
もっといろんな説明があるのですが、いきなり全部は理解できないので実務と並行して勉強していくのが効率的でしょう。
本記事では簡単な例としてWebからテキストデータをユーザーにダウンロードしてもらう機能を作ってみたいと思います。
まずHTMLにダウンロード用のボタンだけ作ります。
<!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>
<div class="container">
<p><button class="btn">ダウンロード</button></p>
</div>
</body>
</html>
ボタンをクリックしたらテキストデータを作成してダウンロードするための関数createBlobを実行させます。
const btn = document.querySelector(".btn");
document.addEventListener("DOMContentLoaded", () => {
btn.addEventListener("click", createBlob);
});
テキストデータを作成する部分は以下のようになります、テキストデータには「hello」という文字を書く先ほどの内容をそのまま転用しています。
const btn = document.querySelector(".btn");
document.addEventListener("DOMContentLoaded", () => {
btn.addEventListener("click", createFile);
});
function createFile() {
let arrBfr = new ArrayBuffer(5);
let dataV = new DataView(arrBfr);
dataV.setInt8(0, 104); // h
dataV.setInt8(1, 101); // e
dataV.setInt8(2, 108); // l
dataV.setInt8(3, 108); // l
dataV.setInt8(4, 111); // 0
let file = new File([arrBfr], "test.txt", { type: "text/plain" });
}
上記の状態からダウンロード機能を追加します。
データのダウンロードはHTMLのaタグによって実行できます。
const btn = document.querySelector(".btn");
document.addEventListener("DOMContentLoaded", () => {
btn.addEventListener("click", createFile);
});
function createFile() {
let arrBfr = new ArrayBuffer(5);
let dataV = new DataView(arrBfr);
dataV.setInt8(0, 104); // h
dataV.setInt8(1, 101); // e
dataV.setInt8(2, 108); // l
dataV.setInt8(3, 108); // l
dataV.setInt8(4, 111); // 0
let file = new File([arrBfr], "test.txt", { type: "text/plain" });
let url = URL.createObjectURL(file);
let a = document.createElement("a");
a.href = url;
a.download = file.name;
a.textContent = `ダウンロード:${file.name}`;
document.querySelector(".container").append(a);
}
ちなみにaタグを使ったダウンロードについては結構使うと思いますので知らなかった人は練習しておくことをおすすします。
以下の記事も参考にしてください。
Blob、Fileのような型付き配列とは何か?
BlobとFileのオブジェクトについてざっくり紹介しましたが、書籍やスクールでは「型付き配列」というキーワードと一緒に紹介されることがあります。
初心者の方だとイメージしにくいのですが通常の配列とは違うものなので注意しましょう。
型付き配列とは、JavaScriptでバイナリデータを効率的に処理する制限つきの配列のことで、主にファイルサイズの大きくなりがちな画像、動画に使用される特別な配列です。
Blobオブジェクトの解説で配列バッファーというものを紹介しましたが、配列バッファーも通常の配列ではなく型付き配列のために使用するものです。
例えば以下のようなコードを書いてみます。
let bfr = new ArrayBuffer(16);
let dv = new DataView(bfr);
console.log(dv);
ここまでは先ほどと同じ流れですが、新しくisViewというクラスをインスタンス化します。
isViewはインスタンス化の引数に渡されたデータが型付き配列(データビューも)かどうかを判定してくれるものです。
let bfr = new ArrayBuffer(16);
let dv = new DataView(bfr);
console.log(dv);
// ここを追加
let isView = ArrayBuffer.isView(dv);
console.log(isView);
こちらはtrueとなりました、試しに通常の配列を引数にしてみましょう。
let bfr = new ArrayBuffer(16);
let dv = new DataView(bfr);
console.log(dv);
// ここを変更
let isView = ArrayBuffer.isView([1, 2, 3]);
console.log(isView);
数字が格納された通常の配列を入れてみるとfalseになりましたね。
こちらからも配列と型付き配列が違うものであることがわかります。
ちなみに型付き配列でも通常の配列と同じメソッドが使えますので、中身の操作はそこまで違いを意識しなくても良いかも知れません。
Unit8Arrayという8 ビット符号なし整数値の型付き配列を作るクラスを使用してみます。
let data = new Uint8Array([3, 5, 7, 9, 11, 13, 15, 17]);
console.log(data);
中身は整数が格納されているので、例えば整数の削除をsliceメソッドを使ってみます。
sliceメソッドは配列内のインデックス番号を引数に指定すると、そのインデックス番号までの要素を全て削除するメソッドです。
let data = new Uint8Array([3, 5, 7, 9, 11, 13, 15, 17]);
console.log(data);
// ここを追加
let sliced = data.slice(2);
console.log(sliced);
上がもとのデータで下がsliceを実行した後のデータになります。
今回sliceメソッドに引数2を入れたことで、0番目の3と1番目の5が削除されています。
このように型付き配列には通常の配列で使用するメソッドもそのまま使えます。
JavaScriptでデータを読み込むFileReaderの使い方
先ほどの例でテキストデータを作成していましたが、実際にはデータを作るよりは既に用意されたデータを読み込むケースの方が多いです。
そこでJavaScriptではFileReaderというクラスが用意されています。
ファイル添付の機能をHTMLでは<input type=”file”/>で作りますが、このinputタグがクリックされたときに返ってくるのがFileReaderオブジェクトです。
Fileとの違いはFileReaderはinputタグをクリックしてパソコン内から添付したいデータを選択したときにのみ使用できます。
例えばパソコンの中のディレクトリからパスを指定してデータを読み込むといったことはFileの方を使わないといけなくなります。
inputタグを使ったファイル添付ではfilesという配列の中からデータを取り出します。
下記の例ではfiles[0]とすることで配列の0番目の要素という意味です。
またFileReaderもインスタンス化して使用し、readAsDataURLというメソッドの引数にfiles[0]を入れると読み込みが開始されます。
さらにresultというプロパティに読み込み結果が保存されます。
下記の例のように添付したファイルを確認のためにプレビューさせるときに使用します。
<p>ファイルを添付してください</p>
<input type="file" onchange="previewFile()" /><br />
<img src="" height="200" alt="画像のプレビュー" />
function previewFile() {
const preview = document.querySelector("img");
const file = document.querySelector("input[type=file]").files[0];
const reader = new FileReader();
reader.addEventListener(
"load",
() => {
// プレビューに表示させるために読み込み結果を取得してaタグのsrcに入れる
preview.src = reader.result;
},
false,
);
if (file) {
// 選択したファイルを読み込む
reader.readAsDataURL(file);
}
}
最近では画像のアップロード機能はいろんな用途で使われますので初心者の方でも練習しておきましょう。
ちなみに先ほどパソコン内のディレクトリパスを使用したファイル選択ではFileReaderは使えないという話をしましたが、厳密にはFileと一緒に使えば実現できます。
fetchを使ってパソコン内のパス指定で画像を選択して、blobメソッドでblobデータに変換しておきます。
fetch("../pc.jpg")
.then((res) => res.blob())
.then((blob) => {
const file = new File([blob], "image", { type: blob.type });
console.log(file);
});
fetchメソッドはデータを読み込んでres.json()とすることが多いですが、あれはJSONファイルを変換するためにやっていますので変換内容が違う形です。
res.blob()でblobデータに一度変換した後は、ここでFileのインスタンス化に指定します。
コンソールで表示させてみるとFileオブジェクトとしてパスで指定したpc.jpgという画像データが存在しているのがわかります。
ここまで来たらFileを使って取得したデータをFileReaderで読み込ませます。
readFileという関数を作って引数に先ほどのFileオブジェクトを入れます。
fetch("../pc.jpg")
.then((res) => res.blob())
.then((blob) => {
const file = new File([blob], "image", { type: blob.type });
console.log(file);
// ここを追加
readFile(file);
});
// ここを追加
function readFile(file) {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.addEventListener("load", () => {
const data = fileReader.result;
console.log(data);
});
}
読み込んだ画像データを定数dataとしてコンソールに表示させてみました。
なんだかよくわからない文字列になっていますね。
こちらは自動生成されたURL形式の文字列になっていて、aタグのhrefに入れてダウンロードさせたりブラウザのローカルストレージに保存することができます。
Fileを使ってFileオブジェクトにする工程を入れるとパソコン内のパス指定でもFileReaderが使える例でした。
JavaScriptで読み込んだデータをZIPファイルに変換する方法
続いて複数データや重たいデータをユーザーにダウンロードしてもらう際にZIP形式に変換することがあります。
そのようなプログラムは自作できますがライブラリで手軽に作成できるので初心者の方はまずはJSZipとFileSaverというものを使った方法を練習してみましょう。
https://cdnjs.com/libraries/jszip
https://cdnjs.com/libraries/FileSaver.js
2つともHTMLにCDNで読み込めば軽量なライブラリです。
ファイル添付の見た目と一緒に以下のようにHTMLを作ってみます。
<!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" />
<!-- ライブラリのcdnの読み込み -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>
<script src="./script.js" defer></script>
</head>
<body>
<input type="file" class="input" multiple />
<button class="btn">ダウンロード</button>
</body>
</html>
注意点としてはinputタグはmultiple属性を追加しておかないとユーザー側から複数ファイルの選択ができなくなる点です。
続いてJavaScriptで要素の読み込みをして、inputタグで作った「ダウンロード」ボタンがクリックされたときにZIPへの変換を行うようにします。
const input = document.querySelector('.input');
const btn = document.querySelector('.btn');
btn.addEventListener('click', () => {
// ここに処理を書く
});
JSZIPはインスタンス化することで使用できるようになります。
さらにインスタンス化するとfileというメソッドが使えるようになり、第一引数にデータ名、第二引数にデータ本体を入れます。
fileメソッドで複数のファイルを一つのグループとしてまとめることができます。
const input = document.querySelector('.input');
const btn = document.querySelector('.btn');
btn.addEventListener('click', () => {
// ここを追加
let zip = new JSZip();
Array.from(input.files).forEach((data) => {
zip.file(data.name, data);
});
});
Array.from(input.files)としているのはNodeListから配列への変換を行なっています。
複数ファイルの処理でforEachが必要ですが配列ではないNodeListへは使用できないのでArray.fromを使って配列に変換しています。
NodeListと配列の違いは以下の記事で解説していますので興味のある方はご覧ください。
続いてgenerateAsyncメソッドというものでZIP変換をします。
引数に1個ずつのデータタイプの指定が必要ですが、今回は画像を取り扱うのでblob形式にしました。
最後にFIleSaverをインストールした事によって使用できるsaveAsというメソッドで、第一引数にデータ、第二引数にZIP形式のファイル名を入れるとZIPとしてダウンロードさせることができるようになります。
それでは画像を3枚選択して「ダウンロード」ボタンをクリックしてみます。