Ç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 bir arada kullandığınızda gerçekten ilgi çekici uygulamalar oluşturabilirsiniz.

Bu eğitimde, bir Web İşleyici içinde HTML5 FileSystem'ten 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 İşçilerinin sorununu gidermek için eşzamanlı bir sürüm tanımlamasıdır.

Senkronize API, çoğu durumda asenkron 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(), Çalışanın global kapsamına açılır:

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

Artık senkron API'yi kullandığımıza ve başarı ve hata geri çağırmalarının olmadığına 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 işlem 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() öğelerinin eşzamanlı 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 ne olduğunu bulmak gerçekten çok 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ılır. İş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);
}

Files, Blob'lar ve ArrayBuffers'tan geçiş yapma

Web İşçileri sahneye ilk geldiğinde postMessage() üzerinden yalnızca dize verilerinin gönderilmesine izin vermişler. 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>

Çalışanlarda dosya okuma

İşleyicide dosyaları okumak için asenkron FileReader API'sini kullanmak tamamen kabul edilebilir. Ancak daha iyi bir yol var. Çalışanlar'da, dosyaların okunmasını kolaylaştıran eşzamanlı bir 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, eşzamanlı 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, eşzamanlı API belirli görevler için çok daha temizdir. Daha az geri arama yapmak güzel bir şeydir ve kesinlikle daha okunaklı bir deneyim sunar. Eşzamanlı API'nin gerçek 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 Çalışana kopyalanır. Bu nedenle, her veri türü iletilemez.

Maalesef FileEntrySync ve DirectoryEntrySync şu anda kabul edilen türler arasında yer almıyor. Peki, arama uygulamasına girişleri 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 aktarılması çok kolaydır. Ayrıca, resolveLocalFileSystemURL() kullanılarak ana uygulamadaki girişlere çözülebilirler. Böylece tekrar FileEntrySync/DirectoryEntrySync nesnesine geçebilirsiniz.

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

Çalışanların yaygın bir kullanım alanı, XHR2 kullanarak bir dizi dosyayı indirmek ve bu dosyaları HTML5 Dosya Sistemi'ne 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 İşçileri, HTML5'in az kullanılan ve değerlendirilmeyen bir özelliğidir. Konuştuğum çoğu geliştirici, hesaplamayla ilgili ek avantajlara ihtiyaç duymuyor, ancak bu avantajlar yalnızca hesaplamadan daha fazlası için kullanılabilmektedir. Bu konuda şüpheleriniz varsa (benim gibi) bu makalenin fikrinizi değiştirmenize yardımcı olduğunu umuyorum. Disk işlemleri (Filesystem API çağrıları) veya HTTP istekleri gibi öğelerin bir Çalışana boşaltılması tamamen bir çözümdür ve kodunuzun bölümlere ayrılmasına yardımcı olur. İşleyicilerdeki HTML5 Dosya API'leri, web uygulamaları için birçok kişinin keşfetmediği yepyeni bir dünya sunuyor.