أساسيات عمال الويب

المشكلة: تزامن JavaScript

هناك عدد من المؤثّرات السلبية التي تمنع نقل التطبيقات المثيرة للاهتمام (على سبيل المثال، من عمليات التنفيذ الكثيفة الخادم) إلى JavaScript من جهة العميل. ويتضمن بعضها توافق المتصفح والكتابة الثابتة وإمكانية الوصول والأداء. ولحسن الحظ، سرعان ما أصبحت طريقة التحميل الجديدة شيئًا من الماضي حيث يعمل موردو المتصفحات على تحسين سرعة محركات JavaScript بشكل سريع.

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

يحاكي المطوّرون "التزامن" باستخدام أساليب مثل setTimeout() وsetInterval() وXMLHttpRequest ومعالجات الأحداث. نعم، تعمل جميع هذه الميزات بشكل غير متزامن، ولكن عدم حظر المحتوى لا يعني بالضرورة التزامن. تتم معالجة الأحداث غير المتزامنة بعد تسليم النص البرمجي المستخدَم حاليًا. والخبر السار هو أن HTML5 يمنحنا شيئًا أفضل من هذه الاختراقات!

إضافة Web Workers: إضافة سلاسل المحادثات إلى JavaScript

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

مربّع حوار نص برمجي لا يستجيب
مربّع حوار نص برمجي شائع لا يستجيب:

يستخدم العاملون تمرير الرسائل في شكل سلسلة رسائل لتحقيق التوازي. وهي مثالية للحفاظ على تحديث واجهة المستخدم والأداء والاستجابة للمستخدمين.

أنواع عمال الويب

وتجدر الإشارة إلى أنّ المواصفات يناقش نوعَين من "العاملين على الويب"، وهما العاملون المكرسون والعاملون المشترَكون. ستتناول هذه المقالة العاملين المتخصصين فقط. سأشير إليهم باسم "عاملو الويب" أو "عاملون" طوال العملية.

الخطوات الأولى

يعمل موظفو الويب في سلسلة محادثات منفصلة. نتيجة لذلك، يجب تضمين التعليمة البرمجية التي تقوم بتنفيذها في ملف منفصل. قبل أن ننفّذ ذلك، عليك أولاً إنشاء عنصر Worker جديد في صفحتك الرئيسية. تأخذ الدالة الإنشائية اسم البرنامج النصي للعامل:

var worker = new Worker('task.js');

في حال توفُّر الملف المحدَّد، سيُنشئ المتصفِّح سلسلة محادثات عامل جديدة يتم تنزيلها بشكل غير متزامن. لن يبدأ العامل حتى يتم تنزيل الملف وتنفيذه بالكامل. إذا كان المسار إلى العامل يعرض خطأ 404، فسيفشل العامل في صمت.

بعد إنشاء عامل التشغيل، يمكنك بدء العملية من خلال طلب الإجراء postMessage():

worker.postMessage(); // Start the worker.

التواصل مع العامل عن طريق تمرير الرسائل

يتم التواصل بين عمل وصفحته الرئيسية باستخدام نموذج فعالية وطريقة postMessage(). بناءً على المتصفّح أو الإصدار، يمكن أن يقبل postMessage() إما سلسلة أو عنصر JSON كوسيطة واحدة. تتيح أحدث إصدارات المتصفّحات الحديثة تمرير كائن JSON.

وفي ما يلي مثال على استخدام سلسلة لتمرير "Hello World" إلى عامل في doWork.js. يقوم العامل ببساطة بعرض الرسالة التي تم تمريرها إليه.

النص الرئيسي:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.

doWork.js (العامل):

self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);

عند استدعاء postMessage() من الصفحة الرئيسية، يتعامل العامل لدينا مع هذه الرسالة من خلال تحديد معالج onmessage لحدث message. يمكن الوصول إلى حمولة الرسالة (في هذه الحالة "Hello World") في Event.data. على الرغم من أنّ هذا المثال ليس مثيرًا للغاية، فهو يوضّح أن postMessage() هي أيضًا وسيلة لتمرير البيانات إلى سلسلة التعليمات الرئيسية. مريح!

