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

مقدمة

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

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

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

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

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

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

بصرف النظر عن هذه الاستثناءات، تتطابق واجهات برمجة التطبيقات. حسنًا، نحن مستعدون للانطلاق!

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

ويحصل تطبيق الويب على إمكانية الوصول إلى نظام الملفات المتزامن من خلال طلب عنصر LocalFileSystemSync من داخل "عامل ويب". يظهر requestFileSystemSync() للنطاق العالمي للعامل:

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

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

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

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

التعامل مع الحصة

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

  1. WORK.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، لن تشعر بالإحباط في أي وقت من الأوقات. الشيء الوحيد الذي يمكن أن يجعل الحياة أسهل هو أن تضع كل تعليمات العامل ذات الصلة في محاولة/جذب. بعد ذلك، في حال حدوث أي أخطاء، أعِد توجيه الخطأ إلى التطبيق الرئيسي باستخدام 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);
}

تمرير الملفات والكائنات الصغيرة والتخزين المؤقت للصفحات

عندما ظهر عمال الويب لأول مرة، سمحوا فقط بإرسال بيانات السلسلة في postMessage(). في وقت لاحق، بدأت المتصفحات في قبول البيانات التسلسلية، والتي يعني أن تمرير كائن JSON كان ممكنًا. في المقابل، تقبل بعض المتصفحات مثل Chrome مؤخرًا تمرير أنواع البيانات الأكثر تعقيدًا إلى postMessage() باستخدام خوارزمية النسخ المنظَّمة.

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

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

يستخدم النموذج أيضًا إصدارًا محسّنًا من أسلوب عامل الويب المضمّن الموضّح في أساسيات عمال الويب.

<!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 غير المتزامنة لقراءة الملفات في "عامل". ومع ذلك، هناك طريقة أفضل. في إصدار 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. يتم دائمًا نسخ البيانات من وإلى العامل عند استدعاء postMessage(). ونتيجةً لذلك، لا يمكن تمرير جميع أنواع البيانات.

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

تتمثل إحدى حالات الاستخدام الشائعة للعاملين في تنزيل مجموعة من الملفات باستخدام XHR2، وكتابة هذه الملفات إلى HTML5 FileSystem. هذه مهمة مثالية لسلسلة Worker 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 إلى عامل مناسبة بشكل طبيعي، كما يساعد في تقسيم الرمز. تفتح واجهات برمجة تطبيقات ملفات HTML5 داخل Workers تجربة رائعة جديدة تمامًا لتطبيقات الويب التي لم يستكشفها الكثير من الأشخاص.