Synchroniczny interfejs API FileSystem dla instancji roboczych

Wstęp

Interfejsy FileSystem API i narzędzia Web Workers w języku HTML5 są pod wieloma względami niezwykle przydatne. Interfejs FileSystem API zapewnia w końcu hierarchiczną pamięć masową i wejście-wyjście plików w aplikacjach internetowych, a zasoby robocze oferują prawdziwe asynchroniczne „wielowątkowość” w języku JavaScript. Jednak łącząc te interfejsy API, można tworzyć naprawdę interesujące aplikacje.

Ten samouczek zawiera przewodnik i przykłady kodu dotyczące wykorzystania systemu plików HTML5 w instancjach roboczych Web Worker. Opiera się na praktycznej znajomości obu interfejsów API. Jeśli nie chcesz jeszcze zagłębiać się w szczegóły interfejsów API lub chcesz dowiedzieć się więcej o tych interfejsach, przeczytaj 2 świetne samouczki z podstawowymi informacjami: Poznawanie interfejsów API FileSystem i Podstawy Web Workers.

Synchroniczne a asynchroniczne interfejsy API

Asynchroniczne interfejsy API JavaScript mogą sprawiać trudności. Są duże. Są one złożone. A najbardziej irytujące jest to, że oferują wiele możliwości, w których coś pójdzie nie tak. Ostatnią rzeczą, jaką musisz się zająć, jest nakładanie się na złożony asynchroniczny interfejs API (FileSystem) w już asynchronicznym świecie (środowisko robocze)! Dobra wiadomość jest taka, że FileSystem API definiuje wersję synchroniczną, aby ułatwić używanie Web Workers.

Synchroniczny interfejs API jest przeważnie taki sam jak jego asynchroniczny kuzyn. Metody, właściwości, funkcje i funkcje będą znane wszystkim użytkownikom. Główne odchylenia to:

  • Synchronicznego interfejsu API można używać tylko w kontekście Web Worker, a asynchronicznego API w obrębie instancji roboczej i poza nią.
  • Wywołania zwrotne zostały zakończone. Metody interfejsu API zwracają teraz wartości.
  • Metody globalne w obiekcie window (requestFileSystem() i resolveLocalFileSystemURL()) zmienią się w requestFileSystemSync() i resolveLocalFileSystemSyncURL().

Oprócz tych wyjątków interfejsy API są takie same. OK, wszystko gotowe.

Żądanie systemu plików

Aplikacja internetowa uzyskuje dostęp do synchronicznego systemu plików, wysyłając żądanie obiektu LocalFileSystemSync z instancji Web Worker. Obiekt requestFileSystemSync() jest dostępny w zakresie globalnym instancji roboczej:

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

Zwróć uwagę na nową wartość zwracaną, ponieważ używamy synchronicznego interfejsu API oraz że nie ma wywołań zwrotnych zakończonych powodzeniem i błędów.

Tak jak w przypadku zwykłego interfejsu API FileSystem, metody mają obecnie prefiksy:

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

Problem z limitem

Obecnie nie można poprosić o limit PERSISTENT w kontekście instancji roboczej. Zalecamy rozwiązanie problemów z limitami poza instancjami roboczymi. Może to wyglądać mniej więcej tak:

  1. worker.js: umieść dowolny kod interfejsu FileSystem API w elemencie try/catch, aby umożliwić wykrywanie błędów QUOTA_EXCEED_ERR.
  2. worker.js: jeśli zostanie przechwycony kod QUOTA_EXCEED_ERR, wyślij żądanie postMessage('get me more quota') z powrotem do aplikacji głównej.
  3. główna aplikacja: po otrzymaniu numeru 2 przejdź przez taniec window.webkitStorageInfo.requestQuota().
  4. główna aplikacja: gdy użytkownik przyzna większy limit, odeślij postMessage('resume writes') do instancji roboczej, aby poinformować ją o dodatkowym miejscu na dane.

Jest to dość skomplikowane obejście, ale powinno zadziałać. Więcej informacji o używaniu miejsca na dane PERSISTENT z interfejsem FileSystem API znajdziesz w artykule na temat zgłaszania prośby o limit.

Praca z plikami i katalogami

Synchroniczna wersja funkcji getFile() i getDirectory() zwraca odpowiednio wartości FileEntrySync i DirectoryEntrySync.

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

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

Tak utworzysz nowy katalog w folderze głównym.

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

Obsługa błędów

Jeśli nigdy nie zdarzyło Ci się debugować kodu Web Worker, zazdroszczę Ci. Zrozumienie, co poszło nie tak, może być prawdziwe.

