Einführung
Die HTML5-FileSystem API und Webworker sind in ihrer jeweiligen Funktion äußerst leistungsfähig. Die FileSystem API bietet Webanwendungen endlich hierarchischen Speicher und Datei-I/O und Worker ermöglichen echtes asynchrones Multithreading in JavaScript. Wenn Sie diese APIs jedoch zusammen verwenden, können Sie wirklich interessante Apps erstellen.
In diesem Tutorial finden Sie einen Leitfaden und Codebeispiele für die Verwendung des HTML5-Dateisystems in einem Webworker. Es werden Vorkenntnisse zu beiden APIs vorausgesetzt. Wenn Sie noch nicht ganz bereit sind, loszulegen, oder mehr über diese APIs erfahren möchten, lesen Sie die beiden Anleitungen zu den Grundlagen: Exploring the FileSystem APIs (Die FileSystem APIs kennenlernen) und Basics of Web Workers (Grundlagen von Webworkern).
Synchrone und asynchrone APIs
Asynchrone JavaScript APIs können schwierig zu verwenden sein. Sie sind groß. Sie sind komplex. Am frustrierendsten ist jedoch, dass sie viele Möglichkeiten bieten, dass etwas schiefgeht. Das Letzte, was Sie wollen, ist eine komplexe asynchrone API (FileSystem) in einer bereits asynchronen Umgebung (Worker) zu verwenden. Die gute Nachricht ist, dass die FileSystem API eine synchrone Version definiert, um die Probleme in Webworkern zu beheben.
Die synchrone API ist im Wesentlichen identisch mit ihrer asynchronen Schwester. Die Methoden, Eigenschaften, Funktionen und Funktionen sind Ihnen bereits vertraut. Die wichtigsten Abweichungen sind:
- Die synchrone API kann nur innerhalb eines Webworker-Kontexts verwendet werden, während die asynchrone API innerhalb und außerhalb eines Workers verwendet werden kann.
- Callbacks sind nicht mehr verfügbar. API-Methoden geben jetzt Werte zurück.
- Die globalen Methoden des Fensterobjekts (
requestFileSystem()
undresolveLocalFileSystemURL()
) werden zurequestFileSystemSync()
undresolveLocalFileSystemSyncURL()
.
Abgesehen von diesen Ausnahmen sind die APIs identisch. Alles klar, wir können loslegen.
Dateisystem anfordern
Eine Webanwendung erhält Zugriff auf das synchrone Dateisystem, indem sie innerhalb eines Web-Workers ein LocalFileSystemSync
-Objekt anfordert. Die requestFileSystemSync()
ist für den globalen Gültigkeitsbereich des Workers verfügbar:
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
Beachten Sie den neuen Rückgabewert, da wir jetzt die synchrone API verwenden, sowie das Fehlen von Erfolg- und Fehler-Callbacks.
Wie bei der normalen FileSystem API werden Methoden derzeit mit einem Präfix versehen:
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
Umgang mit Kontingenten
Derzeit ist es nicht möglich, ein PERSISTENT
-Kontingent in einem Worker-Kontext anzufordern. Ich empfehle, Kontingentprobleme außerhalb von Workers zu beheben.
Der Prozess könnte so aussehen:
- worker.js: Umschließen Sie FileSystem API-Code in
try/catch
, damit alleQUOTA_EXCEED_ERR
-Fehler abgefangen werden. - worker.js: Wenn du eine
QUOTA_EXCEED_ERR
empfängst, sende einepostMessage('get me more quota')
zurück an die Haupt-App. - main app: Führe den
window.webkitStorageInfo.requestQuota()
-Dance aus, wenn #2 empfangen wird. - main app: Nachdem der Nutzer mehr Kontingent gewährt hat, sende
postMessage('resume writes')
zurück an den Worker, um ihn über zusätzlichen Speicherplatz zu informieren.
Das ist eine ziemlich aufwendige Umgehung, aber sie sollte funktionieren. Weitere Informationen zur Verwendung von PERSISTENT
-Speicher mit der FileSystem API finden Sie unter Kontingent anfordern.
Mit Dateien und Verzeichnissen arbeiten
Die synchrone Version von getFile()
und getDirectory()
gibt jeweils FileEntrySync
und DirectoryEntrySync
zurück.
Mit dem folgenden Code wird beispielsweise eine leere Datei namens „log.txt“ im Stammverzeichnis erstellt.
var fileEntry = fs.root.getFile('log.txt', {create: true});
Mit dem folgenden Befehl wird ein neues Verzeichnis im Stammverzeichnis erstellt.
var dirEntry = fs.root.getDirectory('mydir', {create: true});
Fehlerbehebung
Wenn Sie noch nie Web Worker-Code debuggen mussten, beneide ich Sie! Es kann sehr schwierig sein, herauszufinden, was nicht funktioniert.
Da es in der synchronen Welt keine Fehler-Callbacks gibt, ist die Fehlerbehebung schwieriger, als sie sein sollte. Wenn wir die allgemeine Komplexität des Debuggens von Webworker-Code hinzufügen, sind Sie schnell frustriert. Es kann hilfreich sein, den gesamten relevanten Worker-Code in einen try/catch-Block einzubetten. Wenn Fehler auftreten, leite den Fehler mit postMessage()
an die Haupt-App weiter:
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);
}
Dateien, Blobs und ArrayBuffers übergeben
Als Webworker eingeführt wurden, war es nur möglich, Stringdaten in postMessage()
zu senden. Später akzeptierten Browser serialisierbare Daten, was bedeutete, dass ein JSON-Objekt übergeben werden konnte. In letzter Zeit akzeptieren einige Browser wie Chrome jedoch komplexere Datentypen, die mit dem Algorithmus für strukturierte Klone an postMessage()
übergeben werden.
Was bedeutet das? Das bedeutet, dass es viel einfacher ist, binäre Daten zwischen der Haupt-App und dem Worker-Thread zu übergeben. In Browsern, die das strukturierte Klonen für Worker unterstützen, können Sie typisierte Arrays, ArrayBuffer
, File
oder Blob
an Worker übergeben. Auch wenn es sich bei den Daten um eine Kopie handelt, ist die Möglichkeit, eine File
zu übergeben, ein Leistungsvorteil gegenüber dem vorherigen Ansatz, bei dem die Datei in Base64 codiert werden musste, bevor sie an postMessage()
übergeben wurde.
Im folgenden Beispiel wird eine vom Nutzer ausgewählte Liste von Dateien an einen speziellen Worker übergeben.
Der Worker gibt einfach die Dateiliste durch (einfach, um zu zeigen, dass die zurückgegebenen Daten tatsächlich eine FileList
sind), und die Haupt-App liest jede Datei als ArrayBuffer
.
Im Beispiel wird außerdem eine verbesserte Version der Inline-Webworker-Technologie verwendet, die im Artikel Grundlagen von Webworkern beschrieben wird.
<!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>
Dateien in einem Worker lesen
Es ist völlig in Ordnung, die asynchrone FileReader
API zum Lesen von Dateien in einem Worker zu verwenden. Es gibt jedoch eine bessere Lösung. In Workers gibt es eine synchrone API (FileReaderSync
), die das Lesen von Dateien optimiert:
Haupt-App:
<!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);
Wie erwartet sind Callbacks bei der synchronen FileReader
nicht mehr verfügbar. Dadurch wird die Anzahl der Callback-Verschachtelungen beim Lesen von Dateien vereinfacht. Stattdessen geben die readAs*-Methoden die gelesene Datei zurück.
Beispiel: Alle Einträge abrufen
In einigen Fällen ist die synchrone API für bestimmte Aufgaben viel übersichtlicher. Weniger Rückrufe sind schön und sorgen sicherlich für mehr Lesbarkeit. Der eigentliche Nachteil der synchronen API ergibt sich aus den Einschränkungen von Workern.
Aus Sicherheitsgründen werden Daten zwischen der aufrufenden App und einem Webworker-Thread nie weitergegeben. Daten werden immer vom Worker in den Worker kopiert, wenn postMessage()
aufgerufen wird.
Daher kann nicht jeder Datentyp übergeben werden.
Leider gehören FileEntrySync
und DirectoryEntrySync
derzeit nicht zu den zulässigen Typen. Wie können Sie Einträge wieder in die Anruf-App aufnehmen?
Eine Möglichkeit, diese Einschränkung zu umgehen, besteht darin, anstelle einer Liste von Einträgen eine Liste von filesystem: URLs zurückzugeben. filesystem:
URLs sind nur Strings und lassen sich daher ganz einfach weitergeben. Außerdem können sie mit resolveLocalFileSystemURL()
in Einträge in der Haupt-App aufgelöst werden. Dadurch gelangen Sie zu einem FileEntrySync
-/DirectoryEntrySync
-Objekt.
Haupt-App:
<!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);
}
};
Beispiel: Dateien mit XHR2 herunterladen
Ein gängiger Anwendungsfall für Worker besteht darin, mehrere Dateien mit XHR2 herunterzuladen und in das HTML5-Dateisystem zu schreiben. Das ist eine perfekte Aufgabe für einen Worker-Thread.
Im folgenden Beispiel wird nur eine Datei abgerufen und geschrieben. Sie können es jedoch so erweitern, dass eine Reihe von Dateien heruntergeladen wird.
Haupt-App:
<!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);
}
};
Fazit
Webworker sind eine unterausgeschöpfte und unterschätzte Funktion von HTML5. Die meisten Entwickler, mit denen ich spreche, benötigen diese zusätzlichen Rechenressourcen nicht. Sie können aber nicht nur für reine Berechnungen verwendet werden. Wenn Sie skeptisch sind (wie ich es war), hoffe ich, dass dieser Artikel Ihnen dabei geholfen hat, Ihre Meinung zu ändern. Das Auslagern von Vorgängen wie Laufwerksvorgängen (Dateisystem-API-Aufrufe) oder HTTP-Anfragen auf einen Worker ist eine naheliegende Lösung und hilft auch, Ihren Code zu strukturieren. Die HTML5-Datei-APIs in Workern eröffnen Webanwendungen ganz neue Möglichkeiten, die viele noch nicht kennen.