שרשורי אינטרנט עם עובדי מודולים

עכשיו קל יותר להעביר עומסי עבודה כבדים לשרשורי רקע בעזרת מודולים של JavaScript בעובדי האינטרנט.

קוד JavaScript הוא עם שרשור יחיד, כלומר הוא יכול לבצע פעולה אחת בלבד בכל פעם. הדבר אינטואיטיבי ומתאים למקרים רבים באינטרנט, אבל הוא עלול להיות בעייתי כשצריך לבצע משימות מורכבות כמו עיבוד נתונים, ניתוח, חישוב או ניתוח של הנתונים. יותר ויותר אפליקציות מורכבות נמסרות באינטרנט, גובר הצורך ליצור שרשורים מרובים בעיבוד.

בפלטפורמת האינטרנט, הרכיב העיקרי ליצירת שרשורים ומקבילות הוא Workers API עובדים הם הפשטה קלה של מערכת ההפעלה שרשורים שחושפים API שמעביר הודעות לתקשורת בין שרשורים. זה יכול להיות שימושי מאוד כשמבצעים חישובים יקרים על מערכי נתונים גדולים, וכך לאפשר ל-thread הראשי לפעול בצורה חלקה יותר בזמן ביצוע פעולות יקרות על שרשור רקע אחד או יותר.

הנה דוגמה אופיינית לשימוש של עובדים, שבה סקריפט של עובד מאזין להודעות שרשור ומגיב באמצעות שליחת הודעות משלו:

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

Web Worker API זמין ברוב הדפדפנים כבר יותר מעשר שנים. אומנם כלומר, לעובדים יש תמיכת דפדפנים מעולה והם מותאמים היטב, זה גם אומר את המודולים של JavaScript שקודמים ל-JavaScript. מאחר שלא הייתה מערכת מודולים כשהעובדים תוכננו, ממשק ה-API לטעינת קוד ב-Worker ולכתיבת סקריפטים נותרו באופן דומה לסקריפט הסינכרוני וגישות טעינה נפוצות בשנת 2009.

היסטוריה: Classic Works

ה-constructor של Worker משתמש בגרסה הקלאסית סקריפט, יחסית לכתובת ה-URL של המסמך. הוא מחזיר מיד הפניה למכונת ה-Worker החדשה, שחושף ממשק העברת הודעות וגם שיטה terminate() שמפסיקה מיידית משמיד את העובד.

const worker = new Worker('worker.js');

בתוך עובדי אינטרנט אפשר להשתמש בפונקציה importScripts() כדי לטעון קוד נוסף, אבל תשהה את הפעלת ה-Worker כדי לאחזר ולהעריך כל סקריפט. היא גם מפעילה סקריפטים בהיקף הגלובלי כמו תג <script> קלאסי, כלומר המשתנים בסקריפט אחד שהוחלף על ידי המשתנים במשתנה אחר.

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

מהסיבה הזו, עובדי אינטרנט הטכנו בעבר השפעה משמעותית על הארכיטקטורה של תרגום מכונה. המפתחים היו צריכים ליצור כלים חכמים ופתרונות אפשריים כדי לאפשר להשתמש בעובדי אינטרנט בלי לוותר על שיטות פיתוח מודרניות. לדוגמה, חבילות כמו ה-webpack מטמיעים הטמעה קטנה של טוען מודול בקוד שנוצר שמשתמש ב-importScripts() לטעינת קוד, אבל כולל את המודולים בתוך פונקציות כדי למנוע התנגשויות בין משתנים ולדמות של יחסי התלות.

צריך להזין עובדי מודולים

מצב חדש לעובדי אינטרנט עם יתרונות ארגונומיה וביצועים של JavaScript מודולים נשלחים ל-Chrome 80, שנקראים 'עובדי מודול'. ה-constructor של Worker מקבל עכשיו אפשרות חדשה מסוג {type:"module"}, שמשנה את טעינת הסקריפט ואת לביצוע התאמה ל-<script type="module">.

const worker = new Worker('worker.js', {
  type: 'module'
});

