Mengakses DOM dengan aman menggunakan SSR Angular

Gerald Monaco
Gerald Monaco

Selama setahun terakhir, Angular telah memperoleh banyak fitur baru seperti hidrasi dan tampilan yang dapat ditangguhkan untuk membantu developer meningkatkan Data Web Inti dan memastikan pengalaman yang luar biasa bagi pengguna akhir mereka. Penelitian tentang fitur tambahan terkait rendering sisi server yang dibuat berdasarkan fungsi ini juga sedang berlangsung, seperti streaming dan hidrasi parsial.

Sayangnya, ada satu pola yang mungkin mencegah aplikasi atau library Anda memanfaatkan sepenuhnya fitur baru dan yang akan datang ini: manipulasi manual pada struktur DOM yang mendasarinya. Angular mengharuskan struktur DOM tetap konsisten sejak komponen diserialisasi oleh server, hingga terhidrasi di browser. Menggunakan ElementRef, Renderer2, atau DOM API untuk menambahkan, memindahkan, atau menghapus node dari DOM secara manual sebelum hidrasi dapat menimbulkan inkonsistensi yang mencegah fitur ini berfungsi.

Namun, tidak semua manipulasi dan akses DOM manual bermasalah, dan terkadang diperlukan. Kunci untuk menggunakan DOM dengan aman adalah meminimalkan kebutuhan Anda akan DOM tersebut sebanyak mungkin, kemudian menunda penggunaannya selama mungkin. Panduan berikut menjelaskan bagaimana Anda dapat melakukannya dan membangun komponen Angular yang benar-benar universal dan tahan masa depan yang dapat memanfaatkan sepenuhnya semua fitur Angular baru dan mendatang.

Menghindari manipulasi DOM manual

Cara terbaik untuk menghindari masalah yang disebabkan oleh manipulasi DOM manual adalah, tentu saja, menghindarinya sama sekali jika memungkinkan. Angular memiliki API dan pola bawaan yang dapat memanipulasi sebagian besar aspek DOM: Anda harus lebih memilih menggunakannya daripada mengakses DOM secara langsung.

Mengubah elemen DOM milik sebuah komponen

Saat menulis komponen atau perintah, Anda mungkin perlu mengubah elemen host (yaitu, elemen DOM yang cocok dengan pemilih komponen atau perintah), misalnya, untuk menambahkan class, gaya, atau atribut, bukan menargetkan atau memperkenalkan elemen wrapper. Anda tergoda untuk hanya menjangkau ElementRef guna mengubah elemen DOM yang mendasarinya. Sebagai gantinya, Anda harus menggunakan binding host untuk secara deklaratif mengikat nilai ke ekspresi:

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

Sama seperti data binding di HTML, Anda juga dapat, misalnya, mengikat ke atribut dan gaya, serta mengubah 'true' ke ekspresi berbeda yang akan digunakan Angular untuk menambahkan atau menghapus nilai secara otomatis sesuai kebutuhan.

Dalam beberapa kasus, kunci harus dihitung secara dinamis. Anda juga dapat mengikat ke sinyal atau fungsi yang menampilkan kumpulan atau peta nilai:

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

Di aplikasi yang lebih kompleks, Anda mungkin tergoda untuk menggunakan manipulasi DOM manual guna menghindari ExpressionChangedAfterItHasBeenCheckedError. Sebagai gantinya, Anda dapat mengikat nilai ke sinyal seperti pada contoh sebelumnya. Hal ini dapat dilakukan sesuai kebutuhan, dan tidak memerlukan penerapan sinyal di seluruh codebase Anda.

Mengubah elemen DOM di luar template

Anda mungkin ingin mencoba menggunakan DOM untuk mengakses elemen yang biasanya tidak dapat diakses, seperti elemen yang termasuk dalam komponen induk atau turunan lainnya. Namun, hal ini rentan terhadap error, melanggar enkapsulasi, dan menyulitkan perubahan atau upgrade komponen tersebut di masa mendatang.

Sebaliknya, komponen Anda harus menganggap setiap komponen lainnya sebagai kotak hitam. Luangkan waktu untuk mempertimbangkan kapan dan di mana komponen lain (bahkan dalam aplikasi atau library yang sama) mungkin perlu berinteraksi dengan atau menyesuaikan perilaku atau tampilan komponen Anda, lalu ekspos cara yang aman dan terdokumentasi untuk melakukannya. Gunakan fitur seperti injeksi dependensi hierarkis agar API tersedia untuk subhierarki jika properti @Input dan @Output sederhana tidak memadai.

Secara historis, mengimplementasikan fitur seperti dialog modal atau tooltip adalah praktik dengan menambahkan elemen ke akhir <body> atau beberapa elemen host lainnya, lalu memindahkan atau memproyeksikan konten di sana. Namun, pada hari-hari ini, Anda mungkin dapat merender elemen <dialog> sederhana di template:

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

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

Menunda manipulasi DOM manual

Setelah menggunakan panduan sebelumnya untuk meminimalkan manipulasi dan akses DOM langsung Anda sebanyak mungkin, mungkin masih ada sisa yang tidak dapat dihindari. Dalam kasus semacam itu, penting untuk menundanya selama mungkin. Callback afterRender dan afterNextRender adalah cara yang tepat untuk melakukan hal ini, karena hanya berjalan di browser, setelah Angular memeriksa setiap perubahan dan meng-commit-nya ke DOM.

Jalankan JavaScript khusus browser

Dalam beberapa kasus, Anda akan memiliki library atau API yang hanya berfungsi di browser (misalnya, library diagram, beberapa penggunaan IntersectionObserver, dll). Daripada memeriksa secara kondisional apakah Anda berjalan di browser atau menghentikan perilaku di server, Anda dapat menggunakan afterNextRender:

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

Menjalankan tata letak kustom

Terkadang Anda mungkin perlu membaca atau menulis ke DOM untuk melakukan beberapa tata letak yang belum didukung oleh browser target, seperti memosisikan keterangan alat. afterRender adalah pilihan yang tepat untuk hal ini, karena Anda dapat yakin bahwa DOM berada dalam status yang konsisten. afterRender dan afterNextRender menerima nilai phase dari EarlyRead, Read, atau Write. Membaca tata letak DOM setelah menulis akan memaksa browser menghitung ulang tata letak secara sinkron, yang dapat sangat memengaruhi performa (lihat: layout thrashing). Oleh karena itu, penting untuk membagi logika Anda ke dalam fase yang benar dengan hati-hati.

Misalnya, komponen tooltip yang ingin menampilkan tooltip yang relatif terhadap elemen lain di halaman mungkin akan menggunakan dua fase. Fase EarlyRead pertama-tama akan digunakan untuk mendapatkan ukuran dan posisi elemen:

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

Kemudian, fase Write akan menggunakan nilai yang dibaca sebelumnya untuk benar-benar mengubah posisi tooltip:

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

Dengan membagi logika ke dalam fase-fase yang benar, Angular dapat secara efektif mengelompokkan manipulasi DOM di setiap komponen lain dalam aplikasi, sehingga memastikan dampak performa yang minimal.

Kesimpulan

Ada banyak peningkatan baru dan menarik pada rendering sisi server Angular yang akan dihadirkan, dengan tujuan memudahkan Anda dalam memberikan pengalaman yang luar biasa bagi pengguna. Kami berharap tips sebelumnya akan bermanfaat dalam membantu Anda memanfaatkannya sepenuhnya di aplikasi dan koleksi Anda.