ワーカー用の同期 FileSystem API

はじめに

HTML5 の FileSystem APIWeb 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 の外部で対処することをおすすめします。このプロセスは以下のようになります。

  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 コードのデバッグに一般的な複雑さが加わると、すぐにイライラするでしょう。作業を容易にする方法の一つは、関連するワーカーコードをすべて 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() を介して渡す複雑なデータ型を受け入れるようになっています。

つまり、つまり、メインアプリとワーカー スレッド間でバイナリ データを渡す方がはるかに簡単です。ワーカーの構造化クローンをサポートするブラウザでは、型付き配列、ArrayBufferFileBlob をワーカーに渡すことができます。データは引き続きコピーですが、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 との間でコピーされます。そのため、すべてのデータ型を渡すことはできません。

残念ながら、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);
    }
};

まとめ

Web Worker は、HTML5 の機能の中で、十分に活用されておらず、過小評価されている機能です。私が接するほとんどのデベロッパーは、追加のコンピューティングのメリットを必要としません。しかし、コンピューティング以外の用途にも使用できます。懐疑的な方は、この記事がお役に立ちましたら幸いです。 ディスク オペレーション(ファイルシステム API 呼び出し)や HTTP リクエストなどの処理を Worker にオフロードするのは自然な選択であり、コードの分割にも役立ちます。Workers の HTML5 File API により、ウェブ アプリケーションにおけるまったく新しい可能性が開かれます。