« Promiseよ、今まで君のことを理解できていなかったよ | トップページ | フレッツADSL + MS5 + WZR-HP-G301NH構成によるインターネット接続 »

2021.01.09

JavaScriptを使って複数のzipファイルをWebブラウザで並列に展開して表示してみた

2021年が明けました。今年もよろしくお願いします。

今回は、先日JavaScriptを使って複数の画像を含むZipファイルをWebブラウザ上で展開、画像表示するサンプルを作ったので、その過程をメモしておこうと思います。Web Workerを用いた並列処理によりZipファイル展開します。

Zipによるパッケージング、Zip展開を行うために、stuk/jszipを使います。

(2021.01.10 コード修正)

サンプル1 ローカルファイル・アップロード版


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>ZIP Sample 1</title>
</head>
<body>
<h1>Zip Sample 1</h1>
<form>
<label for="file">File: </label>
<input id="file" name="file" type="file" multiple />
<input type="button" value="Clear" id="clear" />
</form>
<!- ->
<hr />
<pre>
<div id="message"> </div>
<span id="images"> </span>
</pre>
<!- ->
<script type="text/javascript" src="[path to]/dist/jszip.min.js"></script>
<script type="text/javascript" src="zipSample.js"></script>
<!- ->
</body>
</html>

zipSample.js


'use strict';

// Variables
var myApp = myApp || {};

// Subroutines
const waitLoad = () => {
return new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded',
() => {resolve();}, false);
});
};

const extractZipFiles = () => {
return new Promise((resolve1, reject1) => {
let myNode = document.getElementById('images');
while (myNode.lastChild) {
myNode.removeChild(myNode.lastChild);
}
let fileList = document.getElementById("file").files;
let worker = new Array();
Promise.all([...Array(fileList.length)].map((_, i) => i).map(
(value, index, array) => {
return new Promise((resolve2, reject2) => {
worker[value] = new Worker('scripts/worker.js');
worker[value].addEventListener('message', (er) => {
worker[value].terminate();
resolve2({'o':er.data.o});
}, false);
worker[value].addEventListener('error', (er) => {
document.getElementById("message")
.textContent = er.message;
worker[value].terminate();
reject2();
}, false);
worker[value].postMessage({
'i':value,
'file':fileList[value]
});
});
})).then((success) => {
let imageDataMap = new Map();
success.map((value, index, array) => {
for (let [k, v] of value.o) {
imageDataMap.set(k, v);
}
});
resolve1(imageDataMap);
}).catch((e) => {
reject1(e);
});
});
};

const displayImages = (imageDataMap) => {
let filenames = new Array();
let blobs = new Array();
for (let [k, v] of imageDataMap) {
filenames.push(k);
blobs.push(v);
}

blobs.map((value, index, array) => {
let image;
image = document.createElement('img');
let br = document.createElement('br');
let myNode = document.getElementById('images');
let reader = new FileReader();
reader.addEventListener('load', (e) => {
image.src = reader.result;
myNode.appendChild(image);
myNode.appendChild(br);
}, true);
reader.readAsDataURL(value);
});
};

const clickClear = () => {
document.getElementById('clear').addEventListener('click', () => {
window.location.reload(false);
}, false);
};

// Define Main Process
const mainProcess = () => {
document.getElementById('file').addEventListener('change', (e) => {
extractZipFiles()
.then((imageDataMap) => {return displayImages(imageDataMap);});
}, false);
};

// Main Routine
waitLoad()
.then(() => {mainProcess();
clickClear();})
.catch((e) => console.log(e));

// EOF

Lines 7-12: コンテンツを読み込むまで待機
Lines 16-19: 再描画時にイメージを削除
Line 20: ファイル情報を取得
Lines 21-41: Web Workerにファイル情報を渡してファイルバイナリを取得
Lines 41-49: ファイル名とファイルバイナリデータをMapオブジェクト化して返却
Lines 55-76: イメージ表示
Lines 78-82: ブラウザを再描画して初期化
Lines 85-90: ファイル指定時に実行する主要プロセス実行順を定義
Lines 93-96: 各オブジェクトを実行

各オブジェクトはイベント起動されて実行されますが、主要プロセスはmainProcessに定義されています。
Line 25-39ではWeb Workerからmessage又はerrorを受信するためのリスナ配置及びWeb Workerスレッドへファイル情報メッセージを送信しています。これらをPromise.allとmapを利用して非同期の並列処理してこれらの結果を待ち合わせ、各々のWeb Workerを終了させた後にPromise.allの結果をMapオブジェクトにして返しています。
Lines 63-75: では、画像イメージに対するリンクをブラウザ表示しています。受け取った画像のblobデータに基づきFileReaderオブジェクトのreadAsDataURLを使って画像Blobオブジェクトを生成し、さらにReader.resultを使って画像Blobオブジェクトをbase64文字列として表示イメージのソースに指定しています。

worker.js


'use strict';
importScripts('[path to]/dist/jszip.min.js');