يتم نسخ الرسائل التي يتم تمريرها بين الصفحة الرئيسية والعاملين، ولا تتم مشاركتها. على سبيل المثال، في المثال التالي، يمكن الوصول إلى خاصية "msg" لرسالة JSON في كلا الموقعين. يبدو أنّه يتم تمرير الكائن مباشرةً إلى العامل على الرغم من تشغيله في مساحة منفصلة ومخصّصة. في الواقع، ما يحدث هو أن العنصر يتم تصويره بشكل تسلسلي عند تسليمه إلى العامل، وبالتالي تتم إزالة تسلسله من المسلسل على الطرف الآخر. لا تتشارك الصفحة والعامل نفس المثيل، لذا فإن النتيجة النهائية هي إنشاء نسخة مكررة في كل بطاقة. تنفّذ معظم المتصفّحات هذه الميزة من خلال ترميز JSON أو فك ترميز القيمة تلقائيًا من أيٍّ من الطرفَين.

فيما يلي مثال أكثر تعقيدًا يمرّر الرسائل باستخدام كائنات JSON.

النص الرئيسي:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
function sayHI() {
worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
}

function stop() {
// worker.terminate() from this script would also stop the worker.
worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
}

function unknownCmd() {
worker.postMessage({'cmd': 'foobard', 'msg': '???'});
}

var worker = new Worker('doWork2.js');

worker.addEventListener('message', function(e) {
document.getElementById('result').textContent = e.data;
}, false);
</script>

doWork2.js:

self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    self.postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
    self.postMessage('WORKER STOPPED: ' + data.msg +
                    '. (buttons will no longer work)');
    self.close(); // Terminates the worker.
    break;
default:
    self.postMessage('Unknown command: ' + data.msg);
};
}, false);

عناصر قابلة للنقل

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

يُعد الاستنساخ المنظم أمرًا رائعًا، لكن النسخة يمكن أن تستغرق مئات المللي ثانية. للتغلب على نتيجة الأداء، يمكنك استخدام العناصر القابلة للتحويل.

باستخدام العناصر القابلة للنقل، يتم نقل البيانات من سياق إلى آخر. إنها نسخة صفرية، مما يحسّن بشكل كبير من أداء إرسال البيانات إلى العامل. فكر في الأمر على أنه إشارة مرجعية إذا كنت من عالم C/C++. على عكس الإشارة المرجعية، لا تكون "النسخة" من سياق الاستدعاء متاحة بعد نقلها إلى السياق الجديد. على سبيل المثال، عند نقل ArrayBuffer من تطبيقك الرئيسي إلى Worker، يتم محو ArrayBuffer الأصلية ولن تكون قابلة للاستخدام بعد ذلك. ويتم نقل محتواها (بشكل هادئ حرفيًا) إلى سياق "العاملين".

لاستخدام العناصر القابلة للنقل، يجب استخدام توقيع postMessage() مختلف قليلاً:

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

في حالة العامل، الوسيطة الأولى هي البيانات والثانية هي قائمة العناصر التي يجب نقلها. ليس بالضرورة أن تكون الوسيطة الأولى ArrayBuffer بالمناسبة. على سبيل المثال، يمكن أن يكون كائن JSON:

worker.postMessage({data: int8View, moreData: anotherBuffer},
                [int8View.buffer, anotherBuffer]);

النقطة المهمة هي: يجب أن تكون الوسيطة الثانية صفيفًا من ArrayBuffers. هذه هي قائمة العناصر القابلة للنقل.

للمزيد من المعلومات حول الأجهزة القابلة للنقل، يمكنك الاطّلاع على مشاركتنا على developer.chrome.com.

بيئة العمال

نطاق العامل

في سياق العامل، يشير كل من self وthis إلى النطاق العام للعامل. وبالتالي، يمكن أيضًا كتابة المثال السابق على النحو التالي:

addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
...
}, false);

بدلاً من ذلك، يمكنك ضبط معالِج أحداث "onmessage" مباشرةً (على الرغم من أنّ خبراء دعم JavaScript يشجعون دائمًا على استخدام addEventListener).

onmessage = function(e) {
var data = e.data;
...
};

الميزات المتاحة للعاملين

لا يمكن لموظفي الويب الوصول إلى مجموعة فرعية من ميزات JavaScript إلا بسبب سلوكهم الذي يتضمن سلاسل محادثات متعددة:

  • الكائن navigator
  • الكائن location (للقراءة فقط)
  • XMLHttpRequest
  • setTimeout()/clearTimeout() وsetInterval()/clearInterval()
  • ذاكرة التخزين المؤقت للتطبيق
  • استيراد النصوص البرمجية الخارجية باستخدام طريقة importScripts()
  • إنتاج عمال الويب الآخرين

ولا يمكن للعمّال الوصول إلى:

  • نموذج كائن المستند (DOM) (ليس متوافقًا مع سلاسل المحادثات)
  • الكائن window
  • الكائن document
  • الكائن parent

جارٍ تحميل النصوص البرمجية الخارجية

