مبانی وب کارگران

مشکل: همزمانی جاوا اسکریپت

تعدادی تنگنا وجود دارد که مانع از انتقال برنامه های کاربردی جالب (مثلاً از پیاده سازی های سنگین سرور) به جاوا اسکریپت سمت کلاینت می شود. برخی از این موارد عبارتند از سازگاری مرورگر، تایپ استاتیک، قابلیت دسترسی و عملکرد. خوشبختانه، دومی به سرعت در حال تبدیل شدن به چیزی از گذشته است زیرا فروشندگان مرورگرها به سرعت سرعت موتورهای جاوا اسکریپت خود را بهبود می بخشند.

یکی از مواردی که مانعی برای جاوا اسکریپت باقی مانده است، خود زبان است. جاوا اسکریپت یک محیط تک رشته ای است، به این معنی که چندین اسکریپت نمی توانند همزمان اجرا شوند. به عنوان مثال، سایتی را تصور کنید که نیاز به مدیریت رویدادهای UI، پرس و جو و پردازش مقادیر زیادی از داده های API و دستکاری DOM دارد. خیلی رایج است، درست است؟ متأسفانه به دلیل محدودیت در زمان اجرای جاوا اسکریپت مرورگرها، همه اینها نمی توانند همزمان باشند. اجرای اسکریپت در یک رشته اتفاق می افتد.

توسعه‌دهندگان با استفاده از تکنیک‌هایی مانند setTimeout() , setInterval() , XMLHttpRequest و کنترل کننده رویداد تقلید می‌کنند. بله، همه این ویژگی ها به صورت ناهمزمان اجرا می شوند، اما مسدود نشدن لزوما به معنای همزمانی نیست. رویدادهای ناهمزمان پس از به دست آوردن اسکریپت اجرایی فعلی پردازش می شوند. خبر خوب این است که HTML5 چیزی بهتر از این هک ها به ما می دهد!

معرفی Web Workers: threading را به جاوا اسکریپت بیاورید

مشخصات Web Workers یک API برای ایجاد اسکریپت های پس زمینه در برنامه وب شما تعریف می کند. Web Workers به ​​شما اجازه می دهد تا کارهایی مانند راه اندازی اسکریپت های طولانی مدت برای انجام کارهای محاسباتی فشرده را انجام دهید، اما بدون مسدود کردن رابط کاربری یا سایر اسکریپت ها برای مدیریت تعاملات کاربر. آن‌ها کمک خواهند کرد تا به آن گفتگوی نامطلوب «اسکریپت بی‌پاسخ» که همه ما عاشق آن شده‌ایم پایان دهیم:

گفتگوی اسکریپت بدون پاسخ
گفتگوی رایج اسکریپت بدون پاسخ.

کارگران برای رسیدن به موازی سازی از ارسال پیام نخ مانند استفاده می کنند. آنها برای تازه نگه داشتن رابط کاربری شما، کارآمد و پاسخگو برای کاربران عالی هستند.

انواع وب کارگران

شایان ذکر است که مشخصات دو نوع Web Workers، Dedicated Workers و Shared Workers را مورد بحث قرار می دهد. این مقاله فقط کارگران متعهد را پوشش می دهد. من از آنها به عنوان "کارگران وب" یا "کارگران" در سراسر جهان یاد خواهم کرد.

شروع شدن

Web Workers در یک رشته مجزا اجرا می شوند. در نتیجه، کدی که آنها اجرا می کنند باید در یک فایل جداگانه قرار گیرد. اما قبل از انجام این کار، اولین کاری که باید انجام دهید این است که یک شی Worker جدید در صفحه اصلی خود ایجاد کنید. سازنده نام اسکریپت کارگر را می گیرد:

var worker = new Worker('task.js');

اگر فایل مشخص شده وجود داشته باشد، مرورگر یک موضوع کارگر جدید ایجاد می کند که به صورت ناهمزمان دانلود می شود. تا زمانی که فایل به طور کامل دانلود و اجرا نشود، کارگر شروع به کار نخواهد کرد. اگر مسیر به کارگر شما یک 404 برگرداند، کارگر بی‌صدا از کار می‌افتد.

پس از ایجاد worker، آن را با فراخوانی متد postMessage() شروع کنید:

worker.postMessage(); // Start the worker.

برقراری ارتباط با یک کارگر از طریق ارسال پیام

ارتباط بین یک اثر و صفحه اصلی آن با استفاده از یک مدل رویداد و متد postMessage() انجام می شود. بسته به مرورگر/نسخه شما، postMessage() می تواند یک رشته یا یک شی JSON را به عنوان آرگومان واحد خود بپذیرد. آخرین نسخه‌های مرورگرهای مدرن از ارسال یک شی JSON پشتیبانی می‌کنند.

