适用于 worker 的同步 FileSystem API

简介

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

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

同步 API 与异步 API

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

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

  • 同步 API 只能在 Web Worker 上下文中使用,而异步 API 可以在 worker 环境中使用,也可以在 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() 的同步版本会分别返回 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 中。然后,如果发生任何错误,请使用 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 进行结构化克隆的浏览器,您可以将类型化数组、ArrayBufferFileBlob 传递给 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 一样,回调已消失。这简化了读取文件时的回调嵌套量。相反,readAs* 方法会返回读取文件。

示例:提取所有条目

在某些情况下,对于某些任务,同步 API 会更简洁。减少回调会很有用,而且肯定会提高可读性。同步 API 的真正缺点源自 Worker 的限制。

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

很抱歉,FileEntrySyncDirectoryEntrySync 目前不属于我们接受的类型。那么,您如何将条目返回到通话应用? 规避此限制的方法之一是返回 filesystem: URLs 列表,而不是条目列表。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 是 HTML5 的一项被低估且利用率较低的功能。我与之交流的大多数开发者都不需要额外的计算优势,但这些优势不仅仅适用于纯计算。如果您对此存有怀疑(就像我一样),希望本文能帮助您改变想法。 将磁盘操作(文件系统 API 调用)或 HTTP 请求等内容分流到 Worker 非常适合,并且有助于对代码进行分区。Worker 中的 HTML5 File API 为 Web 应用开辟了许多人尚未探索过的全新可能性。