يمكنك تحميل ملفات نصوص برمجية أو مكتبات خارجية في مشغِّل باستخدام الدالة importScripts(). تأخذ الطريقة صفرًا أو أكثر من السلاسل التي تمثل أسماء الملفات للموارد المراد استيرادها.

يُحمِّل هذا المثال script1.js وscript2.js في العامل:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

ويمكن كتابتها أيضًا كعبارة استيراد واحدة:

importScripts('script1.js', 'script2.js');

العاملون الفرعيون

بإمكان العمال إنتاج أطفال صغار العمال. يُعد هذا أمرًا رائعًا لتقسيم المهام الكبيرة في وقت التشغيل. ومع ذلك، هناك بعض التنبيهات في العاملين الفرعيين:

  • يجب استضافة العاملين الفرعيين ضمن المصدر نفسه الذي ترتبط به الصفحة الرئيسية.
  • يتم حل معرّفات الموارد المنتظمة (URI) ضمن العاملين الفرعيين وفقًا لموقع العامل الرئيسي (مقابل الصفحة الرئيسية).

يُرجى العلم أنّ معظم المتصفِّحات تنتج عمليات منفصلة لكل عامل. قبل أن تنتج مزرعة عمال، كن حذرًا بشأن استنزاف الكثير من موارد نظام المستخدم. أحد أسباب ذلك هو أن الرسائل المتدفقة بين الصفحات الرئيسية والعاملين يتم نسخها وليس مشاركتها. يُرجى الاطّلاع على التواصل مع عامل عن طريق تمرير الرسائل.

للحصول على نموذج عن كيفية إنتاج عامل فرعي، راجِع المثال في المواصفات.

العاملون المضمّنون

ماذا لو كنت تريد إنشاء نص العامل بسرعة، أو إنشاء صفحة مستقلة بدون الحاجة إلى إنشاء ملفات منفصلة للعامل؟ باستخدام Blob()، يمكنك "تضمين" العامل في ملف HTML نفسه كمنطقك الرئيسي عبر إنشاء مؤشر عنوان URL إلى رمز العامل كسلسلة:

var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

عناوين URL لوحدات تخزين البيانات الثنائية (Blob)

المزايا الرائعة متاحة عند الاتصال برقم window.URL.createObjectURL(). تنشئ هذه الطريقة سلسلة عنوان URL بسيطة يمكن استخدامها للإشارة إلى البيانات المخزّنة في عنصر File أو Blob بتنسيق DOM. مثال:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

عناوين URL الخاصة بذاكرة التخزين المؤقت (Blob) هي عناوين فريدة وتبقى صالحة طوال فترة استخدام تطبيقك (على سبيل المثال، إلى أن يتم إلغاء تحميل document). إذا كنت تنشئ العديد من عناوين URL الخاصة بأخطاء Blob، ننصحك بإصدار المراجع التي لم تعُد ضرورية. يمكنك صراحةً تحرير عناوين URL من النوع Blob من خلال تمريرها إلىwindow.URL.revokeObjectURL():

window.URL.revokeObjectURL(blobURL);

في Chrome، هناك صفحة رائعة لعرض جميع عناوين URL التي تم إنشاؤها عن طريق تخزين ثنائي كبير: chrome://blob-internals/.

مثال كامل

انطلاقًا من ذلك، يمكننا أن نصبح أكثر ذكاءً من خلال كيفية تضمين تعليمات JavaScript للعامل في صفحتنا. يستخدِم هذا الأسلوب علامة <script> لتعريف العامل:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>

<div id="log"></div>

<script id="worker1" type="javascript/worker">
// This script won't be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
    self.postMessage('msg from worker');
};
// Rest of your worker code goes here.
</script>

<script>
function log(msg) {
    // Use a fragment: browser will only render/reflow once.
    var fragment = document.createDocumentFragment();
    fragment.appendChild(document.createTextNode(msg));
    fragment.appendChild(document.createElement('br'));

    document.querySelector("#log").appendChild(fragment);
}

