Çalışanlar için eşzamanlı FileSystem API'si

Giriş

HTML5 FileSystem API ve Web İşleyiciler kendi alanlarında son derece güçlüdür. FileSystem API, web uygulamalarına nihayet hiyerarşik depolama ve dosya G/Ç'si, Workers ise JavaScript'e gerçek anlamda asenkron "çok iş parçacıklı" işlevler getiriyor. Ancak bu API'leri birlikte kullandığınızda gerçekten ilginç uygulamalar oluşturabilirsiniz.

Bu eğitimde, bir Web İşleyici içinde HTML5 FileSystem'den yararlanmayla ilgili bir kılavuz ve kod örnekleri sağlanmaktadır. Bu makalede, her iki API hakkında da bilgi sahibi olduğunuz varsayılır. Bu API'leri kullanmaya başlamaya hazır değilseniz veya bu API'ler hakkında daha fazla bilgi edinmek istiyorsanız temel bilgileri ele alan iki mükemmel eğitim içeriğini inceleyin: FileSystem API'lerini Keşfetme ve Web İşleyicileri ile İlgili Temel Bilgiler.

Eşzamanlı ve Asenkron API'ler

Eşzamansız JavaScript API'lerinin kullanımı zor olabilir. Bunlar büyük. Karmaşıktır. Ancak en can sıkıcı olan şey, bu tür platformların hataya yol açacak çok sayıda fırsat sunmasıdır. Zaten asenkron bir dünyada (işçiler) karmaşık bir asenkron API'yi (FileSystem) katmanlandırmak istemezsiniz. İyi haber ise FileSystem API'nin, Web Çalışanlarında yaşanan sorunları azaltmak için senkron bir sürüm tanımlamasıdır.

Eşzamanlı API, çoğu durumda eşzamansız kuzeniyle tamamen aynıdır. Yöntemler, özellikler, işlevler ve işlevsellik size tanıdık gelecektir. Başlıca sapmalar şunlardır:

  • Senkron API yalnızca bir Web İşleyici bağlamında kullanılabilirken, asenkron API bir İşleyici içinde ve dışında kullanılabilir.
  • Geri aramalar devre dışıdır. API yöntemleri artık değer döndürüyor.
  • Pencere nesnesindeki global yöntemler (requestFileSystem() ve resolveLocalFileSystemURL()), requestFileSystemSync() ve resolveLocalFileSystemSyncURL() olur.

Bu istisnalar dışında API'ler aynıdır. Tamam, hazırız.

Dosya sistemi isteme

Web uygulamaları, bir Web İşleyici içinden LocalFileSystemSync nesnesi isteyerek senkronize dosya sistemine erişim elde eder. requestFileSystemSync(), Worker'ın global kapsamına açıktır:

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

Artık senkron API'yi kullandığımızdan ve başarı ve hata geri çağırmalarının olmadığından yeni döndürülen değere dikkat edin.

Normal FileSystem API'de olduğu gibi, yöntemlere şu anda ön ek eklenmektedir:

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

Kotayla ilgili işlemler

Şu anda, İşçi bağlamında PERSISTENT kotası isteğinde bulunulamaz. Workers dışındaki kota sorunlarını gidermenizi öneririm. Bu süreç aşağıdaki gibi görünebilir:

  1. worker.js: Tüm FileSystem API kodlarını bir try/catch içine alın, böylece tüm QUOTA_EXCEED_ERR hataları yakalanır.
  2. worker.js: Bir QUOTA_EXCEED_ERR yakalarsanız ana uygulamaya postMessage('get me more quota') gönderin.
  3. ana uygulama: 2 numaralı mesaj alındığında window.webkitStorageInfo.requestQuota() dansını yapın.
  4. ana uygulama: Kullanıcı daha fazla kota verdikten sonra postMessage('resume writes') öğesini işçiye geri göndererek ek depolama alanı hakkında bilgi verin.

Bu oldukça karmaşık bir geçici çözümdür ancak işe yarayacaktır. FileSystem API ile PERSISTENT depolama alanını kullanma hakkında daha fazla bilgi için kota isteme bölümüne göz atın.

Dosya ve dizinlerle çalışma

getFile() ve getDirectory() işlevinin senkronize sürümü sırasıyla FileEntrySync ve DirectoryEntrySync döndürür.

Örneğin, aşağıdaki kod kök dizininde "log.txt" adlı boş bir dosya oluşturur.

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

Aşağıdaki komut, kök klasörde yeni bir dizin oluşturur.

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

Hataları işleme

Web Worker kodunda hiç hata ayıklama yapmadıysanız size imreniyorum. Sorunun nereden kaynaklandığını bulmak gerçekten zor olabilir.

Eşzamanlı dünyada hata geri çağırmalarının olmaması, sorunlarla baş etmeyi olması gerekenden daha zor hale getirir. Web İşleyici kodunda hata ayıklamanın genel karmaşıklığını da eklerseniz kısa sürede canınız sıkılabilir. İşinizi kolaylaştıracak bir yöntem, ilgili Worker kodunuzun tamamını try/catch içine almaktır. Ardından, herhangi bir hata oluşursa postMessage() kullanarak hatayı ana uygulamaya iletin:

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

