はじめに
HTML5 の FileSystem API と Web Worker は、それぞれ非常に強力な機能です。FileSystem API により、ウェブ アプリケーションに階層ストレージとファイル I/O が導入され、ワーカーにより JavaScript に真の非同期「マルチスレッド処理」がもたらされます。ただし、これらの API を併用することで、実に興味深いアプリを構築できます。
このチュートリアルでは、Web Worker 内で HTML5 FileSystem を活用するためのガイドとコードサンプルについて説明します。両方の API に関する実践的な知識があることを前提としています。まだこれらの API を試す準備ができていない、または API について詳しく知りたい場合は、基本について説明している FileSystem API の概要と ウェブ ワーカーの基本の 2 つのチュートリアルをご覧ください。
同期 API と非同期 API
非同期 JavaScript API は使いにくい場合があります。サイズが大きい。複雑です。しかし、最もイライラするのは、問題が発生する可能性がたくさんあることです。すでに非同期の世界(ワーカー)で、複雑な非同期 API(FileSystem)を重ねて扱う必要はありません。幸い、FileSystem API には、Web Worker の負担を軽減する同期バージョンが定義されています。
ほとんどの場合、同期 API は非同期 API とまったく同じです。メソッド、プロパティ、機能は使い慣れているはずです。主な相違点は次のとおりです。
- 同期 API は Web Worker コンテキスト内でのみ使用できますが、非同期 API は Worker の内外で使用できます。
- コールバックは廃止されました。API メソッドが値を返すようになりました。
- ウィンドウ オブジェクトのグローバル メソッド(
requestFileSystem()
とresolveLocalFileSystemURL()
)は、requestFileSystemSync()
とresolveLocalFileSystemSyncURL()
になります。
これらの例外を除き、API は同じです。準備完了!
ファイル システムのリクエスト
ウェブ アプリケーションは、ウェブ ワーカー内から LocalFileSystemSync
オブジェクトをリクエストして、同期ファイル システムへのアクセス権を取得します。requestFileSystemSync()
はワーカーのグローバル スコープに公開されます。
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
同期 API を使用しているため、新しい戻り値が返され、成功とエラーのコールバックがないことを確認してください。
通常の FileSystem API と同様に、現時点ではメソッドの先頭に接頭辞が付加されます。
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
割り当ての処理
現在、Worker コンテキストで PERSISTENT
割り当てをリクエストすることはできません。割り当ての問題は Workers の外部で対処することをおすすめします。このプロセスは以下のようになります。
- worker.js: FileSystem API コードを
try/catch
でラップして、QUOTA_EXCEED_ERR
エラーをキャッチします。 - worker.js:
QUOTA_EXCEED_ERR
をキャッチしたら、postMessage('get me more quota')
をメインアプリに送り返します。 - メインアプリ: #2 を受信したら、
window.webkitStorageInfo.requestQuota()
ダンスを踊ります。 - メインアプリ: ユーザーが割り当てを増やしたら、
postMessage('resume writes')
をワーカーに返して、追加の保存容量があることを通知します。
かなり複雑な回避策ですが、機能するはずです。FileSystem API で PERSISTENT
ストレージを使用する方法については、割り当てのリクエストをご覧ください。
ファイルとディレクトリの操作
getFile()
と getDirectory()
の同期バージョンはそれぞれ FileEntrySync
と DirectoryEntrySync
を返します。
たとえば、次のコードは、ルート ディレクトリに「log.txt」という空のファイルを作成します。
var fileEntry = fs.root.getFile('log.txt', {create: true});
次のコマンドは、ルートフォルダに新しいディレクトリを作成します。
var dirEntry = fs.root.getDirectory('mydir', {create: true});
エラー処理
ウェブワーカー コードをデバッグしたことがない方は、羨ましい限りです。問題の原因を特定するのは大変な作業です。
同期の世界ではエラー コールバックがないため、問題に対処するのが難しくなります。Web Worker コードのデバッグに一般的な複雑さが加わると、すぐにイライラするでしょう。作業を容易にする方法の一つは、関連するワーカーコードをすべて try/catch でラップすることです。エラーが発生した場合は、postMessage()
を使用してエラーをメインアプリに転送します。
function onError(e) {
postMessage('ERROR: ' + e.toString());
}
try {
// Error thrown if "log.txt" already exists.
var fileEntry = fs.root.getFile('log.txt', {create: true, exclusive: true});
} catch (e) {
onError(e);
}
File、Blob、ArrayBuffer の受け渡し
ウェブワーカーが初めて登場したとき、postMessage()
で送信できるのは文字列データのみでした。その後、ブラウザはシリアル化可能なデータを受け入れるようになり、JSON オブジェクトを渡せるようになりました。ただし最近では、Chrome などの一部のブラウザでは、構造化クローン アルゴリズムを使用して postMessage()
を介して渡す複雑なデータ型を受け入れるようになっています。
つまり、つまり、メインアプリとワーカー スレッド間でバイナリ データを渡す方がはるかに簡単です。ワーカーの構造化クローンをサポートするブラウザでは、型付き配列、ArrayBuffer
、File
、Blob
をワーカーに渡すことができます。データは引き続きコピーですが、File
を渡すことができるため、ファイルを Base64 エンコードしてから postMessage()
に渡すという以前のアプローチよりもパフォーマンスが向上します。
次の例では、ユーザーが選択したファイルのリストを専用のワーカーに渡します。ワーカーはファイルリストを渡すだけです(返されたデータが実際に FileList
であることを示すため)。メインアプリは各ファイルを ArrayBuffer
として読み取ります。
このサンプルでは、ウェブ ワーカーの基本で説明されているインライン ウェブ ワーカーの手法の改良版も使用しています。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Passing a FileList to a Worker</title>
<script type="javascript/worker" id="fileListWorker">
self.onmessage = function(e) {
// TODO: do something interesting with the files.
postMessage(e.data); // Pass through.
};
</script>
</head>
<body>
</body>
<input type="file" multiple>
<script>
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
var files = this.files;
loadInlineWorker('#fileListWorker', function(worker) {
// Setup handler to process messages from the worker.
worker.onmessage = function(e) {
// Read each file aysnc. as an array buffer.
for (var i = 0, file; file = files[i]; ++i) {
var reader = new FileReader();
reader.onload = function(e) {
console.log(this.result); // this.result is the read file as an ArrayBuffer.
};
reader.onerror = function(e) {
console.log(e);
};
reader.readAsArrayBuffer(file);
}
};
worker.postMessage(files);
});
}, false);
function loadInlineWorker(selector, callback) {
window.URL = window.URL || window.webkitURL || null;
var script = document.querySelector(selector);
if (script.type === 'javascript/worker') {
var blob = new Blob([script.textContent]);
callback(new Worker(window.URL.createObjectURL(blob));
}
}
</script>
</html>
Worker 内のファイルの読み取り
Worker で非同期の FileReader
API を使用してファイルを読み取ることはまったく問題ありません。もっと良い方法があります。Workers には、ファイルを効率的に読み取る同期 API(FileReaderSync
)があります。
メインアプリ:
<!DOCTYPE html>
<html>
<head>
<title>Using FileReaderSync Example</title>
<style>
#error { color: red; }
</style>
</head>
<body>
<input type="file" multiple />
<output id="error"></output>
<script>
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
console.log(e.data); // e.data should be an array of ArrayBuffers.
};
worker.onerror = function(e) {
document.querySelector('#error').textContent = [
'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join('');
};
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
worker.postMessage(this.files);
}, false);
</script>
</body>
</html>
worker.js
self.addEventListener('message', function(e) {
var files = e.data;
var buffers = [];
// Read each file synchronously as an ArrayBuffer and
// stash it in a global array to return to the main app.
[].forEach.call(files, function(file) {
var reader = new FileReaderSync();
buffers.push(reader.readAsArrayBuffer(file));
});
postMessage(buffers);
}, false);
想定どおり、同期 FileReader
ではコールバックは使用されなくなります。これにより、ファイルを読み取る際のコールバックのネスト量が簡素化されます。代わりに readAs* メソッドは読み取りファイルを返します。
例: すべてのエントリを取得する
タスクによっては、同期 API を使用する方がよりクリーンな場合があります。コールバックを減らすことは良いことです。また、コードの可読性が向上します。同期 API の真の欠点は、Worker の制限に起因します。
セキュリティ上の理由から、呼び出し元のアプリと Web Worker スレッド間でデータが共有されることはありません。postMessage()
が呼び出されると、データは常に Worker との間でコピーされます。そのため、すべてのデータ型を渡すことはできません。
残念ながら、FileEntrySync
と DirectoryEntrySync
は現在、許可されているタイプに該当しません。では、通話アプリにエントリを戻すにはどうすればよいでしょうか。この制限を回避する 1 つの方法は、エントリのリストではなく filesystem: URL のリストを返すことです。filesystem:
URL は単なる文字列であるため、簡単に渡すことができます。さらに、resolveLocalFileSystemURL()
を使用して、メインアプリ内のエントリに解決することもできます。これにより、FileEntrySync
/ DirectoryEntrySync
オブジェクトに戻ります。
メインアプリ:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Listing filesystem entries using the synchronous API</title>
</head>
<body>
<script>
window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
window.webkitResolveLocalFileSystemURL;
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
var urls = e.data.entries;
urls.forEach(function(url, i) {
window.resolveLocalFileSystemURL(url, function(fileEntry) {
console.log(fileEntry.name); // Print out file's name.
});
});
};
worker.postMessage({'cmd': 'list'});
</script>
</body>
</html>
worker.js
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
var paths = []; // Global to hold the list of entry filesystem URLs.
function getAllEntries(dirReader) {
var entries = dirReader.readEntries();
for (var i = 0, entry; entry = entries[i]; ++i) {
paths.push(entry.toURL()); // Stash this entry's filesystem: URL.
// If this is a directory, we have more traversing to do.
if (entry.isDirectory) {
getAllEntries(entry.createReader());
}
}
}
function onError(e) {
postMessage('ERROR: ' + e.toString()); // Forward the error to main app.
}
self.onmessage = function(e) {
var data = e.data;
// Ignore everything else except our 'list' command.
if (!data.cmd || data.cmd != 'list') {
return;
}
try {
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
getAllEntries(fs.root.createReader());
self.postMessage({entries: paths});
} catch (e) {
onError(e);
}
};
例: XHR2 を使用したファイルのダウンロード
ワーカーの一般的な使用例は、XHR2 を使用して一連のファイルをダウンロードし、それらのファイルを HTML5 ファイル システムに書き込むことです。これはワーカー スレッドに最適なタスクです。
次の例では、1 つのファイルのみを取得して書き込みますが、拡張して一連のファイルをダウンロードすることもできます。
メインアプリ:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Download files using a XHR2, a Worker, and saving to filesystem</title>
</head>
<body>
<script>
var worker = new Worker('downloader.js');
worker.onmessage = function(e) {
console.log(e.data);
};
worker.postMessage({fileName: 'GoogleLogo',
url: 'googlelogo.png', type: 'image/png'});
</script>
</body>
</html>
downloader.js:
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
function makeRequest(url) {
try {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, false); // Note: synchronous
xhr.responseType = 'arraybuffer';
xhr.send();
return xhr.response;
} catch(e) {
return "XHR Error " + e.toString();
}
}
function onError(e) {
postMessage('ERROR: ' + e.toString());
}
onmessage = function(e) {
var data = e.data;
// Make sure we have the right parameters.
if (!data.fileName || !data.url || !data.type) {
return;
}
try {
var fs = requestFileSystemSync(TEMPORARY, 1024 * 1024 /*1MB*/);
postMessage('Got file system.');
var fileEntry = fs.root.getFile(data.fileName, {create: true});
postMessage('Got file entry.');
var arrayBuffer = makeRequest(data.url);
var blob = new Blob([new Uint8Array(arrayBuffer)], {type: data.type});
try {
postMessage('Begin writing');
fileEntry.createWriter().write(blob);
postMessage('Writing complete');
postMessage(fileEntry.toURL());
} catch (e) {
onError(e);
}
} catch (e) {
onError(e);
}
};
まとめ
Web Worker は、HTML5 の機能の中で、十分に活用されておらず、過小評価されている機能です。私が接するほとんどのデベロッパーは、追加のコンピューティングのメリットを必要としません。しかし、コンピューティング以外の用途にも使用できます。懐疑的な方は、この記事がお役に立ちましたら幸いです。 ディスク オペレーション(ファイルシステム API 呼び出し)や HTTP リクエストなどの処理を Worker にオフロードするのは自然な選択であり、コードの分割にも役立ちます。Workers の HTML5 File API により、ウェブ アプリケーションにおけるまったく新しい可能性が開かれます。