عامل مشترك في جافاسكريبت

عامل مشترك في جافاسكريبت

تشرح هذه المقالة العامل المشترك Shared Worker في جافاسكريبت۔

سنشرح كل شيء من أساسيات العامل المشترك Shared Worker إلى حالات الاستخدام العملية خطوة بخطوة۔

YouTube Video

javascript-shared-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args.map(String).join(' ');
119                output.appendChild(message);
120            };
121
122            // Override console.error
123            const originalError = console.error;
124            console.error = function (...args) {
125                originalError.apply(console, args);
126                const message = document.createElement('div');
127                message.textContent = args.map(String).join(' ');
128                message.style.color = 'red'; // Color error messages red
129                output.appendChild(message);
130            };
131        })();
132
133        document.getElementById('executeBtn').addEventListener('click', () => {
134            // Prevent multiple loads
135            if (document.getElementById('externalScript')) return;
136
137            const script = document.createElement('script');
138            script.src = 'javascript-shared-worker.js';
139            script.id = 'externalScript';
140            //script.onload = () => console.log('javascript-shared-worker.js loaded and executed.');
141            //script.onerror = () => console.log('Failed to load javascript-shared-worker.js.');
142            document.body.appendChild(script);
143        });
144    </script>
145</body>
146</html>

عامل مشترك في جافاسكريبت

ما هو العامل المشترك Shared Worker؟

Shared Worker هو خيط عامل يمكن مشاركته بين عدة صفحات (علامات تبويب، نوافذ، إطارات داخلية، إلخ) داخل نفس المنشأ۔ على عكس الـ Dedicated Worker المرتبط بصفحة معينة، الميزة الرئيسية هي أن عملية خلفية واحدة يمكن مشاركتها بين عدة صفحات۔ تشمل حالات الاستخدام النموذجية ما يلي:۔

  • يمكنك مشاركة اتصال WebSocket واحد عبر عدة علامات تبويب، مما يقلل من عدد الاتصالات ويوحد إعادة الاتصال۔
  • يمكنك مزامنة الحالة بين علامات التبويب (إدارة مركزية للنشر/الاشتراك أو المتاجر)۔
  • يمكنك تسلسل عمليات IndexedDB، والتوسط في الوصول المتزامن۔
  • يمكنك منع تكرار تنفيذ العمليات المكلفة من حيث الحساب۔

Shared Worker له دور مختلف عن Service Worker۔ Service Worker يعمل بشكل أساسي كوكيل شبكة، بينما يركز Shared Worker على إجراء عمليات حسابية عشوائية أو إدارة الحالة المشتركة بين الصفحات۔

واجهة برمجة التطبيقات الأساسية ودورة الحياة

الإنشاء (الخيط الرئيسي)

 1// main.js
 2// The second argument 'name' serves as an identifier to share the same SharedWorker
 3// if both the URL and name are the same.
 4const worker = new SharedWorker('/shared-worker.js', { name: 'app-core' });
 5
 6// Get the communication port (MessagePort) with this SharedWorker
 7const port = worker.port;
 8
 9// Receive messages with onmessage, send messages with postMessage
10port.onmessage = (ev) => {
11    console.log('From SharedWorker:', JSON.stringify(ev.data));
12};
13
14// When using onmessage, start() is not required
15// (start() is needed when using addEventListener instead)
16port.postMessage({ type: 'hello', from: location.pathname });
  • يوضح هذا الكود كيفية إنشاء عامل مشترك Shared Worker وإرسال/استقبال الرسائل من خلال المنفذ الخاص به۔

المستقبل (داخل نطاق العامل المشترك Shared Worker)

 1// shared-worker.js
 2// SharedWorkerGlobalScope has an onconnect event,
 3// and a MessagePort is provided for each connection
 4const ports = new Set();
 5
 6onconnect = (e) => {
 7    const port = e.ports[0];
 8    ports.add(port);
 9
10    port.onmessage = (ev) => {
11        // Log the received message and send a reply back to the sender
12        console.log('From page:', ev.data);
13        port.postMessage({ type: 'ack', received: ev.data });
14    };
15
16    // For handling explicit disconnection from the page (via MessagePort.close())
17    port.onmessageerror = (err) => {
18        console.error('Message error:', err);
19    };
20
21    // Greeting immediately after connection
22    port.postMessage({ type: 'welcome', clientCount: ports.size });
23};
24
25// Explicit termination of the SharedWorker is usually unnecessary
26// (It will be garbage collected when all ports are closed)
  • يوضح هذا الكود كيف يتلقى العامل المشترك Shared Worker منفذ رسائل MessagePort لكل اتصال، ويقوم بمعالجة الرسائل والرد عليها من العملاء۔

