Dasar-dasar pemrograman reaktif menggunakan RxJS. Bagian 2. Operator dan pipa



Dalam artikel sebelumnya, kami melihat aliran apa dan apa yang mereka makan. Pada bagian baru, kita akan berkenalan dengan metode apa yang disediakan RxJS untuk menciptakan aliran, apa itu operator, pipa, dan cara bekerja dengannya.

Seri artikel "Dasar-dasar pemrograman reaktif menggunakan RxJS":



RxJS memiliki API yang kaya. Dokumentasi menjelaskan lebih dari seratus metode. Untuk mengenal mereka sedikit, kita akan menulis aplikasi sederhana dan dalam praktiknya kita akan melihat seperti apa kode reaktif itu. Anda akan melihat bahwa tugas yang sama, yang dulunya tampak rutin dan mengharuskan penulisan banyak kode, memiliki solusi yang elegan jika dilihat dari perspektif reaktivitas. Tetapi sebelum kita mulai berlatih, kita akan melihat bagaimana aliran dapat direpresentasikan secara grafis dan berkenalan dengan metode yang mudah untuk membuat dan memprosesnya.

Representasi grafis utas


Untuk menunjukkan dengan jelas bagaimana aliran tertentu berperilaku, saya akan menggunakan notasi yang diadopsi dalam pendekatan reaktif. Ingat contoh kami dari artikel sebelumnya:

const observable = new Observable((observer) => { observer.next(1); observer.next(2); observer.complete(); }); 

Berikut ini tampilan grafiknya:



Aliran biasanya digambarkan sebagai garis lurus. Jika aliran memancarkan nilai apa pun, maka itu ditampilkan pada garis sebagai lingkaran. Garis lurus di layar adalah sinyal untuk mengakhiri aliran. Untuk menampilkan kesalahan, gunakan simbol - "×".

 const observable = new Observable((observer) => { observer.error(); }); 



Satu baris mengalir


Dalam praktik saya, saya jarang harus membuat contoh saya sendiri yang bisa diamati secara langsung. Sebagian besar metode untuk membuat utas sudah ada di RxJS. Untuk membuat aliran yang memancarkan nilai 1 dan 2, cukup menggunakan metode:

 const observable = of(1, 2); 

Metode menerima sejumlah argumen dan mengembalikan contoh selesai dari Observable. Setelah berlangganan, itu akan memancarkan nilai yang diterima dan menyelesaikan:



Jika Anda ingin mewakili array sebagai aliran, maka Anda dapat menggunakan metode from. Metode from sebagai argumen mengharapkan objek iterable (array, string, dll.) Atau janji, dan memproyeksikan objek ini ke aliran. Beginilah tampilan aliran dari string:

 const observable = from('abc'); 



Jadi, Anda dapat membungkus janji dalam aliran:

 const promise = new Promise((resolve, reject) => { resolve(1); }); const observable = from(promise); 



Catatan: sering utas dibandingkan dengan janji. Bahkan, mereka hanya memiliki satu kesamaan - strategi dorong untuk menyebarkan perubahan. Sisanya adalah entitas yang sama sekali berbeda. Janji tidak dapat menghasilkan banyak nilai. Itu hanya dapat mengeksekusi tekad atau penolakan, mis. hanya memiliki dua negara. Aliran dapat mengirimkan beberapa nilai, dan dapat digunakan kembali.

Apakah Anda ingat contoh dengan interval dari artikel pertama ? Aliran ini adalah penghitung waktu yang menghitung waktu dalam detik dari saat berlangganan.

 const timer = new Observable(observer => { let counter = 0; const intervalId = setInterval(() => { observer.next(counter++); }, 1000); return () => { clearInterval(intervalId); } }); 

Inilah cara Anda dapat menerapkan hal yang sama dalam satu baris:

 const timer = interval(1000); 



Dan akhirnya, metode yang memungkinkan Anda untuk membuat aliran peristiwa untuk elemen DOM:

 const observable = fromEvent(domElementRef, 'keyup'); 

Sebagai nilai, aliran ini akan menerima dan memancarkan objek acara keyup.

