Synchroniczny interfejs API FileSystem dla instancji roboczych

Wprowadzenie

Interfejs FileSystem API w HTML5 oraz Web Workers są bardzo potężne. Interfejs FileSystem API na koniec wprowadza do aplikacji internetowych hierarchiczną pamięć masową i wejścia/wyjście plików, a Workers wprowadzają prawdziwie asynchroniczne „wielowątkowości” do JavaScriptu. Jednak gdy użyjesz tych interfejsów API razem, możesz tworzyć naprawdę interesujące aplikacje.

Ten samouczek zawiera przewodnik i przykłady kodu związane z używaniem obiektu FileSystem HTML5 w środowisku Web Worker. Zakładamy, że znasz oba interfejsy API. Jeśli nie jesteś jeszcze gotowy, aby zacząć, 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).

Synchroniczne a asynchroniczne interfejsy API

Interfejsy JavaScript API asynchroniczne 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ą, jaką musisz się zająć, jest nałożenie złożonego asynchronicznego interfejsu API (FileSystem) w już asynchronicznym świecie (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 synchroniczny interfejs API jest dokładnie taki sam jak jego asynchroniczny kuzyn. Metody, właściwości, funkcje i funkcje będą znajome. Główne odchylenia to:

  • Synchronizacji interfejsu API można używać tylko w kontekście instancji roboczej, a asynchronicznego interfejsu API – zarówno w instancji roboczej, jak i poza nią.
  • Nie można oddzwonić. Metody interfejsu API zwracają teraz wartości.
  • Metody globalne w obiekcie window (requestFileSystem() i resolveLocalFileSystemURL()) zmieniają się w requestFileSystemSync() i 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 synchronicznego systemu plików, wysyłając żądanie do obiektu LocalFileSystemSync z poziomu instancji roboczej. Element requestFileSystemSync() jest widoczny w zakresie globalnym:

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

Zwróć uwagę na nową wartość zwrotną, ponieważ korzystamy z synchronicznego interfejsu API. Zwróć uwagę na to, że nie wystąpiło pomyślne i błędne wywołania zwrotne.

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 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 świecie synchronicznym utrudnia rozwiązywanie problemów. 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 w Workerze 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. Dane są nadal kopią, ale możliwość przekazania File oznacza wzrost wydajności w porównaniu z poprzednim podejściem, które wymagało zaszyfrowania pliku w formacie 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

Używanie asynchronicznego interfejsu API FileReader do odczytu plików w komponencie Worker jest całkowicie dozwolone. Jest jednak lepszy sposób. Dostępny jest synchroniczny interfejs API (FileReaderSync), który upraszcza odczytywanie plików:

Główna aplikacja:

<!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 zostały usunięte z synchronicznego FileReader. Ułatwia to zagnieżdżanie 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ń. Mniejsza liczba wywołań zwrotnych jest ładna i z pewnością bardziej czytelna. 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. Dane są zawsze kopiowane do i z instancji roboczej po wywołaniu funkcji postMessage(). W rezultacie 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.

Poniższy przykład pobiera i zapisuje tylko 1 plik, ale możesz rozwinąć go, aby pobrać zbiór 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 rzadko używana 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 nie wierzysz w to (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, które pomaga również podzielić kod na części. 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.