Organisasi akses ke data server adalah dasar dari hampir semua aplikasi satu halaman. Semua konten dinamis dalam aplikasi tersebut diunduh dari backend.
Dalam kebanyakan kasus, permintaan HTTP ke server berfungsi dengan andal dan mengembalikan hasil yang diinginkan. Namun, dalam beberapa situasi, permintaan mungkin gagal.
Bayangkan bagaimana seseorang bekerja dengan situs web Anda melalui titik akses di kereta api yang bepergian di seluruh negeri dengan kecepatan 200 kilometer per jam. Koneksi jaringan dalam skenario ini bisa lambat, tetapi server meminta, meskipun demikian, melakukan pekerjaan mereka.
Tetapi bagaimana jika kereta itu masuk ke dalam terowongan? Ada kemungkinan besar bahwa koneksi ke Internet akan terputus dan aplikasi web tidak akan dapat "menjangkau" ke server. Dalam hal ini, pengguna harus memuat ulang halaman aplikasi setelah kereta meninggalkan terowongan dan koneksi Internet dipulihkan.
Muat ulang halaman dapat memengaruhi kondisi aplikasi saat ini. Ini berarti bahwa pengguna dapat, misalnya, kehilangan data yang ia masukkan ke dalam formulir.
Daripada hanya berdamai dengan fakta bahwa permintaan tertentu tidak berhasil, akan lebih baik untuk mengulanginya beberapa kali dan menunjukkan kepada pengguna pemberitahuan yang sesuai. Dengan pendekatan ini, ketika pengguna menyadari bahwa aplikasi sedang mencoba untuk mengatasi masalah, ia kemungkinan besar tidak akan memuat ulang halaman.