Pipa & Operator


Pipe adalah metode kelas yang dapat diobservasi ditambahkan dalam RxJS dalam versi 5.5. Berkat itu, kami dapat membangun rantai operator untuk pemrosesan berurutan dari nilai yang diterima dalam aliran. Pipa adalah saluran searah yang menghubungkan operator. Operator itu sendiri adalah fungsi normal yang dijelaskan dalam RxJS yang memproses nilai dari aliran.

Misalnya, mereka dapat mengonversi nilai dan meneruskannya lebih lanjut ke aliran, atau mereka dapat bertindak sebagai filter dan tidak melewatkan nilai apa pun jika tidak memenuhi kondisi yang ditentukan.

Mari kita lihat operator yang sedang beraksi. Lipat gandakan setiap nilai dari aliran dengan 2 menggunakan operator peta:

 of(1,2,3).pipe( map(value => value * 2) ).subscribe({ next: console.log }); 

Inilah yang terlihat seperti aliran sebelum menerapkan operator peta:



Setelah pernyataan peta:



Mari kita gunakan operator filter. Pernyataan ini berfungsi seperti fungsi filter di kelas Array. Metode mengambil fungsi sebagai argumen pertama, yang menggambarkan suatu kondisi. Jika nilai dari aliran memenuhi kondisi, maka diteruskan:

 of(1, 2, 3).pipe( //     filter(value => value % 2 !== 0), map(value = value * 2) ).subscribe({ next: console.log }); 

Dan inilah bagaimana keseluruhan skema aliran kita akan terlihat:



Setelah filter:



Setelah peta:



Catatan: pipa! == berlangganan. Metode pipa menyatakan perilaku aliran, tetapi tidak berlangganan. Sampai Anda memanggil metode berlangganan, streaming Anda tidak akan mulai berfungsi.

Kami sedang menulis aplikasi


Sekarang kita telah mengetahui apa itu pipa dan operator, Anda dapat langsung berlatih. Aplikasi kita akan melakukan satu tugas sederhana: menampilkan daftar repositori github terbuka dengan nama panggilan pemilik yang dimasukkan.

Akan ada beberapa persyaratan:

  • Jangan jalankan permintaan API jika string yang dimasukkan dalam input berisi kurang dari 3 karakter;
  • Agar tidak memenuhi permintaan untuk setiap karakter yang dimasukkan oleh pengguna, perlu mengatur penundaan (debounce) 700 milidetik sebelum mengakses API;

Untuk mencari repositori, kami akan menggunakan API github . Saya sarankan menjalankan contohnya sendiri di stackblitz . Di sana saya meletakkan implementasi selesai. Tautan disediakan di akhir artikel.

Mari kita mulai dengan markup html. Mari kita jelaskan elemen input dan ul:

 <input type="text"> <ul></ul> 

Kemudian, dalam file js atau ts, kita mendapatkan tautan ke elemen saat ini menggunakan API browser:

 const input = document.querySelector('input'); const ul = document.querySelector('ul'); 

Kami juga membutuhkan metode yang akan menjalankan permintaan ke API github. Di bawah ini adalah kode untuk fungsi getUsersRepsFromAPI, yang menerima nama panggilan pengguna dan melakukan permintaan ajax menggunakan fetch. Kemudian ia mengembalikan janji, mengubah respons sukses ke json di sepanjang jalan:

 const getUsersRepsFromAPI = (username) => { const url = `https://api.github.com/users/${ username }/repos`; return fetch(url) .then(response => { if(response.ok) { return response.json(); } throw new Error(''); }); } 

Selanjutnya, kami menulis metode yang akan mencantumkan nama-nama repositori:

 const recordRepsToList = (reps) => { for (let i = 0; i < reps.length; i++) { //    ,    if (!ul.children[i]) { const newEl = document.createElement('li'); ul.appendChild(newEl); } //      const li = ul.children[i]; li.innerHTML = reps[i].name; } //    while (ul.children.length > reps.length) { ul.removeChild(ul.lastChild); } } 

Persiapan sudah selesai. Saatnya untuk melihat tindakan RxJS. Kita perlu mendengarkan acara masukan kami. Pertama-tama, kita harus memahami bahwa dalam pendekatan reaktif, kita bekerja dengan aliran. Untungnya, RxJS sudah menyediakan opsi serupa. Ingat metode fromEvent yang saya sebutkan di atas. Kami menggunakannya:

 const keyUp = fromEvent(input, 'keyup'); keyUp.subscribe({ next: console.log }); 

Sekarang acara kami disajikan sebagai streaming. Jika kita melihat apa yang ditampilkan di konsol, kita akan melihat objek bertipe KeyboardEvent. Tetapi kita membutuhkan nilai yang dimasukkan pengguna. Di sinilah metode pipa dan operator peta berguna:

 fromEvent(input, 'keyup').pipe( map(event => event.target.value) ).subscribe({ next: console.log }); 

Kami melanjutkan ke implementasi persyaratan. Untuk memulainya, kami akan menjalankan kueri ketika nilai yang dimasukkan berisi lebih dari dua karakter. Untuk melakukan ini, gunakan operator filter:

 fromEvent(input, 'keyup').pipe( map(event => event.target.value), filter(value => value.length > 2) ) 

Kami berurusan dengan persyaratan pertama. Kita lanjutkan ke yang kedua. Kita perlu menerapkan debounce. RxJS memiliki pernyataan debounceTime. Operator ini sebagai argumen pertama mengambil jumlah milidetik selama nilai akan ditahan sebelum diteruskan. Dalam hal ini, setiap nilai baru akan mengatur ulang timer. Jadi, pada output kita mendapatkan nilai terakhir, setelah 700 milidetik berlalu.

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(value => value.length > 2) ) 

Inilah yang tampak seperti aliran kami tanpa debounceTime:



Dan ini adalah bagaimana aliran yang sama melewati pernyataan ini akan terlihat seperti:



Dengan debounceTime, kita cenderung menggunakan API, yang akan menghemat lalu lintas dan membongkar server.

Untuk optimasi tambahan, saya sarankan menggunakan operator lain - differUntilChanged. Metode ini akan menyelamatkan kita dari duplikat. Yang terbaik adalah menunjukkan hasil kerjanya menggunakan contoh:

 from('aaabccc').pipe( distinctUntilChanged() ) 

Tanpa perubahan khusus Hingga:



Dengan differentUntilChanged:



Tambahkan pernyataan ini segera setelah pernyataan debounceTime. Dengan demikian, kami tidak akan mengakses API jika nilai baru karena beberapa alasan bertepatan dengan yang sebelumnya. Situasi serupa dapat terjadi ketika pengguna memasukkan karakter baru dan kemudian menghapusnya lagi. Karena kami telah menerapkan penundaan, hanya nilai terakhir yang akan jatuh ke aliran, jawaban yang sudah kami miliki.

Pergi ke server


Sudah sekarang kita dapat menggambarkan logika permintaan dan pemrosesan respons. Sementara kita hanya bisa bekerja dengan janji. Oleh karena itu, kami menggambarkan operator peta lain yang akan memanggil metode getUsersRepsFromAPI. Di pengamat, kami menggambarkan logika pemrosesan janji kami:

 /*  !     RxJS    promise,      */ fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), map(value => getUsersRepsFromAPI(value)) ).subscribe({ next: promise => promise.then(reps => recordRepsToList(reps)) }); 

