مقدمة
إنّ واجهتَي برمجة التطبيقات FileSystem API وWeb Workers في HTML5 هما من الواجهات التي تُعدّ فعالة للغاية في مجالهما. تقدّم واجهة برمجة التطبيقات FileSystem API أخيرًا ميزة التخزين الهرمي وعمليات قراءة/كتابة الملفات إلى تطبيقات الويب، كما توفّر واجهة برمجة التطبيقات Worker ميزة "تعدد المواضيع" غير المتزامنة للغة JavaScript. ومع ذلك، عند استخدام واجهات برمجة التطبيقات هذه معًا، يمكنك إنشاء بعض التطبيقات المثيرة للاهتمام حقًا.
يوفّر هذا البرنامج التعليمي دليلاً وأمثلة على الرموز البرمجية للاستفادة من FileSystem في HTML5 داخل Web Worker. يفترض هذا الدليل معرفة عملية بأحد واجهتَي برمجة التطبيقات. إذا لم تكن مستعدًا للبدء أو كنت مهتمًا بمعرفة مزيد من المعلومات عن واجهات برمجة التطبيقات هذه، يمكنك قراءة درسين رائعَين يناقشان الأساسيات: استكشاف واجهات برمجة التطبيقات FileSystem و أساسيات Web Workers.
واجهات برمجة التطبيقات المتزامنة مقابل واجهات برمجة التطبيقات غير المتزامنة
قد يكون من الصعب استخدام واجهات برمجة تطبيقات JavaScript غير المتزامنة. إنّها كبيرة. إنّها معقّدة. ولكن ما يثير الإحباط هو أنّها توفّر الكثير من الفرص لحدوث مشاكل. إنّ آخر ما تريد التعامل معه هو إضافة واجهة برمجة تطبيقات معقدة غير متزامنة (FileSystem) في عالم غير متزامن (Workers) من الأساس. والخبر السارّ هو أنّ واجهة برمجة التطبيقات FileSystem API تحدّد إصدارًا متزامنًا لتسهيل التعامل مع Web Workers.
في الغالب، تكون واجهة برمجة التطبيقات المتزامنة متطابقة تمامًا مع واجهة برمجة التطبيقات غير المتزامنة. ستكون الطرق والخواص والميزات والوظائف مألوفة لك. في ما يلي الانحرافات الرئيسية:
- لا يمكن استخدام واجهة برمجة التطبيقات المتزامنة إلا في سياق Web Worker، في حين يمكن استخدام واجهة برمجة التطبيقات غير المتزامنة داخل 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. أنصحك بمعالجة مشاكل الحصة خارج Workers.
قد تبدو العملية على النحو التالي:
- worker.js: لَفِّ أي رمز برمجي لواجهة برمجة التطبيقات FileSystem API في
try/catch
حتى يتم رصد أي أخطاءQUOTA_EXCEED_ERR
. - worker.js: إذا رصدت خطأ
QUOTA_EXCEED_ERR
، أرسِلpostMessage('get me more quota')
مرة أخرى إلى التطبيق الرئيسي. - التطبيق الرئيسي: اتّبِع الخطوات الواردة في
window.webkitStorageInfo.requestQuota()
عند تلقّي الرسالة 2. - التطبيق الرئيسي: بعد أن يمنح المستخدم حصة إضافية، أرسِل
postMessage('resume writes')
مرة أخرى إلى العامل لإعلامه بمساحة التخزين الإضافية.
هذه طريقة حلّ بديلة معقّدة إلى حدٍ ما، ولكن من المفترض أن تنجح. اطّلِع على طلب الحصة للحصول على مزيد من المعلومات عن استخدام مساحة تخزين PERSISTENT
مع واجهة برمجة التطبيقات FileSystem API.
العمل مع الملفات والأدلة
يعرض الإصدار المتزامن من 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 مخصّص.
يمرّل 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
API لقراءة الملفات في 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* الملف المقروء.
مثال: جلب جميع الإدخالات
في بعض الحالات، تكون واجهة برمجة التطبيقات المتزامنة أكثر وضوحًا لمهام معيّنة. إنّ استخدام عدد أقل من طلبات إعادة الاتصال يُعدّ أمرًا جيدًا ويجعل الأمور أكثر سهولة في القراءة. يرجع الجانب السلبي الحقيقي لواجهة برمجة التطبيقات المتزامنة إلى القيود المفروضة على Workers.
لأسباب تتعلق بالأمان، لا تتم أبدًا مشاركة البيانات بين التطبيق المُرسِل وسلسلة مهام 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 ميزات جديدة رائعة لتطبيقات الويب لم يستكشفها الكثير من المستخدمين.