Dosya, Blob ve ArrayBuffer aktarımı

Web işçileri ilk kullanıma sunulduğunda yalnızca postMessage() içinde dize verilerinin gönderilmesine izin veriyordu. Daha sonra tarayıcılar serileştirilebilir verileri kabul etmeye başladı. Bu da JSON nesnesi iletmenin mümkün olduğu anlamına geliyordu. Ancak son zamanlarda Chrome gibi bazı tarayıcılar, yapılandırılmış kopya algoritması kullanılarak postMessage() üzerinden daha karmaşık veri türlerinin iletilmesini kabul ediyor.

Peki bu tam olarak ne anlama geliyor? Bu, ana uygulama ile işleyici iş parçacığı arasında ikili verilerin aktarılmasının çok daha kolay olduğu anlamına gelir. İşçiler için yapılandırılmış klonlamayı destekleyen tarayıcılar, İşçilere türü belirtilmiş diziler, ArrayBuffer, File veya Blob öğeleri iletmenize olanak tanır. Veriler hâlâ bir kopya olsa da File iletebilmek, postMessage()'e iletilmeden önce dosyanın base64'e dönüştürülmesini içeren önceki yaklaşıma kıyasla performans avantajı sağlar.

Aşağıdaki örnekte, kullanıcı tarafından seçilen bir dosya listesi özel bir İşleyici'ye iletilmektedir. İşleyici, dosya listesini basitçe iletir (iade edilen verilerin aslında bir FileList olduğunu göstermek kolaydır) ve ana uygulama her dosyayı bir ArrayBuffer olarak okur.

Sanalda, Web Çalışanlarının Temel Özellikleri bölümünde açıklanan satır içi Web Çalışanı tekniğinin geliştirilmiş bir sürümü de kullanılmaktadır.

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

İşleyicide dosya okuma

İşleyicide dosyaları okumak için asenkron FileReader API'sini kullanmak tamamen kabul edilebilir. Ancak daha iyi bir yol var. Workers'da, dosya okumayı kolaylaştıran senkron API (FileReaderSync) vardır:

Ana uygulama:

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

Beklenen gibi, senkronize FileReader ile geri çağırma işlevi kaldırıldı. Bu, dosyaları okurken geri çağırma iç içe yerleştirme miktarını basitleştirir. Bunun yerine, readAs* yöntemleri okunan dosyayı döndürür.

Örnek: Tüm girişleri getirme

Bazı durumlarda, belirli görevler için senkronize API çok daha kullanışlıdır. Daha az geri arama yapmak güzel bir şeydir ve kesinlikle daha okunaklı bir deneyim sunar. Senkronize API'nin asıl dezavantajı, Çalışanların sınırlamalarından kaynaklanır.

Güvenlik nedeniyle, arayan uygulama ile Web İşleyici iş parçacığı arasındaki veriler hiçbir zaman paylaşılmaz. postMessage() çağrıldığında veriler her zaman İşçi'ye ve İşçi'den kopyalanır. Bu nedenle, her veri türü iletilemez.

Maalesef FileEntrySync ve DirectoryEntrySync şu anda kabul edilen türler arasında yer almıyor. Girişleri arama uygulamasına nasıl geri alabilirsiniz? Sınırlamayı aşmanın bir yolu, giriş listesi yerine filesystem: URL'leri listesi döndürmektir. filesystem: URL'ler yalnızca dize olduğundan aktarmaları çok kolaydır. Ayrıca, resolveLocalFileSystemURL() kullanılarak ana uygulamadaki girişlere çözülebilirler. Bu işlem sizi tekrar FileEntrySync/DirectoryEntrySync nesnesine döndürür.

Ana uygulama:

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

Örnek: XHR2 kullanarak dosya indirme

İşçiler için yaygın bir kullanım alanı, XHR2'yi kullanarak bir grup dosyayı indirip bu dosyaları HTML5 FileSystem'e yazmaktır. Bu, işleyici iş parçacığı için mükemmel bir görevdir.

Aşağıdaki örnekte yalnızca bir dosya getirilip yazılıyor ancak bu örneği bir dosya grubunu indirmek için genişletebilirsiniz.

Ana uygulama:

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

Sonuç

Web çalışanları, HTML5'in yeterince kullanılmayan ve yeterince takdir edilmeyen bir özelliğidir. Görüştüğüm çoğu geliştiricinin ek hesaplama avantajlarına ihtiyacı yoktur ancak bu avantajlar yalnızca hesaplama için değil, daha fazla alanda kullanılabilir. Bu konuda şüpheleriniz varsa (benim gibi) bu makalenin fikrinizi değiştirmenize yardımcı olduğunu umuyorum. Disk işlemleri (Dosya Sistemi API çağrıları) veya HTTP istekleri gibi işlemleri bir İşleyiciye aktarmak doğal bir seçimdir ve ayrıca kodunuzu bölümlere ayırmanıza yardımcı olur. İşleyicilerdeki HTML5 Dosya API'leri, web uygulamaları için birçok kişinin keşfetmediği yepyeni bir dünya sunuyor.