Synchroniczny interfejs API FileSystem dla instancji roboczych

Wprowadzenie

Interfejs FileSystem API w HTML5 oraz Web Workers są bardzo potężne. Interfejs FileSystem API w końcu udostępnia hierarchiczną pamięć masową i operacje wejścia/wyjścia na pliki aplikacjom internetowym, a Workery zapewniają prawdziwe asynchroniczne wielowątkowe działanie w JavaScript. Jednak gdy użyjesz tych interfejsów API razem, możesz tworzyć naprawdę interesujące aplikacje.

W tym samouczku znajdziesz przewodnik i przykłady kodu dotyczące korzystania z interfejsu FileSystem w Web Worker. Zakładamy, że znasz oba interfejsy API. Jeśli nie jesteś jeszcze gotowy na zagłębianie się w temat lub chcesz dowiedzieć się więcej o tych interfejsach API, zapoznaj się z 2 przydatnymi samouczkami, które omawiają podstawy: Exploring the FileSystem APIs (Poznawanie interfejsów FileSystem API) i Basics of Web Workers (Podstawy dotyczące Web Workers).

Interfejsy API synchroniczne i asynchroniczne

Interfejsy API JavaScriptu asynchronicznego mogą być trudne w użyciu. Są duże. Są złożone. Najbardziej frustrujące jest jednak to, że dają one wiele możliwości popełnienia błędu. Ostatnią rzeczą, z którą chcesz się zmierzyć, jest dodanie warstwy do złożonego asynchronicznego interfejsu API (FileSystem) w świecie, który jest już asynchroniczny (Workers)! Dobra wiadomość jest taka, że interfejs FileSystem API definiuje wersję synchroniczną, która ułatwia pracę z elementami Web Workers.

W większości przypadków interfejs API synchroniczny działa tak samo jak jego odpowiednik asynchroniczny. Metody, właściwości, funkcje i funkcjonalność będą znajome. Najważniejsze odstępstwa to:

  • Interfejsu synchronicznego można używać tylko w kontekście Web Worker, natomiast interfejsu asynchronicznego można używać zarówno w Workerze, jak i poza nim.
  • Wywołania zwrotne są wyłączone. Metody interfejsu API zwracają teraz wartości.
  • Metody globalne obiektu window (requestFileSystem()resolveLocalFileSystemURL()) stają się odpowiednio requestFileSystemSync()resolveLocalFileSystemSyncURL().

Poza tymi wyjątkami interfejsy API są takie same. OK, wszystko gotowe.

Przesyłanie prośby o system plików

Aplikacja internetowa uzyskuje dostęp do synchronicznej pamięci masowej, wysyłając żądanie obiektu LocalFileSystemSync z poziomu Web Workera. Element requestFileSystemSync() jest widoczny w zakresie globalnym:

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

Zwróć uwagę na nową wartość zwracaną, ponieważ używamy teraz interfejsu API synchronicznego, a także na brak funkcji wywołujących w przypadku sukcesu i błędu.

Podobnie jak w przypadku zwykłego interfejsu FileSystem API, metody mają obecnie przedrostek:

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

Praca z limitem

Obecnie nie można poprosić o limit PERSISTENT w kontekście Workera. Zalecamy rozwiązanie problemów z limitem poza usługą Workers. Proces może wyglądać mniej więcej tak:

  1. worker.js: owiń kod interfejsu FileSystem API w funkcji try/catch, aby wykrywać błędy QUOTA_EXCEED_ERR.
  2. worker.js: jeśli uda Ci się złapać QUOTA_EXCEED_ERR, prześlij postMessage('get me more quota') z powrotem do aplikacji głównej.
  3. główna aplikacja: wykonaj czynności window.webkitStorageInfo.requestQuota(), gdy otrzymasz #2.
  4. główna aplikacja: gdy użytkownik przyzna więcej miejsca, wyślij postMessage('resume writes') do pracownika, aby poinformować go o dodatkowym miejscu na dane.

To dość skomplikowane obejście, ale powinno zadziałać. Więcej informacji o używaniu miejsca na dane PERSISTENT w ramach interfejsu FileSystem API znajdziesz w sekcji Prośba o limit.

Praca z plikami i katalogami

Wersja synchroniczna funkcji getFile()getDirectory() zwraca odpowiednio wartości FileEntrySyncDirectoryEntrySync.

Na przykład ten kod tworzy pusty plik o nazwie „log.txt” w katalogu głównym.

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

Poniższy kod tworzy nowy katalog w folderze głównym.

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

Obsługa błędów

Jeśli nigdy nie musiałeś debugować kodu Web Worker, zazdroszczę Ci! Może być naprawdę trudno ustalić, co jest nie tak.

