לוח זמינות נראה כמו אתר פשוט: כמה כרטיסים ומצב עדכני ליד כל אחד. אבל אם המידע משתנה בזמן שאנשים מסתכלים עליו, צריך לענות על שאלות שלא קיימות בעמוד תוכן רגיל. מה מקבל מי שנכנס עכשיו? מה קורה כשהחיבור נקטע? ואיך מונעים מעדכון ישן להחליף מידע שכבר התקדם?
Node.js יכולה לשמש בסיס לצד השרת של מערכת כזאת. היא מתאימה לעבודה עם בקשות ועם חיבורים שממתינים לנתונים, אבל אינה פותרת לבדה את סדר האירועים או את שמירת המידע. נבחן את ההחלטות דרך דוגמה של מעבדת יצירה קהילתית שמפרסמת את זמינות הציוד שלה. זו מערכת מידע על הציוד ולא מערכת שמפעילה אותו.
מה Node.js עושה מאחורי העמוד
Node.js היא סביבת הרצה של JavaScript מחוץ לדפדפן. בצד השרת היא יכולה לקבל בקשות HTTP, לקרוא נתונים ממסד נתונים ולהחזיר תשובות לממשק. היא אינה ספרייה שמציירת את הכרטיסים על המסך. את החלק הזה אפשר לבנות באמצעות React או בטכנולוגיית ממשק אחרת.
במעבדת היצירה יש עמדות עבודה ומדפסות תלת־ממד. מפעילה מעדכנת עמדה מפנויה לתחזוקה, והלוח הציבורי צריך לשקף את השינוי. Node.js יכולה לקבל את העדכון, לשמור אותו ולהודיע לדפדפנים שמחוברים ללוח. החלוקה בין השפה לסביבת ההרצה ולממשק מוסברת גם במדריך על שפות וטכנולוגיות לבניית אתרים מודרניים.
Event Loop אינה מעבד שמחשב הכול במקביל
בלב העבודה של Node.js נמצאת לולאת האירועים. היא מתאמת טיפול בבקשות ובתוצאות של פעולות אסינכרוניות. בזמן שממתינים לתשובה מרשת או ממסד נתונים, אין הכרח להשאיר את קוד ה־JavaScript עסוק בהמתנה. אפשר לחזור ולטפל באירועים אחרים עד שהתוצאה מוכנה.
מכאן מגיע חלק מההתאמה למערכות שמנהלות הרבה פעולות קלט ופלט. אבל צריך להבחין בין המתנה לבין חישוב. קריאה לשירות אחר יכולה להמתין באופן אסינכרוני. לולאה שמבצעת חישוב ארוך ב־JavaScript ממשיכה לצרוך את זמן הריצה שלה. הוספת המילה async לפונקציה אינה מעבירה את החישוב אוטומטית לליבה אחרת.
בדוגמה שלנו, החזרת מצב של עמדה היא פעולה קצרה יחסית. לעומת זאת, הפקת ניתוח מורכב מכל היסטוריית הפעילות יכולה להיות עבודה כבדה. אם היא מבוצעת באותו מסלול שמטפל בעדכוני הזמינות, משתמשים אחרים עלולים להמתין גם כשהבקשות שלהם פשוטות.
Node.js נעזרת במערכת ההפעלה ובמאגר עובדים פנימי עבור סוגים מסוימים של עבודה. המאגר הזה אינו אומר שכל פונקציית JavaScript ארוכה עוברת אליו מעצמה. גם Worker Threads שמפעילים קוד JavaScript במקביל הם מנגנון נפרד שצריך לבחור ולנהל. כדי להחליט מה מתאים, קודם בודקים אם הזמן מתבזבז בהמתנה או בחישוב.
קודם מגדירים מה פירוש מצב עדכני
כרטיס בלוח יכול להציג שם עמדה ומצב כמו פנויה או בתחזוקה. רצוי שיכיל גם מועד אימות של המידע. העובדה שהדפדפן מחובר לשרת אינה מוכיחה שמישהו בדק לאחרונה את העמדה. חיבור תקין ומידע טרי הם שני דברים שונים.
כדאי להפריד בין מועד שינוי המצב לבין מועד האימות האחרון. ייתכן שהעמדה נשארה פנויה לאורך זמן והמפעילה רק אישרה שהמידע עדיין נכון. אם שומרים רק את מועד השינוי, הלוח עלול להציג מידע שנראה ישן אף שנבדק עכשיו. אם מעדכנים את זמן השינוי בכל בדיקה, מאבדים את ההיסטוריה של השינוי האמיתי.
הנתונים הציבוריים צריכים להיות ממוקדים בשאלה שהלוח עונה עליה. שם העמדה, מצבה והערה תפעולית קצרה יכולים להספיק. פרטים אישיים של מי שמשתמש בציוד אינם חלק הכרחי מלוח זמינות. כדאי להגדיר מבנה תשובה ברור ולא לשלוח לדפדפן כל שדה שקיים ברשומה הפנימית.
מספר גרסה לכל מצב או מספר רצף לאירועים יכולים לעזור בהמשך לסדר עדכונים. חותמת זמן טובה להצגה לאדם, אך לא תמיד כדאי לבסס עליה לבדה את ההכרעה איזה עדכון חדש יותר. שעונים ומקורות שונים יכולים להתנהג אחרת. סדר שמוגדר במקום שמנהל את הנתונים נותן למערכת בסיס ברור יותר.
בקשה רגילה או חיבור שנשאר פתוח?
מי שנכנס ללוח צריך קודם תמונת מצב מלאה. בקשת HTTP ל־API יכולה להחזיר את רשימת העמדות ואת מצבן הנוכחי. לאחר מכן מחליטים איך לקבל שינויים. אין חובה לבחור באותה דרך גם עבור התמונה הראשונית וגם עבור העדכונים.
| דרך העדכון | מתי היא מתאימה |
|---|---|
| בקשות תקופתיות | כשהמידע משתנה מעט והמתנה קצרה לעדכון מקובלת. |
| SSE | כשהשרת שולח עדכונים ללוח והדפדפן בעיקר מקשיב להם. |
| WebSocket | כשנדרשת תקשורת מתמשכת בשני הכיוונים כחלק מהשימוש. |
SSE הם אירועים שנשלחים מהשרת לדפדפן דרך חיבור מתמשך. בדפדפן אפשר לקבל אותם באמצעות EventSource. הכיוון הוא מהשרת ללקוח. זה מתאים ללוח הזמינות הציבורי: כשעמדה משתנה, הכרטיס שלה מתעדכן. פעולה של המפעילה יכולה עדיין להישלח בבקשת HTTP רגילה ונפרדת.
WebSocket מאפשר להעביר הודעות בשני הכיוונים באותו חיבור. הוא יכול להתאים לממשק שיתופי שבו כמה אנשים מעדכנים מצב ועוקבים אחר פעולות זה של זה. אין צורך לבחור בו רק כי המילה זמן אמת מופיעה באפיון. לוח שרק מקבל עדכונים אינו דורש בהכרח ערוץ דו־כיווני.
צריך לחשוב גם על קצב ההודעות. ממשק WebSocket הרגיל בדפדפן אינו מספק מנגנון אוטומטי של backpressure שמתאים את הקצב ליכולת העיבוד של הממשק. בלוח זמינות אין בהכרח צורך להציג כל הודעת ביניים. אפשר לתכנן איחוד עדכונים לאותה עמדה כשהמשמעות נשמרת, במקום לצבור רשימה הולכת וגדלה של פעולות תצוגה. היסטוריה שחייבים לשמור נשארת עניין נפרד מתדירות הציור על המסך.
גם בקשות תקופתיות הן אפשרות לגיטימית. אם הזמינות נבדקת לעיתים רחוקות, ייתכן שהן יספיקו ויפשטו את התפעול. הבחירה צריכה להתחשב במשך ההמתנה שהקורא יכול לקבל ובתמיכת סביבת האחסון בחיבורים מתמשכים. שם הטכנולוגיה אינו מבטיח שהעדכון יופיע ברגע הנכון.

