适用于 worker 的同步 FileSystem API

简介

HTML5 FileSystem APIWeb Worker 在很大程度上决定了 为自己加油打气。FileSystem API 终于提供分层存储和 为 Web 应用和 Worker 实现文件 I/O,从而实现了真正的异步“多线程” 到 JavaScript。不过,当您结合使用这些 API 时,您可以构建一些真正有趣的应用。

本教程提供了使用 HTML5 的指南和代码示例 Web Worker 中的 FileSystem。学习本课程前,您需要具备以下知识: 两个 API如果您还没有准备好深入学习或有兴趣学习 如需详细了解这些 API,请阅读两篇优秀的教程,了解相关基础知识: 探索 FileSystem APIWeb Worker 基础知识

同步 API 与异步 API

异步 JavaScript API 可能很难使用。它们很大。它们很复杂。 但最令人失望的是,它们提供了大量导致出错的机会。 最后一项任务是在复杂的异步 API (FileSystem) 上分层 好消息是, FileSystem API 定义了一个同步版本,以缓解 Web Workers 中的难点。

在大多数情况下,同步 API 与其异步同类 API 完全相同。 方法、属性、特性和功能应是用户熟悉的。主要差异包括:

  • 同步 API 只能在 Web Worker 环境中使用,而 可在 worker 内外使用异步 API。
  • 回调已输出。API 方法现在会返回值。
  • 窗口对象的全局方法(requestFileSystem()resolveLocalFileSystemURL())变为“requestFileSystemSync()”,并且 resolveLocalFileSystemSyncURL()

除了这些例外情况,API 均相同。好的,可以出发了!

请求文件系统

Web 应用可通过请求 LocalFileSystemSync 对象。requestFileSystemSync() 可供 Worker 的全局范围使用:

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

请注意新的返回值,因为我们现在使用的是同步 API, 成功和错误回调的概率。

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

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

处理配额

目前,您无法在工作器上下文中请求 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 代码的一般复杂性, 你很快就会感到沮丧的。为了让生活更轻松 在 try/catch 中找到相关的 Worker 代码。然后,如果发生任何错误,请转发 使用 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 意味着 与前一种方法相比,这种方法的性能优势在于对文件进行 base64 编码, 然后再将其传递到 postMessage() 中。

以下示例将用户选择的文件列表传递给专用 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>

读取工作器中的文件

使用异步 FileReader API 读取 worker 中的文件完全可以接受。不过,有一种更好的方法。在 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 Worker 线程之间的数据 从未共享。调用 postMessage() 时,数据始终会复制到 worker 或从 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 下载文件

Worker 的一个常见用例是使用 XHR2 下载大量文件, 并将这些文件写入 HTML5 FileSystem。对于工作器线程来说,这是完美的任务!

以下示例仅提取和写入一个文件,但您可以 以下载一组文件。

主应用

<!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 是一种未得到充分利用但未被充分利用的功能 。与我交流的大多数开发者都不需要这种额外的计算优势, 但它们不仅可用于单纯的计算, 如果您持怀疑态度(就像我一样),希望本文对您改变了主意有所帮助。 将磁盘操作(文件系统 API 调用)或 HTTP 请求等内容分流到工作器 非常适合,还有助于划分代码。HTML5 File API “Inside the Workers”为许多用户尚未探索过的 Web 应用提供了全新的酷炫功能。