Aplikasi reaktif tanpa Redux / NgRx



Hari ini kita akan menganalisis secara rinci aplikasi sudut reaktif ( repositori github ), yang sepenuhnya ditulis pada strategi OnPush . Aplikasi lain menggunakan formulir reaktif, yang cukup khas untuk aplikasi perusahaan.

Kami tidak akan menggunakan Flux, Redux, NgRx dan sebagai gantinya mengambil keuntungan dari kapabilitas yang sudah tersedia di Scripteks, Angular dan RxJS. Faktanya adalah bahwa alat-alat ini bukan peluru perak dan dapat menambah kompleksitas yang tidak perlu bahkan untuk aplikasi sederhana. Kami sejujurnya diperingatkan tentang hal ini oleh salah satu penulis Flux , dan penulis Redux dan penulis NgRx .

Tetapi alat ini memberikan aplikasi kami fitur yang sangat bagus:

  • Alur data yang dapat diprediksi;
  • Mendukung OnPush dengan desain;
  • Kekekalan data, kurangnya akumulasi efek samping dan hal-hal menyenangkan lainnya.

Kami akan mencoba untuk mendapatkan karakteristik yang sama, tetapi tanpa memperkenalkan kompleksitas tambahan.

Seperti yang akan Anda lihat di akhir artikel, ini adalah tugas yang cukup sederhana - jika Anda menghapus rincian Angular dan OnPush dari artikel tersebut, maka hanya ada beberapa ide sederhana.

Artikel ini tidak menawarkan pola universal baru, tetapi hanya berbagi dengan pembaca beberapa ide yang, untuk semua kesederhanaannya, untuk beberapa alasan tidak langsung terlintas dalam pikiran. Juga, solusi yang dikembangkan tidak bertentangan atau menggantikan Flux / Redux / NgRx. Mereka dapat terhubung, jika ini benar-benar diperlukan .

Untuk membaca artikel dengan nyaman, diperlukan pemahaman tentang istilah komponen cerdas, presentasi, dan wadah .

Rencana aksi


Logika aplikasi, serta urutan penyajian materi, dapat dijelaskan dalam bentuk langkah-langkah berikut:

  1. Pisahkan data untuk membaca (GET) dan menulis (PUT / POST)
  2. Muat status sebagai aliran dalam komponen wadah
  3. Mendistribusikan Status ke hierarki komponen OnPush
  4. Beri tahu Angular tentang perubahan komponen
  5. Pengeditan data terenkapsulasi

Untuk mengimplementasikan OnPush, kita perlu menguraikan semua cara untuk menjalankan deteksi perubahan di Angular. Hanya ada empat metode seperti itu, dan kami akan mempertimbangkannya secara berurutan di seluruh artikel.

Jadi ayo pergi.

Bagikan data untuk membaca dan menulis


Biasanya, aplikasi frontend dan backend menggunakan kontrak yang diketik (jika tidak mengapa naskah sama sekali?).

Proyek demo yang kami pertimbangkan tidak memiliki backend nyata, tetapi berisi file deskripsi yang sudah disiapkan sebelumnya swagger.json . Berdasarkan itu, kontrak naskah dihasilkan oleh utilitas sw2dts .

Kontrak yang dihasilkan memiliki dua sifat penting.

Pertama, membaca dan menulis dilakukan dengan menggunakan kontrak yang berbeda. Kami menggunakan konvensi kecil dan merujuk untuk membaca kontrak dengan suffix "State", dan menulis kontrak dengan suffix "Model".

Dengan memisahkan kontrak dengan cara ini, kami membagikan aliran data dalam aplikasi. Dari atas ke bawah, status hanya baca disebarkan melalui hierarki komponen. Untuk memodifikasi data, sebuah model dibuat yang awalnya diisi dengan data dari negara, tetapi ada sebagai objek yang terpisah. Pada akhir pengeditan, model dikirim ke backend sebagai perintah.

Poin penting kedua adalah bahwa semua bidang Negara ditandai dengan pengubah hanya baca. Jadi kami mendapatkan dukungan kekebalan di tingkat naskah. Sekarang kita tidak akan dapat mengubah status kode secara tidak sengaja atau mengikatnya menggunakan [(ngModel)] - saat mengompilasi aplikasi dalam mode AOT, kita akan mendapatkan kesalahan.

Muat status sebagai aliran dalam komponen wadah


