ワーカー用の同期 FileSystem API

はじめに

HTML5 FileSystem APIウェブ ワーカーは、それぞれ非常に強力なツールです。FileSystem API は最終的に、ウェブ アプリケーションに階層型ストレージとファイル I/O をもたらし、Worker は JavaScript で真の非同期「マルチスレッド」を実現します。ただし、これらの API を併用することで、非常に興味深いアプリを構築できます。

このチュートリアルでは、ウェブ ワーカー内で HTML5 ファイル システムを活用するためのガイドとコードサンプルについて説明します。両方の API の実用的な知識があることを前提としています。これらの API について詳しく知りたい場合や、詳しく知りたい場合は、FileSystem API の確認ウェブ ワーカーの基本という、基本事項について説明する 2 つの優れたチュートリアルをご覧ください。

同期 API と非同期 API

非同期 JavaScript API は使いにくい場合があります。大規模です。複雑です。 最も不満は、失敗する機会がたくさんあることです。最後にもう一つ考えられるのは、すでに非同期な環境(ワーカー)で複雑な非同期 API(FileSystem)を階層化することです。幸い、FileSystem API では、ウェブワーカーの負担を軽減するために同期バージョンを定義しています。

同期 API の大部分は、非同期 API とまったく同じです。メソッド、プロパティ、特徴、機能はなじみのあるものになります。主な逸脱は次のとおりです。

  • 同期 API はウェブ ワーカーのコンテキスト内でのみ使用できます。非同期 API は Worker の内部および外部で使用できます。
  • コールバックは終了しています。API メソッドが値を返すようになりました。
  • ウィンドウ オブジェクトのグローバル メソッド(requestFileSystem()resolveLocalFileSystemURL())は、requestFileSystemSync()resolveLocalFileSystemSyncURL() になります。

これらの例外を除き、API は同じです。いいですね!

ファイル システムのリクエスト

ウェブ アプリケーションは、ウェブ ワーカー内から LocalFileSystemSync オブジェクトをリクエストすることで、同期ファイルシステムへのアクセス権を取得します。requestFileSystemSync() は、Worker のグローバル スコープに公開されます。

var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);

ここでは新しい戻り値である同期 API を使用していることと、成功コールバックとエラー コールバックがないことに注目してください。

通常の FileSystem API と同様に、現時点ではメソッドに接頭辞が付きます。

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                 self.requestFileSystemSync;

割り当ての操作

現在、Worker のコンテキストで PERSISTENT の割り当てをリクエストすることはできません。Worker 以外の割り当ての問題には対処することをおすすめします。プロセスは次のようになります。

  1. worker.js: すべての FileSystem API コードを try/catch でラップして、QUOTA_EXCEED_ERR エラーをキャッチします。
  2. worker.js: QUOTA_EXCEED_ERR を検出した場合は、postMessage('get me more quota') をメインアプリに送り返します。
  3. メインアプリ: #2 を受け取ったら window.webkitStorageInfo.requestQuota() ダンスを行います。
  4. メインアプリ: ユーザーが追加の割り当てを許可したら、postMessage('resume writes') をワーカーに返して、追加の保存容量を通知します。

かなり複雑な回避策ですが、うまくいくはずです。FileSystem API で PERSISTENT ストレージを使用する方法については、割り当てのリクエストをご覧ください。

ファイルとディレクトリの操作

getFile()getDirectory() の同期バージョンはそれぞれ FileEntrySyncDirectoryEntrySync を返します。

たとえば、次のコードは、ルート ディレクトリに「log.txt」という空のファイルを作成します。

var fileEntry = fs.root.getFile('log.txt', {create: true});

次のコマンドは、ルートフォルダに新しいディレクトリを作成します。

var dirEntry = fs.root.getDirectory('mydir', {create: true});

エラー処理

Web Worker のコードをデバッグしたことがないとしたら、うらやましいです。何が問題なのかを突き止めるのは 大変なことです

同期環境にはエラー コールバックがないため、問題に対処するのが本来より複雑になります。ウェブ ワーカーコードのデバッグが複雑になりがちです。作業を容易にする方法の一つは、関連するすべての 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);
}

Files、Blob、ArrayBuffer の受け渡し

ウェブワーカーが初めて現場に到着したとき、postMessage() での送信は文字列データのみを許可していました。その後、ブラウザはシリアル化可能なデータを受け入れるようになりました。つまり、JSON オブジェクトを渡すことができるようになりました。しかし最近では、Chrome などの一部のブラウザでは、構造化クローン アルゴリズムを使用して、より複雑なデータ型を postMessage() 経由で渡すことができるようになりました。

具体的にはどういうことでしょうか。つまり、メインアプリとワーカー スレッドの間でバイナリデータを渡すことがはるかに簡単になります。Worker の構造化クローン作成をサポートしているブラウザでは、型付き配列、ArrayBufferFile、または Blob を Worker に渡すことができます。データはまだコピーですが、File を渡せるということは、postMessage() に渡す前にファイルを base64 で取得する前者よりもパフォーマンス上のメリットがあるということです。

次の例では、ユーザーが選択したファイルのリストを専用の Worker に渡します。Worker はファイルリストを渡すだけで(返されたデータを示すのは 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 を使用してファイルを読み取ることはまったく問題ありません。ただし、もっと良い方法があります。Worker には、ファイルの読み取りを合理化する同期 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 の制限に起因します。

セキュリティ上の理由から、呼び出し元アプリとウェブ ワーカー スレッドの間でデータが共有されることはありません。postMessage() が呼び出されたとき、データは常に Worker との間でコピーされます。そのため、すべてのデータ型を渡せるわけではありません。

残念ながら、FileEntrySyncDirectoryEntrySync は現在使用可能なタイプに該当しません。では、エントリを呼び出し元のアプリに戻すにはどうすればよいでしょうか。 この制限を回避する方法の 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);
    }
};

まとめ

ウェブワーカーは、十分に活用されていない HTML5 の機能です。私が話を伺ったデベロッパーのほとんどは、計算上のメリットを増やす必要はありませんが、純粋な計算以外の用途にも活用できます。 疑念をお持ちの方に、この記事がお役に立てば幸いです。 ディスク オペレーション(ファイル システム API 呼び出し)や HTTP リクエストなどを Worker にオフロードすることは、最適な方法であり、コードの区分けにも役立ちます。Workers 内の HTML5 File API は、多くの人がまだ扱っていないウェブアプリにまったく新しい可能性をもたらします。