مقدمه

در درس قبل با Dedicated Worker آشنا شدیم که یک ارتباط یک-به-یک بین یک اسکریپت و یک ریسمان پس‌زمینه برقرار می‌کرد. اما گاهی اوقات نیاز داریم که چندین زمینه مختلف (مانند چند تب باز از یک سایت، یا یک صفحه و <iframe>های داخل آن) با یک ریسمان پس‌زمینه واحد ارتباط برقرار کرده و یک وضعیت (state) مشترک را مدیریت کنند. Shared Worker دقیقاً برای این منظور طراحی شده است.

یک Shared Worker به تمام صفحات و اسکریپت‌هایی که از یک مبدأ (origin) یکسان هستند، اجازه می‌دهد تا به یک نمونه Worker مشترک متصل شوند. این قابلیت برای مدیریت منابع مشترک مانند یک اتصال WebSocket واحد، همگام‌سازی وضعیت بین تب‌ها، یا مدیریت یک کش مشترک بسیار قدرتمند است.

مدل ارتباطی جدید: پورت‌ها

از آنجایی که یک Shared Worker باید بتواند با چندین کلاینت به صورت همزمان صحبت کند، مدل ارتباطی آن کمی با Dedicated Worker متفاوت است. در اینجا، ارتباط از طریق مفهومی به نام «پورت» (MessagePort) انجام می‌شود. هر کلاینتی که به Shared Worker متصل می‌شود، یک پورت ارتباطی منحصر به فرد دریافت می‌کند.

سمت کلاینت (اسکریپت اصلی)

در اسکریپت اصلی، فرآیند تقریباً مشابه قبل است، با این تفاوت که به جای کار با خود شیء Worker، با پراپرتی port آن کار می‌کنیم.

  • ایجاد/اتصال به Worker: با new SharedWorker('worker.js') یک نمونه می‌سازیم. اگر این اولین باری باشد که از این Worker استفاده می‌شود، یک ریسمان جدید ایجاد می‌شود. در غیر این صورت، به همان Worker موجود متصل می‌شویم.
  • ارتباط: برای ارسال و دریافت پیام، از worker.port.postMessage() و worker.port.onmessage استفاده می‌کنیم.
  • شروع پورت: باید متد worker.port.start() را فراخوانی کنیم تا پورت به صورت فعال برای دریافت پیام‌ها آماده شود (اگرچه در برخی مرورگرها با ثبت onmessage این کار به صورت ضمنی انجام می‌شود).

سمت ورکر (اسکریپت ورکر)

تغییر اصلی در اسکریپت خود Worker است. به جای رویداد message، یک Shared Worker به رویداد connect گوش می‌دهد.

  • رویداد connect: هر بار که یک اسکریپت جدید به Worker متصل می‌شود، این رویداد در حوزه سراسری Worker اجرا می‌شود.
  • دسترسی به پورت: شیء رویداد در این حالت، یک پراپرتی به نام ports دارد که یک آرایه است و اولین عضو آن (event.ports[0]) همان پورت ارتباطی با کلاینت جدید است.
  • ارتباط با کلاینت خاص: Worker باید ارجاع به این پورت را ذخیره کرده و از طریق آن با کلاینت مربوطه پیام رد و بدل کند (port.onmessage و port.postMessage()).

مثال عملی: شمارنده اشتراکی بین تب‌ها

برای درک بهتر، بیایید یک شمارنده بسازیم که مقدار آن بین تمام تب‌های باز از سایت ما به اشتراک گذاشته شود. هر تب می‌تواند شمارنده را افزایش دهد و تمام تب‌های دیگر بلافاصله مقدار جدید را مشاهده خواهند کرد.

Copy Icon JAVASCRIPT - a file named shared-counter.js
// shared-counter.js
let count = 0;
const connectedPorts = [];

// Listen for new connections
self.onconnect = function(e) {
    // Get the port for this specific connection
    const port = e.ports[0];
    connectedPorts.push(port);

    // Listen for messages on this specific port
    port.onmessage = function(event) {
        if (event.data === 'increment') {
            count++;
            // Broadcast the new count to ALL connected tabs
            connectedPorts.forEach(p => {
                p.postMessage({ count: count });
            });
        } else if (event.data === 'get') {
            // Send the current count only to the requesting tab
            port.postMessage({ count: count });
        }
    };
    
    // Start the port
    port.start();
};

این اسکریپت Shared Worker است. یک متغیر count و یک آرایه برای نگهداری تمام پورت‌های متصل شده دارد. با هر اتصال جدید، پورت آن را به آرایه اضافه می‌کند. وقتی پیامی دریافت می‌کند، بسته به نوع آن، یا شمارنده را افزایش داده و به همه پورت‌ها اطلاع می‌دهد، یا فقط مقدار فعلی را برای پورت درخواست‌دهنده ارسال می‌کند.

Copy Icon JAVASCRIPT - main.js (used in all pages)
const worker = new SharedWorker('shared-counter.js');
const incrementBtn = document.getElementById('increment-btn');
const countSpan = document.getElementById('count');

// Ask for the initial count when the page loads
worker.port.postMessage('get');

incrementBtn.addEventListener('click', () => {
    worker.port.postMessage('increment');
});

// Listen for updates from the worker
worker.port.onmessage = (event) => {
    countSpan.textContent = event.data.count;
};

این اسکریپت سمت کلاینت است که در تمام صفحات استفاده می‌شود. هر صفحه به Shared Worker متصل شده و با پورت خود با آن ارتباط برقرار می‌کند. اگر شما دو تب مختلف را با این صفحه باز کنید و در یکی از آنها دکمه را فشار دهید، خواهید دید که شمارنده در هر دو تب به صورت همزمان به‌روز می‌شود.

در این درس با Shared Workers و الگوی پیچیده‌تر اما قدرتمند آنها برای مدیریت وضعیت اشتراکی آشنا شدیم. این نوع ورکر برای سناریوهای خاصی که نیاز به هماهنگی بین تب‌ها دارند، بسیار مفید است و از ایجاد منابع تکراری در هر تب جلوگیری می‌کند. در درس پایانی این فصل، به سراغ Service Workers خواهیم رفت که قدرتمندترین نوع ورکر هستند و با ایفای نقش به عنوان یک پراکسی شبکه، قابلیت‌هایی انقلابی مانند کار آفلاین و push notification را برای وب به ارمغان می‌آورند.