Unggahan file secara interaktif ke server menggunakan RxJS



Sudah lama sejak saya menulis artikel terakhir saya tentang dasar-dasar RxJS. Dalam komentar, saya diminta untuk menunjukkan contoh yang lebih kompleks yang mungkin berguna dalam praktik. Jadi saya memutuskan untuk mencairkan teori sedikit dan hari ini kita akan berbicara tentang mengunggah file.

Apa yang kita lakukan?

  • Kami akan menulis halaman kecil di mana pengguna dapat memilih file untuk mengunggahnya ke server
  • Tambahkan bilah kemajuan sehingga kemajuan unggahan file ditampilkan.
  • Tambahkan kemampuan untuk membatalkan unduhan dengan mengklik tombol batal

Untuk memahami artikel ini, Anda membutuhkan pengetahuan dasar RxJS. Apa yang diamati , operator , serta operator HOO

Kami tidak akan menarik ekor kucing dan segera turun ke bisnis!

Latihan


Pertama, kita membutuhkan server yang dapat menerima permintaan pengunduhan file. Server apa pun bisa cocok untuk ini, saya akan menggunakan node.js bersamaan dengan express dan multer untuk artikel :

const express = require("express");
const multer  = require("multer");

const app = express();
const upload = multer({ dest:"files" });

app.post("/upload", upload.single("file"), function (req, res) {
    const { file } = req;

    if (file) {
        res.send("File uploaded successfully");
    } else {
        res.error("Error");
    }

});

app.listen(3000);

Sekarang buat halaman html di mana kita akan menempatkan semua elemen yang diperlukan:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>File uploading</title>
</head>
<body>
    <label for="file">load file: <input id="file" type="file"></label>
    <button id="upload">Upload</button>
    <button id="cancel">Cancel</button>
    <div id="progress-bar" style="width: 0; height: 2rem; background-color: aquamarine;"></div>
    <script src="index.js" type="text/javascript"></script>
</body>
</html>

Sekarang di halaman kami memiliki 4 elemen yang akan berinteraksi dengan pengguna:

  • Input jenis file sehingga pengguna dapat memilih file untuk diunggah
  • Tombol unggah, ketika diklik, kami akan mulai mengunggah
  • Tombol batal yang akan membatalkan unduhan
  • Bilah progres dengan lebar mulai dari 0. Selama proses pengunggahan, kami akan mengubah lebarnya

Di akhir tag body, saya menambahkan tautan ke skrip index.js, yang juga perlu kita buat:

//   ,     
const input = document.querySelector('#file');
const uploadBtn = document.querySelector('#upload');
const progressBar = document.querySelector('#progress-bar');
const cancelBtn = document.querySelector('#cancel');

Seharusnya terlihat seperti ini:



Semuanya mengalir


Untuk memberi tahu browser file mana yang akan dipilih, pengguna harus mengklik tombol “Pilih file”. Setelah itu, kotak dialog sistem operasi Anda akan terbuka, di mana pohon folder akan ditampilkan. Setelah memilih file, browser akan mengunduh semua informasi yang diperlukan tentangnya.

Bagaimana kami memahami bahwa pengguna telah memilih file? Ada acara "perubahan" untuk ini. Setelah acara dipicu, kita dapat beralih ke array file di input, di mana data file akan ditulis.

Bagaimana kita mendengarkan acara perubahan? Anda dapat menggunakan metode addEventListener dan bekerja dengannya. Namun kami bekerja dengan RxJS, tempat acara apa pun dapat direpresentasikan sebagai aliran:

fromEvent(input, 'change').pipe(
    //    
    map(() => input.files[0])
).subscribe({
    next: data => console.log(file)
});

Tambahkan fungsi unggah, yang akan mengunggah file ke server. Untuk saat ini, biarkan tubuhnya kosong:

function upload(file) {
  console.log(file);
}

Fungsi unggah harus dipanggil setelah mengklik tombol unggah:

fromEvent(uploadBtn, 'click').subscribe({
  next: () => upload(input.files[0])
});

Gabungkan Streaming


Sekarang kode kami tidak berbeda dari apa yang akan kami tulis menggunakan addEventListener. Ya, itu bekerja, tetapi jika kita membiarkannya seperti itu, maka kita akan kehilangan kelebihan yang ditawarkan RxJS kepada kita.

