FileSystem API sinkron untuk pekerja

Pengantar

FileSystem API dan Web Worker HTML5 sangat andal dalam hal masing-masing. FileSystem API akhirnya menghadirkan penyimpanan hierarkis dan I/O file ke aplikasi web, dan Pekerja menghadirkan 'multi-threading' asinkron yang sebenarnya ke JavaScript. Namun, jika menggunakan API ini secara bersamaan, Anda dapat membuat beberapa aplikasi yang benar-benar menarik.

Tutorial ini memberikan panduan dan contoh kode untuk memanfaatkan FileSystem HTML5 di dalam Web Worker. Tutorial ini mengasumsikan bahwa Anda memiliki pengetahuan tentang kedua API tersebut. Jika Anda belum siap untuk mempelajarinya atau tertarik untuk mempelajari API tersebut lebih lanjut, baca dua tutorial bagus yang membahas dasar-dasarnya: Menjelajahi FileSystem API dan Dasar-Dasar Web Worker.

API Sinkron vs. Asinkron

JavaScript API asinkron dapat sulit digunakan. Ukurannya besar. Mereka kompleks. Namun, yang paling menjengkelkan adalah mereka menawarkan banyak peluang untuk terjadinya kesalahan. Hal terakhir yang ingin Anda tangani adalah membuat lapisan pada API asinkron yang kompleks (FileSystem) di dunia yang sudah asinkron (Pekerja). Kabar baiknya adalah FileSystem API menentukan versi sinkron untuk meringankan masalah di Web Worker.

Sebagian besar, API sinkron sama persis dengan API asinkron. Metode, properti, fitur, dan fungsionalitasnya tidak akan asing. Penyimpangan utamanya adalah:

  • API sinkron hanya dapat digunakan dalam konteks Web Worker, sedangkan API asinkron dapat digunakan di dalam dan di luar Worker.
  • Callback tidak ada. Metode API kini menampilkan nilai.
  • Metode global pada objek jendela (requestFileSystem() dan resolveLocalFileSystemURL()) menjadi requestFileSystemSync() dan resolveLocalFileSystemSyncURL().

Selain pengecualian ini, API-nya sama. Oke, kita sudah siap!

Meminta sistem file

Aplikasi web mendapatkan akses ke sistem file sinkron dengan meminta objek LocalFileSystemSync dari dalam Web Worker. requestFileSystemSync() ditampilkan ke cakupan global Pekerja:

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

Perhatikan nilai yang ditampilkan baru sekarang setelah kita menggunakan API sinkron serta tidak adanya callback keberhasilan dan error.

Seperti FileSystem API normal, metode diberi awalan saat ini:

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

Menangani kuota

Saat ini, Anda tidak dapat meminta kuota PERSISTENT dalam konteks Pekerja. Sebaiknya tangani masalah kuota di luar Pekerja. Prosesnya mungkin terlihat seperti ini:

  1. worker.js: Gabungkan kode FileSystem API apa pun dalam try/catch sehingga error QUOTA_EXCEED_ERR apa pun tertangkap.
  2. worker.js: Jika Anda menangkap QUOTA_EXCEED_ERR, kirim postMessage('get me more quota') kembali ke aplikasi utama.
  3. aplikasi utama: Lakukan tarian window.webkitStorageInfo.requestQuota() saat #2 diterima.
  4. aplikasi utama: Setelah pengguna memberikan lebih banyak kuota, kirim kembali postMessage('resume writes') ke pekerja untuk memberi tahu ruang penyimpanan tambahan.

Solusi ini cukup rumit, tetapi akan berhasil. Lihat meminta kuota untuk mengetahui informasi selengkapnya tentang penggunaan penyimpanan PERSISTENT dengan FileSystem API.

Bekerja dengan file dan direktori

Versi sinkron getFile() dan getDirectory() masing-masing menampilkan FileEntrySync dan DirectoryEntrySync.

Misalnya, kode berikut membuat file kosong bernama "log.txt" di direktori root.

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

Perintah berikut akan membuat direktori baru di folder root.

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

Menangani error

Jika Anda belum pernah harus men-debug kode Web Worker, saya iri dengan Anda. Mencari tahu masalahnya memang sangat merepotkan.

Tidak adanya callback error di dunia sinkron membuat penanganan masalah lebih rumit dari yang seharusnya. Jika kita menambahkan kompleksitas umum proses debug kode Web Worker, Anda akan segera frustrasi. Satu hal yang bisa membuat hidup lebih mudah adalah dengan menggabungkan semua kode Pekerja yang relevan dalam try/catch. Kemudian, jika terjadi error, teruskan error ke aplikasi utama menggunakan 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);
}

