عامل مشترك في جافاسكريبت
تشرح هذه المقالة العامل المشترك 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 & 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 أيضًا.۔