self.addEventListener('message', (e) => {
let zip = new JSZip();
zip.loadAsync(e.data.file).then((b) => {
let imageDataMap = new Map();
let list = new Array();
for (let f in b.files) {
list.push(f);
}
Promise.all(list.map((value, index, array) => {
return new Promise((resolve, reject) => {
zip.file(value).async("blob").then((blob) => {
imageDataMap.set(value, blob);
resolve();
});
});
})).then((success) => {
postMessage({'o':imageDataMap});
});
});
}, false);

Web Workerの内部では、messageを受信するリスナを配置しています。messageを受信すると、loadAsyncを使ってZipファイルに含まれるファイル情報を取得し、asyncメソッドによりファイルのblobデータを取得、imageDataMapへZipファイルに含まれるファイルのblobデータをすべて格納すると、postMessageを使って呼び出し元のスレッドへデータ送信します。

サンプル2 ajax版


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>ZIP Sample 2</title>
</head>
<body>
<h1>Zip Sample 2</h1>
<form>
<label for="file">File: </label>
<input id="filename" name="filename" type="text" size='50' value='http://localhost/~tonop/examples/202101/jszip/images/file01.zip' />
<input type="button" value="送信" name="submit" id="btn" />
<input type="button" value="Clear" id="clear" />
</form>
<!- ->
<hr />
<pre>
<div id="message"> </div>
<span id="images"> </span>
</pre>
<!- ->
<script type="text/javascript" src="[path to]/dist/jszip.min.js"></script>
<script type="text/javascript" src="zipSample2.js"></script>
<!- ->
</body>
</html>

Sample2.js


'use strict';

// Variables
var myApp = myApp || {};

// Subroutines
const waitLoad = () => {
return new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded',
() => {resolve();}, false);
});
};

const readZipFiles = () => {
return new Promise((resolve, reject) => {
let filename = document.getElementById('filename').value;
let message = document.getElementById('message');
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let blob = xhr.response;
message.textContent = filename + 'を取得しました。';
resolve(blob);
} else {
message.textContent =
'サーバーエラーが発生しました。';
reject();
}
} else {
message.textContent = '通信中...';
}
};
xhr.open('GET', filename, true);
xhr.responseType = 'blob';
xhr.send(null);
});
};

const extractZipFiles = (blob) => {
return new Promise((resolve1, reject1) => {
let myNode = document.getElementById('images');
while (myNode.lastChild) {
myNode.removeChild(myNode.lastChild);
}
let length = 1;
let worker = new Array();
Promise.all([...Array(length)].map((_, i) => i).map(
(value, index, array) => {
return new Promise((resolve2, reject2) => {
worker[value] = new Worker('scripts/worker.js');
worker[value].addEventListener('message', (er) => {
worker[value].terminate();
resolve2({'o':er.data.o});
}, false);
worker[value].addEventListener('error', (er) => {
document.getElementById("message")
.textContent = er.message;
worker[value].terminate();
reject2();
}, false);
worker[value].postMessage({
'i':value,
'file':blob
});
});
})).then((success) => {
let imageDataMap = new Map();
success.map((value, index, array) => {
for (let [k, v] of value.o) {
imageDataMap.set(k, v);
}
});
resolve1(imageDataMap);
}).catch((e) => {
reject1(e);
});
});
};

const displayImages = (imageDataMap) => {
let filenames = new Array();
let blobs = new Array();
for (let [k, v] of imageDataMap) {
filenames.push(k);
blobs.push(v);
}
blobs.map((value, index, array) => {
let image = document.createElement('img');
let br = document.createElement('br');
let myNode = document.getElementById('images');
let reader = new FileReader();
reader.addEventListener('load', (e) => {
image.src = reader.result;
myNode.appendChild(image);
myNode.appendChild(br);
}, true);
reader.readAsDataURL(value);
});
};

onst clickClear = () => {
document.getElementById('clear').addEventListener('click', () => {
window.location.reload(false);
}, false);
};

// Define Main Process
const mainProcess = () => {
document.getElementById('btn').addEventListener('click', () => {
readZipFiles()
.then((blob) => {return extractZipFiles(blob);})
.then((imageDataMap) => {return displayImages(imageDataMap);});
}, false);
};

// Main Routine
waitLoad()
.then(() => {mainProcess();
clickClear();})
.catch((e) => console.log(e));

// EOF

サンプル2がサンプル1と大きく異なっているのは、Lines 14-38にあるreadZipFilesを追加している点です。readZipFilesではajaxを使って画像ファイルを読み込み、読み込んだBlobデータをextractZipFilesに渡しています。

このサンプルではZipファイルを1つのみ指定可能としてますが、当然サンプル1同様コードをPromiseを使って書き直せば、ファイルを複数選択して並行して読み込ませることも可能です。

|

« Promiseよ、今まで君のことを理解できていなかったよ | トップページ | フレッツADSL + MS5 + WZR-HP-G301NH構成によるインターネット接続 »

パソコン・インターネット」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« Promiseよ、今まで君のことを理解できていなかったよ | トップページ | フレッツADSL + MS5 + WZR-HP-G301NH構成によるインターネット接続 »