Saat ini, kami telah mengimplementasikan semua yang kami inginkan. Tetapi contoh kita memiliki satu kelemahan besar: tidak ada penanganan kesalahan. Pengamat kami hanya menerima janji dan tidak tahu bahwa ada sesuatu yang salah.

Tentu saja, kita dapat menggantungkan janji pada metode berikutnya, tetapi karena ini, kode kita akan mulai semakin menyerupai "panggilan balik neraka". Jika tiba-tiba kita perlu menjalankan satu permintaan lagi, maka kompleksitas kode akan meningkat.

Catatan: menggunakan janji dalam kode RxJS dianggap antipattern. Janji memiliki banyak kelemahan dibandingkan dengan yang bisa diamati. Itu tidak bisa diurungkan, dan tidak bisa digunakan kembali. Jika Anda punya pilihan, pilih yang bisa diamati. Hal yang sama berlaku untuk metode toPromise dari kelas Observable. Metode ini diterapkan untuk kompatibilitas dengan perpustakaan yang tidak dapat bekerja dengan aliran.

Kita dapat menggunakan metode dari untuk memproyeksikan janji ke aliran, tetapi metode ini penuh dengan panggilan tambahan untuk metode berlangganan, dan juga akan mengarah pada pertumbuhan dan kompleksitas kode.

