توفير سلاسل محادثات على الويب باستخدام عمال الوحدات

أصبح نقل المهام الصعبة إلى سلاسل المحادثات في الخلفية أسهل الآن باستخدام وحدات JavaScript في Web Workers.

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

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

في ما يلي مثال نموذجي على استخدام العامل، حيث يستمع نص العامل إلى الرسائل الواردة من سلسلة التعليمات الرئيسية ويستجيب عن طريق إرسال رسائل خاصة به:

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

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

السجلّ: العمال الكلاسيكيون

يستخدم دالة Worker عنوان URL لنص برمجي كلاسيكي، وهو مرتبط بعنوان URL للمستند. وتعرض على الفور مرجعًا إلى مثيل العامل الجديد، الذي يعرض واجهة مراسلة بالإضافة إلى طريقة terminate() التي توقف العامل وتدمّره على الفور.

const worker = new Worker('worker.js');

تتوفّر دالة importScripts() ضمن مهام الويب لتحميل رمز إضافي، ولكنها توقف تنفيذ المهمة مؤقتًا من أجل جلب كل نص برمجي وتقييمه. وتنفّذ أيضًا النصوص البرمجية في النطاق العام مثل علامة <script> الكلاسيكية، ما يعني أنّه يمكن استبدال المتغيّرات في نص برمجي واحد بالمتغيّرات في نص برمجي آخر.

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

لهذا السبب، فرضت أدوات Web Worker في السابق تأثيرًا كبيرًا على بنية التطبيق. اضطر المطوّرون إلى إنشاء أدوات وحلول ذكية تتيح استخدام Web Workers بدون التخلي عن ممارسات التطوير الحديثة. على سبيل المثال، تدمج أدوات التجميع مثل webpack عملية تحميل وحدة صغيرة في الرمز البرمجي الذي تم إنشاؤه والذي يستخدم importScripts() لتحميل الرمز البرمجي، ولكنها تلتف حول الوحدات في الدوال لتجنُّب تضارب المتغيّرات ومحاكاة عمليات استيراد وتصدير التبعيات.

إدخال عمال الوحدات

يتم طرح وضع جديد لبرامج الويب النصية في Chrome 80، يُعرف باسم "برامج الوحدات النصية"، ويوفّر مزايا سهولة الاستخدام والأداء التي توفّرها وحدات JavaScript. يقبل مُنشئ Worker الآن خيار {type:"module"} جديدًا، والذي يغيّر تحميل النص البرمجي وتنفيذه ليتطابق مع <script type="module">.

const worker = new Worker('worker.js', {
  type: 'module'
});

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

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

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

لضمان الأداء العالي، لا تتوفّر طريقة importScripts() القديمة في وحدات worker. يعني تبديل العمال لاستخدام وحدات JavaScript أنّه يتم تحميل جميع الرموز في وضع التدقيق الصارم. ومن التغييرات الملحوظة الأخرى أنّ قيمة this في النطاق ذي المستوى الأعلى لوحدة JavaScript هي undefined، في حين أنّ القيمة في وحدات worker الكلاسيكية هي النطاق العام لوحدة worker. لحسن الحظ، كان هناك دائمًا self عام يوفر مرجعًا للنطاق العام. وهي متاحة في جميع أنواع مشغّلي الخدمات، بما في ذلك مشغّلو الخدمات، وكذلك في نموذج DOM.

تحميل العمال مسبقًا باستخدام modulepreload

من التحسينات الكبيرة في الأداء التي تأتي مع وحدات العاملين هي إمكانية التحميل المُسبق للعاملين وعناصرهم التابعة. باستخدام وحدات العامل، يتم تحميل النصوص البرمجية وتنفيذها كوحدات JavaScript عادية، ما يعني أنّه يمكن تحميلها مسبقًا وحتى تحليلها مسبقًا باستخدام modulepreload:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

يمكن أيضًا استخدام الوحدات التي تم تحميلها مسبقًا من خلال كل من سلسلة التعليمات الرئيسية وعاملي الوحدات. هذا مفيد للوحدات التي يتم استيرادها في كلا السياقين، أو في الحالات التي لا يمكن فيها معرفة ما إذا كان سيتم استخدام وحدة في سلسلة التعليمات الرئيسية أو في عامل مسبقًا.

في السابق، كانت الخيارات المتاحة لتحميل نصوص Web Worker البرمجية مسبقًا محدودة وغير موثوقة بالضرورة. كان لدى العاملين الكلاسيكيين نوع مورد "العامل" الخاص بهم للتحميل المُسبق، ولكن لم يتم تنفيذ أي متصفّحات <link rel="preload" as="worker">. نتيجةً لذلك، كانت الطريقة الأساسية المتاحة لتحميل Web Workers مسبقًا هي استخدام <link rel="prefetch">، والتي كانت تعتمد بالكامل على ذاكرة التخزين المؤقت لبروتوكول HTTP. وعند استخدام هذه السمة مع عناوين التخزين المؤقت الصحيحة، أصبح من الممكن تجنُّب انتظار إنشاء مثيل المشغِّل لتنزيل النص البرمجي للمشغِّل. ومع ذلك، على عكس modulepreload، لم تكن هذه التقنية تتيح التحميل المُسبق للتبعيات أو التحليل المُسبق.

ماذا عن المنفّذين المشتركين؟

تم تعديل Shared workers لتتوافق مع وحدات JavaScript اعتبارًا من الإصدار 83 من Chrome. على غرار مشغِّلات العمال المخصّصة، يؤدي إنشاء مشغِّل عامل مشترك باستخدام الخيار {type:"module"} الآن إلى تحميل النص البرمجي للمشغِّل كـ وحدة بدلاً من نص برمجي كلاسيكي:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

قبل إتاحة وحدات JavaScript، كان مُنشئ SharedWorker() يتوقّع فقط عنوان URL ووسيطة name اختيارية. سيستمر هذا في العمل مع استخدام العامل المشترك الكلاسيكي، ولكن يتطلب إنشاء عمال مشتركين في الوحدة استخدام الوسيطة options الجديدة. الخيارات المتاحة هي نفسها الخيارات المتاحة للعامل المخصّص، بما في ذلك الخيار name الذي يحل محل الوسيطة name السابقة.

ماذا عن مشغِّل الخدمات؟

تم تعديل مواصفات مشغّل الخدمات ليتيح قبول وحدة JavaScript كنقطة دخول، باستخدام خيار {type:"module"} نفسه الذي تستخدمه مشغّلات الوحدات، ولكن لم يتم تنفيذ هذا التغيير في المتصفّحات بعد. بعد ذلك، سيكون من الممكن إنشاء مثيل لـ "مشغّل خدمات" باستخدام وحدة JavaScript من خلال الرمز التالي:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

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

مراجع إضافية ومحتوى إضافي للقراءة