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