Masalah ini dapat dipecahkan menggunakan operator mergeMap:

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => from(getUsersRepsFromAPI(value))) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log }) 

Sekarang kita tidak perlu menulis logika pemrosesan janji. Metode from membuat aliran janji, dan operator mergeMap memprosesnya. Jika janji itu terpenuhi dengan sukses, maka metode selanjutnya disebut, dan pengamat kita akan menerima objek yang sudah jadi. Jika kesalahan terjadi, metode kesalahan akan dipanggil, dan pengamat kami akan menampilkan kesalahan di konsol.

Operator mergeMap sedikit berbeda dari operator yang pernah bekerja sama dengan kami, ini milik apa yang disebut Observable Order Tinggi , yang akan saya bahas di artikel selanjutnya. Tetapi, melihat ke depan, saya akan mengatakan bahwa metode mergeMap sendiri berlangganan aliran.

Menangani kesalahan


Jika utas kami menerima kesalahan, maka itu akan berakhir. Dan jika kita mencoba berinteraksi dengan aplikasi setelah kesalahan, maka kita tidak akan mendapatkan reaksi, karena utas kita telah selesai.

Di sini operator catchError akan membantu kami. catchError dimunculkan hanya ketika kesalahan terjadi di aliran. Ini memungkinkan Anda untuk mencegatnya, memprosesnya dan kembali ke aliran nilai biasa, yang tidak akan mengarah pada penyelesaiannya.

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => from(getUsersRepsFromAPI(value))), catchError(err => of([])) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log }) 

Kami menangkap kesalahan dalam catchError dan sebagai gantinya mengembalikan aliran dengan array kosong. Sekarang, ketika kesalahan terjadi, kami akan menghapus daftar repositori. Namun kemudian aliran berakhir kembali.

Masalahnya adalah bahwa catchError menggantikan aliran asli kami dengan yang baru. Dan kemudian pengamat kami hanya mendengarkannya. Ketika aliran memancarkan array kosong, metode lengkap akan dipanggil.

Agar tidak mengganti utas asli kami, kami memanggil operator catchError di utas dari dalam operator mergeMap.

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => { return from(getUsersRepsFromAPI(value)).pipe( catchError(err => of([])) ) }) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log }) 

Dengan demikian, aliran asli kami tidak akan melihat apa pun. Alih-alih kesalahan, itu akan mendapatkan array kosong.

Kesimpulan


Kami akhirnya mulai berlatih dan melihat apa gunanya pipa dan operator. Kami melihat cara mengurangi kode menggunakan API kaya yang disediakan oleh RxJS. Tentu saja, aplikasi kami belum selesai, di bagian selanjutnya kami akan menganalisis bagaimana Anda dapat memproses yang lain dalam satu utas dan cara membatalkan permintaan http kami untuk menghemat lebih banyak lalu lintas dan sumber daya aplikasi kami. Dan agar Anda dapat melihat perbedaannya, saya memberikan contoh tanpa menggunakan RxJS, Anda dapat melihatnya di sini . Pada tautan ini Anda akan menemukan kode lengkap dari aplikasi saat ini. Untuk menghasilkan sirkuit, saya menggunakan visualizer RxJS .

Saya harap artikel ini membantu Anda lebih memahami cara kerja RxJS. Saya berharap Anda berhasil dalam studi Anda!

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


All Articles