همگام FileSystem API برای کارگران

معرفی

HTML5 FileSystem API و Web Workers از نظر خودشان بسیار قدرتمند هستند. FileSystem API در نهایت ذخیره‌سازی سلسله مراتبی و I/O فایل را به برنامه‌های کاربردی وب می‌آورد و Workers «چند رشته‌ای» ناهمزمان واقعی را به جاوا اسکریپت می‌آورد. با این حال، هنگامی که از این API ها با هم استفاده می کنید، می توانید برنامه های واقعا جالبی بسازید.

این آموزش یک راهنما و نمونه کد برای اعمال نفوذ سیستم فایل HTML5 در داخل یک Web Worker ارائه می دهد. این فرض بر دانش کاری هر دو API است. اگر برای غواصی کاملاً آماده نیستید یا علاقه مند به یادگیری بیشتر در مورد آن APIها هستید، دو آموزش عالی را بخوانید که در مورد اصول اولیه بحث می کند: کاوش در APIهای FileSystem و اصول Web Workers .

APIهای همزمان در مقابل ناهمزمان

استفاده از APIهای جاوا اسکریپت ناهمزمان ممکن است دشوار باشد. آنها بزرگ هستند. آنها پیچیده هستند. اما چیزی که بیش از همه ناامید کننده است این است که آنها فرصت های زیادی را برای اشتباه پیش رفتن ارائه می دهند. آخرین چیزی که می‌خواهید با آن مقابله کنید، لایه‌بندی روی یک API ناهمزمان پیچیده (FileSystem) در یک دنیای از قبل ناهمزمان (Workers) است! خبر خوب این است که FileSystem API یک نسخه همزمان را برای کاهش درد در Web Workers تعریف می کند.

در بیشتر موارد، API همزمان دقیقاً مشابه پسرعموی ناهمزمان آن است. روش ها، ویژگی ها، ویژگی ها و عملکرد آشنا خواهند بود. عمده انحرافات عبارتند از:

  • API همزمان را می توان فقط در یک زمینه Web Worker استفاده کرد، در حالی که API ناهمزمان را می توان در داخل و خارج از Worker استفاده کرد.
  • تماس های تلفنی خارج شده اند. روش های API اکنون مقادیر را برمی گرداند.
  • متدهای سراسری در شی پنجره ( requestFileSystem() و resolveLocalFileSystemURL() ) به requestFileSystemSync() و resolveLocalFileSystemSyncURL() تبدیل می شوند.

جدا از این استثناها، APIها یکسان هستند. خوب، ما خوب هستیم!

درخواست فایل سیستم

یک برنامه وب با درخواست یک شی LocalFileSystemSync از داخل یک Web Worker به فایل سیستم همزمان دسترسی پیدا می کند. requestFileSystemSync() در معرض دامنه جهانی Worker است:

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

اکنون که از API همگام استفاده می کنیم و همچنین عدم موفقیت و خطا، به مقدار بازگشتی جدید توجه کنید.

مانند API معمولی FileSystem، متدها در حال حاضر پیشوند دارند:

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

برخورد با سهمیه

در حال حاضر، امکان درخواست سهمیه PERSISTENT در زمینه Worker وجود ندارد. توصیه می کنم به مسائل مربوط به سهمیه خارج از کارگران رسیدگی کنید. روند می تواند چیزی شبیه به این باشد:

  1. worker.js: هر کد API FileSystem را در یک try/catch بپیچید تا خطاهای QUOTA_EXCEED_ERR شناسایی شوند.
  2. worker.js: اگر QUOTA_EXCEED_ERR را گرفتید، یک postMessage('get me more quota') را به برنامه اصلی ارسال کنید.
  3. برنامه اصلی: هنگامی که #2 دریافت شد، از طریق رقص window.webkitStorageInfo.requestQuota() بروید.
  4. برنامه اصلی: پس از اینکه کاربر سهمیه بیشتری را اعطا کرد، postMessage('resume writes') برای کارگر ارسال کنید تا از فضای ذخیره سازی اضافی مطلع شود.

این یک راه حل نسبتاً درگیر است، اما باید کار کند. برای اطلاعات بیشتر در مورد استفاده از فضای ذخیره سازی PERSISTENT با FileSystem API به درخواست سهمیه مراجعه کنید.

کار با فایل ها و دایرکتوری ها

نسخه همزمان getFile() و getDirectory() به ترتیب یک FileEntrySync و DirectoryEntrySync را برمی گرداند.

به عنوان مثال، کد زیر یک فایل خالی به نام "log.txt" در پوشه اصلی ایجاد می کند.

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

زیر یک دایرکتوری جدید در پوشه root ایجاد می کند.

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

رسیدگی به خطاها

اگر هرگز مجبور به اشکال زدایی کد Web Worker نشده اید، من به شما حسادت می کنم! فهمیدن اینکه چه مشکلی دارد می تواند واقعاً دردناک باشد.

عدم پاسخگویی خطا در دنیای همزمان، برخورد با مشکلات را دشوارتر از آنچه باید باشد، می کند. اگر پیچیدگی کلی اشکال زدایی کد Web Worker را اضافه کنیم، در کمترین زمان ناامید خواهید شد. یکی از چیزهایی که می‌تواند زندگی را آسان‌تر کند این است که همه کد Worker مربوطه خود را در یک امتحان / catch قرار دهید. سپس، اگر خطایی رخ داد، با استفاده از 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);
}

