واجهة برمجة التطبيقات FileSystem المتزامنة للعاملين

مقدمة

إنّ واجهتَي برمجة التطبيقات FileSystem API وWeb Workers في HTML5 هما من بين الواجهات التي تُعدّ فائقة الفعالية في مجالهما. تقدّم واجهة برمجة التطبيقات FileSystem API أخيرًا ميزة التخزين الهرمي وعمليات قراءة/كتابة الملفات إلى تطبيقات الويب، كما توفّر واجهة برمجة التطبيقات Worker ميزة "تعدد المواضيع" غير المتزامنة للغة JavaScript. ومع ذلك، عند استخدام واجهات برمجة التطبيقات هذه معًا، يمكنك إنشاء بعض التطبيقات المثيرة للاهتمام حقًا.

يوفّر هذا البرنامج التعليمي دليلاً وأمثلة على الرموز البرمجية للاستفادة من FileSystem في HTML5 داخل Web Worker. يفترض هذا الدليل معرفة عملية بأحد واجهتَي برمجة التطبيقات. إذا لم تكن مستعدًا للبدء أو كنت مهتمًا بمعرفة مزيد من المعلومات عن واجهات برمجة التطبيقات هذه، يمكنك قراءة درسين رائعَين يتناولان الأساسيات: استكشاف واجهات برمجة التطبيقات FileSystem و أساسيات Web Workers.

واجهات برمجة التطبيقات المتزامنة مقابل واجهات برمجة التطبيقات غير المتزامنة

قد يكون من الصعب استخدام واجهات برمجة تطبيقات JavaScript غير المتزامنة. إنّها كبيرة. إنها معقدة. ولكن أكثر ما يزعجك هو أنّها توفّر الكثير من الفرص التي قد تؤدّي إلى حدوث أخطاء. آخر ما تريد التعامل معه هو وضع طبقات على واجهة برمجة تطبيقات معقدة غير متزامنة (FileSystem) في عالم غير متزامن بالفعل (العاملون)! والخبر السارّ هو أنّ واجهة برمجة التطبيقات FileSystem API تحدّد إصدارًا متزامنًا لتسهيل التعامل مع Web Workers.

في الغالب، تكون واجهة برمجة التطبيقات المتزامنة مماثلة تمامًا لواجهة برمجة التطبيقات غير المتزامنة. ستكون الطرق والخواص والميزات والوظائف مألوفة لك. في ما يلي الانحرافات الرئيسية:

  • لا يمكن استخدام واجهة برمجة التطبيقات المتزامنة إلا ضمن سياق Web Worker، بينما يمكن استخدام واجهة برمجة التطبيقات غير المتزامنة داخل عامل التشغيل وخارجه.
  • طلبات معاودة الاتصال غير متاحة. أصبحت طُرق واجهة برمجة التطبيقات تُرجع القيم.
  • تصبح الطريقتان العامتان في كائن النافذة (requestFileSystem() وresolveLocalFileSystemURL()) requestFileSystemSync() وresolveLocalFileSystemSyncURL().

وباستثناء هذه الاستثناءات، تكون واجهات برمجة التطبيقات متطابقة. حسنًا، تم حلّ المشكلة.

طلب نظام ملفات

يحصل تطبيق الويب على إذن الوصول إلى نظام الملفات المتزامن من خلال طلب عنصر LocalFileSystemSync من داخل Web Worker. يتم عرض requestFileSystemSync() في النطاق العام لـ Worker:

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

يُرجى ملاحظة القيمة الجديدة التي يتم عرضها الآن بعد أن بدأنا استخدام واجهة برمجة التطبيقات المتزامنة، بالإضافة إلى عدم توفّر وظائف الاستدعاء لحالات النجاح والأخطاء.

كما هو الحال مع واجهة برمجة التطبيقات العادية FileSystem API، يتمّ تضمين بادئة في الطرق في الوقت الحالي:

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

جارٍ التعامل مع الحصة

لا يمكن حاليًا طلب حصة PERSISTENT في سياق Worker. أنصحك بحل مشاكل الحصة خارج نطاق العمّال. قد تبدو العملية على النحو التالي:

  1. worker.js: عليك لصق أي رمز برمجي من واجهة برمجة التطبيقات FileSystem في try/catch ليتم رصد أي أخطاء QUOTA_EXCEED_ERR.
  2. worker.js: إذا رصدت خطأ QUOTA_EXCEED_ERR، أرسِل postMessage('get me more quota') مرة أخرى إلى التطبيق الرئيسي.
  3. التطبيق الرئيسي: يمكنك إجراء الرقصة window.webkitStorageInfo.requestQuota() عند استلام رقم 2.
  4. التطبيق الرئيسي: بعد أن يمنح المستخدم حصة إضافية، أرسِل postMessage('resume writes') مرة أخرى إلى العامل لإعلامه بمساحة التخزين الإضافية.

هذه طريقة حلّ بديلة معقّدة إلى حدٍ ما، ولكن من المفترض أن تنجح. يمكنك الاطّلاع على طلب الحصة لمزيد من المعلومات عن استخدام مساحة تخزين PERSISTENT مع واجهة برمجة التطبيقات FileSystem.