Untuk memuat dan menginisialisasi keadaan, kami akan menggunakan layanan sudut biasa. Mereka akan bertanggung jawab atas skenario berikut:

  • Contoh klasik memuat melalui HttpClient menggunakan parameter id yang diperoleh oleh komponen dari router.
  • Menginisialisasi keadaan kosong saat membuat entitas baru. Misalnya, jika bidang memiliki nilai default atau untuk menginisialisasi, Anda perlu meminta data tambahan dari backend.
  • Mem-boot ulang keadaan yang sudah dimuat setelah pengguna melakukan operasi yang mengubah data ke backend.
  • Status boot ulang dengan pemberitahuan push, misalnya, saat mengedit data bersama. Dalam hal ini, layanan menggabungkan negara bagian dan negara bagian yang diperoleh dari backend.

Dalam aplikasi demo, kami akan mempertimbangkan dua skenario pertama sebagai yang paling umum. Juga, skenario ini sederhana dan memungkinkan layanan untuk diimplementasikan sebagai objek stateless sederhana dan tidak terganggu oleh kompleksitas, yang bukan subjek dari artikel khusus ini.

Contoh layanan dapat ditemukan dalam file some-entitas.service.ts .

Tetap mendapatkan layanan melalui DI dalam komponen kontainer dan status muatan. Ini biasanya dilakukan seperti ini:

route.params .pipe( pluck('id'), filter((id: any) => { return !!id; }), switchMap((id: string) => { return myFormService.get(id); }) ) .subscribe(state => { this.state = state; }); 

Tetapi dengan pendekatan ini, dua masalah muncul:

  • Anda harus berhenti berlangganan secara manual dari langganan yang dibuat, jika tidak akan terjadi kebocoran memori.
  • Jika Anda mengganti komponen ke strategi OnPush, itu akan berhenti merespons pemuatan data.

Pipa Async datang untuk menyelamatkan. Dia mendengarkan langsung ke Observable dan berhenti berlangganan darinya bila perlu. Juga, ketika menggunakan pipa async, Angular secara otomatis memicu deteksi perubahan setiap kali Observable mempublikasikan nilai baru.

Contoh menggunakan pipa async dapat ditemukan dalam template untuk komponen some-entitas.component .

Dan dalam kode komponen, kami menghapus logika berulang ke operator RxJS kustom, menambahkan skrip untuk membuat keadaan kosong, menggabungkan kedua sumber negara menjadi satu aliran dengan operator gabungan dan membuat formulir untuk mengedit, yang akan kita bahas nanti:

 this.state$ = merge( route.params.pipe( switchIfNotEmpty("id", (requestId: string) => requestService.get(requestId) ) ), route.params.pipe( switchIfEmpty("id", () => requestService.getEmptyState()) ) ).pipe( tap(state => { this.form = new SomeEntityFormGroup(state); }) ); 

Ini semua yang harus dilakukan dalam komponen wadah. Dan kami menempatkan celengan sebagai cara pertama untuk memanggil deteksi perubahan pada komponen OnPush - pipa async. Ini akan bermanfaat bagi kita lebih dari sekali.

Mendistribusikan Status ke hierarki komponen OnPush


Saat Anda perlu menampilkan status kompleks, kami membuat hierarki komponen kecil - ini adalah cara kami menangani kompleksitas.

Sebagai aturan, komponen dibagi ke dalam hierarki yang mirip dengan hirarki data, dan setiap komponen menerima sepotong datanya sendiri melalui parameter Input untuk menampilkannya dalam templat.

Karena kita akan mengimplementasikan semua komponen sebagai OnPush, mari kita menyimpang sejenak dan membahas apa itu dan bagaimana Angular bekerja dengan komponen OnPush. Jika Anda sudah tahu materi ini - silakan gulir ke bagian akhir.

Selama kompilasi aplikasi, Angular menghasilkan detektor perubahan kelas khusus untuk setiap komponen, yang "mengingat" semua binding yang digunakan dalam templat komponen. Pada waktu berjalan, kelas yang dihasilkan mulai memeriksa ekspresi yang tersimpan dengan setiap loop deteksi perubahan. Jika centang menunjukkan bahwa hasil dari ekspresi apa pun telah berubah, maka Angular menggambar ulang komponen.

Secara default, Angular tidak tahu apa-apa tentang komponen kami dan tidak dapat menentukan komponen mana yang akan terpengaruh, misalnya, setTimeout yang baru saja dipicu atau permintaan AJAX yang telah berakhir. Oleh karena itu, ia terpaksa memeriksa seluruh aplikasi secara harfiah untuk setiap peristiwa di dalam aplikasi - bahkan jendela gulir sederhana berulang kali memicu deteksi perubahan untuk seluruh hierarki komponen aplikasi.

