גישה בטוחה ל-DOM באמצעות Angular SSR

Gerald Monaco
Gerald Monaco

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

לצערנו, יש דפוס אחד שעלול למנוע מהאפליקציה או מהספרייה שלכם לנצל את כל התכונות החדשות והתכונות שצפויות בקרוב: מניפולציה ידנית של מבנה ה-DOM הבסיסי. ב-Angular, המבנה של DOM נשאר עקבי מרגע שהשרת מסדר את הרכיב בסדרה, ועד שהוא מתחדש בדפדפן. שימוש ב-ElementRef, ב-Renderer2 או בממשקי API של DOM כדי להוסיף, להעביר או להסיר צמתים באופן ידני מ-DOM לפני ההוספה שלהם למבנה יכול לגרום לאי-עקביות שמונעת את הפעולה של התכונות האלה.

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

הימנעות ממניפולציה ידנית של DOM

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

שינוי של רכיב DOM משלו של רכיב

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

@Component({
  selector: 'my-component',
  template: `...`,
  host: {
    '[class.foo]': 'true'
  },
})
export class MyComponent {
  /* ... */
}

בדומה לקישור נתונים ב-HTML, אפשר גם, למשל, לקשר למאפיינים ולסגנונות, ולשנות את 'true' לביטוי אחר ש-Angular תשתמש בו כדי להוסיף או להסיר את הערך באופן אוטומטי לפי הצורך.

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

@Component({
  selector: 'my-component',
  template: `...`,
  host: {
    '[class.foo]': 'true',
    '[class]': 'classes()'
  },
})
export class MyComponent {
  size = signal('large');
  classes = computed(() => {
    return [`size-${this.size()}`];
  });
}

באפליקציות מורכבות יותר, יכול להיות שתתפתתו לבצע מניפולציה ידנית של DOM כדי להימנע מ-ExpressionChangedAfterItHasBeenCheckedError. במקום זאת, אפשר לשייך את הערך לאות, כמו בדוגמה הקודמת. אפשר לעשות זאת לפי הצורך, ולא צריך להשתמש באותות בכל קוד הבסיס.

שינוי של רכיבי DOM מחוץ לתבנית

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

במקום זאת, הרכיב צריך להתייחס לכל רכיב אחר כקופסה שחורה. כדאי להקדיש זמן כדי לחשוב מתי ואיפה רכיבים אחרים (אפילו באותה אפליקציה או ספרייה) עשויים להזדקק לאינטראקציה עם הרכיב או להתאמה אישית של ההתנהגות או המראה שלו, ולאחר מכן לחשוף דרך בטוחה ומתוועדת לעשות זאת. אפשר להשתמש בתכונות כמו הזרקת יחסי תלות היררכיים כדי להפוך ממשק API לזמין להסתעפות עץ כשמאפיינים פשוטים של @Input ו-@Output לא מספיקים.

בעבר, היה מקובל להטמיע תכונות כמו תיבת דו-שיח מודאלית או טיפים על ידי הוספת רכיב לסוף ה-<body> או לרכיב מארח אחר, ולאחר מכן העברת התוכן או הקרנתו שם. עם זאת, בימים אלה סביר להניח שתוכלו להציג במקום זאת רכיב <dialog> פשוט בתבנית:

@Component({
  selector: 'my-component',
  template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
  @ViewChild('dialog') dialogRef!: ElementRef;

  constructor() {
    afterNextRender(() => {
      this.dialogRef.nativeElement.showModal();
    });
  }
}

דחיית מניפולציה ידנית של DOM

אחרי שתשתמשו בהנחיות הקודמות כדי לצמצם ככל האפשר את הגישה והמניפולציה הישירה של DOM, יכול להיות שעדיין יהיו לכם פעולות שאי אפשר להימנע מהן. במקרים כאלה, חשוב לדחות את העניין כמה שיותר. פונקציות ה-callbacks של afterRender ו-afterNextRender הן דרך מצוינת לעשות זאת, כי הן פועלות רק בדפדפן, אחרי ש-Angular בודקת אם יש שינויים ומטמיעה אותם ב-DOM.

הפעלת JavaScript בדפדפן בלבד

במקרים מסוימים, תהיה לכם ספרייה או ממשק API שפועלים רק בדפדפן (לדוגמה, ספריית תרשימים, שימוש מסוים ב-IntersectionObserver וכו'). במקום לבדוק באופן מותנה אם אתם מריצים את הקוד בדפדפן, או ליצור סטאב של התנהגות בשרת, אתם יכולים פשוט להשתמש ב-afterNextRender:

@Component({
  /* ... */
})
export class MyComponent {
  @ViewChild('chart') chartRef: ElementRef;
  myChart: MyChart|null = null;
  
  constructor() {
    afterNextRender(() => {
      this.myChart = new MyChart(this.chartRef.nativeElement);
    });
  }
}

ביצוע פריסה מותאמת אישית

לפעמים צריך לקרוא או לכתוב ב-DOM כדי לבצע פריסה מסוימת שעדיין לא נתמכת בדפדפני היעד, כמו מיקום של תיבת עזרה. afterRender היא בחירה מצוינת לכך, כי אפשר להיות בטוחים שה-DOM נמצא במצב עקבי. השדות afterRender ו-afterNextRender מקבלים ערך phase של EarlyRead,‏ Read או Write. קריאת הפריסה של DOM אחרי הכתיבה שלה מאלצת את הדפדפן לחשב מחדש את הפריסה באופן סינכרוני, דבר שעלול להשפיע באופן משמעותי על הביצועים (ראו רעידת פריסה). לכן חשוב לפצל את הלוגיקה שלכם לשלבים הנכונים.

לדוגמה, רכיב של תיאור קצר שרוצים להציג תיאור קצר ביחס לרכיב אחר בדף, ישתמש בשני שלבים. השלב EarlyRead ישמש קודם כל כדי לקבל את הגודל והמיקום של הרכיבים:

afterRender(() => {
    targetRect = targetEl.getBoundingClientRect();
    tooltipRect = tooltipEl.getBoundingClientRect();
  }, { phase: AfterRenderPhase.EarlyRead },
);

לאחר מכן, השלב Write ישתמש בערך שנקרא קודם כדי למקם מחדש את ההסבר הקצר:

afterRender(() => {
    tooltipEl.style.setProperty('left', `${targetRect.left + targetRect.width / 2 - tooltipRect.width / 2}px`);
    tooltipEl.style.setProperty('top', `${targetRect.bottom - 4}px`);
  }, { phase: AfterRenderPhase.Write },
);

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

סיכום

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