العمل مع الملفات والأدلة

وتعرض النسخة المتزامنة من getFile() وgetDirectory() كلاً من FileEntrySync وDirectoryEntrySync على التوالي.

على سبيل المثال، تُنشئ التعليمة البرمجية التالية ملفًا فارغًا باسم "log.txt" في الدليل الجذر.

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

يؤدي ما يلي إلى إنشاء دليل جديد في المجلد الجذر.

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

معالجة الأخطاء

أعتقد أنّكِ بحاجة إلى تصحيح أخطاء رمز Web Worker. قد يكون من الصعب معرفة المشكلة .

إنّ عدم توفّر وظائف الاستدعاء عند حدوث خطأ في العمليات المتزامنة يجعل التعامل مع المشاكل أكثر صعوبة مما ينبغي. وإذا أضفنا إلى ذلك التعقيد العام لتصحيح أخطاء رمز Web Worker، ستشعر بالضيق في وقت قصير. من بين الإجراءات التي يمكن أن تسهّل عليك الأمور هي تضمين كل رمز Worker ذي الصلة في بنية try/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. في الآونة الأخيرة، بدأت بعض المتصفّحات، مثل Chrome، في قبول أنواع بيانات أكثر تعقيدًا ليتم تمريرها من خلال postMessage() باستخدام خوارزمية النسخة المنظَّمة.

ما معنى ذلك حقًا؟ وهذا يعني أنّه من الأسهل بكثير تمرير data الثنائية بين التطبيق الرئيسي وسلسلة مهام Worker. تتيح لك المتصفّحات التي تتيح النسخ المنظّم في Worker تمرير مصفوفات من النوع المحدّد أو ArrayBuffer أو File أو Blob إلى Worker. على الرغم من أنّ البيانات لا تزال نسخة، فإنّ إمكانية تمرير File تعني حصولك على ميزة أداء أفضل مقارنةً بالنهج السابق الذي كان يتضمن ترميز الملف بترميز base64 قبل تمريره إلى postMessage().

يمرر المثال التالي قائمة ملفات يختارها المستخدم إلى عامل مخصص. يمرّل Worker ببساطة قائمة الملفات (من السهل عرض البيانات المعروضة وهي في الواقع FileList) ويقرأ التطبيق الرئيسي كل ملف على أنّه ArrayBuffer.

يستخدم العيّنة أيضًا إصدارًا محسّنًا من أسلوب Web Worker المضمّن описан في أساسيات 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

من المقبول تمامًا استخدام واجهة برمجة التطبيقات غير المتزامنة FileReader لقراءة الملفات في Worker. ومع ذلك، هناك طريقة أفضل. في Workers، تتوفّر واجهة برمجة تطبيقات غير متزامنة (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* الملف المقروء.

مثال: جلب جميع الإدخالات

في بعض الحالات، تكون واجهة برمجة التطبيقات المتزامنة أكثر وضوحًا لمهام معيّنة. إنّ استخدام عدد أقل من طلبات إعادة الاتصال يُعدّ أمرًا جيدًا ويجعل الأمور أكثر سهولة في القراءة. ينبع الجانب السلبي الحقيقي لواجهة برمجة التطبيقات المتزامنة من قيود العمال.

لأسباب تتعلق بالأمان، لا تتم أبدًا مشاركة البيانات بين التطبيق المُرسِل وسلسلة مهام Web Worker. تتم دائمًا نسخ البيانات من Worker وإليه عند استدعاء postMessage(). ونتيجةً لذلك، لا يمكن تمرير كل أنواع البيانات.

لا يندرج FileEntrySync وDirectoryEntrySync حاليًا ضمن الأنواع المقبولة. كيف يمكنك إعادة إدخال الأرقام إلى تطبيق الاتصال؟ وإحدى الطرق لتجنُّب هذا القيد هي عرض قائمة filesystem: عناوين 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، وكتابة هذه الملفات في نظام ملفات HTML5. هذه مهمة مثالية لسلسلة مهام Worker.

لا يسترجع المثال التالي سوى ملف واحد ويكتبه، ولكن يمكنك تخيل توسيع نطاقه لتنزيل مجموعة من الملفات.

التطبيق الرئيسي:

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

الخاتمة

عمال الويب هي ميزة في HTML5 يتم استخدامها بشكل غير كافٍ ولا يتم تقديرها بشكل كافٍ. لا يحتاج معظم المطوّرين الذين أتحدّث إليهم إلى المزايا الحسابية الإضافية، ولكن يمكن استخدامها لأكثر من مجرد العمليات الحسابية البحتة. إذا كنت متشكّكًا (كما كنت)، آمل أن تكون هذه المقالة قد ساعدتك في تغيير رأيك. إنّ تفريغ عمليات مثل عمليات القرص (طلبات واجهة برمجة التطبيقات لنظام الملفات) أو طلبات HTTP إلى Worker هو أمر مناسب بشكلٍ طبيعي ويساعد أيضًا في تقسيم الرمز البرمجي إلى أقسام. توفّر واجهات برمجة تطبيقات ملفات HTML5 داخل Workers ميزات جديدة رائعة لتطبيقات الويب لم يستكشفها الكثير من المستخدمين.