מה עושים כשהטלפון מאבד את החיבור
לוח חי צריך להציג גם מצב של מידע שאינו מתעדכן כרגע. אפשר להשאיר את הנתונים האחרונים על המסך ולסמן שהחיבור מתחדש. מחיקת כל הכרטיסים בזמן ניתוק קצר אינה תמיד מועילה, אבל גם הצגת המצב הישן כאילו הוא ודאי ועדכני עלולה להטעות.
EventSource מנסה לחדש חיבור שנקטע כברירת מחדל. זה חוסך חלק מהעבודה בדפדפן, אך אינו מבטיח שכל אירוע שהתרחש בזמן הניתוק יישלח שוב. SSE מאפשר לצרף מזהה לאירוע. כדי להשתמש במזהים להשלמת פער, גם השרת צריך לשמור היסטוריה מתאימה ולדעת להחזיר את מה שחסר.
בלוח זמינות יכול להיות פתרון פשוט יותר: אחרי חיבור מחדש מבקשים תמונת מצב טרייה. אם כל מה שצריך הוא לדעת מה פנוי עכשיו, לא בהכרח חשוב להציג כל מעבר שהתרחש בזמן הניתוק. אם המערכת נדרשת גם להיסטוריית פעילות מלאה, זו דרישה נפרדת שצריכה להישמר בנתונים.
יש גם מפגש בין התמונה הראשונית לזרם העדכונים. נניח שבקשת תמונת המצב התחילה, ובינתיים התקבל עדכון חדש של עמדה. אם התשובה הישנה מגיעה מאוחר יותר, היא לא צריכה לדרוס את המצב החדש. מספרי גרסה מאפשרים לממשק לזהות שהוא כבר התקדם מעבר לתשובה הזאת.
הנקודה אינה ש־WebSocket הופך את סדר ההודעות בתוך חיבור יחיד. הבלבול יכול להיווצר באפליקציה כשמשלבים בקשות נפרדות, חיבור מחודש או מקורות שונים. לכן מגדירים כיצד מסנכרנים תמונה מלאה עם עדכונים וכיצד חוזרים למצב מוסכם אם מתגלה פער.
המצב לא יכול לחיות רק בזיכרון של Node.js
לוח קטן עשוי לעבוד בהדגמה עם מערך של עמדות בזיכרון התהליך. אבל מה יקרה אחרי הפעלה מחדש? ומה יראה מבקר שהגיע למופע אחר של השרת? זיכרון מקומי נוח לעבודה זמנית, אך אינו מקור מתמשך ואחיד למצב המערכת.
מסד הנתונים צריך לשמור את המצב שהמערכת מסתמכת עליו. עדכון הזמינות נשמר שם ורק לאחר מכן מופץ למי שצריך להציגו. עצם שליחת הודעה לדפדפנים אינה אישור שהמידע נשמר. מנגד, שמירה מוצלחת אינה מוכיחה שכל דפדפן מחובר כבר קיבל את ההודעה.
אם שתי מפעילות עורכות את אותה עמדה, כדאי לזהות עדכון שמתבסס על גרסה ישנה. למשל, אחת סימנה תחזוקה בזמן שהשנייה עדיין רואה מצב פנוי. אפשר להשוות את גרסת הרשומה בזמן השמירה ולבקש הכרעה במקום להחליף בשקט את המצב החדש. העובדה ששתי הבקשות הגיעו ל־Node.js אינה פותרת את ההתנגשות הזאת; זו החלטה של שכבת הנתונים והתהליך.
כשיש כמה מופעי Node.js, צריך דרך להעביר עדכונים גם לחיבורים שנמצאים במופעים האחרים. מנגנון הפצת אירועים יכול לעזור בכך. הוא אינו מחליף את מסד הנתונים. אם מבקר מתחבר באיחור, עדיין צריך לתת לו תמונת מצב נכונה ולא להניח ששמע את כל ההודעות הקודמות.
כדאי להגדיר גם כיצד מתאוששים ממצב שבו הנתון נשמר אבל ההפצה התעכבה. אפשר להסתמך על סנכרון מחודש מהמצב השמור או על רישום מתמשך של אירועים שממתינים להפצה. הבחירה תלויה בכמה חשוב לשמר כל מעבר ולא רק את התוצאה האחרונה.
Worker Threads ותור משימות פותרים בעיות שונות
בהמשך מבקשים להוסיף דוח שימוש תקופתי למעבדת היצירה. הפקת הדוח אינה צריכה לעצור את עדכוני הזמינות. אם עיקר העבודה הוא חישוב JavaScript כבד, Worker Threads יכולים להעביר אותו לשרשורי עבודה נפרדים. הם אינם פתרון אוטומטי לקריאה איטית ממסד נתונים.
להפעלת עובדים ולהעברת מידע ביניהם יש עלות. לכן בדרך כלל לא יוצרים עובד חדש לכל פעולה קטנה. אפשר להשתמש במאגר עובדים ולתת לכל אחד משימה מתאימה. עדיין צריך להגביל את כמות העבודה שמתחילה יחד; יותר עובדים אינם יוצרים משאבי מעבד שלא קיימים.
תור משימות עוסק בשאלה אחרת: איזו עבודה ממתינה לביצוע ומה קורה אם היא נעצרת. הוא יכול לשמש לדוחות שאינם חייבים להסתיים בתוך בקשת האתר. מערכת תורים מתאימה יכולה לשמור משימות ולהעביר אותן לתהליך עבודה נפרד. זו שכבה שמתכננים או מוסיפים למערכת ולא תכונה שמגיעה מעצם כתיבת פונקציה אסינכרונית.
Worker יכול לבצע את החישוב של משימה שנלקחה מתור. אין סתירה בין השניים. חשוב להגדיר מה קורה כשהמשימה מבוצעת שוב אחרי תקלה, כדי שדוח לא יישמר פעמיים או יופיע כגמור לפני שנוצר. משימה שממתינה לתשובה משירות אחר ומשימה שמחשבת נתונים מקומיים עשויות לדרוש דרכי ביצוע שונות.
איך יודעים שהלוח באמת מעודכן
מספר החיבורים הפתוחים מספר רק חלק מהסיפור. כדאי לעקוב גם אחר גיל הנתונים שמוצגים, הזמן שעובר משמירת שינוי ועד להפצתו ומספר הניתוקים והחיבורים החוזרים. במערכת שיש בה עבודת רקע בודקים גם כמה זמן משימות ממתינות ומה נכשל.
אם תגובות השרת מתעכבות, מנסים לזהות אם הוא ממתין למקור נתונים או עסוק בעבודה שחוסמת את לולאת האירועים. הוספת מופעים לפני שמבינים את הגורם יכולה להשאיר את אותה תקלה בכל אחד מהם. המדידה צריכה להתאים לפעולה שמפריעה למשתמש ולא רק לגרף עומס כללי.
למערכת פעילה בוחרים ענף Node.js שנתמך ב־Active LTS או ב־Maintenance LTS ומתאים לתלויות הפרויקט. מספר גרסה חדש יותר אינו לבדו סיבה לשנות סביבת ייצור. אחרי עדכון בודקים את זרימת הנתונים ואת החיבורים המתמשכים בסביבה המתאימה לפני מעבר מלא.
TypeScript יכולה לעזור להגדיר את מבנה האירועים שהשרת והממשק מכירים. היא אינה מוכיחה שכל הודעה שמגיעה בזמן ריצה תואמת למבנה הזה. ההבחנה בין טיפוסים לבין בדיקת נתונים נכנסים מפורטת במדריך על TypeScript בפיתוח אתרים.
שאלות על Node.js באתר שמתעדכן בזמן אמת
האם Node.js מחליפה את הממשק בדפדפן?
לא. היא יכולה לטפל בשרת ובנתונים בזמן שהדפדפן מציג את הממשק. במסגרת כמו Next.js אפשר לשלב את החלקים בפרויקט אחד ועדיין לשמור על ההפרדה ביניהם. המדריך על מה רץ בשרת ומה רץ בדפדפן ב־Next.js מסביר כיצד נראית החלוקה הזאת.
האם async הופכת חישוב כבד לעבודה מקבילית?
לא. היא מאפשרת לכתוב זרימה אסינכרונית, אך החישוב עצמו אינו עובר אוטומטית לשרשור אחר. אם עיקר הזמן נצרך בחישוב JavaScript, בוחנים פיצול עבודה או Worker Threads. אם מחכים לקלט ופלט, שימוש נכון בממשקים אסינכרוניים יכול להספיק.
האם חייבים WebSocket כדי שהלוח יתעדכן בלי רענון?
לא. גם SSE וגם בקשות תקופתיות יכולות לעדכן את הממשק בלי לטעון מחדש את העמוד. WebSocket מתאים כשיש צורך אמיתי בתקשורת מתמשכת בשני הכיוונים. בכל אפשרות עדיין צריך לתכנן מצב התחלתי וחיבור מחדש כדי שהמידע המוצג יישאר אמין.



