适用于 worker 的同步 FileSystem API

简介

HTML5 FileSystem APIWeb Worker 本身就非常强大。FileSystem API 最终为 Web 应用带来了分层存储和文件 I/O,而工作器为 JavaScript 带来了真正的异步“多线程”功能。不过,当您结合使用这些 API 时,可以构建一些真正有趣的应用。

本教程提供了有关如何在 Web Worker 内利用 HTML5 FileSystem 的指南和代码示例。它假定您具备这两种 API 的应用知识。如果您尚未准备好深入了解这些 API,或者有兴趣详细了解这些 API,请阅读两篇讨论基础知识的精彩教程:探索 FileSystem APIWeb Worker 基础知识

同步 API 与异步 API

异步 JavaScript API 可能难以使用。它们很大。它们很复杂。 但最令人沮丧的是,他们提供了大量出错机会。 您要处理的最后一项任务是在已经异步世界(工作器)中叠加复杂的异步 API (FileSystem)!好消息是,FileSystem API 定义了同步版本,以缓解 Web Worker 中的痛苦。

在大多数情况下,同步 API 与其异步同级 API 完全相同。您将熟悉其中的方法、属性、特性和功能。主要差异为:

  • 同步 API 只能在 Web 工作器上下文中使用,而异步 API 只能在工作器内外使用。
  • 回调已出。API 方法现在会返回值。
  • 窗口对象的全局方法(requestFileSystem()resolveLocalFileSystemURL())变为 requestFileSystemSync()resolveLocalFileSystemSyncURL()

除了这些例外之外,API 是相同的。好,我们就可以出发了!

请求文件系统

Web 应用通过从 Web Worker 中请求 LocalFileSystemSync 对象来获取对同步文件系统的访问权限。requestFileSystemSync() 将向 worker 的全局范围公开:

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

请注意,现在我们使用的是同步 API,并且没有成功和错误回调,因此会出现新的返回值。

与普通 FileSystem API 一样,方法目前带有前缀:

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

处理配额

目前,无法在 worker 上下文中请求 PERSISTENT 配额。我建议处理工作器之外的配额问题。该过程可能如下所示:

  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 代码,我真是太幸福了!弄清楚出了什么问题可能非常困难

同步环境中缺少错误回调会使处理问题变得更加棘手。如果我们调试 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);
}

传递文件、Blob 和 ArrayBuffer

Web Worker 首次出现时,只允许在 postMessage() 中发送字符串数据。后来,浏览器开始接受可序列化数据,这意味着可以传递 JSON 对象。不过,Chrome 等一些浏览器最近接受使用结构化克隆算法通过 postMessage() 传递更复杂的数据类型。

这意味着什么?这意味着在主应用和工作器线程之间传递二进制数据变得非常简单。借助支持 worker 结构化克隆的浏览器,您可以将类型化数组、ArrayBufferFileBlob 传递给 worker。虽然数据仍然是副本,但能够传递 File 意味着优于前一种方法的性能优势,后者涉及在将文件传递到 postMessage() 之前对文件进行 base64 处理。

以下示例将用户选择的文件列表传递给专用 worker。worker 仅传递文件列表(简单来说,返回的数据实际上是 FileList),主应用会以 ArrayBuffer 的形式读取每个文件。

此示例还使用了 Web Worker 基础知识中所述的内嵌 Web Worker 技术的改进版本。

<!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 中的文件

使用异步 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 的限制。

出于安全考虑,发起调用的应用与 Web 工作器线程之间的数据绝不会共享。调用 postMessage() 时,系统始终会将数据复制到 worker 或从中复制数据。因此,并非每种数据类型都可以传递。

遗憾的是,FileEntrySyncDirectoryEntrySync 目前不属于可接受的类型。那么,如何将条目返回给发起调用的应用呢? 规避此限制的一种方法是返回 filesystem: 网址s 列表,而不是条目列表。filesystem: 网址只是字符串,因此非常容易传递。此外,还可以使用 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 下载文件

Workers 的一个常见用例是使用 XHR2 下载一批文件,然后将这些文件写入 HTML5 文件系统。对于工作器线程来说,这是一个完美的任务!

以下示例仅提取和写入一个文件,但您可以图片将其展开即可下载一组文件。

主应用

<!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 中一个未得到充分利用且没得到重视的功能。我联系的大多数开发者并不需要这些额外的计算优势,但它们可用于的不仅仅是单纯的计算。如果您像我一样持怀疑态度,衷心希望这篇文章有助于改变主意。 将磁盘操作(Filesystem API 调用)或 HTTP 请求等分流到 worker 是一种自然的合适方法,并且有助于拆分代码。Workers 内的 HTML5 File API 为 Web 应用开辟了一块全新炫酷功能,许多人尚未探索过。