Angular SSR ile DOM'a güvenli bir şekilde erişme

Gerald Monaco
Gerald Monaco

Geçen yıl Angular, geliştiricilerin Core Web Vitals'ı iyileştirmelerine ve son kullanıcılarına mükemmel bir deneyim sunmalarına yardımcı olmak için hidrasyon ve ertelenebilir görünümler gibi birçok yeni özellik kazandı. Bu işlevi temel alan, sunucu tarafı oluşturma ile ilgili ek özellikler (ör. akış ve kısmi hidrasyon) ile ilgili araştırmalar da devam etmektedir.

Maalesef uygulamanızın veya kitaplığınızın yeni ve yakında kullanıma sunulacak tüm bu özelliklerden tam olarak yararlanmasını engelleyebilecek bir kalıp vardır: temel DOM yapısının manuel olarak değiştirilmesi. Angular, bir bileşenin sunucu tarafından seri hale getirilmesinden tarayıcıda bulunana kadar DOM yapısının tutarlı kalmasını gerektirir. Hidrasyondan önce DOM'ye düğüm eklemek, taşımak veya kaldırmak için ElementRef, Renderer2 veya DOM API'lerini kullanmak, bu özelliklerin çalışmasını engelleyen tutarsızlıklara neden olabilir.

Ancak manuel DOM değiştirme ve erişimi her zaman sorunlu değildir ve bazen gerekli olabilir. DOM'yi güvenli bir şekilde kullanmanın anahtarı, ihtiyacınızı mümkün olduğunca en aza indirmek ve ardından kullanımınızı mümkün olduğunca ertelemektir. Aşağıdaki yönergelerde, bunu nasıl başarabileceğiniz ve Angular'ın yeni ve yakında kullanıma sunulacak tüm özelliklerinden tam olarak yararlanabilecek, gerçekten evrensel ve geleceğe hazır Angular bileşenleri nasıl oluşturulacağını açıklanmaktadır.

Manuel DOM manipülasyonundan kaçınma

Manuel DOM manipülasyonunun neden olduğu sorunlardan kaçınmanın en iyi yolu, hiç de şaşırtıcı olmayan bir şekilde, mümkün olduğunda bundan tamamen kaçınmaktır. Angular, DOM'nin çoğu özelliğini işleyebilen yerleşik API'lere ve kalıplara sahiptir. DOM'ye doğrudan erişmek yerine bunları kullanmayı tercih etmeniz gerekir.

Bileşenin kendi DOM öğesini değiştirme

Bir bileşen veya yönerge yazarken, bir sarmalayıcı öğesini hedeflemek veya eklemek yerine ana makine öğesini (bileşenin veya yönergenin seçicisiyle eşleşen DOM öğesini) değiştirmeniz gerekebilir. Örneğin, bir sınıf, stil ya da özellik ekleyebilirsiniz. Temel DOM öğesini değiştirmek için yalnızca ElementRef öğesine ulaşmak cazip gelebilir. Bunun yerine, değerleri bir ifadeye bildirerek bağlamak için ana makine bağlamalarını kullanmanız gerekir:

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

HTML'deki veri bağlamada olduğu gibi, özelliklere ve stillere bağlanabilir ve 'true' değerini, değeri gerektiğinde otomatik olarak eklemek veya kaldırmak için Angular'ın kullanacağı farklı bir ifadeyle değiştirebilirsiniz.

Bazı durumlarda anahtarın dinamik olarak hesaplanması gerekir. Ayrıca, bir değer grubu veya eşlemesi döndüren bir sinyale veya işleve de bağlayabilirsiniz:

@Component({
  selector: 'my-component',
  template: `...`,
  host: {
    '[class.foo]': 'true',
    '[class]': 'classes()'
  },
})
export class MyComponent {
  size = signal('large');
  classes = computed(() => {
    return [`size-${this.size()}`];
  });
}

Daha karmaşık uygulamalarda, ExpressionChangedAfterItHasBeenCheckedError'ten kaçınmak için manuel DOM manipülasyonuna başvurmak daha cazip gelebilir. Bunun yerine, değeri önceki örnekte olduğu gibi bir sinyale bağlayabilirsiniz. Bu işlemi ihtiyaç duyduğunuzda yapabilirsiniz. Kod tabanınızın tamamında sinyal benimsemeniz gerekmez.

Şablonun dışındaki DOM öğelerini değiştirme

Diğer üst veya alt bileşenlere ait olanlar gibi, normalde erişilemeyen öğelere erişmek için DOM'yi kullanmak daha cazip gelebilir. Ancak bu yöntem hataya açıktır, kapsüllemeyi ihlal eder ve gelecekte bu bileşenlerin değiştirilmesini veya yükseltilmesini zorlaştırır.

