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

جيرالد موناكو
جيرالد موناكو

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

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

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