Introduction
L'API FileSystem et les nœuds de calcul Web HTML5 sont extrêmement puissants à leur propre égard. L'API FileSystem apporte enfin un stockage hiérarchique et des E/S de fichiers aux applications Web, et les workers apportent un véritable "multithreading" asynchrone à JavaScript. Toutefois, lorsque vous utilisez ces API ensemble, vous pouvez créer des applications vraiment intéressantes.
Ce tutoriel fournit un guide et des exemples de code pour exploiter le FileSystem HTML5 dans un Web Worker. Il suppose une connaissance pratique des deux API. Si vous n'êtes pas encore prêt à vous lancer ou si vous souhaitez en savoir plus sur ces API, consultez deux excellents tutoriels qui abordent les principes de base : Explorer les API FileSystem et Principes de base des Web Workers.
API synchrones et asynchrones
Les API JavaScript asynchrones peuvent être difficiles à utiliser. Ils sont volumineux. Elles sont complexes. Mais le plus frustrant, c'est qu'elles offrent de nombreuses possibilités en cas de problème. La dernière chose que vous voulez gérer est la superposition d'une API asynchrone complexe (FileSystem) dans un monde déjà asynchrone (travailleurs) ! Bonne nouvelle : l'API FileSystem définit une version synchrone afin de simplifier la tâche des nœuds de calcul Web.
Dans la plupart des cas, l'API synchrone est exactement la même que son cousine asynchrone. Les méthodes, propriétés, fonctionnalités et fonctionnalités vous seront familières. Voici les principales divergences :
- L'API synchrone ne peut être utilisée que dans un contexte de nœud de calcul Web, tandis que l'API asynchrone peut être utilisée dans et en dehors d'un nœud de calcul.
- Les rappels sont obsolètes. Les méthodes d'API renvoient désormais des valeurs.
- Les méthodes globales de l'objet de fenêtre (
requestFileSystem()
etresolveLocalFileSystemURL()
) deviennentrequestFileSystemSync()
etresolveLocalFileSystemSyncURL()
.
En dehors de ces exceptions, les API sont identiques. OK, c'est bon.
Demander un système de fichiers
Une application Web obtient l'accès au système de fichiers synchrone en demandant un objet LocalFileSystemSync
à partir d'un nœud de calcul Web. requestFileSystemSync()
est exposé au champ d'application global du worker :
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
Notez la nouvelle valeur renvoyée maintenant que nous utilisons l'API synchrone, ainsi que l'absence de rappels de réussite et d'erreur.
Comme pour l'API FileSystem standard, les méthodes sont actuellement préfixées :
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
Gérer les quotas
Pour le moment, il n'est pas possible de demander un quota PERSISTENT
dans un contexte de worker. Je vous recommande de résoudre les problèmes de quota en dehors de Workers.
Le processus peut se présenter comme suit :
- worker.js: encapsulez le code de l'API FileSystem dans un
try/catch
afin de détecter les erreursQUOTA_EXCEED_ERR
. - worker.js : si vous détectez un
QUOTA_EXCEED_ERR
, renvoyez unpostMessage('get me more quota')
à l'application principale. - application principale : effectuez la danse
window.webkitStorageInfo.requestQuota()
lorsque le numéro 2 est reçu. - application principale : une fois que l'utilisateur a accordé plus de quota, renvoyez
postMessage('resume writes')
au nœud de calcul pour l'informer de l'espace de stockage supplémentaire.
Il s'agit d'une solution de contournement assez complexe, mais elle devrait fonctionner. Pour en savoir plus sur l'utilisation du stockage PERSISTENT
avec l'API FileSystem, consultez la section Demander un quota.
Travailler avec des fichiers et des répertoires
La version synchrone de getFile()
et getDirectory()
renvoie un FileEntrySync
et un DirectoryEntrySync
, respectivement.
Par exemple, le code suivant crée un fichier vide appelé "log.txt" dans le répertoire racine.
var fileEntry = fs.root.getFile('log.txt', {create: true});
La commande suivante crée un répertoire dans le dossier racine.
var dirEntry = fs.root.getDirectory('mydir', {create: true});
Traiter les erreurs
Si vous n'avez jamais eu à déboguer du code de Web Worker, je vous envie ! Il peut être très difficile de déterminer le problème.
L'absence de rappels d'erreur dans le monde synchrone rend la gestion des problèmes plus délicate qu'elle ne devrait l'être. Si nous ajoutons la complexité générale du débogage du code du Web Worker, vous serez rapidement frustré. Une chose qui peut vous faciliter la tâche est d'encapsuler tout le code de vos nœuds de calcul pertinent dans une méthode try/catch. Ensuite, si des erreurs se produisent, transmettez-les à l'application principale à l'aide de 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);
}
Transmettre des fichiers, des blobs et des ArrayBuffers
Lorsque les Web Workers sont arrivés sur la scène, ils n'ont autorisé l'envoi de données de chaîne que dans postMessage()
. Plus tard, les navigateurs ont commencé à accepter les données sérialisables, ce qui signifie qu'il était possible de transmettre un objet JSON. Toutefois, récemment, certains navigateurs tels que Chrome acceptent que des types de données plus complexes soient transmis via postMessage()
à l'aide de l'algorithme de clonage structuré.
Qu'est-ce que cela signifie réellement ? Cela signifie qu'il est beaucoup plus facile de transmettre des données binaires entre l'application principale et le thread Worker. Les navigateurs compatibles avec le clonage structuré pour les nœuds de calcul vous permettent de transmettre des tableaux typés, des ArrayBuffer
, des File
ou des Blob
dans les nœuds de calcul. Bien que les données soient toujours une copie, la possibilité de transmettre un File
offre un avantage en termes de performances par rapport à l'ancienne approche, qui consistait à encoder le fichier en base64 avant de le transmettre à postMessage()
.
L'exemple suivant transmet une liste de fichiers sélectionnée par l'utilisateur à un nœud de calcul dédié.
Le nœud de calcul parcourt simplement la liste de fichiers (pour montrer que les données renvoyées sont en fait un FileList
) et l'application principale lit chaque fichier en tant que ArrayBuffer
.
L'exemple utilise également une version améliorée de la technique de Web Worker intégré décrite dans la section Principes de base des Web Workers.
<!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>
Lire des fichiers dans un nœud de calcul
Il est tout à fait acceptable d'utiliser l'API FileReader
asynchrone pour lire des fichiers dans un nœud de calcul. Toutefois, il existe une meilleure solution. Dans les nœuds de calcul, une API synchrone (FileReaderSync
) simplifie la lecture des fichiers :
Application principale:
<!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);
Comme prévu, les rappels ont disparu avec FileReader
synchrone. Cela simplifie le nombre d'imbrications de rappels lors de la lecture de fichiers. À la place, les méthodes readAs* renvoient le fichier de lecture.
Exemple : Récupérer toutes les entrées
Dans certains cas, l'API synchrone est beaucoup plus claire pour certaines tâches. Moins de rappels est une bonne chose et rend les choses plus lisibles. Le véritable inconvénient de l'API synchrone réside dans les limites des nœuds de calcul.
Pour des raisons de sécurité, les données entre l'application appelante et un thread de Web Worker ne sont jamais partagées. Les données sont toujours copiées vers et depuis le nœud de calcul lorsque postMessage()
est appelé.
Par conséquent, tous les types de données ne peuvent pas être transmis.
Malheureusement, FileEntrySync
et DirectoryEntrySync
ne font pas partie des types acceptés pour le moment. Comment pouvez-vous récupérer les entrées dans l'application d'appel ?
Pour contourner cette limitation, vous pouvez renvoyer une liste d'URL de système de fichiers au lieu d'une liste d'entrées. Les URL filesystem:
ne sont que des chaînes et sont donc très faciles à transmettre. De plus, elles peuvent être résolues en entrées dans l'application principale à l'aide de resolveLocalFileSystemURL()
. Vous revenez alors à un objet FileEntrySync
/DirectoryEntrySync
.
Application principale :
<!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);
}
};
Exemple: Télécharger des fichiers à l'aide de XHR2
Un cas d'utilisation courant des travailleurs consiste à télécharger un ensemble de fichiers à l'aide de XHR2 et à les écrire dans le système de fichiers HTML5. C'est une tâche parfaite pour un thread de travail.
L'exemple suivant n'extrait et n'écrit qu'un seul fichier, mais vous pouvez l'étendre pour télécharger un ensemble de fichiers.
Application principale :
<!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);
}
};
Conclusion
Les Web Workers sont une fonctionnalité sous-utilisée et sous-estimée de HTML5. La plupart des développeurs avec lesquels je discute n'ont pas besoin des avantages de calcul supplémentaires, mais ils peuvent être utilisés pour plus que de simples calculs. Si vous êtes sceptique (comme je l'étais), j'espère que cet article vous aura convaincu. Décharger des éléments tels que des opérations de disque (appels d'API de système de fichiers) ou des requêtes HTTP vers un worker est tout à fait approprié et permet également de compartimenter votre code. Les API de fichiers HTML5 dans les travailleurs ouvrent un tout nouveau monde d'applications Web que beaucoup de gens n'ont pas exploré.