دورة حياة العامل المشترك Shared Worker

Shared Worker يبدأ عند إنشاء أول اتصال وقد ينهي عند إغلاق آخر منفذ۔ يتم إغلاق الاتصالات عند إعادة تحميل أو إغلاق الصفحة، ويتم إعادة إنشاء العامل إذا لزم الأمر۔

Shared Worker هو "سكريبت طويل العمر يُشارك داخل المتصفح"۔ إذا لم تكن على دراية بدورة حياة Shared Worker، فمن المرجح أن تحدث مشكلات مثل تسرب الموارد، أو بقاء الحالة القديمة، أو عمليات إعادة التشغيل غير المقصودة۔

جرّب ذلك: البث بين علامات التبويب

هذا تطبيق بسيط حيث تتصل عدة علامات تبويب بنفس العامل المشترك Shared Worker وأي رسالة يتم إرسالها تُبث لجميع علامات التبويب۔

main.js

 1const worker = new SharedWorker('/shared-worker.js', { name: 'bus' });
 2const port = worker.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'message') {
 7        const li = document.createElement('li');
 8        li.textContent = `[${new Date(msg.at).toLocaleTimeString()}] ${msg.payload}`;
 9        document.querySelector('#messages').appendChild(li);
10    } else if (msg.type === 'ready') {
11        console.log(`Connected. clients=${msg.clients}`);
12    }
13};
14
15document.querySelector('#form').addEventListener('submit', (e) => {
16    e.preventDefault();
17    const input = document.querySelector('#text');
18    port.postMessage({ type: 'publish', payload: input.value });
19    input.value = '';
20});
21
22document.querySelector('#ping').addEventListener('click', () => {
23    port.postMessage({ type: 'ping' });
24});
  • يقوم هذا الكود بالاتصال بعامل مشترك Shared Worker، وتنفيذ استقبال/عرض الرسائل، وإرسالها من نموذج إرسال، وإرسال إشارة بنقرة زر۔

shared-worker.js

 1// Minimal pub/sub bus in a SharedWorker
 2const subscribers = new Set();
 3
 4/** Broadcast to all connected ports */
 5function broadcast(message) {
 6    for (const port of subscribers) {
 7        try {
 8            port.postMessage(message);
 9        } catch (e) {
10            // Unsubscribe if sending fails, just in case
11            subscribers.delete(port);
12        }
13    }
14}
15
16onconnect = (e) => {
17    const port = e.ports[0];
18    subscribers.add(port);
19
20    port.onmessage = (ev) => {
21        const msg = ev.data;
22        if (msg && msg.type === 'publish') {
23            broadcast({ type: 'message', payload: msg.payload, at: Date.now() });
24        } else if (msg && msg.type === 'ping') {
25            port.postMessage({ type: 'pong', at: Date.now() });
26        }
27    };
28
29    port.postMessage({ type: 'ready', clients: subscribers.size });
30};
  • يُنفذ هذا الكود ميزة نشر/اشتراك بسيطة داخل العامل المشترك Shared Worker لإعادة توجيه الرسائل بين عدة عملاء۔

index.html

 1<!doctype html>
 2<html>
 3    <body>
 4        <form id="form">
 5            <input id="text" placeholder="say something" />
 6            <button>Send</button>
 7        </form>
 8        <button id="ping">Ping</button>
 9        <ul id="messages"></ul>
10        <script src="/main.js" type="module"></script>
11    </body>
12</html>
  • إذا فتحت هذه الصفحة في عدة علامات تبويب، فسيتم إعلام جميع العلامات بالرسائل المرسلة من أي علامة تبويب۔

تصميم الرسائل: الطلب/الاستجابة ومعرّف الارتباط

عندما يتفاعل عدة عملاء مع العامل المشترك Shared Worker، غالبًا ما تحتاج إلى تحديد أي استجابة تتعلق بأي طلب۔ لذلك، من الممارسات القياسية تضمين معرف الارتباط لربط الردود بالطلبات۔