در زیر مثالی از استفاده از یک رشته برای ارسال "Hello World" به یک کارگر در doWork.js آمده است. کارگر به سادگی پیامی را که به آن ارسال می شود برمی گرداند.

فیلمنامه اصلی:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.

doWork.js (کارگر):

self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);

وقتی postMessage() از صفحه اصلی فراخوانی می شود، کارگر ما با تعریف یک onmessage handler برای رویداد message ، آن پیام را مدیریت می کند. محموله پیام (در این مورد 'Hello World') در Event.data قابل دسترسی است. اگرچه این مثال خاص خیلی هیجان‌انگیز نیست، اما نشان می‌دهد که postMessage() نیز وسیله‌ای برای انتقال داده‌ها به رشته اصلی است. راحت!

پیام‌های ارسال شده بین صفحه اصلی و کارگران کپی می‌شوند، به اشتراک گذاشته نمی‌شوند. به عنوان مثال، در مثال بعدی ویژگی 'msg' پیام JSON در هر دو مکان قابل دسترسی است. به نظر می رسد که شی مستقیماً به کارگر منتقل می شود حتی اگر در یک فضای اختصاصی مجزا در حال اجرا باشد. در واقع، چیزی که اتفاق می‌افتد این است که شیء در حین تحویل به کارگر سریال می‌شود و متعاقباً از طرف دیگر سریال‌زدایی می‌شود. صفحه و کارگر نمونه مشابهی را به اشتراک نمی گذارند، بنابراین نتیجه نهایی این است که در هر پاس یک نسخه تکراری ایجاد می شود. اکثر مرورگرها این ویژگی را با رمزگذاری/رمزگشایی خودکار مقدار JSON در دو طرف اجرا می کنند.

مثال زیر یک مثال پیچیده تر است که پیام ها را با استفاده از اشیاء JSON ارسال می کند.

فیلمنامه اصلی:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
function sayHI() {
worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
}

function stop() {
// worker.terminate() from this script would also stop the worker.
worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
}

function unknownCmd() {
worker.postMessage({'cmd': 'foobard', 'msg': '???'});
}

var worker = new Worker('doWork2.js');

worker.addEventListener('message', function(e) {
document.getElementById('result').textContent = e.data;
}, false);
</script>

doWork2.js:

self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    self.postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
    self.postMessage('WORKER STOPPED: ' + data.msg +
                    '. (buttons will no longer work)');
    self.close(); // Terminates the worker.
    break;
default:
    self.postMessage('Unknown command: ' + data.msg);
};
}, false);

اشیاء قابل انتقال

اکثر مرورگرها الگوریتم شبیه سازی ساختاریافته را پیاده سازی می کنند، که به شما امکان می دهد انواع پیچیده تری را به داخل/خارج از Workers مانند اشیاء File ، Blob ، ArrayBuffer و JSON ارسال کنید. با این حال، هنگام ارسال این نوع داده ها با استفاده از postMessage() یک کپی همچنان ساخته می شود. بنابراین، اگر یک فایل بزرگ 50 مگابایتی را ارسال می کنید (به عنوان مثال)، سربار قابل توجهی در قرار دادن آن فایل بین کارگر و رشته اصلی وجود دارد.

شبیه سازی ساختاریافته عالی است، اما یک کپی می تواند صدها میلی ثانیه طول بکشد. برای مبارزه با ضربه perf، می توانید از Objects Transferable استفاده کنید.

با اشیاء قابل انتقال، داده ها از یک زمینه به زمینه دیگر منتقل می شوند. کپی صفر است که عملکرد ارسال داده ها به Worker را بسیار بهبود می بخشد. اگر اهل دنیای C/C++ هستید، آن را به عنوان یک مرجع گذرا در نظر بگیرید. با این حال، برخلاف مرجع گذرا، «نسخه» از زمینه فراخوانی پس از انتقال به زمینه جدید دیگر در دسترس نیست. به عنوان مثال، هنگام انتقال یک ArrayBuffer از برنامه اصلی خود به Worker، ArrayBuffer اصلی پاک می شود و دیگر قابل استفاده نیست. محتویات آن (به معنای واقعی کلمه آرام) به بافت کارگر منتقل می شود.

برای استفاده از اشیاء قابل انتقال، از یک امضای کمی متفاوت از postMessage() استفاده کنید:

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

مورد کارگر، اولین آرگومان داده و دومی لیست مواردی است که باید منتقل شوند. به هر حال، اولین آرگومان نباید یک ArrayBuffer باشد. به عنوان مثال، می تواند یک شی JSON باشد:

worker.postMessage({data: int8View, moreData: anotherBuffer},
                [int8View.buffer, anotherBuffer]);

نکته مهم این است که: آرگومان دوم باید آرایه ای از ArrayBuffer s باشد. این لیست موارد قابل انتقال شما است.

