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

Gerald Monaco
Gerald Monaco

Angular, son bir yılda geliştiricilerin Core Web Vitals puanlarını iyileştirmesine ve son kullanıcılarına mükemmel bir deneyim sunmasına yardımcı olmak için rehydrate ve ertelenebilir görünümler gibi birçok yeni özellik kazandı. Akış ve kısmi besleme gibi bu işlevi temel alan ek sunucu tarafı oluşturma özellikleriyle 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. DOM'u beslemeden önce DOM'a manuel olarak ekleme, taşıma veya DOM'dan kaldırma yapmak 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şimin tamamı sorunlu değildir ve bazen gereklidir. 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 gelecekteki tüm özelliklerinden tam olarak yararlanabilecek, gerçekten evrensel ve geleceğe hazır Angular bileşenleri oluşturabileceğiniz 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'un çoğu yönünü değiştirebilen yerleşik API'lere ve kalıplara sahiptir: DOM'a doğrudan erişmek yerine bunları kullanmayı tercih etmeniz gerekir.

Bir bileşenin kendi DOM öğesini değiştirme

Bir bileşen veya yönerge yazarken, barındırıcı öğeyi (yani bileşenin veya yönergenin seçicisiyle eşleşen DOM öğesini) değiştirmeniz gerekebilir. Örneğin, bir sarmalayıcı öğeyi hedeflemek veya tanıtmak yerine sınıf, stil veya özellik eklemek için barındırıcı öğeyi değiştirmeniz gerekebilir. Temel DOM öğesini değiştirmek için ElementRef öğesine başvurmak cazip gelebilir. Bunun yerine, değerleri bir ifadeye açık bir şekilde 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'de veri bağlama ile aynı şekilde, örneğin özelliklere ve stillere de bağlama yapabilirsiniz. Ayrıca 'true' değerini, Angular'ın gerektiğinde değeri otomatik olarak eklemek veya kaldırmak için kullanacağı farklı bir ifadeyle değiştirebilirsiniz.

Bazı durumlarda anahtarın dinamik olarak hesaplanması gerekir. Ayrıca, bir değer kümesi veya haritası döndüren bir sinyale ya da işleve de bağlanabilirsiniz:

@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'den kaçınmak için manuel DOM manipülasyonuna başvurmak cazip gelebilir. Bunun yerine, değeri önceki örnekte olduğu gibi bir sinyale bağlayabilirsiniz. Bu işlem gerektiğinde yapılabilir ve kod tabanınızın tamamında sinyallerin benimsenmesini gerektirmez.

Bir ş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 bu bileşenlerin gelecekte 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) ne zaman ve nerede bileşeninizin davranışıyla veya görünümüyle etkileşim kurması ya da özelleştirmesi gerekebileceğini iyi düşünün ve bunu yapmanın güvenli ve belgelenmiş bir yolunu sunun. Basit @Input ve @Output özellikleri yeterli olmadığında bir alt ağacın API'sini kullanabilmesi için hiyerarşik bağımlılık yerleştirme gibi özellikleri kullanın.

Geçmişte, modal iletişim kutuları veya ipuçları gibi özellikleri uygulamak için <body> öğesinin sonuna veya başka bir ana öğeye bir öğe ekleyip içeriği buraya taşımak ya da yansıtmak yaygın bir uygulamaydı. Ancak günümüzde şablonunuzda bunun yerine 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 değiştirme işlemini erteleme

Doğrudan DOM'unuzu değiştirmeyi en aza indirmek ve mümkün olduğunca fazla erişmek için önceki kuralları kullandıktan sonra, kaçınılmaz olarak bazı işlemler yapmanız gerekebilir. Bu tür durumlarda, görüşmeyi mümkün olduğunca ertelemek önemlidir. afterRender ve afterNextRender geri çağırma işlevleri, Angular değişiklikleri kontrol edip DOM'a kaydettikten sonra yalnızca tarayıcıda çalıştıkları için bunu yapmanın mükemmel bir yoludur.

Yalnızca tarayıcıda JavaScript çalıştırma

Bazı durumlarda yalnızca tarayıcıda çalışan bir kitaplığınız veya API'niz olur (örneğin, 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üzen gerçekleştirme

Bazen, hedef tarayıcılarınızın henüz desteklemediği bazı düzenleri (ör. ipucu konumlandırma) gerçekleştirmek için DOM'u okumanız veya DOM'a yazmanız gerekebilir. DOM'un tutarlı bir durumda olduğundan emin olabileceğiniz için afterRender bu işlem için mükemmel bir seçimdir. afterRender ve afterNextRender, EarlyRead, Read veya Write değerine sahip bir phase değerini kabul eder. DOM düzenini yazıldıktan sonra okumak, tarayıcıyı düzeni senkronize olarak yeniden hesaplamaya zorlar. Bu da performansı ciddi şekilde etkileyebilir (düzen karmaşası bölümüne bakın). Bu nedenle mantığınızı doğru aşamalara dikkatlice ayırmanız ö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ı, öncelikle öğelerin boyutunu ve konumunu elde etmek 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 ayırarak Angular, uygulamadaki diğer tüm bileşenlerde DOM'u etkili bir şekilde toplu olarak değiştirebilir ve performans üzerinde en az düzeyde etki sağlar.

Sonuç

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