main.js

 1function createClient(workerUrl, name) {
 2    const w = new SharedWorker(workerUrl, { name });
 3    const port = w.port;
 4    const pending = new Map(); // id -> {resolve,reject}
 5    let nextId = 1;
 6
 7    port.onmessage = (ev) => {
 8        const { id, ok, result, error } = ev.data || {};
 9        if (!id || !pending.has(id)) return;
10        const { resolve, reject } = pending.get(id);
11        pending.delete(id);
12        ok ? resolve(result) : reject(new Error(error));
13    };
14
15    function call(method, params) {
16        const id = nextId++;
17        return new Promise((resolve, reject) => {
18            pending.set(id, { resolve, reject });
19            port.postMessage({ id, method, params });
20        });
21    }
22
23    return { call };
24}
25
26// usage
27const client = createClient('/shared-worker.js', 'rpc');
28client.call('add', { a: 2, b: 3 }).then(console.log);   // -> 5
29client.call('sleep', { ms: 500 }).then(console.log);     // -> 'done'
  • ينفذ هذا الكود عميل RPC بسيط يمكنه إجراء استدعاءات طرق غير متزامنة إلى العامل المشترك Shared Worker۔ هنا، يشير خادم RPC (خادم استدعاء الإجراءات البعيدة) إلى خادم يوفر آلية لاستدعاء الدوال والإجراءات من برامج أو عمليات أخرى۔
  • في هذا المثال، يتم زيادة الـ id ببساطة، لكن يمكنك أيضًا استخدام UUID، أو سلسلة عشوائية، أو دمج طابع زمني مع عداد لإنشاء مفتاح فريد۔

shared-worker.js

 1// A small request/response router with correlation ids
 2onconnect = (e) => {
 3    const port = e.ports[0];
 4
 5    port.onmessage = async (ev) => {
 6        const { id, method, params } = ev.data || {};
 7        try {
 8            let result;
 9            switch (method) {
10                case 'add':
11                    result = (params?.a ?? 0) + (params?.b ?? 0);
12                    break;
13                case 'sleep':
14                    await new Promise(r => setTimeout(r, params?.ms ?? 0));
15                    result = 'done';
16                    break;
17                default:
18                    throw new Error(`Unknown method: ${method}`);
19            }
20            port.postMessage({ id, ok: true, result });
21        } catch (err) {
22            port.postMessage({ id, ok: false, error: String(err) });
23        }
24    };
25};
  • يقوم هذا الكود بتنفيذ خادم RPC بسيط يعالج العمليات وفقًا لمتغير method ويرسل النتيجة مرة أخرى مع معرف الطلب۔

أنماط الاستخدام النموذجية

هناك عدة أنماط عملية عند استخدام العامل المشترك Shared Worker، مثل:۔

تعدد الإرسال عبر WebSocket واحد (مشترك بين علامات التبويب)

إذا فتحت كل علامة تبويب اتصال WebSocket خاص بها، سيؤثر ذلك على حمل الخادم وحد الاتصال۔ ضع WebSocket واحد فقط داخل العامل المشترك Shared Worker، وكل تبويب يرسل/يستقبل الرسائل عبر العامل۔

main.js

 1const w = new SharedWorker('/shared-worker.js', { name: 'ws' });
 2const port = w.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'data') {
 7        console.log('Server says:', msg.payload);
 8    } else if (msg.type === 'socket') {
 9        console.log('WS state:', msg.state);
10    }
11};
12
13function send(payload) {
14    port.postMessage({ type: 'send', payload });
15}
  • ينفذ هذا الكود عملية إرسال/استقبال الرسائل إلى خادم WebSocket وتلقي إشعارات حالة الاتصال من خلال عامل مشترك Shared Worker۔

shared-worker.js

 1let ws;
 2const ports = new Set();
 3
 4function ensureSocket() {
 5    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
 6        return;
 7    }
 8    ws = new WebSocket('wss://example.com/stream');
 9    ws.onopen = () => broadcast({ type: 'socket', state: 'open' });