Di sinilah letak sumber masalah kinerja potensial - semakin kompleks templat komponen, semakin sulit pemeriksaan detektor perubahan. Dan jika ada banyak komponen dan pemeriksaan sering dijalankan, maka perubahan deteksi mulai membutuhkan waktu yang cukup lama.

Apa yang harus dilakukan

Jika komponen tidak tergantung pada efek global (omong-omong, lebih baik untuk merancang komponen dengan cara ini), maka keadaan internalnya ditentukan oleh:

  • Parameter input ( @ Input);
  • Peristiwa yang terjadi di komponen itu sendiri ( @Output ).

Kami akan menunda titik kedua untuk saat ini dan menganggap bahwa keadaan komponen kami hanya bergantung pada parameter Input.

Jika semua parameter Input komponen adalah objek yang tidak dapat diubah, maka kita dapat menandai komponen sebagai OnPush. Kemudian, sebelum menjalankan deteksi perubahan, Angular akan memeriksa apakah tautan ke parameter Input komponen telah berubah sejak pemeriksaan sebelumnya. Dan, jika mereka tidak berubah, maka Angular akan melewatkan deteksi perubahan untuk komponen itu sendiri dan semua komponen anaknya.

Jadi, jika kita membangun seluruh aplikasi kita sesuai dengan strategi OnPush, kita akan menghilangkan seluruh kelas masalah kinerja dari awal.

Karena Status dalam aplikasi kita sudah tidak dapat diubah, objek yang tidak dapat diubah juga ditransfer ke parameter Input komponen anak. Artinya, kami siap mengaktifkan OnPush untuk komponen anak dan mereka akan merespons perubahan status.
Sebagai contoh, ini adalah komponen readonly-info.component dan nested-items.component

Sekarang mari kita lihat bagaimana menerapkan perubahan dalam keadaan komponen dalam paradigma OnPush.

Bicaralah dengan Angular tentang kondisi Anda


Status presentasi - ini adalah parameter yang bertanggung jawab untuk penampilan komponen: memuat indikator, tanda visibilitas elemen atau aksesibilitas ke pengguna dari satu atau tindakan lain, terpaku dari tiga bidang ke satu baris, nama lengkap pengguna, dll.

Setiap kali status presentasi suatu komponen berubah, kita harus memberi tahu Angular agar dapat menampilkan perubahan pada UI.

Bergantung pada apa sumber status komponen, ada beberapa cara untuk memberi tahu Angular.

Keadaan presentasi, dihitung berdasarkan parameter Input


Ini adalah opsi termudah. Kami menempatkan logika perhitungan status presentasi di kait ngOnChanges. Ubah deteksi akan mulai dengan mengubah @ Input-parameter. Dalam demo, ini hanya baca-info.component .

 export class ReadOnlyInfoComponent implements OnChanges { @Input() public state: Backend.SomeEntityState; public traits: ReadonlyInfoTraits; public ngOnChanges(changes: { state: SimpleChange }): void { this.traits = new ReadonlyInfoTraits(changes.state.currentValue); } } 

Semuanya sangat sederhana, tetapi ada satu hal yang harus diperhatikan.

Jika keadaan presentasi komponen kompleks, dan terutama jika beberapa bidangnya dihitung berdasarkan yang lain, juga dihitung oleh parameter Input, letakkan status komponen dalam kelas yang terpisah, buat tidak berubah dan buat kembali ngOnChanges setiap kali dimulai. Dalam proyek demo, contohnya adalah kelas ReadonlyInfoComponentTraits . Dengan menggunakan pendekatan ini, Anda melindungi diri dari kebutuhan untuk menyinkronkan data dependen ketika itu berubah.

Pada saat yang sama, ada baiknya mempertimbangkan: mungkin komponen memiliki keadaan yang sulit karena fakta bahwa ada terlalu banyak logika di dalamnya. Contoh khas adalah upaya dalam satu komponen agar sesuai dengan representasi untuk pengguna yang berbeda yang memiliki cara kerja yang sangat berbeda dengan sistem.

Komponen Acara Asli


Untuk komunikasi antara komponen aplikasi, kami menggunakan acara Output. Ini juga merupakan cara ketiga untuk menjalankan deteksi perubahan. Angular beranggapan bahwa jika suatu komponen menghasilkan suatu peristiwa, maka sesuatu dapat berubah dalam keadaannya. Oleh karena itu, Angular mendengarkan semua peristiwa Output Komponen dan memicu deteksi perubahan ketika mereka terjadi.

Dalam proyek demo, ini sepenuhnya sintetik, tetapi contohnya adalah komponen submit-button.component , yang melempar acara FormSaved . Komponen wadah berlangganan ke acara ini dan menampilkan peringatan dengan pemberitahuan.

