L'API FileSystem sincrona per i worker

Introduzione

L'API FileSystem HTML5 e i web worker sono molto efficaci per loro. L'API FileSystem porta infine l'archiviazione gerarchica e l'I/O dei file alle applicazioni web, mentre i worker portano il vero "multi-threading" asincrono a JavaScript. Tuttavia, se le utilizzi insieme, puoi creare app davvero interessanti.

Questo tutorial fornisce una guida ed esempi di codice per sfruttare il file system HTML5 all'interno di un web worker. Si presume una conoscenza pratica di entrambe le API. Se non vuoi iniziare subito o se vuoi saperne di più su queste API, leggi due ottimi tutorial che illustrano le nozioni di base: Esplorazione delle API FileSystem e Nozioni di base sui web worker.

API sincrone e asincrone

Le API JavaScript asincrone possono essere difficili da utilizzare. Sono grandi. Sono complessi. La cosa più frustrante è che offrono svariate opportunità di errore. L'ultima cosa che vuoi fare è applicare il layering a un'API asincrona complessa (FileSystem) in un mondo già asincrono (Workers). La buona notizia è che l'API FileSystem definisce una versione sincrona per semplificare i problemi nei worker web.

Per la maggior parte, l'API sincrona è esattamente uguale alla sua cugina asincrona. I metodi, le proprietà, le caratteristiche e le funzionalità ti saranno familiari. Le principali deviazioni sono:

  • L'API sincrona può essere utilizzata solo all'interno di un contesto di worker web, mentre l'API asincrona può essere utilizzata all'interno e all'esterno di un worker.
  • Le chiamate di ritorno non sono disponibili. I metodi dell'API ora restituiscono valori.
  • I metodi globali dell'oggetto window (requestFileSystem() e resolveLocalFileSystemURL()) diventano requestFileSystemSync() e resolveLocalFileSystemSyncURL().

A parte queste eccezioni, le API sono le stesse. Ok, possiamo iniziare.

Richiesta di un file system

Un'applicazione web ottiene l'accesso al file system sincrono richiedendo un oggetto LocalFileSystemSync da un web worker. requestFileSystemSync() è esposto all'ambito globale del worker:

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

Notare il nuovo valore restituito ora che utilizziamo l'API sincrona, nonché l'assenza di callback di successo ed errore.

Come per la normale API FileSystem, al momento i metodi sono preceduti da un prefisso:

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

Gestione della quota

Al momento, non è possibile richiedere una quota PERSISTENT in un contesto di worker. Ti consiglio di occuparti dei problemi di quota al di fuori di Workers. La procedura potrebbe avere il seguente aspetto:

  1. worker.js: racchiudi qualsiasi codice dell'API FileSystem in un try/catch in modo da rilevare eventuali errori QUOTA_EXCEED_ERR.
  2. worker.js: se ottieni un QUOTA_EXCEED_ERR, invia un postMessage('get me more quota') all'app principale.
  3. app principale: esegui la danza di window.webkitStorageInfo.requestQuota() quando viene ricevuto il messaggio 2.
  4. app principale: dopo che l'utente ha concesso una quota maggiore, invia postMessage('resume writes') al worker per informarlo della disponibilità di spazio di archiviazione aggiuntivo.

Si tratta di una soluzione alternativa piuttosto complessa, ma dovrebbe funzionare. Per saperne di più sull'utilizzo dello spazio di archiviazione PERSISTENT con l'API FileSystem, consulta la sezione relativa alla richiesta di quota.

Lavorare con file e directory

La versione sincrona di getFile() e getDirectory() restituisce rispettivamente un FileEntrySync e un DirectoryEntrySync.

Ad esempio, il seguente codice crea un file vuoto denominato "log.txt" nella directory principale.

var fileEntry = fs.root.getFile('log.txt', {create: true});

Il seguente comando crea una nuova directory nella cartella principale.

var dirEntry = fs.root.getDirectory('mydir', {create: true});

Gestione degli errori

Se non hai mai dovuto eseguire il debug del codice di un web worker, ti invidio. Può essere un vero problema capire cosa non va.

