Introdução
A API FileSystem e os Web Workers do HTML5 são muito poderosos. A API FileSystem finalmente traz armazenamento hierárquico e E/S de arquivos para aplicativos da Web, e os workers trazem uma verdadeira "multitarefa" assíncrona para JavaScript. No entanto, quando você usa essas APIs juntas, é possível criar apps realmente interessantes.
Este tutorial fornece um guia e exemplos de código para aproveitar o FileSystem do HTML5 em um Web Worker. Ele pressupõe que você tenha conhecimento prático das duas APIs. Se você ainda não estiver com tudo pronto para começar ou tiver interesse em aprender mais sobre essas APIs, leia dois ótimos tutoriais que discutem os conceitos básicos: Conheça as APIs FileSystem e Noções básicas sobre web workers.
APIs síncronas e assíncronas
As APIs JavaScript assíncronas podem ser difíceis de usar. Eles são grandes. Eles são complexos. Mas o mais frustrante é que eles oferecem muitas oportunidades para que as coisas deem errado. A última coisa com que você quer lidar é a criação de camadas em uma API assíncrona complexa (FileSystem) em um mundo já assíncrono (Workers). A boa notícia é que a API FileSystem define uma versão síncrona para facilitar o uso de Web Workers.
Em sua maioria, a API síncrona é exatamente igual à sub-rede assíncrona. Os métodos, propriedades, atributos e funcionalidade serão familiares. As principais variações são:
- A API síncrona só pode ser usada em um contexto de worker da Web, enquanto a API assíncrona pode ser usada dentro e fora de um worker.
- Os callbacks foram desativados. Os métodos da API agora retornam valores.
- Os métodos globais no objeto de janela (
requestFileSystem()
eresolveLocalFileSystemURL()
) se tornamrequestFileSystemSync()
eresolveLocalFileSystemSyncURL()
.
Com exceção dessas exceções, as APIs são as mesmas. Tudo pronto!
Como solicitar um sistema de arquivos
Um aplicativo da Web obtém acesso ao sistema de arquivos síncrono solicitando um objeto LocalFileSystemSync
de dentro de um web worker. O requestFileSystemSync()
é exposto ao escopo global do worker:
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
Observe o novo valor de retorno agora que estamos usando a API síncrona, bem como a ausência de callbacks de sucesso e erro.
Assim como na API FileSystem normal, os métodos têm um prefixo no momento:
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
Como lidar com a cota
No momento, não é possível solicitar a cota PERSISTENT
em um contexto de worker. Recomendo que você cuide dos problemas de cota fora dos workers.
O processo pode ser parecido com este:
- worker.js: una qualquer código da API FileSystem em um
try/catch
para que todos os errosQUOTA_EXCEED_ERR
sejam detectados. - worker.js: se você capturar uma
QUOTA_EXCEED_ERR
, envie umapostMessage('get me more quota')
de volta ao app principal. - app principal: faça a dança
window.webkitStorageInfo.requestQuota()
quando receber a mensagem 2. - app principal: depois que o usuário conceder mais cota, envie
postMessage('resume writes')
de volta ao worker para informar sobre o espaço de armazenamento extra.
Essa é uma solução alternativa um tanto complexa, mas deve funcionar. Consulte Como solicitar cota para mais informações sobre o uso do armazenamento PERSISTENT
com a API FileSystem.
Como trabalhar com arquivos e diretórios
A versão síncrona de getFile()
e getDirectory()
retorna um FileEntrySync
e um DirectoryEntrySync
, respectivamente.
Por exemplo, o código a seguir cria um arquivo vazio chamado "log.txt" no diretório raiz.
var fileEntry = fs.root.getFile('log.txt', {create: true});
O comando a seguir cria um novo diretório na pasta raiz.
var dirEntry = fs.root.getDirectory('mydir', {create: true});
Como processar os erros
Se você nunca teve que depurar código do Web Worker, invejo de você! Pode ser muito difícil descobrir o que está errado.
A falta de callbacks de erro no mundo síncrono torna os problemas mais complicados do que deveriam ser. Se adicionarmos a complexidade geral de depuração do código do Web Worker,
você vai ficar frustrado em pouco tempo. Uma coisa que pode facilitar a vida é agrupar todo
o código relevante do worker em um try/catch. Em seguida, se ocorrer algum erro, encaminhe
o erro para o app principal usando 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);
}
Como transmitir Files, Blobs e ArrayBuffers
Quando os Web Workers surgiram, eles só permitiam que dados de string fossem
enviados em postMessage()
. Mais tarde, os navegadores começaram a aceitar dados serializáveis, o que
significava que era possível transmitir um objeto JSON. Recentemente, no entanto, alguns navegadores, como o Chrome, aceitam tipos de dados mais complexos para serem transmitidos por postMessage()
usando o algoritmo de clone estruturado.
O que isso quer dizer? Isso significa que é muito mais fácil transmitir
dados binários entre o app principal e a linha de execução do worker. Os navegadores que oferecem suporte a clonagem estruturada
para workers permitem que você transmita matrizes tipadas, ArrayBuffer
s, File
s ou Blob
s
para workers. Embora os dados ainda sejam uma cópia, poder transmitir um File
significa
um benefício de desempenho em relação à abordagem anterior, que envolvia a base64 do arquivo
antes de transmiti-lo para postMessage()
.
O exemplo a seguir passa uma lista de arquivos selecionados pelo usuário para um worker dedicado.
O worker simplesmente passa pela lista de arquivos (é simples mostrar que os dados retornados
são, na verdade, um FileList
) e o app principal lê cada arquivo como um ArrayBuffer
.
O exemplo também usa uma versão aprimorada da técnica de Web Worker inline descrita em Noções básicas de 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>
Como ler arquivos em um worker
É perfeitamente aceitável usar a API FileReader
assíncrona para ler arquivos em um worker. No entanto, há uma maneira melhor. Nos workers, há uma
API síncrona (FileReaderSync
) que simplifica a leitura de arquivos:
App principal:
<!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);
Como esperado, os callbacks foram removidos com o FileReader
síncrono. Isso simplifica
a quantidade de aninhamento de callbacks ao ler arquivos. Em vez disso, os métodos readAs*
retornam o arquivo lido.
Exemplo: buscar todas as entradas
Em alguns casos, a API síncrona é muito mais simples para determinadas tarefas. Menos callbacks são bons e certamente tornam as coisas mais legíveis. A real desvantagem da API síncrona ocorre nas limitações dos workers.
Por motivos de segurança, os dados entre o app de chamada e uma linha de execução do Web Worker nunca são
compartilhados. Os dados são sempre copiados para e do worker quando postMessage()
é chamado.
Como resultado, nem todos os tipos de dados podem ser transmitidos.
No momento, FileEntrySync
e DirectoryEntrySync
não se enquadram
nos tipos aceitos. Como você pode fazer com que as entradas voltem ao app de chamada?
Uma maneira de contornar a limitação é retornar uma lista de filesystem: URLs em vez de uma lista de entradas. Os URLs filesystem:
são apenas strings,
portanto, são muito fáceis de transmitir. Além disso, elas podem ser resolvidas para
entradas no app principal usando resolveLocalFileSystemURL()
. Isso retorna
a um objeto FileEntrySync
/DirectoryEntrySync
.
App principal:
<!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);
}
};
Exemplo: fazer o download de arquivos usando XHR2
Um caso de uso comum para workers é fazer o download de vários arquivos usando XHR2 e gravar esses arquivos no FileSystem do HTML5. Essa é uma tarefa perfeita para uma linha de execução de worker.
O exemplo a seguir busca e grava apenas um arquivo, mas é possível expandir a imagem para fazer o download de um conjunto de arquivos.
App principal:
<!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);
}
};
Conclusão
Os Web Workers são um recurso subutilizado e pouco apreciado do HTML5. A maioria dos desenvolvedores com quem converso não precisa dos benefícios computacionais extras, mas eles podem ser usados para mais do que apenas a computação pura. Se você tem dúvidas (como eu tinha), espero que este artigo tenha ajudado a mudar de ideia. O descarregamento de operações de disco (chamadas de API do Filesystem) ou solicitações HTTP para um worker é uma opção natural e também ajuda a compartimentar seu código. As APIs de arquivo HTML5 dentro do Workers abrem uma nova lata de coisas incríveis para aplicativos da Web que muitas pessoas ainda não exploram.