Brak wywołań zwrotnych o błędach w świecie synchronicznym sprawia, że radzenie sobie z problemami jest trudniejsze, niż powinno. Po dodaniu ogólnej złożoności debugowania kodu Web Worker szybko będzie powodować frustrację. Jedną z rzeczy, które mogą ułatwić życie, jest umieszczenie całego istotnego kodu zasobu Worker w jednej z tych funkcji. Jeśli następnie wystąpią błędy, prześlij błąd do głównej aplikacji 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 plików, blobów i obiektów ArrayBuffers

Gdy zasoby Web Worker pojawiły się po raz pierwszy, pozwalały na wysyłanie tylko danych w postaci ciągów znaków w elemencie postMessage(). Później przeglądarki zaczęły akceptować dane możliwe do serializacji, co oznaczało, że przekazanie obiektu JSON było możliwe. Ostatnio jednak niektóre przeglądarki takie jak Chrome akceptują bardziej złożone typy danych, które są przekazywane za pomocą funkcji postMessage() za pomocą algorytmu klonowania uporządkowanych.

Co to tak naprawdę oznacza? Oznacza to, że przesyłanie danych binarnych między główną aplikacją a wątkiem instancji roboczej jest o wiele łatwiejsze. Przeglądarki, które obsługują klonowanie strukturalne w instancjach roboczych, umożliwiają przekazywanie do instancji roboczych tablic z typem, ArrayBuffer, File lub Blob. Chociaż dane nadal są kopiami, możliwość przekazania reguły File oznacza większą wydajność w porównaniu z poprzednim podejściem, co wymagało stworzenia pliku base64 przed jego przekazaniem do postMessage().

W poniższym przykładzie przekazujemy wybraną przez użytkownika listę plików do dedykowanej instancji roboczej. Instancja robocza po prostu przechodzi przez listę plików (można pokazać, że zwrócone dane to w rzeczywistości FileList), a główna aplikacja odczytuje każdy plik jako ArrayBuffer.

W przykładzie zastosowano też ulepszoną wersję wbudowanej techniki Web Worker opisanej w sekcji Podstawowe informacje o instancjach roboczych.

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

Odczytywanie plików w instancji roboczej

Używanie asynchronicznego interfejsu API FileReader do odczytu plików w instancji roboczej jest całkowicie dozwolone. Jest jednak lepszy sposób. W środowiskach roboczych 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 nie są uwzględniane w przypadku synchronicznego komponentu FileReader. Upraszcza to zagnieżdżanie wywołań zwrotnych podczas odczytywania plików. Zamiast tego metody readAs* zwracają plik do odczytu.

Przykład: pobieranie wszystkich wpisów

W niektórych przypadkach synchroniczny interfejs API jest znacznie czystszy w przypadku niektórych zadań. Mniejsza liczba wywołań zwrotnych jest korzystna i na pewno jest bardziej przejrzysta. Prawdziwa wada synchronicznego interfejsu API wynika z ograniczeń instancji roboczych.

Ze względów bezpieczeństwa dane między aplikacją wywołującą a wątkiem Web Worker nigdy nie są udostępniane. Przy wywołaniu metody postMessage() dane są zawsze kopiowane do i z instancji roboczej. W efekcie nie wszystkie typy danych można przekazywać.

Typy FileEntrySync i DirectoryEntrySync nie należą obecnie do akceptowanych typów. Jak więc przywrócić wpisy do aplikacji do rozmów? Jednym ze sposobów obejścia tego ograniczenia jest zwrócenie listy systemu plików: adresy URL zamiast listy pozycji. Adresy URL filesystem: to po prostu ciągi tekstowe, dzięki czemu można je bardzo łatwo przekazać. Dodatkowo można je kierować na wpisy w głównej aplikacji za pomocą resolveLocalFileSystemURL(). Wrócisz do obiektu FileEntrySync/DirectoryEntrySync.

Główna aplikacja:

<!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 przypadkiem użycia zasobów roboczych jest pobranie plików za pomocą XHR2 i zapisanie ich w HTML5 FileSystem. To zadanie idealnie pasuje do wątku instancji roboczej.

Poniższy przykład powoduje pobranie i zapisanie tylko 1 pliku, ale możesz rozwinąć go jako obraz, aby pobrać zestaw plików.

Główna aplikacja:

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

Procesy internetowe to niedostatecznie wykorzystywana i niedoceniana funkcja HTML5. Większość programistów, z którymi rozmawiam, nie potrzebuje dodatkowych korzyści związanych z wykorzystaniem mocy obliczeniowej. Jeśli jesteś sceptyczny (tak jak ja), mam nadzieję, że ten artykuł pomógł Ci zmienić zdanie. Przeniesienie do instancji roboczej takich działań jak operacje dysków (wywołania interfejsu API systemu plików) lub żądania HTTP do instancji roboczej jest naturalne i pomaga też podzielić kod na segmenty. Interfejsy API plików HTML5 w środowisku Workers otwierają zupełnie nowe możliwości dla aplikacji internetowych, z których wiele osób jeszcze nie znało.