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

ג'רלד מונקו
ג'רלד מונקו

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

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

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

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

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

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

כשכותבים רכיב או הנחיה, ייתכן שתצטרכו לשנות את אלמנט המארח (כלומר, את רכיב ה-DOM שתואם לבורר של הרכיב או של ההוראה) כדי, למשל, להוסיף מחלקה, סגנון או מאפיין, במקום לטרגט או להציג רכיב wrapper. מפתה להגיע רק אל 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. במקום זאת, אפשר לקשור את הערך לאות כמו בדוגמה הקודמת. אפשר לעשות את זה לפי הצורך ואין צורך לאמץ אותות בכל ה-codebase.

שינוי רכיבי 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 ואת הגישה אליו, יכול להיות שנשארו לכם עוד כמה פעולות בלתי נמנעות. במקרים כאלה, חשוב לדחות את התשלום ככל האפשר. קריאות חוזרות (callback) של 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 בצד השרת, באופק במטרה להקל עליכם לספק למשתמשים שלכם חוויה מעולה. אנחנו מקווים שהטיפים הקודמים יעזרו לכם לנצל את מלוא היתרונות שלהם באפליקציות ובספריות שלכם.