برای اطلاعات بیشتر در مورد موارد قابل انتقال، به پست ما در developer.chrome.com مراجعه کنید.

محیط کارگر

محدوده کارگری

در زمینه یک کارگر، هم self و this به دامنه جهانی برای کارگر اشاره دارد. بنابراین، مثال قبلی را نیز می توان به صورت زیر نوشت:

addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
...
}, false);

از طرف دیگر، می‌توانید کنترل‌کننده رویداد onmessage مستقیماً تنظیم کنید (اگرچه addEventListener همیشه توسط نینجاهای جاوا اسکریپت تشویق می‌شود).

onmessage = function(e) {
var data = e.data;
...
};

ویژگی هایی که برای کارگران در دسترس است

به دلیل رفتار چند رشته ای، Web Workers تنها به زیر مجموعه ای از ویژگی های جاوا اسکریپت دسترسی دارد:

  • شی navigator
  • شی location (فقط خواندنی)
  • XMLHttpRequest
  • setTimeout()/clearTimeout() و setInterval()/clearInterval()
  • کش برنامه
  • وارد کردن اسکریپت های خارجی با استفاده از متد importScripts()
  • ایجاد سایر کارگران وب

کارگران به موارد زیر دسترسی ندارند:

  • DOM (این مورد امن نیست)
  • شی window
  • شی document
  • شی parent

بارگیری اسکریپت های خارجی

می‌توانید فایل‌های اسکریپت یا کتابخانه‌های خارجی را با تابع importScripts() در یک worker بارگذاری کنید. این روش از صفر یا چند رشته که نام فایل را برای منابع وارد می کند، استفاده می کند.

این مثال script1.js و script2.js در worker بارگیری می کند:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

که همچنین می تواند به عنوان یک عبارت import واحد نوشته شود:

importScripts('script1.js', 'script2.js');

کارگران فرعی

کارگران توانایی تخم ریزی کودکان کارگر را دارند. این برای تجزیه بیشتر وظایف بزرگ در زمان اجرا عالی است. با این حال، subworkers با چند هشدار همراه هستند:

  • زیرکارگرها باید در همان مبدأ میزبان صفحه اصلی باشند.
  • URIهای درون کارگران فرعی نسبت به مکان کارگر والدینشان (برخلاف صفحه اصلی) حل و فصل می شوند.

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

برای نمونه ای از نحوه تخم ریزی یک subworker، به مثال در مشخصات مراجعه کنید.

کارگران درون خطی

اگر بخواهید اسکریپت worker خود را در لحظه ایجاد کنید، یا یک صفحه مستقل بدون نیاز به ایجاد فایل‌های کارگر جداگانه ایجاد کنید، چه؟ با Blob() می‌توانید با ایجاد یک نشانی URL برای کد کارگر به‌عنوان یک رشته، worker خود را در همان فایل HTML به‌عنوان منطق اصلی خود «داخلی» کنید:

var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

URL های حباب

جادو همراه با فراخوانی window.URL.createObjectURL() است. این روش یک رشته URL ساده ایجاد می کند که می تواند برای ارجاع داده های ذخیره شده در یک File DOM یا شی Blob استفاده شود. مثلا:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

URL های Blob منحصر به فرد هستند و برای طول عمر برنامه شما (مثلا تا زمانی که document تخلیه نشود) دوام می آورد. اگر URL های Blob زیادی ایجاد می کنید، ایده خوبی است که منابعی را منتشر کنید که دیگر مورد نیاز نیستند. شما می توانید به صراحت URL های Blob را با ارسال آن به window.URL.revokeObjectURL() آزاد کنید:

window.URL.revokeObjectURL(blobURL);

در کروم، یک صفحه خوب برای مشاهده همه URLهای حباب ایجاد شده وجود دارد: chrome://blob-internals/ .

مثال کامل

با برداشتن این یک قدم جلوتر، می‌توانیم با نحوه درج کد JS کارگر در صفحه ما هوشمندانه عمل کنیم. این تکنیک از یک تگ <script> برای تعریف کارگر استفاده می کند:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>

<div id="log"></div>

<script id="worker1" type="javascript/worker">
// This script won't be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
    self.postMessage('msg from worker');
};
// Rest of your worker code goes here.
</script>

<script>
function log(msg) {
    // Use a fragment: browser will only render/reflow once.
    var fragment = document.createDocumentFragment();
    fragment.appendChild(document.createTextNode(msg));
    fragment.appendChild(document.createElement('br'));

    document.querySelector("#log").appendChild(fragment);
}

