Shared Worker בג'אווהסקריפט

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 &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?

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 בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video