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

Gerald Monaco
Gerald Monaco

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

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

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

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

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

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

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