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

Gerald Monaco
Gerald Monaco

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

هناك نمط واحد قد يمنع تطبيقك أو مكتبتك من الاستفادة إلى أقصى حد من كل هذه الميزات الجديدة والمقبلة، وهو التلاعب اليدوي ببنية 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) في حالة متّسقة. يقبل كل من 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، وذلك بهدف تسهيل تقديم تجربة رائعة للمستخدمين. نأمل أن تكون النصائح السابقة مفيدة لمساعدتك في الاستفادة منها بالكامل في تطبيقاتك ومكتباتك.