Gunakan acara Output untuk tujuan yang dimaksudkan, yaitu, membuatnya untuk komunikasi dengan komponen induk, dan bukan untuk memicu deteksi perubahan. Kalau tidak, kemungkinan, setelah berbulan-bulan dan bertahun-tahun, tidak ingat mengapa acara ini tidak perlu bagi siapa pun di sini, dan menghapusnya, merusak segalanya.

Perubahan komponen pintar


Kadang-kadang keadaan komponen ditentukan oleh logika kompleks: panggilan layanan yang tidak serempak, menghubungkan ke soket web, memeriksa berjalan melalui setInterval, tetapi Anda tidak pernah tahu apa lagi. Komponen semacam itu disebut komponen pintar.

Secara umum, komponen yang kurang pintar dalam aplikasi yang bukan komponen wadah, semakin mudah untuk hidup. Tetapi kadang-kadang Anda tidak dapat melakukannya tanpa mereka.

Cara paling sederhana untuk mengaitkan keadaan komponen cerdas dengan deteksi perubahan adalah dengan mengubahnya menjadi Observable dan menggunakan pipa async yang sudah dibahas di atas. Misalnya, jika sumber perubahan adalah panggilan layanan atau status formulir reaktif, maka ini adalah Observable yang sudah jadi. Jika keadaan terbentuk dari sesuatu yang lebih kompleks, Anda dapat menggunakan fromPromise , websocket , timer , interval dari komposisi RxJS. Atau hasilkan aliran sendiri menggunakan Subjek .

Jika tidak ada opsi yang cocok


Dalam kasus di mana tidak ada dari tiga metode yang sudah dipelajari yang cocok, kami masih memiliki opsi antipeluru - menggunakan ChangeDetectorRef secara langsung. Kita berbicara tentang metode detectChanges dan markForCheck dari kelas ini.

Dokumentasi yang komprehensif menjawab semua pertanyaan, jadi kami tidak akan memikirkannya. Tetapi perhatikan bahwa penggunaan ChangeDetectorRef harus dibatasi pada kasus di mana Anda memahami dengan jelas apa yang Anda lakukan, karena ini masih merupakan dapur internal Angular.

Selama ini kami hanya menemukan beberapa kasus di mana metode ini mungkin diperlukan:

  1. Pekerjaan manual dengan deteksi perubahan - digunakan dalam implementasi komponen tingkat rendah dan hanya "Anda jelas mengerti apa yang Anda lakukan".
  2. Hubungan kompleks antara komponen - misalnya, ketika Anda perlu membuat tautan ke komponen dalam templat dan meneruskannya sebagai parameter ke komponen lain yang terletak lebih tinggi dalam hierarki atau bahkan di cabang lain dari hierarki komponen. Kedengarannya rumit? Begitulah. Dan lebih baik untuk hanya memperbaiki kode seperti itu, karena itu akan membawa rasa sakit tidak hanya dengan deteksi perubahan.
  3. Kekhasan perilaku Angular itu sendiri - misalnya, saat menerapkan ControlValueAccessor kustom , Anda mungkin menemukan bahwa nilai kontrol diubah oleh Angular secara tidak sinkron dan perubahan tidak diterapkan ke siklus deteksi perubahan yang diinginkan.

Sebagai contoh penggunaan dalam aplikasi demo, ada kelas dasar OnPushControlValueAccessor , yang memecahkan masalah yang dijelaskan dalam paragraf terakhir. Juga dalam proyek ini ada pewaris kelas ini - kustom radio-button.component .

Sekarang kita telah membahas keempat cara untuk menjalankan deteksi perubahan dan opsi implementasi OnPush untuk ketiga jenis komponen: wadah, cerdas, presentasi. Kami melewati titik akhir - mengedit data dengan formulir reaktif.

Pengeditan data terenkapsulasi


Bentuk reaktif memiliki sejumlah keterbatasan, tetapi tetap ini adalah salah satu hal terbaik yang terjadi di ekosistem Angular.

Pertama-tama, mereka merangkum bekerja dengan baik dengan negara dan menyediakan semua alat yang diperlukan untuk menanggapi perubahan secara reaktif.

Bahkan, bentuk reaktif adalah semacam toko mini yang merangkum pekerjaan dengan negara: data dan status dinonaktifkan / valid / tertunda.

Tetap bagi kami untuk mendukung enkapsulasi ini sebanyak mungkin dan menghindari pencampuran logika presentasi dan logika bentuk.

Dalam aplikasi demo, Anda dapat melihat kelas formulir individual yang merangkum spesifik pekerjaan mereka: validasi, membuat formGroup anak, bekerja dengan keadaan dinonaktifkan bidang input.