La mancanza di callback di errore nel mondo sincrono rende la gestione dei problemi più complicata di quanto dovrebbe essere. Se aggiungiamo la complessità generale del debug del codice dei web worker, sarai frustrato in pochissimo tempo. Una cosa che può semplificare la vita è il wrapping di tutto il codice worker pertinente in una prova/catch. Se si verificano errori, inoltrali all'app principale utilizzando 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);
}

Trasmissione di file, blob e ArrayBuffer

Quando i web worker sono stati introdotti per la prima volta, consentivano di inviare solo dati di stringa in postMessage(). In seguito, i browser hanno iniziato ad accettare dati serializzabili, il che significava che era possibile passare un oggetto JSON. Tuttavia, di recente alcuni browser come Chrome accettano tipi di dati più complessi da trasmettere tramite postMessage() utilizzando l'algoritmo di clonazione strutturata.

Che cosa significa davvero? Ciò significa che è molto più facile trasmettere i dati binari tra l'app principale e il thread di lavoro. I browser che supportano la clonazione strutturata per i worker consentono di passare array digitati, ArrayBuffer, File o Blob ai worker. Sebbene i dati siano ancora una copia, la possibilità di passare un File comporta un vantaggio in termini di prestazioni rispetto all'approccio precedente, che prevedeva la codifica base64 del file prima di passarlo a postMessage().

L'esempio seguente passa un elenco di file selezionato dall'utente a un worker dedicato. Il worker semplicemente passa attraverso l'elenco dei file (semplice per mostrare che i dati restituiti sono in realtà un FileList) e l'app principale legge ogni file come ArrayBuffer.

L'esempio utilizza anche una versione migliorata della tecnica di Web Worker in linea описана в Nozioni di base sui 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>

Lettura dei file in un worker

È perfettamente accettabile utilizzare l'API asincrona FileReader per leggere i file in un worker. Tuttavia, esiste un modo migliore. In Workers è disponibile un'API sincrona (FileReaderSync) che semplifica la lettura dei file:

App 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);

Come previsto, i callback non sono più disponibili con FileReader sincrono. Ciò semplifica la quantità di nidificazione dei callback durante la lettura dei file. I metodi readAs* invece restituiscono il file letto.

Esempio: recupero di tutte le voci

In alcuni casi, l'API sincrona è molto più chiara per determinate attività. Meno chiamate di callback sono utili e rendono il codice più leggibile. Il vero svantaggio dell'API sincrona deriva dai limiti dei worker.

Per motivi di sicurezza, i dati tra l'app chiamante e un thread dei worker web non vengono mai condivisi. I dati vengono sempre copiati verso e dal worker quando viene chiamato postMessage(). Di conseguenza, non è possibile trasmettere tutti i tipi di dati.

Purtroppo, al momento FileEntrySync e DirectoryEntrySync non rientrano tra i tipi accettati. Come puoi ripristinare le voci nell'app Telefono? Un modo per aggirare la limitazione è restituire un elenco di filesystem: URL anziché un elenco di voci. filesystem: Gli URL sono solo stringhe, quindi sono molto facili da trasmettere. Inoltre, possono essere risolti in voci nell'app principale utilizzando resolveLocalFileSystemURL(). In questo modo, tornerai a un oggetto FileEntrySync/DirectoryEntrySync.

App 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);
    }
};

Esempio: download di file utilizzando XHR2

Un caso d'uso comune per i worker è scaricare una serie di file utilizzando XHR2 e scrivere questi file nel file system HTML5. Questa è un'attività perfetta per un thread di lavoro.

L'esempio seguente recupera e scrive un solo file, ma puoi immaginarne l'espansione per scaricare un insieme di file.

App 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);
    }
};

Conclusione

I web worker sono una funzionalità sottoutilizzata e sottovalutata di HTML5. La maggior parte degli sviluppatori con cui parlo non ha bisogno dei vantaggi computazionali aggiuntivi, ma possono essere utilizzati per qualcosa di più del semplice calcolo. Se sei scettico (come lo ero io), spero che questo articolo ti abbia aiutato a cambiare idea. Il trasferimento di operazioni come le operazioni sul disco (chiamate all'API Filesystem) o le richieste HTTP a un worker è una soluzione naturale e aiuta anche a suddividere il codice. Le API File HTML5 inнутри dei worker aprono un mondo completamente nuovo di funzionalità per le app web che molti non hanno ancora esplorato.