عبور از فایل ها، Blobs و ArrayBuffers

زمانی که Web Workers برای اولین بار وارد صحنه شدند، فقط اجازه دادند که داده های رشته ای در postMessage() ارسال شود. بعداً، مرورگرها شروع به پذیرش داده‌های سریال‌سازی کردند که به این معنی بود که ارسال یک شی JSON امکان‌پذیر بود. اما اخیراً، برخی از مرورگرها مانند کروم انواع داده های پیچیده تری را می پذیرند تا با استفاده از الگوریتم کلون ساخت یافته از طریق postMessage() منتقل شوند.

این واقعا به چه معناست؟ این بدان معنی است که انتقال داده های باینری بین برنامه اصلی و موضوع Worker بسیار آسان تر است. مرورگرهایی که از شبیه‌سازی ساختاریافته برای Workers پشتیبانی می‌کنند، به شما امکان می‌دهند تا Arrays Typed، ArrayBufferFile s یا Blob را به Workers منتقل کنید. اگرچه داده ها هنوز یک کپی هستند، توانایی ارسال یک File به معنای مزیت عملکرد نسبت به رویکرد قبلی است، که شامل base64 کردن فایل قبل از ارسال آن به postMessage() بود.

مثال زیر لیستی از فایل های انتخاب شده توسط کاربر را به یک Worker اختصاصی ارسال می کند. Worker به سادگی از لیست فایل عبور می کند (به سادگی نشان می دهد که داده های برگشتی در واقع یک FileList است) و برنامه اصلی هر فایل را به عنوان یک ArrayBuffer می خواند.

این نمونه همچنین از نسخه بهبودیافته تکنیک Web Worker که در Basics of Web Workers توضیح داده شده است استفاده می کند.

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

خواندن فایل ها در Worker

استفاده از API ناهمزمان FileReader برای خواندن فایل‌ها در Worker کاملاً قابل قبول است. با این حال، راه بهتری وجود دارد. در Workers، یک API همزمان ( FileReaderSync ) وجود دارد که خواندن فایل‌ها را ساده می‌کند:

برنامه اصلی:

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

همانطور که انتظار می‌رفت، تماس‌های برگشتی با FileReader همگام حذف شدند. این مقدار تودرتوی برگشت به تماس را هنگام خواندن فایل ها ساده می کند. در عوض، متدهای readAs* فایل خوانده شده را برمی گرداند.

مثال: واکشی همه ورودی ها

در برخی موارد، API همزمان برای کارهای خاص بسیار تمیزتر است. تماس‌های کمتر خوب هستند و مطمئناً چیزها را خواناتر می‌کنند. نقطه ضعف واقعی API همزمان از محدودیت‌های Workers ناشی می‌شود.

به دلایل امنیتی، داده‌های بین برنامه تماس و رشته Web Worker هرگز به اشتراک گذاشته نمی‌شود. زمانی که postMessage() فراخوانی می شود، داده ها همیشه به و از Worker کپی می شوند. در نتیجه، هر نوع داده ای را نمی توان ارسال کرد.

متاسفانه، FileEntrySync و DirectoryEntrySync در حال حاضر در انواع پذیرفته شده قرار نمی گیرند. بنابراین چگونه می توانید ورودی ها را به برنامه تماس برگردانید؟ یکی از راه‌های دور زدن محدودیت این است که به جای فهرستی از ورودی‌ها، فهرستی از سیستم فایل: URLها را برگردانید. filesystem: URL ها فقط رشته هستند، بنابراین انتقال آنها بسیار آسان است. علاوه بر این، می‌توان آنها را به ورودی‌های برنامه اصلی با استفاده از resolveLocalFileSystemURL() حل کرد. این شما را به یک شی FileEntrySync / DirectoryEntrySync برمی گرداند.

برنامه اصلی:

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

مثال: دانلود فایل ها با استفاده از XHR2

یک مورد رایج برای Workers دانلود دسته ای از فایل ها با استفاده از XHR2 و نوشتن آن فایل ها در FileSystem HTML5 است. این یک کار عالی برای یک موضوع کارگر است!

مثال زیر فقط یک فایل را واکشی می کند و می نویسد، اما شما می توانید تصویر را گسترش دهید تا مجموعه ای از فایل ها را دانلود کنید.

برنامه اصلی:

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

نتیجه

Web Workers یکی از ویژگی‌های استفاده نشده و کمتر مورد توجه HTML5 است. اکثر توسعه دهندگانی که با آنها صحبت می کنم به مزایای محاسباتی اضافی نیاز ندارند، اما می توان از آنها برای چیزی فراتر از محاسبات خالص استفاده کرد. اگر شک دارید (همانطور که من بودم)، امیدوارم این مقاله به تغییر نظر شما کمک کرده باشد. بارگذاری مواردی مانند عملیات دیسک (تماس‌های API سیستم فایل) یا درخواست‌های HTTP به Worker یک تناسب طبیعی است و همچنین به تقسیم کد شما کمک می‌کند. APIهای فایل HTML5 داخل Workers یک قوطی کاملاً شگفت‌انگیز را برای برنامه‌های وب باز می‌کند که بسیاری از افراد آن را کاوش نکرده‌اند.