Shared Worker בג'אווהסקריפט
מאמר זה מסביר את ה-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
?
Shared Worker
הוא תהליך רקע שניתן לשתף בין מספר עמודים (טאבים, חלונות, iframes וכו') תחת אותו מקור (origin). בניגוד ל-Dedicated Worker
שמוקצה לדף מסוים, התכונה העיקרית היא ש-תהליך רקע אחד יכול להיות משותף בין מספר דפים. מקרי שימוש טיפוסיים כוללים את הבאים:.
- אפשר לשתף חיבור WebSocket יחיד בין מספר טאבים, לצמצם את מספר החיבורים ולרכז את תהליך ההתחברות מחדש.
- אפשר לסנכרן מצב בין טאבים (ניהול מרכזי עבור Pub/Sub או חנויות נתונים).
- אפשר לסדר באופן סדרתי (serialize) פעולות IndexedDB, ולתווך גישה סימולטנית.
- אפשר למנוע הרצה כפולה של תהליכים יקרים חישובית.
Shared Worker
ממלא תפקיד שונה מ-Service Worker
.Service Worker
משמש בעיקר כמתווך רשת, בעוד ש-Shared Worker
מתמקד ב-ביצוע חישובים שרירותיים או ניהול מצב משותף בין דפים.
API בסיסי ומחזור חיים
יצירה (ה-thread הראשי)
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
ולשלוח/לקבל הודעות באמצעות ה-port שלו.
קבלת הודעות (בתוך מרחב ה-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
מתחיל כאשר החיבור הראשון נוצר ויכול להסתיים כאשר הפורט האחרון נסגר. חיבורים נסגרים כאשר העמוד נטען מחדש/נסגר, וה-worker נוצר מחדש במקרה הצורך.
Shared Worker
הוא "סקריפט ארוך טווח שמשותף בתוך הדפדפן". אם אינך מודע למחזור החיים של ה-Shared Worker
, בעיות כמו דליפת משאבים, שימור מצב ישן ואתחולים בלתי צפויים עלולות להתרחש ביתר קלות.
נסו זאת: שידור (broadcast) בין טאבים
זוהי מימוש מינימלי שבו מספר טאבים מחוברים לאותו 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};
- הקוד הזה מממש יכולת pub/sub פשוטה בתוך ה-
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>
- אם תפתחו את הדף הזה בכמה טאבים, ההודעות שנשלחו מכל טאב יודיעו לכל השאר.
עיצוב הודעות: Request/Response ומזהה מתאם (Correlation ID)
כאשר מספר לקוחות מתקשרים עם Shared Worker
, לעיתים קרובות תרצו לזהות איזה תשובה שייכת לאיזו בקשה. לכן, נהוג להוסיף מזהה התאמה (correlation ID) כדי לשייך תגובות לבקשות.
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 (Remote Procedure Call) הוא שרת המספק מנגנון לקריאה לפונקציות ופרוצדורות מתוכניות או תהליכים אחרים. - בדוגמה זו, ה-
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
, כמו הבאים:.
ריבוב (Multiplexing) של WebSocket אחד (משותף בין טאבים)
אם כל לשונית פותחת חיבור WebSocket משל עצמה, הדבר ישפיע על עומס השרת ועל מגבלת החיבורים. הכניסו WebSocket אחד בלבד ל-Shared Worker
, וכל טאב ישלח/יקבל את ההודעות דרך ה-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
, כך שהגישה הסדרתית מכל הטאבים מונעת קונפליקטים והמתנה למנעולים.
המלצות למודולריות ו-Bundlers
אם תרצו לכתוב את סקריפט ה-Shared Worker
שלכם כ-Modul ES, ההתנהגות והתמיכה משתנות בין סביבות שונות. לרוב, עדיף לבחור באחת מהאפשרויות הבאות:.
- כתבו בפורמט worker קלאסי, והשתמשו ב-
importScripts()
לטעינת תלותיות אם יש צורך. - השתמשו ב-worker entry של bundler שלכם (למשל Vite / Webpack / esbuild וכו'), וצרו bundle נפרד ל-
Shared Worker
בזמן הבנייה (build).
טיפים להתמודדות עם שגיאות, זיהוי ניתוקים וחוסן מערכת
קחו בחשבון את הנקודות הבאות עבור טיפול בשגיאות וחוסן:.
-
שליחה לפני שהחיבור נוצר הכניסו להמתנה הודעות שמגיעות לפני שה-port מוכן.
-
זיהוי ניתוק ל-
MessagePort
אין handler סטנדרטי בשםonclose
. בצד הראשי, שלחו הודעה{type: 'bye'}
עםport.postMessage
בזמןbeforeunload
, או לחלופין הגדירו זאת באופן ברור בפרוטוקול כדי לוודא שה-worker מנוקה. -
התחברות מחדש כאשר עמוד נטען מחדש או טאב נפתח שוב, נוצר port חדש. הכינו הודעת סנכרון ראשונית (לשלוח את כל המצב ברגע אחד).
-
Backpressure (עומס יתר בשידור) בעת שידור כבד, עברו להשהיה/דילול (throttling/debouncing) או שליחת snapshots.
-
אבטחה
Shared Worker
משותף באופן עקרוני בתוך אותו origin. אם משבצים מידע רגיש בתוך ה-worker, שקלו להוסיף סימוכין לצד הקורא בעזרת טוקן או מנגנון דומה.
כיצד להשתמש נכון ב-Dedicated Worker
, Shared Worker
, ו-Service Worker
לכל סוג של Worker יש את המאפיינים הבאים:.
-
Dedicated Worker
Dedicated Worker
מיועד לשימוש על ידי דף יחיד בלבד. הוא מאפשר הפרדה של אחד לאחד בין חישובים לבין ממשק המשתמש (UI). -
Shared Worker
Shared Worker
ניתן לשיתוף בין מספר דפים עם אותו מקור (origin). הוא אידאלי עבור תקשורת בין טאבים ו-שיתוף חיבור יחיד. -
Service Worker
Service Worker
יכול לשמש עבור פרוקסי רשת, מטמון (caching), עבודה לא מקוונת, שליחת התראות (push notifications) וסנכרון ברקע. היתרון המרכזי שלו הוא היכולת ל-יירט בקשות fetch.
ככלל אצבע: השתמשו ב-
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 ID).
- הוא אידאלי עבור תהליכים שפועלים טוב יותר באיחוד בין כל הטאבים, כגון מולטיפלקסינג של חיבורי WebSocket או סידור סדר גישה ל-IndexedDB.
- כיוונון פרטים כמו bundlers, הגדרות סוגים, וחיבור מחדש יכולים לשפר משמעותית את התחזוקה וחוויית המשתמש.
תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.