Bunun yerine, bileşeniniz diğer her bileşeni bir siyah kutu olarak değerlendirmelidir. Diğer bileşenlerin (aynı uygulama veya kitaplıkta olsa bile) bileşeninizin davranışı veya görünümüyle ne zaman ve nerede etkileşim kurması ya da onu özelleştirmesi gerekebileceğine dikkat edin ve bunun için güvenli ve belgelenmiş bir yöntem sunun. Basit @Input ve @Output özellikleri yeterli olmadığında alt ağacın API'sini kullanabilmesi için hiyerarşik bağımlılık ekleme gibi özellikleri kullanın.

Eskiden, kalıcı iletişim kutuları veya ipuçları gibi özellikleri, <body> öğesinin veya başka bir barındırma öğesinin sonuna bir öğe ekleyip ardından içeriği oraya taşıyarak veya yansıtarak uygulamak yaygındı. Ancak bugünlerde şablonunuzda basit bir <dialog> öğesi oluşturabilirsiniz:

@Component({
  selector: 'my-component',
  template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
  @ViewChild('dialog') dialogRef!: ElementRef;

  constructor() {
    afterNextRender(() => {
      this.dialogRef.nativeElement.showModal();
    });
  }
}

Manuel DOM işlemeyi ertele

Doğrudan DOM manipülasyonunuzu en aza indirmek ve olabildiğince çok erişim elde etmek için önceki yönergeleri kullandıktan sonra, kaçınılmaz bazı şeyleriz kalabilir. Bu tür durumlarda, görüşmeyi mümkün olduğunca ertelemek önemlidir. afterRender ve afterNextRender geri çağırmaları, Angular herhangi bir değişiklik olup olmadığını kontrol edip bunları DOM'ye aktardıktan sonra yalnızca tarayıcıda çalıştıkları için bunu yapmanın harika bir yoludur.

Yalnızca tarayıcılara yönelik JavaScript çalıştırma

Bazı durumlarda yalnızca tarayıcıda çalışan bir kitaplığınız veya API'niz olur (ör. grafik kitaplığı, bazı IntersectionObserver kullanımları vb.). Tarayıcıda çalışıp çalışmadığınızı koşullu olarak kontrol etmek veya sunucudaki davranışı çalmak yerine afterNextRender kullanabilirsiniz:

@Component({
  /* ... */
})
export class MyComponent {
  @ViewChild('chart') chartRef: ElementRef;
  myChart: MyChart|null = null;
  
  constructor() {
    afterNextRender(() => {
      this.myChart = new MyChart(this.chartRef.nativeElement);
    });
  }
}

Özel düzeni gerçekleştirin

Bazen, hedef tarayıcılarınızın henüz desteklemediği bazı düzenleri (ör. ipucu konumlandırmak) gerçekleştirmek için DOM'yi okumanız veya DOM'ye yazmanız gerekebilir. DOM'nin tutarlı bir durumda olduğundan emin olabileceğinizden, afterRender bunun için harika bir seçimdir. afterRender ve afterNextRender; EarlyRead, Read veya Write phase değerini kabul eder. DOM düzenini yazdıktan sonra okumak, tarayıcıyı eşzamanlı olarak düzeni yeniden hesaplamaya zorlar. Bu durum, performansı önemli ölçüde etkileyebilir (bkz. düzen karması). Bu nedenle, mantığınızı dikkatli bir şekilde doğru aşamalara bölmeniz önemlidir.

Örneğin, sayfadaki başka bir öğeye göre ipucu görüntülemek isteyen bir ipucu bileşeninin iki aşamayı kullanması muhtemeldir. EarlyRead aşaması, ilk olarak öğelerin boyutunu ve konumunu öğrenmek için kullanılır:

afterRender(() => {
    targetRect = targetEl.getBoundingClientRect();
    tooltipRect = tooltipEl.getBoundingClientRect();
  }, { phase: AfterRenderPhase.EarlyRead },
);

Ardından Write aşamasında, ipucunu yeniden konumlandırmak için önceden okunmuş değer kullanılır:

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 },
);

Mantığımızı doğru aşamalara bölen Angular, DOM işlemeyi uygulamadaki diğer tüm bileşenlerde etkili bir şekilde topluca yapabiliyor ve performans üzerindeki etkiyi asgari düzeyde tutabiliyor.

Sonuç

Kullanıcılarınıza mükemmel bir deneyim sunmanızı kolaylaştırmak amacıyla, yakında Angular sunucu tarafı oluşturmada pek çok yeni ve heyecan verici iyileştirme yapılacak. Önceki ipuçlarının, uygulamalarınızda ve kitaplıklarınızda bunlardan tam olarak yararlanmanıza yardımcı olacağını umuyoruz.