10    ws.onclose = () => broadcast({ type: 'socket', state: 'closed' });
11    ws.onerror = (e) => broadcast({ type: 'socket', state: 'error', detail: String(e) });
12    ws.onmessage = (m) => broadcast({ type: 'data', payload: m.data });
13}
14
15function broadcast(msg) {
16    for (const p of ports) p.postMessage(msg);
17}
18
19onconnect = (e) => {
20    const port = e.ports[0];
21    ports.add(port);
22    ensureSocket();
23
24    port.onmessage = (ev) => {
25        const msg = ev.data;
26        if (msg?.type === 'send' && ws?.readyState === WebSocket.OPEN) {
27            ws.send(JSON.stringify(msg.payload));
28        }
29    };
30};
  • ينفذ هذا الكود آلية داخل العامل المشترك Shared Worker لمشاركة اتصال WebSocket واحد، مع بث حالة الاتصال والبيانات المستلمة إلى جميع العملاء، في حين تُرسل طلبات كل عميل المنطلقة إلى الخادم عبر WebSocket۔

  • من خلال مركزية استراتيجيات إعادة الاتصال وطابور الرسائل غير المرسلة في Shared Worker، يصبح السلوك موحدًا عبر جميع علامات التبويب۔

وساطة IndexedDB (التسلسل)

عند الوصول إلى نفس قاعدة البيانات من عدة علامات تبويب، إذا أردت تجنب التعارضات الناتجة عن المعاملات المتزامنة أو الانتظار بسبب الأقفال، يمكنك ترتيبها في طابور داخل العامل المشترك Shared Worker ومعالجتها بشكل تسلسلي۔

 1// db-worker.js (very simplified – error handling omitted)
 2let db;
 3async function openDB() {
 4    if (db) return db;
 5    db = await new Promise((resolve, reject) => {
 6        const req = indexedDB.open('appdb', 1);
 7        req.onupgradeneeded = () => req.result.createObjectStore('kv');
 8        req.onsuccess = () => resolve(req.result);
 9        req.onerror = () => reject(req.error);
10    });
11    return db;
12}
13
14async function put(key, value) {
15    const d = await openDB();
16    return new Promise((resolve, reject) => {
17        const tx = d.transaction('kv', 'readwrite');
18        tx.objectStore('kv').put(value, key);
19        tx.oncomplete = () => resolve(true);
20        tx.onerror = () => reject(tx.error);
21    });
22}
23
24async function get(key) {
25    const d = await openDB();
26    return new Promise((resolve, reject) => {
27        const tx = d.transaction('kv', 'readonly');
28        const req = tx.objectStore('kv').get(key);
29        req.onsuccess = () => resolve(req.result);
30        req.onerror = () => reject(req.error);
31    });
32}
33
34onconnect = (e) => {
35    const port = e.ports[0];
36    port.onmessage = async (ev) => {
37        const { id, op, key, value } = ev.data;
38        try {
39            const result = op === 'put' ? await put(key, value) : await get(key);
40            port.postMessage({ id, ok: true, result });
41        } catch (err) {
42            port.postMessage({ id, ok: false, error: String(err) });
43        }
44    };
45};
  • ينفذ هذا الكود آلية يتم فيها ترتيب عمليات القراءة/الكتابة إلى IndexedDB في طابور عبر العامل المشترك Shared Worker، لتسلسل الوصول من عدة علامات تبويب وتجنب التعارضات ومشاكل الأقفال۔

النمذجة ونصائح حول أدوات التجميع

إذا أردت كتابة نص العامل المشترك Shared Worker كـ ES Module، قد يختلف السلوك والدعم حسب البيئة۔ عمليًا، من الأكثر أمانًا اختيار أحد الحلول التالية:۔

  • اكتب بشكل عامل كلاسيكي، واستخدم importScripts() لتحميل التبعيات إذا لزم الأمر۔
  • استخدم ميزة إدخال العامل في أداة التجميع الخاصة بك (مثل Vite أو Webpack أو esbuild، إلخ)، وأنشئ حزمة منفصلة للعامل المشترك Shared Worker أثناء البناء۔

نصائح لمعالجة الأخطاء، واكتشاف انقطاع الاتصال، والمتانة