Kami membuat formulir root dalam komponen wadah pada saat waktu dimuat, dan dengan setiap negara reboot, formulir dibuat kembali. Ini bukan prasyarat, tetapi dengan cara ini kita dapat yakin bahwa tidak ada akumulasi efek dalam bentuk logika yang tersisa dari keadaan dimuat sebelumnya.

Di dalam formulir itu sendiri, kami membangun kontrol dan "mendorong" data yang berasal dari mereka, mengubahnya dari kontrak Negara ke kontrak Model. Struktur formulir, sejauh mungkin, cocok dengan kontrak model. Akibatnya, properti nilai dari formulir memberi kami model yang siap pakai untuk mengirim ke backend.

Jika di masa depan kondisi struktur model atau perubahan, kita akan mendapatkan kesalahan kompilasi naskah tepat di tempat kita perlu menambahkan / menghapus bidang, yang sangat nyaman.

Juga, jika keadaan dan objek model memiliki struktur yang benar-benar identik, pengetikan struktural yang digunakan dalam naskah ketik menghilangkan kebutuhan untuk membangun pemetaan yang tidak bermakna satu sama lain.

Total, bentuk logika diisolasi dari logika presentasi dalam komponen dan hidup "dengan sendirinya", tanpa meningkatkan kompleksitas aliran data aplikasi kita secara keseluruhan.

Itu hampir semuanya. Ada kasus perbatasan yang tersisa ketika kami tidak dapat mengisolasi logika formulir dari aplikasi lainnya:

  1. Perubahan bentuk yang mengarah ke perubahan status presentasi - misalnya, visibilitas blok data tergantung pada nilai yang dimasukkan. Kami menerapkannya dalam komponen dengan berlangganan untuk membentuk acara. Anda dapat melakukan ini melalui sifat-sifat abadi yang dibahas sebelumnya.
  2. Jika Anda memerlukan validator asinkron yang memanggil backend, kami membuat AsyncValidatorFn dalam komponen dan meneruskannya ke konstruktor formulir, bukan layanan.

Dengan demikian, semua "batas" logika tetap di tempat yang paling menonjol - dalam komponen.

Kesimpulan


Mari kita simpulkan apa yang kita dapatkan dan poin lain apa yang ada untuk studi dan pengembangan.

Pertama-tama, pengembangan strategi OnPush memaksa kita untuk hati-hati merancang aliran data aplikasi, karena sekarang kita mendikte aturan permainan ke Angular, dan bukan dia.

Ada dua konsekuensi dari situasi ini.

Pertama, kami mendapatkan kontrol yang menyenangkan atas aplikasi tersebut. Tidak ada lagi sihir yang โ€œentah bagaimana berhasilโ€. Anda jelas mengetahui apa yang terjadi pada waktu tertentu dalam aplikasi Anda. Intuisi secara bertahap berkembang, yang memungkinkan Anda untuk memahami alasan bug yang ditemukan, bahkan sebelum Anda membuka kode.

Kedua, sekarang kita harus menghabiskan lebih banyak waktu merancang aplikasi, tetapi hasilnya akan selalu menjadi yang paling "langsung", dan karena itu, solusi paling sederhana. Ini jelas membawa ke nol kemungkinan situasi di mana, ketika aplikasi tumbuh, itu menjadi rakasa kompleksitas yang sangat besar, para pengembang telah kehilangan kendali atas kompleksitas ini dan pengembangan sekarang lebih mirip ritual mistis.

Kompleksitas yang terkontrol dan tidak adanya "sihir" mengurangi kemungkinan timbulnya seluruh kelas masalah, misalnya, dari pembaruan data siklus atau akumulasi efek samping. Sebagai gantinya, kami menghadapi masalah yang sudah terlihat selama pengembangan, ketika aplikasi tidak berfungsi. Dan terpaksa, Anda harus membuat aplikasi berfungsi sederhana dan jelas.

Kami juga menyebutkan efek yang baik pada kinerja. Sekarang, menggunakan alat yang sangat sederhana, seperti profiler.timeChangeDetection , kita dapat memeriksa kapan saja bahwa aplikasi kita masih dalam kondisi yang baik.

Juga sekarang adalah dosa untuk tidak mencoba menonaktifkan NgZone . Pertama, ini memungkinkan Anda untuk tidak memuat seluruh pustaka saat startup aplikasi. Kedua, itu akan menghapus cukup banyak sihir dari aplikasi Anda.

Di situlah kita mengakhiri cerita kita.

Kami akan menghubungi Anda!

Source: https://habr.com/ru/post/id426819/


All Articles