أصبح نقل المهام الصعبة إلى سلاسل الخلفية أسهل الآن باستخدام وحدات 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 نسبي إلى عنوان URL الخاص بالمستند. تعرض هذه الدالة على الفور مرجعًا إلى مثيل العامل الجديد،
الذي يعرض واجهة مراسلة بالإضافة إلى الدالة terminate()
التي توقف العامل وتتلفه على الفور.
const worker = new Worker('worker.js');
تتوفّر الدالة importScripts()
ضمن Web Workers لتحميل تعليمات برمجية إضافية، ولكنها توقف تنفيذ العامل مؤقتًا من أجل جلب كل نص برمجي وتقييمه. ويتم أيضًا تنفيذ النصوص البرمجية في النطاق العمومي مثل علامة <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 Workers تأثير كبير على بنية التطبيق. اضطر المطوّرون إلى إنشاء أدوات وحلول بديلة ذكية لإتاحة استخدام Web Workers بدون التخلي عن ممارسات التطوير الحديثة. على سبيل المثال، تدمج أدوات التجميع، مثل webpack، عملية تنفيذ صغيرة لأداة تحميل الوحدات في الرمز الذي تم إنشاؤه والذي يستخدم importScripts()
لتحميل الرمز، ولكنها تغلف الوحدات في دوال لتجنُّب تعارض المتغيرات ومحاكاة عمليات استيراد وتصدير التبعيات.
إدخال العاملين في الوحدة
سيتم طرح وضع جديد لبرامج Web Workers في الإصدار 80 من Chrome، وهو وضع يوفّر مزايا وحدات JavaScript
النموذجية من حيث سهولة الاستخدام والأداء، ويُطلق عليه اسم "برامج Web Workers النموذجية". يقبل الدالة الإنشائية
Worker
الآن الخيار الجديد {type:"module"}
، الذي يغيّر طريقة تحميل البرنامج النصي وتنفيذه لتتطابق مع <script type="module">
.
const worker = new Worker('worker.js', {
type: 'module'
});
بما أنّ وحدات العامل هي وحدات JavaScript عادية، يمكنها استخدام عبارات الاستيراد والتصدير. كما هو الحال مع جميع وحدات JavaScript، لا يتم تنفيذ التبعيات إلا مرة واحدة في سياق معيّن (العملية الرئيسية أو العامل أو غير ذلك)، وتشير جميع عمليات الاستيراد المستقبلية إلى مثيل الوحدة الذي تم تنفيذه من قبل. تعمل المتصفّحات أيضًا على تحسين عملية تحميل وحدات JavaScript وتنفيذها. يمكن تحميل العناصر التابعة لوحدة معيّنة قبل تنفيذ الوحدة، ما يتيح تحميل جميع عناصر الوحدة بشكل متوازٍ. يخزّن تحميل الوحدات مؤقتًا الرمز الذي تم تحليله، ما يعني أنّه يجب تحليل الوحدات المستخدَمة في سلسلة التعليمات الرئيسية وفي عامل مرة واحدة فقط.
يتيح الانتقال إلى وحدات JavaScript أيضًا استخدام dynamic
import لتحميل الرمز البرمجي بشكل غير متزامن بدون حظر تنفيذ
العامل. تكون عملية الاستيراد الديناميكي أكثر وضوحًا من استخدام 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()
القديمة ضمن وحدات العاملين. عند التبديل إلى استخدام وحدات JavaScript، يتم تحميل جميع الرموز في الوضع الصارم. من التغييرات الأخرى الجديرة بالذكر أنّ قيمة this
في النطاق ذي المستوى الأعلى لوحدة JavaScript هي undefined
، بينما تكون القيمة في العاملين التقليديين هي النطاق العام للعامل. لحسن الحظ، كان هناك دائمًا self
global يوفّر مرجعًا للنطاق العام. وهي متاحة في جميع أنواع العاملين، بما في ذلك مشغّلو الخدمات، بالإضافة إلى 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>
يمكن أيضًا استخدام الوحدات المحمَّلة مسبقًا من خلال كلّ من سلسلة التعليمات الرئيسية وعمال الوحدات. ويكون ذلك مفيدًا للوحدات التي يتم استيرادها في كلا السياقين، أو في الحالات التي لا يمكن فيها معرفة مسبقًا ما إذا كان سيتم استخدام وحدة في سلسلة التعليمات الرئيسية أو في عامل.
في السابق، كانت الخيارات المتاحة للتحميل المُسبَق لبرامج نصية خاصة بعامل الويب محدودة وغير موثوقة بالضرورة. كان لدى Classic Workers نوع مورد "عامل" خاص به للتحميل المُسبَق، ولكن لم تنفّذ أي متصفحات <link rel="preload" as="worker">
. نتيجةً لذلك، كانت التقنية الأساسية المتاحة للتحميل المسبق للعاملين على الويب هي استخدام <link rel="prefetch">
، والتي كانت تعتمد بشكل كامل على ذاكرة التخزين المؤقت لبروتوكول HTTP. وعند استخدامها مع عناوين التخزين المؤقت الصحيحة، أتاح ذلك تجنُّب انتظار إنشاء المشغّل إلى حين تنزيل النص البرمجي الخاص به. ومع ذلك، لم تكن هذه التقنية تتيح التحميل المُسبَق للعناصر التابعة أو التحليل المُسبَق، على عكس modulepreload
.
ماذا عن العمال المشتركين؟
تم تعديل العاملين المشترَكين لتوفير دعم لوحدات 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 عند استخدامها مع عاملي الخدمة. بالإضافة إلى ذلك، يجب أن تتمكّن برامج الخدمة من تجاوز ذاكرة التخزين المؤقت للبرامج النصية في حالات معيّنة عند التحقّق من توفّر تحديثات.