var blob = new Blob([document.querySelector('#worker1').textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
    log("Received: " + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>

أعتقد أن هذا النهج الجديد أكثر وضوحًا ووضوحًا إلى حد ما. وتحدّد علامة نص برمجي باستخدام id="worker1" وtype='javascript/worker' (حتى لا يحلل المتصفّح JavaScript). يتم استخراج هذا الرمز كسلسلة باستخدام document.querySelector('#worker1').textContent ويتم تمريره إلى Blob() لإنشاء الملف.

جارٍ تحميل النصوص البرمجية الخارجية

عند استخدام هذه الأساليب لتضمين رمز العامل، لن يعمل importScripts() إلا في حال توفير معرّف موارد منتظم (URI) مطلق. وإذا حاولت تمرير عنوان URI نسبي، سيقدم المتصفح خطأ متعلقًا بالأمان. السبب هو: سيتم التعامل مع العامل (الذي يتم إنشاؤه الآن من عنوان URL ذي كائن فقاعة) باستخدام البادئة blob:، بينما سيتم تشغيل تطبيقك من مخطّط مختلف (من المفترض http://). وبالتالي، سيرجع عدم نجاح هذه العملية إلى القيود المفروضة على جميع المصادر.

إحدى طرق استخدام importScripts() في عامل مضمّن هي "إدخال" عنوان URL الحالي للنص البرمجي الرئيسي الذي يتم تشغيله من خلال تمريره إلى العامل المضمّن وإنشاء عنوان URL المطلق يدويًا. سيضمن ذلك استيراد النص البرمجي الخارجي من المصدر نفسه. لنفترض أن تطبيقك الرئيسي يعمل من خلال "http://example.com/index.html":

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
var data = e.data;

if (data.url) {
var url = data.url.href;
var index = url.indexOf('index.html');
if (index != -1) {
    url = url.substring(0, index);
}
importScripts(url + 'engine.js');
}
...
};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage(<b>{url: document.location}</b>);
</script>

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

وكما هو الحال مع أي منطق في JavaScript، سترغب في معالجة أي أخطاء تظهر في عمال الويب. في حال حدوث خطأ أثناء تنفيذ عامل، سيتم تنشيط ErrorEvent. تحتوي الواجهة على ثلاث سمات مفيدة لمعرفة سبب الخطأ: filename - اسم نص العامل البرمجي الذي تسبب في حدوث الخطأ، وlineno - رقم السطر الذي حدث فيه الخطأ، وmessage - وصف مفيد للخطأ. في ما يلي مثال على إعداد معالج أحداث "onerror" لطباعة خصائص الخطأ:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
function onError(e) {
document.getElementById('error').textContent = [
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join('');
}

function onMsg(e) {
document.getElementById('result').textContent = e.data;
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('message', onMsg, false);
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
</script>

مثال: يحاول employeeWithError.js تنفيذ 1/x، حيث تكون x غير معرَّفة.

// TODO: DevSite - تمت إزالة نموذج التعليمة البرمجية نظرًا لاستخدامه معالِجات الأحداث المضمنة

workerWithError.js:

self.addEventListener('message', function(e) {
postMessage(1/x); // Intentional error.
};

لمحة عن الأمان

قيود الوصول المحلي

بسبب القيود الأمنية في Google Chrome، لن يعمل العاملون على الجهاز (على سبيل المثال من file://) في أحدث إصدارات المتصفّح. بدلاً من ذلك، يفشلون بصمت! لتشغيل تطبيقك من خلال مخطط "file://"، عليك تشغيل Chrome باستخدام مجموعة علامات --allow-file-access-from-files.

ولا تفرض المتصفّحات الأخرى التقييد نفسه.

اعتبارات من المصدر نفسه

يجب أن تكون النصوص البرمجية للمشغِّلات ملفات خارجية لها نفس مخطط صفحة الاتصال. وبالتالي، لا يمكنك تحميل نص برمجي من عنوان URL data: أو عنوان URL للسمة javascript:، ولا يمكن للصفحة https: بدء النصوص البرمجية للعاملين التي تبدأ بـ http: من عناوين URL.

حالات الاستخدام

إذًا، ما نوع التطبيق الذي سيستخدم موظفي الويب؟ إليك بعض الأفكار الإضافية لتحفيز عقلك:

  • يجلب البيانات مسبقًا و/أو ويخزّنها مؤقتًا لاستخدامها لاحقًا.
  • تمييز بنية التعليمة البرمجية أو تنسيقات نصية أخرى في الوقت الفعلي.
  • المدقق الإملائي.
  • تحليل بيانات الفيديو أو الصوت.
  • عمليات الإدخال والإخراج الأساسية أو استطلاع بشأن خدمات الويب:
  • معالجة الصفائف الكبيرة أو استجابات JSON الضخمة
  • فلترة الصور في <canvas>
  • تحديث العديد من صفوف قاعدة بيانات الويب المحلية.

لمزيد من المعلومات عن حالات الاستخدام التي تتضمّن Web Workers API، يُرجى الانتقال إلى نظرة عامة على العاملين.

إصدارات تجريبية

المراجع