Brak wywołań zwrotnych błędów w środowisku synchronicznym sprawia, że rozwiązywanie problemów jest trudniejsze, niż powinno być. Jeśli dodamy ogólną złożoność debugowania kodu Web Worker, szybko się zniechęcisz. Jednym ze sposobów ułatwienia sobie pracy jest umieszczenie całego kodu Workera w bloku try/catch. Następnie, jeśli wystąpią jakiekolwiek błędy, prześlij je do aplikacji głównej za pomocą 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);
}

Przekazywanie obiektów File, Blob i ArrayBuffer

Gdy funkcje web worker pojawiły się po raz pierwszy, pozwalały one na wysyłanie tylko danych typu string w funkcji postMessage(). Później przeglądarki zaczęły akceptować dane nadające się do serializacji, co oznaczało, że można było przekazywać obiekty JSON. Ostatnio jednak niektóre przeglądarki, np. Chrome, akceptują bardziej złożone typy danych, które są przekazywane przez postMessage() za pomocą algorytmu uporządkowanego klona.

Co to tak naprawdę oznacza? Oznacza to, że przekazywanie danych binarnych między aplikacją główną a wątkiem Worker jest znacznie łatwiejsze. Przeglądarki obsługujące strukturalne klonowanie w przypadku Workers umożliwiają przekazywanie do Workers tablic typowanych, obiektów ArrayBuffer, File lub Blob. Chociaż dane są nadal kopią, możliwość przekazania File oznacza wzrost wydajności w porównaniu z poprzednim podejściem, które polegało na zaszyfrowaniu pliku za pomocą kodu base64 przed przekazaniem go do postMessage().

W tym przykładzie wybrana przez użytkownika lista plików jest przekazywana do dedykowanego interfejsu Worker. Worker po prostu przechodzi przez listę plików (aby pokazać zwrócone dane, które są w rzeczywistości FileList), a aplikacja główna odczytuje każdy plik jako ArrayBuffer.

Przykład korzysta też z ulepszonej wersji techniki osadzonego Web Workera opisanej w artykule Podstawy Web Workera.

<!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>

Czytanie plików w procesie Worker

Do odczytu plików w Workerze można używać asynchronicznego interfejsu API FileReader. Jest jednak lepszy sposób. W Workers jest dostępny asynchroniczny interfejs API (FileReaderSync), który upraszcza odczytywanie plików:

Aplikacja główna:

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

Zgodnie z oczekiwaniami wywołania zwrotne nie są już dostępne w przypadku synchronicznych FileReader. Upraszcza to ilość zagnieżdżonych wywołań zwrotnych podczas odczytu plików. Zamiast tego metody readAs* zwracają odczytany plik.

Przykład: pobieranie wszystkich wpisów

W niektórych przypadkach interfejs API synchroniczny jest znacznie czystszy w przypadku niektórych zadań. Mniej wywołań zwrotnych jest lepsze i zdecydowanie ułatwia czytelność. Prawdziwą wadą synchronicznego interfejsu API są ograniczenia dotyczące Workers.

Ze względów bezpieczeństwa dane między aplikacją wywołującą a wątkiem Web Worker nigdy nie są udostępniane. Podczas wywołania metody postMessage() dane są zawsze kopiowane do i z Workera. W związku z tym nie wszystkie typy danych mogą być przekazywane.

Niestety FileEntrySyncDirectoryEntrySync nie należą obecnie do akceptowanych typów. Jak więc można przywrócić wpisy do aplikacji do połączeń? Jednym ze sposobów na obejście tego ograniczenia jest zwrócenie listy filesystem: URLs zamiast listy wpisów. filesystem: Adresy URL to tylko ciągi znaków, więc można je łatwo przekazywać. Ponadto można je rozwiązywać na podstawie wpisów w aplikacji głównej za pomocą elementu resolveLocalFileSystemURL(). W ten sposób wracasz do obiektu FileEntrySync/DirectoryEntrySync.

Aplikacja główna:

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

Przykład: pobieranie plików za pomocą XHR2

Typowym zastosowaniem wątków jest pobieranie wielu plików za pomocą XHR2 i zapisywanie ich w systemie plików HTML5. To idealne zadanie dla wątku Worker.

W tym przykładzie pobierany i zapisywany jest tylko jeden plik, ale możesz go rozszerzyć, aby pobrać zestaw plików.

Aplikacja główna:

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

Podsumowanie

Web Workers to mało wykorzystywana i niedoceniana funkcja HTML5. Większość deweloperów, z którymi rozmawiam, nie potrzebuje dodatkowych możliwości obliczeniowych, ale można ich używać do innych celów niż tylko do obliczeń. Jeśli masz wątpliwości (tak jak ja), mam nadzieję, że ten artykuł pomoże Ci zmienić zdanie. Przenoszenie operacji na dysku (wywołania interfejsu API systemu plików) lub żądań HTTP do Workera jest naturalnym rozwiązaniem i pomaga w segregacji kodu. Interfejsy API plików HTML5 w Workerach otwierają przed aplikacjami internetowymi zupełnie nowe możliwości, których wielu programistów jeszcze nie odkryło.