מכיוון שעובדי מודולים הם מודולים רגילים של JavaScript, הם יכולים להשתמש בדוחות ייבוא וייצוא. בתור בכל המודולים של JavaScript, יחסי התלות מבוצעים פעם אחת בלבד בהקשר נתון (thread ראשי, worker וכו'), וכל הייבוא העתידי מפנים למופע של המודול שכבר בוצע. הטעינה מתבצעת וביצוע מודולים של JavaScript גם עובר אופטימיזציה על ידי דפדפנים. יחסי התלות של מודול יכולים להיות נטענה לפני הרצת המודול, וכך ניתן לטעון את עצי המודול שלמים מקביל. טעינת מודול גם שומרת במטמון קוד מנותח, כלומר מודולים שנמצאים בשימוש ברשת צריך לנתח את השרשורים וב-Worker רק פעם אחת.

המעבר למודולים של JavaScript מאפשר גם להשתמש במודלים דינמיים ייבוא עבור קוד בטעינה מדורגת בלי לחסום את הביצוע של את העובד. ייבוא דינמי הרבה יותר מפורש משימוש ב-importScripts() כדי לטעון יחסי תלות, מכיוון שהייצוא של המודול המיובא מוחזר במקום להסתמך על משתנים גלובליים.

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

כדי להבטיח ביצועים מעולים, השיטה הישנה של importScripts() לא זמינה במודול ב-Google Workspace for Education. כשמחליפים את העובדים לשימוש במודולים של JavaScript, כל הקוד נטען במצב מחמיר . עוד השינוי המשמעותי הוא שהערך של this בהיקף ברמה העליונה של מודול JavaScript הוא undefined, ואילו בעובדים הקלאסיים הערך הוא ההיקף הגלובלי של העובד. למזלנו, תמיד היה גלובלי self שמספק הפניה להיקף הגלובלי. המוצר זמין ב: כל סוגי העובדים, כולל עובדי שירות, וגם ב-DOM.

טעינה מראש של עובדים באמצעות modulepreload

שיפור משמעותי אחד בביצועים שמגיע עם עובדי מודול הוא היכולת לבצע טעינה מראש על העובדים ויחסי התלות שלהם. ברכיבי העבודה עם המודולים, הסקריפטים נטענים ומופעלים כסטנדרט כלומר, ניתן לטעון אותם מראש ואפילו לנתח אותם מראש באמצעות modulepreload:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

גם עובדי ה-thread הראשי וגם העובדים של המודול יכולים להשתמש במודולים שנטענו מראש. האפשרות הזאת שימושית במקרים שמיובאים בשני ההקשרים, או במקרים שבהם לא ניתן לדעת מראש. אם ייעשה שימוש במודול ב-thread הראשי או ב-worker.

בעבר, האפשרויות שהיו זמינות לטעינה מראש של סקריפטים של worker באינטרנט היו מוגבלות, בהכרח מהימנים. לעובדים הקלאסיים היה "פועל" משלהם סוג המשאב לטעינה מראש, אבל לא הוטמעו <link rel="preload" as="worker"> בדפדפנים. כתוצאה מכך, השיטה הראשית שהיה זמין לעובדי אינטרנט לטעינה מראש היה להשתמש ב-<link rel="prefetch">, שמסתמך באופן מלא במטמון ה-HTTP. הודות לשימוש בשילוב עם הכותרות הנכונות לשמירה במטמון, התאפשר כדי להימנע מיצירת סקריפט העובד שיהיה צורך להמתין להורדת סקריפט ה-Worker. אבל בשונה modulepreload השיטה הזו לא תמכה ביחסי תלות בטעינה מראש או בניתוח מראש.

מה לגבי עובדים משותפים?

לעובדים משותפים יש עודכן במודולים של JavaScript החל מגרסה 83 של Chrome. בדומה לעובדים ייעודיים, בבניית worker משותף עם האפשרות {type:"module"}, עכשיו הסקריפט של העובד יטען במקום סקריפט קלאסי:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

לפני התמיכה במודולים של JavaScript, ה-constructor של SharedWorker() ציפה רק כתובת URL וארגומנט name אופציונלי. זה ימשיך לפעול עבור שימוש בגרסה הקלאסית של עומסי עבודה משותפים; עם זאת כדי ליצור כלי עבודה משותפים במודול, צריך להשתמש בארגומנט options החדש. הזמינות אפשרויות זהות לאלה של עובד ייעודי, כולל האפשרות name שמחליפה את את הארגומנט name הקודם.

מה לגבי Service Worker?

מפרט ה-Service Worker כבר מעודכן כדי לתמוך בקבלת מודול JavaScript כנקודת הכניסה, תוך שימוש באותה אפשרות {type:"module"} כמו עובדי מודול, עם זאת, השינוי הזה עדיין לא הוטמע בדפדפנים. לאחר מכן, ניתן יהיה כדי ליצור קובץ שירות (service worker) באמצעות מודול JavaScript באמצעות הקוד הבא:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

עכשיו, לאחר שהמפרט עודכן, הדפדפנים מתחילים ליישם את ההתנהגות החדשה. התהליך הזה לוקח זמן, כי יש כמה סיבוכים נוספים שקשורים לשימוש ב-JavaScript. ל-service worker. רישום של Service Worker צריך להשוות בין סקריפטים מיובאים עם הגרסאות הקודמות שנשמרו במטמון, קביעה אם להפעיל עדכון, ויש להטמיע אותו במודולים של JavaScript כשעושים בו שימוש ב-Service Worker. כמו כן, עובדי שירות צריכים להיות מסוגלים לעקוף את שמירה במטמון במקרים מסוימים, מתבצעת בדיקה אם יש עדכונים.

מקורות מידע נוספים ומידע נוסף