Meneruskan File, Blob, dan ArrayBuffer

Saat Web Worker pertama kali muncul di scene, mereka hanya mengizinkan data string untuk dikirim di postMessage(). Kemudian, browser mulai menerima data yang dapat diserialisasi, yang berarti objek JSON dapat diteruskan. Namun, baru-baru ini, beberapa browser seperti Chrome menerima jenis data yang lebih kompleks untuk diteruskan melalui postMessage() menggunakan algoritma clone terstruktur.

Apa artinya? Artinya, jauh lebih mudah untuk meneruskan data biner antara aplikasi utama dan thread Pekerja. Browser yang mendukung cloning terstruktur untuk Pekerja memungkinkan Anda meneruskan Array Berjenis, ArrayBuffer, File, atau Blob ke Pekerja. Meskipun data masih berupa salinan, kemampuan untuk meneruskan File berarti mendapatkan manfaat performa dibandingkan pendekatan sebelumnya, yang melibatkan file base64 sebelum meneruskannya ke postMessage().

Contoh berikut meneruskan daftar file yang dipilih pengguna ke Pekerja khusus. Pekerja hanya meneruskan daftar file (mudah untuk menampilkan data yang ditampilkan sebenarnya adalah FileList) dan aplikasi utama membaca setiap file sebagai ArrayBuffer.

Contoh ini juga menggunakan versi yang ditingkatkan dari teknik Web Worker inline yang dijelaskan dalam Dasar-Dasar Pekerja Web.

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

Membaca file di Pekerja

Anda dapat menggunakan FileReader API asinkron untuk membaca file di Pekerja. Namun, ada cara yang lebih baik. Di Workers, ada API sinkron (FileReaderSync) yang menyederhanakan pembacaan file:

Aplikasi utama:

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

Seperti yang diharapkan, callback hilang dengan FileReader sinkron. Hal ini menyederhanakan jumlah tingkatan callback saat membaca file. Sebagai gantinya, metode readAs* akan menampilkan file baca.

Contoh: Mengambil semua entri

Dalam beberapa kasus, API sinkron jauh lebih bersih untuk tugas tertentu. Lebih sedikit callback akan lebih baik dan tentunya membuat semuanya lebih mudah dibaca. Kelemahan sebenarnya dari API sinkron berasal dari keterbatasan Pekerja.

Untuk alasan keamanan, data antara aplikasi pemanggil dan thread Pekerja Web tidak akan pernah dibagikan. Data selalu disalin ke dan dari Pekerja saat postMessage() dipanggil. Akibatnya, tidak semua jenis data dapat diteruskan.

Sayangnya, FileEntrySync dan DirectoryEntrySync saat ini tidak termasuk dalam jenis yang diterima. Jadi, bagaimana cara mengembalikan entri ke aplikasi panggilan? Salah satu cara untuk mengakali batasan ini adalah dengan menampilkan daftar filesystem: URL, bukan daftar entri. URL filesystem: hanyalah string, sehingga sangat mudah untuk diteruskan. Selain itu, keduanya dapat di-resolve ke entri di aplikasi utama menggunakan resolveLocalFileSystemURL(). Tindakan tersebut akan mengembalikan Anda ke objek FileEntrySync/DirectoryEntrySync.

Aplikasi utama:

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

Contoh: Mendownload file menggunakan XHR2

Kasus penggunaan umum untuk Pekerja adalah mendownload banyak file menggunakan XHR2, dan menulis file tersebut ke FileSystem HTML5. Ini adalah tugas yang sempurna untuk thread Pekerja.

Contoh berikut hanya mengambil dan menulis satu file, tetapi Anda dapat membayangkan memperluasnya untuk mendownload sekumpulan file.

Aplikasi utama:

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

Kesimpulan

Web Worker adalah fitur HTML5 yang kurang dimanfaatkan dan kurang dihargai. Sebagian besar developer yang saya ajak bicara tidak memerlukan manfaat komputasi tambahan, tetapi manfaat tersebut dapat digunakan untuk lebih dari sekadar komputasi murni. Jika Anda ragu (seperti saya), saya harap artikel ini telah membantu Anda berubah pikiran. Memindahkan hal-hal seperti operasi disk (panggilan Filesystem API) atau permintaan HTTP ke Pekerja adalah hal yang wajar dan juga membantu memisahkan kode Anda. HTML5 File API di dalam Pekerja membuka banyak hal baru yang luar biasa untuk aplikasi web yang belum banyak dijelajahi orang.