适用于 worker 的同步 FileSystem API

简介

HTML5 FileSystem API 和 Web Worker 本身就非常强大。FileSystem API 终于为 Web 应用带来了分层存储和文件 I/O,而 Worker 则为 JavaScript 带来了真正的异步“多线程”功能。不过,如果将这些 API 搭配使用,您可以构建一些真正有趣的应用。

本教程提供了有关如何在 Web Worker 中利用 HTML5 文件系统的指南和代码示例。本文假定您具备这两个 API 的应用知识。如果您还不太准备好深入了解这些 API,或者有兴趣进一步了解这些 API,请阅读以下两个介绍基本知识的优秀教程:探索 FileSystem API 和 Web Worker 基础知识。

同步 API 与异步 API

异步 JavaScript API 可能很难使用。它们很大。它们很复杂。 但最令人沮丧的是,它们会给出错留出很多机会。您最不想处理的情况是在已经是异步环境 (Worker) 的情况下,再叠加复杂的异步 API (FileSystem)!好消息是,FileSystem API 定义了同步版本,以便解决 Web Worker 中的问题。

在大多数情况下,同步 API 与其异步同类 API 完全相同。您会发现方法、属性、特性和功能都很熟悉。主要偏差如下:

  • 同步 API 只能在 Web Worker 上下文中使用,而异步 API 可以在 Worker 内外使用。
  • 回调已弃用。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') 发回给 worker,以告知其有额外的存储空间。

这是一个相当复杂的权宜解决方法,但应该可以解决问题。如需详细了解如何将 PERSISTENT 存储空间与 FileSystem API 搭配使用,请参阅申请配额。

使用文件和目录

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 代码,我真羡慕您!找出问题所在可能非常困难。

由于同步环境中缺少错误回调,因此处理问题的难度超出了预期。如果再加上调试 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 线程之间传递二进制数据变得非常简单。通过支持为 Worker 进行结构化克隆的浏览器,您可以将类型化数组、ArrayBuffer、File 或 Blob 传递给 Worker。虽然数据仍然是副本,但能够传递 File 意味着相较于之前的方法(需要先将文件转换为 base64 格式,然后再将其传递给 postMessage()),性能有所提升。

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

在 Worker 中使用异步 FileReader API 读取文件完全可行。不过,还有更好的方法。在工作器中,有一个同步 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 会消除回调。这样可以简化读取文件时的回调嵌套量。而是返回已读取的文件。

示例:提取所有条目

在某些情况下,同步 API 在处理某些任务时会更为简洁。减少回调会很有用,而且肯定会让代码更易于阅读。同步 API 的真正缺点源自 Worker 的限制。

出于安全考虑,调用应用与 Web Worker 线程之间绝不会共享数据。调用 postMessage() 时,系统始终会将数据复制到 Worker 和从 Worker 复制数据。 因此,并非所有数据类型都可以传递。

很抱歉,FileEntrySync 和 DirectoryEntrySync 目前不属于我们接受的类型。那么,您如何将条目返回到通话应用? 规避此限制的方法之一是返回 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。这对 Worker 线程来说是绝佳的任务!

以下示例仅提取并写入一个文件,但您可以想象将其扩展为下载一组文件。

主要应用:

<!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 非常合适,还有助于对代码进行分区。Worker 中的 HTML5 File API 为 Web 应用开启了许多人尚未探索过的全新功能。