ضع في اعتبارك النقاط التالية لمعالجة الأخطاء وزيادة المتانة:۔

  • التعامل مع الإرسال قبل إنشاء الاتصال رتب الرسائل في طابور التي تصل قبل أن يصبح المنفذ جاهزًا۔

  • اكتشاف انقطاع الاتصال لا يحتوي MessagePort على معالج قياسي لـ onclose۔ من الطرف الرئيسي، أرسل رسالة {type: 'bye'} باستخدام port.postMessage أثناء حدث beforeunload، أو حدد ذلك بوضوح في البروتوكول لضمان تنظيف العامل۔

  • إعادة الاتصال عند إعادة تحميل الصفحة أو إعادة فتح علامة التبويب، يتم إنشاء منفذ جديد۔ جهز رسالة مزامنة أولية (لإرسال الحالة الكاملة دفعة واحدة)۔

  • ضغط العودة (Backpressure) خلال البث الكثيف، قم بالتبديل إلى تقليل التردد/تثبيط الإرسال أو إرسال لقطات الحالة۔

  • الأمان العامل المشترك Shared Worker يُشارك أساسًا ضمن نفس النطاق (المنشأ)۔ إذا وضعت أسرارًا داخل العامل، ففكر في تصميم تحقق جانبي باستخدام رموز أو وسائل مشابهة للجهة المستدعية۔

كيفية استخدام Dedicated Worker و Shared Worker و Service Worker بشكل مناسب

كل نوع من العمال (Worker) له الخصائص التالية:۔

  • Dedicated Worker Dedicated Worker مخصص للاستخدام من قبل صفحة واحدة فقط۔ يوفر فصل العمليات الحسابية عن واجهة المستخدم بنسبة 1:1۔

  • Shared Worker Shared Worker يمكن مشاركته بين عدة صفحات من نفس الأصل۔ هو مثالي لـ الاتصال بين الصفحات و مشاركة اتصال واحد۔

  • Service Worker Service Worker يمكن استخدامه لتوكيل الشبكة، التخزين المؤقت، العمل دون اتصال بالإنترنت، الإشعارات الفورية، والمزامنة الخلفية۔ قوته تكمن في اعتراض طلبات الجلب (fetch requests)۔

كقاعدة عامة: استخدم Shared Worker لـ "مشاركة المعلومات والمعالجة العشوائية بين علامات التبويب"، وService Worker لـ "التحكم في الشبكة"، وDedicated Worker إذا كنت تريد فقط تفريغ المعالجة الثقيلة من واجهة المستخدم۔

الأخطاء الشائعة

عند استخدام Shared Worker، يجب الانتباه للنقاط التالية۔

  • نسيان استدعاء start() أو الاستدعاءات غير الضرورية عند استخدام port.addEventListener('message', ...)، يجب عليك استدعاء port.start()۔ هذا غير ضروري إذا استخدمت port.onmessage = ...۔

  • البث غير المقيد يزداد الحمل مع ازدياد عدد الصفحات المفتوحة (التبويبات)۔ يمكنك تقليل الحمل بتنفيذ التوصيل التفاضلي أو مواضيع الاشتراك (التصفية حسب الموضوع)۔

  • تكلفة نسخ الكائنات postMessage يقوم بنسخ البيانات۔ في حالة البيانات الكبيرة، فكر في إرسالها كـ بيانات قابلة للنقل (Transferable) مثل ArrayBuffer أو باستخدام الذاكرة المشتركة (SharedArrayBuffer) مع Atomics۔

  • مدة الحياة وإعادة التهيئة Shared Worker قد ينتهي عند قطع الاتصال من آخر عميل۔ صمم عملية التهيئة بشكل صحيح للاتصالات الأولى وإعادة التشغيل لتجنب الآثار الجانبية والأخطاء۔

الملخص

  • Shared Worker مكان لتنفيذ منطق مخصص طويل المدى يُشترك بين عدة صفحات۔
  • حدد بوضوح عقد الرسائل (الأنواع/البروتوكولات) واجعل تصميم الطلب/الاستجابة الخاص بك قويًا بإرفاق معرّفات الترابط (correlation IDs)۔
  • وهو مثالي للعمليات التي تعمل بشكل أفضل عند توحيدها عبر جميع الصفحات المفتوحة، مثل تعدد اتصالات WebSocket أو تسلسل الوصول إلى IndexedDB۔
  • ضبط التفاصيل مثل أدوات الربط (bundlers)، تعريفات الأنواع، وإعادة الاتصال يمكن أن يحسن بشكل كبير من سهولة الصيانة وتجربة المستخدم۔

يمكنك متابعة المقالة أعلاه باستخدام Visual Studio Code على قناتنا على YouTube.۔ يرجى التحقق من القناة على YouTube أيضًا.۔

YouTube Video