フロントエンド上で、zipファイルを展開したいというのは一度は考えたことあるはずです。
自分も、コーディングテスト機能を作るときに、バックエンド(Laravel)からAPI経由で降ってくるzipファイルをReact上で展開しようとしました。
その時のノウハウをここに書いていきます。
前提知識
JSZipとはJavaScriptのライブラリの一つで、zipファイルを解答・圧縮するライブラリです。
今回はこのJSZipを用いた場合の展開のやり方を紹介します。
BLOB(Binary Large OBject)とは、データベースで用いられるデータ型の一つであり、ビデオや音声、圧縮ファイル、実行ファイルなどの非構造化データ、プレーンテキスト(平文の文字数字)以外のバイナリデータを格納する際に用いられます。
要は、文字以外のデータです。
MySQLにバイナリデータというのは入れることは出来るのですが、そのときに用いられる型です。
このBlob型の知識がないと、今回のようなバイナリデータをブラウザ上で展開するといったことはできません。
今回扱うzipファイルについて
ディレクトリを圧縮したものとなります。ディレクトリの中にファイルが格納されており、今回はそのファイルを展開するという流れになります。
実際のコード
const [fileNames, setFileNames] = useState([]);
const [fileContents, setFileContents] = useState([]);
const fetchData = async () => {
//バックエンド側にテスト問題のzipファイルをリクエスト
const response = await fetch('http://localhost:8000/api/exam/workBook');
//zipファイルをblob型として変数に格納する(でないとエラーを吐く)
const zipFileData = await response.blob();
//zipを読み込む
const zip = await JSZip.loadAsync(zipFileData);
const names = [];
const contents = [];
await Promise.all(
Object.entries(zip.files).map(async ([relativePath, zipEntry]) => {
if (!zipEntry.dir) {
const fileName = relativePath.replace('exam/', '');
const content = await zipEntry.async('text');
names.push(fileName);
contents.push({ fileName, content });
}
})
);
setFileNames(names);
setFileContents(contents);
};
解説
const [fileNames, setFileNames] = useState([]);
const [fileContents, setFileContents] = useState([]);
これらのuseStateには、ファイル名とファイルの内容が配列型で格納されます。
ブラウザはファイルを直接展開することはできません。
ファイルを展開したい場合は、
「ファイル名」
と
「ファイルの内容」
に分けてそれぞれ展開する必要があります。
const response = await fetch('http://localhost:8000/api/exam/workBook');
まず、fetchAPIを用いて、Laravel側にテスト問題を要求します。
フロントエンドにおいておくと、不正な操作をするユーザーがテスト問題の中身を見ることが出来るので、必ずバックエンド側に置きましょう。
次は、zipファイルの読み込みからしていきます。
const zip = await JSZip.loadAsync(zipFileData);
loadAsync関数にblob型として格納したデータを格納します。
loadAsyncとはそもそも何?
JSZipの関数で、引数次第で様々な読み込み方をすることができます。
例
var zip = new JSZip();
zip.loadAsync("UEsDBAoDAAAAAJxs8T...AAAAAA==", {base64: true});
上記のように、第一引数に格納しているものを、base64にエンコードして読み込むこともできます。
他にも、checkCRC32型といった豊富な読み込みをさせることもできます。
const names = [];
const contents = [];
ファイル名とファイルの内容を格納するためにファイル名を格納する配列namesとファイルの内容を格納する配列contentsを宣言しておきます。
さて、ここからが本番の作業です。
Object.entries(zip.files).map(async ([relativePath, zipEntry]) => {
Object.entries(zip.files)で、引数に配列を渡します。その後map関数にコールバック関数を渡してそれぞれの配列の要素に対して処理を加えていきます
.map(async ([relativePath, zipEntry]) => {
コールバック関数として無名関数を渡し、引数に連想配列を渡します。
変数relativePathには、フォルダの中身のパスが文字列として、変数zipEntryにはファイルの内容が格納されています。
↑
これなんでなのかよくわかりません・・・
if (!zipEntry.dir) {
で、zipEntryがdirのときにのみ(でないとなぞの空の名前の要素に対しても処理を行ってしまう)処理をするようにする。
const fileName = relativePath.replace('exam/', '');
const content = await zipEntry.async('text');
ディレクトリの中身のファイルを展開する際に、現在のブラウザの挙動だと、ディレクトリのパスが表示されてしまいます。
なので、ディレクトリの名前とバックスラッシュは消しておきましょう。
.async('text') という、JSZip ライブラリで非同期にファイルの内容をテキストとして読み込むメソッドを用いてzipEntryの内容をtextに変換しましょう。
names.push(fileName);
contents.push({ fileName, content });
配列にそれぞれの結果を格納していきます。
setFileNames(names);
setFileContents(contents);
useStateに配列の値を保存しましょう。
以上で紹介は終了です。
ありがとうございました。
色々わかっていないところがあるので、教えていただけると幸いです。