الوصول إلى نموذج العناصر في المستند (DOM) بأمان باستخدام Angular SSR

Gerald Monaco
Gerald Monaco

على مدار العام الماضي، حصلت Angular على العديد من الميزات الجديدة، مثل hydration وviews deferrable لمساعدة المطوّرين على تحسين مؤشرات أداء الويب الأساسية وضمان تجربة رائعة للمستخدمين النهائيين. نحن نُجري أيضًا أبحاثًا حول ميزات إضافية ذات صلة بالعرض من جهة الخادم تستند إلى هذه الوظيفة، مثل البث وإعادة الترطيب الجزئي.

هناك نمط واحد قد يمنع تطبيقك أو مكتبتك من الاستفادة إلى أقصى حد من كل هذه الميزات الجديدة والمقبلة، وهو التلاعب اليدوي ببنية DOM الأساسية. تتطلّب Angular أن تظل بنية DOM متّسقة منذ أن يُسلسل الخادم المكوّن إلى أن يتم إعادة تحميله في المتصفّح. إنّ استخدام واجهتَي برمجة التطبيقات ElementRef أو Renderer2 أو DOM لإضافة عقد أو نقلها أو إزالتها يدويًا من DOM قبل معالجتها قد يؤدي إلى حدوث تناقضات تمنع عمل هذه الميزات.

ومع ذلك، ليس كلّ الوصول إلى DOM وتعديله يدويًا يشكّل مشكلة، بل يكون ضروريًا في بعض الأحيان. إنّ مفتاح استخدام DOM بأمان هو تقليل الحاجة إليه قدر الإمكان، ثم تأجيل استخدامه لأطول فترة ممكنة. توضّح الإرشادات التالية كيفية تحقيق ذلك وإنشاء مكونات Angular عالمية ومناسبة للمستقبل يمكنها الاستفادة إلى أقصى حد من جميع ميزات Angular الجديدة والمقبلة.

تجنُّب التلاعب يدويًا بنموذج DOM

من غير المفاجئ أنّ أفضل طريقة لتجنُّب المشاكل التي يسببها التلاعب اليدوي بـ DOM هي تجنُّبه تمامًا كلما أمكن ذلك. تتضمّن Angular واجهات برمجة تطبيقات وأنماطًا مدمجة يمكنها التلاعب بمعظم جوانب نموذج 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 للوصول إلى العناصر التي لا يمكن الوصول إليها عادةً، مثل تلك التي تنتمي إلى مكوّنات رئيسية أو فرعية أخرى. ومع ذلك، فإنّ هذا الأسلوب معرّض للأخطاء ويخالف عملية الحظر ويصعّب تغيير هذه المكوّنات أو ترقيتها في المستقبل.

بدلاً من ذلك، يجب أن يتعامل المكوّن مع كل مكوّن آخر على أنّه صندوق أسود. ننصحك بالتوقّف قليلاً للتفكير في الأوقات والأماكن التي قد تحتاج فيها المكوّنات الأخرى (حتى داخل التطبيق أو المكتبة نفسها) إلى التفاعل مع سلوك المكوّن أو مظهره أو تخصيصه، ثمّ توفير طريقة آمنة وموثَّقة لإجراء ذلك. استخدِم ميزات مثل حقن التبعيات الهرمي لإتاحة واجهة برمجة التطبيقات لمجموعة فرعية من الأشجار عندما لا تكون السمتَان @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 والوصول إليه قدر الإمكان، قد تبقى بعض الحالات التي لا يمكن تجنّبها. في هذه الحالات، من المهم تأجيله لأطول فترة ممكنة. إنّ وظائف الاستدعاء afterRender وafterNextRender هي طريقة رائعة لإجراء ذلك، لأنّها لا تعمل إلا على المتصفّح، بعد أن تتحقّق Angular من أي تغييرات وتُجريها على عنصر DOM.

تشغيل JavaScript في المتصفّح فقط

في بعض الحالات، ستحصل على مكتبة أو واجهة برمجة تطبيقات تعمل في المتصفّح فقط (مثل مكتبة الرسوم البيانية وبعض استخدامات 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) في حالة متّسقة. يمكن إدخال القيمة EarlyRead أو Read أو Write في الحقل phase للعنصرَين afterRender وafterNextRender. إنّ قراءة تنسيق نموذج 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، وذلك بهدف تسهيل تقديم تجربة رائعة للمستخدمين. نأمل أن تكون النصائح السابقة مفيدة لمساعدتك في الاستفادة منها بالكامل في تطبيقاتك ومكتباتك.