Apa yang bisa kita lakukan? Kami akan menjelaskan urutan langkah-langkah untuk mengunggah file:

  • Pemilihan file
  • Klik uploadBtn
  • Ekstrak file dari input.files
  • Unggah file

Sekarang kami mentransfer urutan ini ke kode. Tetapi bagaimana cara menggabungkan input dan upload stream? Operator switchMap akan membantu kami dalam hal ini, yang memungkinkan kami memproyeksikan satu utas ke yang lain:

fromEvent(input, 'change').pipe(
    switchMap(() => fromEvent(uploadBtn, 'click')),
    map(() => input.files[0])
).subscribe({
    next: file => upload(file)
});

Kode ini sangat mirip dengan urutan instruksi yang kami jelaskan di atas. Pengguna memilih file, switchMap dipicu, dan kami berlangganan uploadBtn. Tetapi kemudian tidak ada yang akan terjadi.

switchMap hanya memungkinkan nilai-nilai yang dihasilkan oleh fromEvent (uploadBtn, 'klik') untuk diteruskan ke aliran eksternal . Untuk mulai mengunggah file, Anda perlu menjalankan instruksi kedua, yaitu, klik uploadBtn. Kemudian akan bekerja metode peta, yang akan mengekstrak file dari array, dan metode unggah akan dipanggil sudah berlangganan.

Yang paling menarik di sini adalah urutan instruksi tidak rusak. Agar fungsi unggahan berfungsi, acara 'perubahan' harus diaktifkan sebelumnya.

Tapi tetap saja, satu masalah tetap ada. Pengguna dapat memilih file dan kemudian membatalkan pilihannya. Dan kemudian ketika kami mencoba mengunggah file, kami meneruskan fungsi unggah - tidak ditentukan. Untuk menghindari situasi ini, kita harus menambahkan cek:

fromEvent(input, 'change').pipe(
    switchMap(() => fromEvent(uploadBtn, 'click')),
    map(() => input.files[0]),
    filter(file => !!file)
).subscribe({
    next: file => upload(file)
});

Bekerja dengan xhr


Sudah waktunya untuk mengimplementasikan yang paling sulit - proses pembongkaran. Saya akan menunjukkannya pada contoh bekerja dengan xhr, karena mengambil, pada saat penulisan, tidak tahu bagaimana melacak kemajuan unggahan .
Anda dapat mengimplementasikan pembongkaran menggunakan perpustakaan lain, misalnya axios atau jQuery.ajax.


Karena saya menggunakan multer di sisi server, saya harus mentransfer file di dalam formulir (multer hanya menerima data dalam format ini). Untuk ini, saya menulis fungsi createFormData:

function createFormData(file) {
    const form = new FormData();
    //       file
    form.append('file', file);
    return form;
}

fromEvent(input, 'change').pipe(
    switchMap(() => fromEvent(uploadBtn, 'click')),
    map(() => input.files[0]),
    filter(file => !!file),
    map(file => createFormData(file))
).subscribe({
    next: data => upload(data)
});

Kami akan menurunkan formulir melalui XMLHttpRequest. Kita perlu membuat instance objek ini dan mendefinisikan metode unload dan onerror di atasnya. Yang pertama akan aktif saat unggahan selesai, yang kedua saat kesalahan terjadi.

function upload(data) {
    const xhr = new XMLHttpRequest();

    //        
    xhr.onload = () => console.log('success');

    //    
    xhr.onerror = e => console.error(e);

    //  
    xhr.open('POST', '/upload', true);

    //  
    xhr.send(data);
}

Sekarang kita memiliki contoh kerja. Tapi itu mengandung beberapa kelemahan:

  • Tidak ada cara untuk membatalkan unduhan
  • Jika Anda mengklik tombol uploadBtn n kali, maka kita akan memiliki koneksi paralel untuk mengunggah satu file

Ini karena fungsi unggah tidak berfungsi. Dia hidup sendiri. Kita perlu memperbaikinya. Mari kita buat fungsi mengembalikan Observable kepada kita. Kemudian kita dapat mengontrol unggahan file:

function upload(data) {
    return new Observable(observer => {
        const xhr = new XMLHttpRequest();

        //    ,      
        //   
        xhr.onload = () => {
            observer.next();
            observer.complete();
        };
        
        xhr.onerror = e => observer.error(e);
        
        xhr.open('POST', '/upload', true);
        xhr.send(data);

        //   -  
        return () => xhr.abort();
    });
}

Perhatikan fungsi panah yang dikembalikan ke dalam Observable. Metode ini akan dipanggil pada saat berhenti berlangganan dan membatalkan unggahan.

Masukkan panggilan unggah ke switchMap:

fromEvent(input, 'change').pipe(
    switchMap(() => fromEvent(uploadBtn, 'click')),
    map(() => input.files[0]),
    filter(file => !!file),
    map(file => createFormData(file)),
    switchMap(data => upload(data))
).subscribe({
    next: () => console.log('File uploaded')
});

Sekarang, jika pengguna mengklik tombol unggah lagi, permintaan sebelumnya akan dibatalkan, tetapi yang baru akan dibuat.

Batalkan permintaan klik


Kami masih memiliki tombol calcelBtn. Kami harus menerapkan pembatalan permintaan. Operator takeUntil akan membantu di sini.

takeUntil diterjemahkan menjadi "take bye". Operator ini mengambil nilai dari aliran eksternal dan memberikannya lebih jauh ke bawah rantai. Selama utas internal ada dan tidak menghasilkan apa-apa. Segera setelah utas internal menghasilkan nilai, takeUntil akan memanggil metode berhenti berlangganan dan berhenti berlangganan dari utas eksternal.

Sebelum menambahkan operator, kita perlu menentukan aliran mana yang ingin kita hentikan berlangganan. Kami tertarik untuk mengunggah, karena hanya perlu menyelesaikan unggahan file, mis. Berhenti berlangganan dari utas internal:

fromEvent(input, 'change').pipe(
    switchMap(() => fromEvent(uploadBtn, 'click')),
    map(() => input.files[0]),
    filter(file => !!file),
    map(file => createFormData(file)),
    switchMap(data => upload(data).pipe(
        //    upload
        takeUntil(fromEvent(cancelBtn, 'click'))
    ))
).subscribe({
    next: () => console.log('File uploaded')
});

Bilah kemajuan


Tetap menambahkan bilah kemajuan. Untuk melacak kemajuan, kita perlu mendefinisikan metode xhr.upload.onprogress. Metode ini dipanggil saat peristiwa ProgressEvent terjadi. Objek acara berisi beberapa properti yang berguna bagi kami:

  • lengthComputable - jika benar, maka kita tahu ukuran file lengkap (dalam kasus kami, selalu benar)
  • total - jumlah total byte
  • dimuat - jumlah byte yang dikirim

Buat perubahan pada fungsi unggah:

function upload(data) {
    return new Observable(observer => {
        const xhr = new XMLHttpRequest();

        xhr.upload.onprogress = e => {
            //  
            const progress = e.loaded / e.total * 100;
            observer.next(progress);
        };

        xhr.onerror = e => observer.error(e);
        xhr.onload = () => observer.complete();

        xhr.open('POST', '/upload', true);
        xhr.send(data);

        return () => xhr.abort();
    });
}

Sekarang unggah mengeluarkan status unggahan ke dalam aliran. Tetap menulis fungsi yang akan mengubah properti style dari elemen progressBar:

function setProgressBarWidth(width) {
    progressBar.style.width = `${width}%`;
}

fromEvent(input, 'change').pipe(
   /* ..
   
   */
).subscribe({
    next: width => setProgressBarWidth(width)
});


Kiat cepat: agar file Anda tidak diunggah secara lokal dengan begitu cepat, aktifkan pengaturan "3G Cepat" atau "Lambat 3G" di tab "Kinerja" di Chrome devtools.

Mengingatkan


Kami mendapat aplikasi yang berfungsi penuh. Masih menambahkan beberapa stroke. Sekarang, ketika Anda mengklik tombol unggah, kami membatalkan unggahan sebelumnya dan memulai yang baru. Tapi kami sudah memiliki tombol batal.

Saya ingin tombol unggah Btn tidak menanggapi klik berikutnya sampai kami mengunggah file (atau sampai kami membatalkan unggahan). Apa yang bisa dilakukan?