Bahan, terjemahan yang kami terbitkan hari ini, dikhususkan untuk analisis beberapa cara mengulangi permintaan yang gagal dalam aplikasi Angular.
Ulangi permintaan yang gagal
Mari kita mereproduksi situasi yang mungkin ditemui pengguna yang bekerja di Internet dari kereta. Kami akan membuat backend yang memproses permintaan secara tidak benar selama tiga upaya pertama untuk mengaksesnya, mengembalikan data hanya dari upaya keempat.
Biasanya, menggunakan Angular, kami membuat layanan, menghubungkan
HttpClient
dan menggunakannya untuk mendapatkan data dari backend.
import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError} from 'rxjs/operators'; @Injectable() export class GreetingService { private GREET_ENDPOINT = 'http://localhost:3000'; constructor(private httpClient: HttpClient) { } greet(): Observable<string> { return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe( catchError(() => {
Tidak ada yang istimewa di sini. Kami menyambungkan modul
HttpClient
Angular dan menjalankan permintaan GET sederhana. Jika permintaan mengembalikan kesalahan, kami menjalankan beberapa kode untuk memprosesnya dan mengembalikan yang
Observable
diamati (objek yang dapat diobservasi) untuk menginformasikan tentang apa yang memulai permintaan. Kode ini, seolah-olah, mengatakan: "Ada kesalahan, tetapi semuanya beres, saya bisa mengatasinya."
Sebagian besar aplikasi melakukan permintaan HTTP dengan cara ini. Dalam kode di atas, permintaan dieksekusi hanya sekali. Setelah itu, ia mengembalikan data yang diterima dari server, atau tidak berhasil.
Bagaimana cara mengulang permintaan jika titik akhir
/greet
tidak tersedia atau mengembalikan kesalahan? Mungkin ada pernyataan RxJS yang cocok? Tentu saja ada. RxJS memiliki operator untuk semuanya.
Hal pertama yang mungkin terpikirkan dalam situasi ini adalah
retry
. Mari kita lihat definisinya: “Mengembalikan Observable yang memainkan Observable original kecuali untuk
error
. Jika
error
Observable original dapat diobservasi, maka metode ini, alih-alih menyebarkan kesalahan, akan berlangganan kembali ke Observable original.
Jumlah maksimum berlangganan ulang terbatas untuk
count
(ini adalah parameter numerik yang diteruskan ke metode). "
retry
sangat mirip dengan yang kita butuhkan. Jadi mari kita tanamkan dalam rantai kita.
import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError, retry, shareReplay} from 'rxjs/operators'; @Injectable() export class GreetingService { private GREET_ENDPOINT = 'http://localhost:3000'; constructor(private httpClient: HttpClient) { } greet(): Observable<string> { return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe( retry(3), catchError(() => {
Kami telah berhasil menggunakan operator
retry
. Mari kita lihat bagaimana ini mempengaruhi perilaku permintaan HTTP yang dieksekusi di aplikasi eksperimental.
Berikut adalah file GIF besar yang menunjukkan layar aplikasi ini dan tab Jaringan alat pengembang browser. Anda akan menemukan beberapa demonstrasi lagi di sini.
Aplikasi kami sangat sederhana. Itu hanya membuat permintaan HTTP ketika tombol
PING THE SERVER
diklik.
Seperti yang telah disebutkan, backend mengembalikan kesalahan saat melakukan tiga upaya pertama untuk mengeksekusi permintaan, dan ketika permintaan keempat datang, itu mengembalikan respons normal.
Pada tab Alat pengembang jaringan, Anda dapat melihat bahwa
retry
memecahkan tugas yang ditugaskan untuk itu dan mengulangi pelaksanaan permintaan gagal tiga kali. Upaya terakhir berhasil, aplikasi menerima respons, pesan yang sesuai muncul di halaman.
Semua ini sangat bagus. Sekarang aplikasi dapat mengulangi permintaan yang gagal.
Namun, contoh ini masih bisa diperbaiki. Harap perhatikan bahwa sekarang permintaan yang berulang akan dieksekusi segera setelah eksekusi permintaan yang tidak berhasil. Perilaku sistem ini tidak akan membawa banyak manfaat dalam situasi kita - ketika kereta memasuki terowongan dan koneksi internet terputus untuk sementara waktu.
Coba lagi permintaan yang gagal
Kereta yang masuk ke terowongan tidak langsung meninggalkannya. Dia menghabiskan beberapa waktu di sana. Oleh karena itu, kita perlu "meregangkan" periode di mana kita melakukan permintaan berulang ke server. Anda dapat melakukan ini dengan menunda coba lagi.
Untuk melakukan ini, kita perlu lebih mengontrol proses mengeksekusi permintaan berulang. Kita harus dapat membuat keputusan tentang kapan tepatnya mengulangi permintaan. Ini berarti bahwa kemampuan operator
retry
lagi tidak lagi cukup bagi kami. Karena itu, kami kembali membuka dokumentasi tentang RxJS.
Dokumentasi berisi deskripsi
retryWhen
, yang tampaknya cocok untuk kita. Dalam dokumentasi, dijelaskan sebagai berikut: “Mengembalikan Observable yang memainkan Observable asli dengan pengecualian
error
. Jika
error
dapat diobservasi asli, maka metode ini akan melempar Throwable, yang menyebabkan kesalahan, Observable kembali dari
notifier
. Jika panggilan yang diamati ini
complete
atau
error
, maka metode ini akan memanggil
complete
atau
error
pada langganan anak. Jika tidak, metode ini akan berlangganan kembali ke Observable original. "
Ya, definisi itu tidak sederhana. Mari kita gambarkan hal yang sama dalam bahasa yang lebih mudah diakses.
retryWhen
menerima panggilan balik yang mengembalikan Observable. Observable yang dikembalikan memutuskan bagaimana
retryWhen
operator akan berperilaku berdasarkan beberapa aturan. Yaitu, ini adalah bagaimana
retryWhen
operator
retryWhen
:
- Itu berhenti bekerja dan melempar kesalahan jika yang diamati kembali melemparkan kesalahan.
- Itu keluar jika laporan yang diamati diamati selesai.
- Dalam kasus lain, ketika Observable kembali dengan sukses, ia mengulangi eksekusi Observable yang asli
Panggilan balik hanya dipanggil saat Observable yang asli melontarkan kesalahan untuk pertama kalinya.
Sekarang kita bisa menggunakan pengetahuan ini untuk membuat mekanisme coba lagi yang ditunda untuk permintaan gagal menggunakan coba lagi
retryWhen
.
retryWhen((errors: Observable<any>) => errors.pipe( delay(delayMs), mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxEntry)) )) )
Jika Observable asli, yang merupakan permintaan HTTP kami, mengembalikan kesalahan, maka
retryWhen
pernyataan
retryWhen
. Dalam panggilan balik, kami memiliki akses ke kesalahan yang menyebabkan kegagalan. Kami menangguhkan
errors
, mengurangi jumlah percobaan ulang, dan mengembalikan Observable baru yang melempar kesalahan.
Berdasarkan aturan
retryWhen
, diamati ini, karena
retryWhen
,
retryWhen
permintaan. Jika pengulangan tidak berhasil beberapa kali dan nilai variabel
retries
menurun menjadi 0, maka kami mengakhiri tugas dengan kesalahan yang terjadi saat menjalankan permintaan.
Hebat! Tampaknya, kita dapat mengambil kode di atas dan mengganti operator
retry
di rantai kami dengannya. Tapi di sini kita sedikit melambat.
Bagaimana cara
retries
variabel? Variabel ini berisi kondisi saat ini dari sistem coba lagi permintaan yang gagal. Di mana dia diumumkan? Kapan kondisinya diatur ulang? Negara perlu dikelola di dalam aliran, bukan di luarnya.
▍Membuat pernyataanRedri Anda sendiri yang tertunda
Kita dapat memecahkan masalah manajemen negara dan meningkatkan keterbacaan kode dengan menulis kode di atas sebagai operator RxJS terpisah.
Ada berbagai cara untuk membuat operator RxJS Anda sendiri. Metode mana yang digunakan tergantung pada bagaimana operator terstruktur.
Operator kami didasarkan pada operator RxJS yang ada. Akibatnya, kita dapat menggunakan cara paling sederhana untuk membuat operator kita sendiri. Dalam kasus kami, operator RxJ hanya fungsi dengan tanda tangan berikut:
const customOperator = (src: Observable<A>) => Observable<B>
Pernyataan ini mengambil Observable asli dan mengembalikan Observable lainnya.
Karena operator kami memungkinkan pengguna untuk menentukan seberapa sering permintaan berulang harus dieksekusi, dan berapa kali mereka perlu dieksekusi, kita perlu membungkus deklarasi fungsi di atas dalam fungsi pabrik, yang membutuhkan
delayMs
(keterlambatan antara
maxRetry
) dan
maxRetry
( jumlah pengulangan maksimum).
const customOperator = (delayMs: number, maxRetry: number) => { return (src: Observable<A>) => Observable<B> }
Jika Anda ingin membuat operator yang tidak didasarkan pada operator yang ada, Anda perlu memperhatikan penanganan kesalahan dan langganan. Selain itu, Anda perlu memperluas kelas yang Dapat
Observable
dan mengimplementasikan fungsi
lift
.
Jika Anda tertarik, silakan lihat di
sini .
Jadi, berdasarkan cuplikan kode di atas, mari kita tulis operator RxJ kita sendiri.
import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) => `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up`; const DEFAULT_MAX_RETRIES = 5; export function delayedRetry(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES) { let retries = maxRetry; return (src: Observable<any>) => src.pipe( retryWhen((errors: Observable<any>) => errors.pipe( delay(delayMs), mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxRetry)) )) ) ); }
Bagus Sekarang kita dapat mengimpor operator ini ke dalam kode klien. Kami akan menggunakannya saat menjalankan permintaan HTTP.
return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe( delayedRetry(1000, 3), catchError(error => { console.log(error);
Kami menempatkan operator
delayedRetry
di rantai dan meneruskan angka 1000 dan 3. sebagai parameter. Parameter pertama menetapkan penundaan dalam milidetik di antara upaya membuat permintaan berulang. Parameter kedua menentukan jumlah maksimum permintaan berulang.
Mulai ulang aplikasi dan
lihat bagaimana operator baru bekerja.
Setelah menganalisis perilaku program menggunakan alat pengembang peramban, kita dapat melihat bahwa pelaksanaan upaya berulang untuk mengeksekusi permintaan ditunda selama sedetik. Setelah menerima jawaban yang benar untuk permintaan tersebut, pesan yang sesuai akan muncul di jendela aplikasi.
Permintaan eksponensial tertunda
Mari kita kembangkan gagasan penundaan coba lagi permintaan yang gagal. Sebelumnya, kami selalu menunda eksekusi setiap permintaan yang berulang pada saat yang bersamaan.
Di sini kita berbicara tentang cara meningkatkan penundaan setelah setiap upaya. Upaya pertama untuk mencoba kembali permintaan dilakukan setelah satu detik, yang kedua setelah dua detik, yang ketiga setelah tiga.
Buat pernyataan baru,
retryWithBackoff
, yang mengimplementasikan perilaku ini.
import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) => `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up.`; const DEFAULT_MAX_RETRIES = 5; const DEFAULT_BACKOFF = 1000; export function retryWithBackoff(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES, backoffMs = DEFAULT_BACKOFF) { let retries = maxRetry; return (src: Observable<any>) => src.pipe( retryWhen((errors: Observable<any>) => errors.pipe( mergeMap(error => { if (retries-- > 0) { const backoffTime = delayMs + (maxRetry - retries) * backoffMs; return of(error).pipe(delay(backoffTime)); } return throwError(getErrorMessage(maxRetry)); } )))); }
Jika Anda menggunakan operator ini dalam aplikasi dan mengujinya, Anda dapat
melihat bagaimana penundaan dalam menjalankan permintaan yang diulang meningkat setelah setiap upaya baru.
Setelah setiap upaya, kami menunggu waktu tertentu, ulangi permintaan dan menambah waktu tunggu. Di sini, seperti biasa, setelah server mengembalikan jawaban yang benar ke permintaan, kami menampilkan pesan di jendela aplikasi.
Ringkasan
Mengulangi permintaan HTTP yang gagal membuat aplikasi lebih stabil. Ini sangat penting ketika melakukan permintaan yang sangat penting, tanpa data yang diperoleh, aplikasi tidak dapat bekerja secara normal. Misalnya, itu bisa berupa data konfigurasi yang berisi alamat server yang dengannya aplikasi perlu berinteraksi.
Dalam sebagian besar skenario,
retry
coba lagi RxJ tidak cukup untuk menyediakan sistem coba lagi yang dapat diandalkan untuk permintaan gagal.
retryWhen
memberi pengembang tingkat kontrol yang lebih tinggi atas permintaan berulang. Ini memungkinkan Anda untuk mengkonfigurasi interval untuk permintaan yang berulang. Karena kemampuan operator ini, dimungkinkan untuk menerapkan skema pengulangan tertunda atau pengulangan tertunda secara eksponensial.
Ketika menerapkan pola perilaku yang cocok untuk digunakan kembali dalam rantai RxJS, disarankan agar mereka diformat sebagai operator baru.
Berikut adalah repositori tempat kode digunakan dalam artikel ini.
Pembaca yang budiman! Bagaimana Anda memecahkan masalah mencoba kembali permintaan HTTP yang gagal?