var blob = new Blob([document.querySelector('#worker1').textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
    log("Received: " + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>

به نظر من، این رویکرد جدید کمی تمیزتر و خواناتر است. یک تگ اسکریپت با id="worker1" و type='javascript/worker' تعریف می کند (بنابراین مرورگر JS را تجزیه نمی کند). این کد به عنوان یک رشته با استفاده از document.querySelector('#worker1').textContent استخراج می شود و برای ایجاد فایل به Blob() ارسال می شود.

بارگیری اسکریپت های خارجی

هنگام استفاده از این تکنیک ها برای درون خطی کد کارگر خود، importScripts() تنها در صورتی کار می کند که یک URI مطلق ارائه کنید. اگر سعی کنید یک URI نسبی را ارسال کنید، مرورگر با یک خطای امنیتی شکایت می کند. دلیل آن این است: کارگر (اکنون از یک URL blob ایجاد شده است) با پیشوند blob: حل می شود، در حالی که برنامه شما از یک طرح متفاوت (احتمالاً http:// ) اجرا می شود. از این رو، شکست به دلیل محدودیت های مبدا متقاطع خواهد بود.

یکی از راه‌های استفاده از importScripts() در یک inline worker این است که URL فعلی اسکریپت اصلی شما را از طریق ارسال آن به inline worker و ساخت URL مطلق به صورت دستی اجرا کنید. این اطمینان حاصل می کند که اسکریپت خارجی از همان مبدا وارد شده است. با فرض اینکه برنامه اصلی شما از http://example.com/index.html اجرا می شود:

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
var data = e.data;

if (data.url) {
var url = data.url.href;
var index = url.indexOf('index.html');
if (index != -1) {
    url = url.substring(0, index);
}
importScripts(url + 'engine.js');
}
...
};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage(<b>{url: document.location}</b>);
</script>

رسیدگی به خطاها

مانند هر منطق جاوا اسکریپت، شما می خواهید هر گونه خطایی را که در وب سایت شما ایجاد می شود مدیریت کنید. اگر هنگام اجرای یک کارگر خطایی رخ دهد، ErrorEvent فعال می شود. رابط شامل سه ویژگی مفید برای پی بردن به اشتباه است: filename - نام اسکریپت کارگری که باعث خطا شده است، lineno - شماره خطی که در آن خطا رخ داده است، و message - توصیف معنی‌دار خطا. در اینجا نمونه ای از راه اندازی یک کنترل کننده رویداد onerror برای چاپ ویژگی های خطا آورده شده است:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
function onError(e) {
document.getElementById('error').textContent = [
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join('');
}

function onMsg(e) {
document.getElementById('result').textContent = e.data;
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('message', onMsg, false);
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
</script>

مثال : workerWithError.js سعی می کند 1/x را انجام دهد، جایی که x تعریف نشده است.

// TODO: DevSite - نمونه کد حذف شد زیرا از کنترل کننده رویداد درونی استفاده می کرد

workerWithError.js:

self.addEventListener('message', function(e) {
postMessage(1/x); // Intentional error.
};

یک کلمه در مورد امنیت

محدودیت با دسترسی محلی

به دلیل محدودیت های امنیتی Google Chrome، کارگران به صورت محلی (به عنوان مثال از file:// ) در آخرین نسخه های مرورگر اجرا نخواهند شد. در عوض بی سر و صدا شکست می خورند! برای اجرای برنامه خود از طرح file:// ، Chrome را با مجموعه پرچم --allow-file-access-from-files اجرا کنید.

سایر مرورگرها محدودیت مشابهی را اعمال نمی کنند.

ملاحظات منشا یکسان

اسکریپت های کارگر باید فایل های خارجی با طرحی مشابه با صفحه فراخوانی آنها باشند. بنابراین، نمی‌توانید یک اسکریپت را از یک data: URL یا javascript: URL، و یک صفحه https: نمی‌تواند اسکریپت‌های کارگری را که با آدرس‌های http: شروع می‌شوند شروع کند.

موارد استفاده کنید

بنابراین چه نوع برنامه ای از وب کارگران استفاده می کند؟ در اینجا چند ایده دیگر برای تحریک مغز شما وجود دارد:

  • واکشی و/یا ذخیره سازی داده ها برای استفاده بعدی.
  • برجسته کردن نحو کد یا دیگر قالب‌بندی متن بلادرنگ.
  • بررسی کننده غلط املایی.
  • تجزیه و تحلیل داده های تصویری یا صوتی.
  • ورودی/خروجی پس‌زمینه یا نظرسنجی وب‌سرویس‌ها.
  • پردازش آرایه‌های بزرگ یا پاسخ‌های پرجمعیت JSON.
  • فیلتر کردن تصویر در <canvas> .
  • به روز رسانی بسیاری از ردیف های پایگاه داده وب محلی.

برای اطلاعات بیشتر درباره موارد استفاده مربوط به Web Workers API، از Workers Overview بازدید کنید.

دموها

منابع