Anda dapat menggantung atribut penonaktifan hingga proses pengunggahan selesai. Tetapi ada opsi lain - operator exhaustMap. Pernyataan ini akan mengabaikan nilai baru dari utas eksternal hingga utas internal selesai. Ganti switchMap dengan exhaustMap:

exhaustMap(data => upload(data).pipe(
  takeUntil(fromEvent(cancelBtn, 'click'))
))

Dan sekarang kita dapat menganggap aplikasi kita lengkap. Sedikit refactoring dan dapatkan versi final:

import { fromEvent, Observable } from "rxjs";
import { map, switchMap, filter, takeUntil, exhaustMap } from "rxjs/operators";

const input = document.querySelector('#file');
const uploadBtn = document.querySelector('#upload');
const progressBar = document.querySelector('#progress-bar');
const cancelBtn = document.querySelector('#cancel');

const fromUploadBtn = fromEvent(uploadBtn, 'click');
const fromCancelBtn = fromEvent(cancelBtn, 'click');

fromEvent(input, 'change').pipe(
    switchMap(() => fromUploadBtn),
    map(() => input.files[0]),
    filter(file => !!file),
    map(file => createFormData(file)),
    exhaustMap(data => upload(data).pipe(
        takeUntil(fromCancelBtn)
    ))
).subscribe({
    next: width => setProgressBarWidth(width)
});

function setProgressBarWidth(width) {
    progressBar.style.width = `${width}%`;
}

function createFormData(file) {
    const form = new FormData();
    form.append('file', file);
    return form;
}

function upload(data) {
    return new Observable(observer => {
        const xhr = new XMLHttpRequest();

        xhr.upload.onprogress = e => {
            const progress = e.loaded / e.total * 100;
            observer.next(progress);
        };

        xhr.onerror = e => observer.error(e);
        xhr.onload = () => observer.complete();

        xhr.open('POST', '/upload', true);
        xhr.send(data);

        return () => xhr.abort();
    });
}

Saya memposting versi saya di sini .

Sudut dan HttpClient


Jika Anda bekerja dengan Angular, maka Anda tidak perlu menggunakan xhr secara langsung. Angular memiliki layanan HttpClient. Layanan ini dapat melacak kemajuan pemuatan / pembongkaran, karena ini sudah cukup untuk melewatkan parameter berikut ke metode posting:

  • reportProgress: true - dapatkan informasi unggah / unduh
  • mengamati: "acara" - menunjukkan bahwa kami ingin menerima HttpEvents dari aliran

Inilah yang akan terlihat seperti metode pengunggahan di Angular:

export class UploaderService {
  constructor(private http: HttpClient) { }

  public upload(data: FormData): Observable<number> {
    return this.http.post('/upload', data, { reportProgress: true, observe: 'events' })
      .pipe(
        filter(event => event.type === HttpEventType.UploadProgress),
        map(event => event as HttpProgressEvent),
        map(event => event.loaded / event.total * 100)
      );
  }
}

Pernyataan filter menyaring hanya mengunggah acara. Acara lain tidak menarik bagi kami. Selanjutnya kami membawa acara tersebut ke HttpProgressEvent untuk mengakses properti yang dimuat dan total. Kami mempertimbangkan persentasenya.

HttpClient hanyalah pembungkus lebih dari xhr yang menyelamatkan kita dari boilerplate dan membuat bekerja dengan HTTP lebih mudah.

Contoh aplikasi pada Angular dapat ditemukan di sini .

Kesimpulan


RxJS adalah alat yang sangat kuat di tangan pengembang. Dalam gudang senjatanya, ada banyak operator untuk semua kesempatan. Sayangnya, karena ini, ambang untuk memasuki teknologi ini cukup tinggi. Dan seringkali, orang tanpa sadar mulai menulis "sepeda" mereka, yang membuat kode sulit dipertahankan.

Oleh karena itu, saya ingin berharap semua pembaca tidak diam dan tidak takut untuk bereksperimen. Pelajari RxJS. Tiba-tiba, Anda menemukan operator yang dapat mengubah 10 baris kode menjadi satu. Atau itu membantu membuat kode sedikit lebih jelas.

Semoga berhasil

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


All Articles