ऐंगुलर एसएसआर की मदद से, डीओएम को सुरक्षित तरीके से ऐक्सेस करना

Gerald Monaco
Gerald Monaco

पिछले साल, Angular में कई नई सुविधाएं जोड़ी गई हैं. जैसे, हाइड्रेशन और देर से लोड होने वाले व्यू. इनकी मदद से, डेवलपर अपनी वेबसाइट की परफ़ॉर्मेंस की अहम जानकारी को बेहतर बना सकते हैं. साथ ही, असली उपयोगकर्ताओं को बेहतर अनुभव दे सकते हैं. इस सुविधा के आधार पर, सर्वर साइड रेंडरिंग से जुड़ी अन्य सुविधाओं पर भी काम चल रहा है. जैसे, स्ट्रीमिंग और आंशिक हाइड्रेशन.

माफ़ करें, एक ऐसा पैटर्न है जिसकी वजह से आपके ऐप्लिकेशन या लाइब्रेरी को इन सभी नई और आने वाली सुविधाओं का पूरा फ़ायदा नहीं मिल पाएगा: DOM स्ट्रक्चर में मैन्युअल तरीके से बदलाव करना. Angular के लिए ज़रूरी है कि सर्वर से कॉम्पोनेंट को सीरियलाइज़ करने के बाद, ब्राउज़र पर उसे हाइड्रेट करने तक, डीओएम का स्ट्रक्चर एक जैसा बना रहे. हाइड्रेशन से पहले, DOM में मैन्युअल तरीके से नोड जोड़ने, उन्हें हटाने या उन्हें एक जगह से दूसरी जगह ले जाने के लिए, ElementRef, Renderer2 या DOM API का इस्तेमाल करने पर, इन सुविधाओं के काम न करने की समस्याएं आ सकती हैं.

हालांकि, मैन्युअल रूप से DOM में बदलाव करने और उसे ऐक्सेस करने से हमेशा समस्या नहीं होती. कभी-कभी ऐसा करना ज़रूरी भी होता है. डीओएम को सुरक्षित तरीके से इस्तेमाल करने के लिए, इसकी ज़रूरत को कम से कम करें. इसके बाद, इसका इस्तेमाल ज़्यादा से ज़्यादा देर तक न करें. यहां दिए गए दिशा-निर्देशों में बताया गया है कि ऐसा कैसे किया जा सकता है. साथ ही, ऐसे यूनिवर्सल और आने वाले समय के हिसाब से काम करने वाले Angular कॉम्पोनेंट बनाने का तरीका भी बताया गया है जो Angular की सभी नई और आने वाली सुविधाओं का पूरा फ़ायदा ले सकते हैं.

डीओएम में मैन्युअल तरीके से बदलाव करने से बचना

मैन्युअल डीओएम मैनिपुलेशन से होने वाली समस्याओं से बचने का सबसे अच्छा तरीका यह है कि जहां भी हो सके, वहां इसका इस्तेमाल न करें. Angular में पहले से मौजूद एपीआई और पैटर्न हैं, जो DOM के ज़्यादातर पहलुओं में बदलाव कर सकते हैं: आपको सीधे DOM को ऐक्सेस करने के बजाय, इनका इस्तेमाल करना चाहिए.

कॉम्पोनेंट के अपने डीओएम एलिमेंट में बदलाव करना

कॉम्पोनेंट या डायरेक्टिव लिखते समय, आपको होस्ट एलिमेंट (यानी, कॉम्पोनेंट या डायरेक्टिव के सिलेक्टर से मैच करने वाला DOM एलिमेंट) में बदलाव करना पड़ सकता है. उदाहरण के लिए, किसी क्लास, स्टाइल या एट्रिब्यूट को जोड़ने के लिए, रैपर एलिमेंट को टारगेट करने या उसे शामिल करने के बजाय. मौजूदा डीओएम एलिमेंट में बदलाव करने के लिए, ElementRef का इस्तेमाल करना आसान है. इसके बजाय, आपको होस्ट बाइंडिंग का इस्तेमाल करके, वैल्यू को एक्सप्रेशन से एलान के तौर पर बांधना चाहिए:

@Component({
  selector: 'my-component',
  template: `...`,
  host: {
    '[class.foo]': 'true'
  },
})
export class MyComponent {
  /* ... */
}

एचटीएमएल में डेटा बाइंडिंग की तरह ही, एट्रिब्यूट और स्टाइल को भी बाइंड किया जा सकता है. साथ ही, '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()}`];
  });
}

ज़्यादा जटिल ऐप्लिकेशन में, ExpressionChangedAfterItHasBeenCheckedError से बचने के लिए, मैन्युअल रूप से 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();
    });
  }
}

डीओएम में मैन्युअल तरीके से बदलाव करने की प्रोसेस को बाद के लिए छोड़ना

डीओएम में सीधे तौर पर बदलाव करने और ज़्यादा से ज़्यादा ऐक्सेस करने के लिए, पिछले दिशा-निर्देशों का इस्तेमाल करने के बाद भी, आपके पास कुछ ऐसा डेटा रह सकता है जिसे हटाया नहीं जा सकता. ऐसे मामलों में, इसे ज़्यादा से ज़्यादा समय तक टालना ज़रूरी है. 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);
    });
  }
}

कस्टम लेआउट लागू करना

कभी-कभी, आपको टारगेट किए गए ब्राउज़र पर काम न करने वाले लेआउट को लागू करने के लिए, डीओएम को पढ़ने या उसमें बदलाव करने की ज़रूरत पड़ सकती है. जैसे, टूलटिप की पोज़िशन तय करना. 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 ऐप्लिकेशन के हर दूसरे कॉम्पोनेंट में डीओएम में बदलाव को असरदार तरीके से बैच कर सकता है. इससे, परफ़ॉर्मेंस पर कम से कम असर पड़ता है.

नतीजा

Angular के सर्वर-साइड रेंडरिंग में कई नए और दिलचस्प सुधार किए जा रहे हैं. इनका मकसद, आपके लिए अपने उपयोगकर्ताओं को बेहतर अनुभव देना है. हमें उम्मीद है कि पिछले लेख में दी गई सलाह, आपके ऐप्लिकेशन और लाइब्रेरी में इनका पूरा फ़ायदा पाने में मददगार साबित होंगी!