Faz muito tempo desde que escrevi meu último artigo sobre o básico do RxJS. Nos comentários, me pediram para mostrar exemplos mais complexos que podem ser úteis na prática. Decidi diluir um pouco a teoria e hoje falaremos sobre o upload de arquivos.O que nós fazemos?- Escreveremos uma pequena página na qual o usuário poderá selecionar um arquivo para enviá-lo ao servidor
- Adicione uma barra de progresso para que o progresso do upload do arquivo seja exibido.
- Adicione a capacidade de cancelar o download clicando no botão Cancelar
Para entender este artigo, você precisará de conhecimentos básicos sobre RxJS. O que são observáveis , operadores e operadores HOO
Não puxaremos o gato pela cauda e começaremos imediatamente aos negócios!Treinamento
Primeiro, precisamos de um servidor que possa aceitar solicitações de download de arquivos. Qualquer servidor pode ser adequado para isso, usarei o node.js em conjunto com express e multer para o artigo :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);
Agora crie uma página html na qual colocaremos todos os elementos necessários:<!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>
Agora, na página, temos 4 elementos com os quais o usuário irá interagir:- Entrada do tipo arquivo para que o usuário possa selecionar o arquivo a ser carregado
- Carregar botão, quando clicado, começaremos a carregar
- Botão Cancelar que cancelará o download
- Barra de progresso com uma largura inicial de 0. Durante o processo de upload, alteraremos sua largura
No final da tag body, adicionei um link ao script index.js, que também será necessário criar:
const input = document.querySelector('#file');
const uploadBtn = document.querySelector('#upload');
const progressBar = document.querySelector('#progress-bar');
const cancelBtn = document.querySelector('#cancel');
Deve ser algo como isto:
Tudo é um fluxo
Para informar ao navegador qual arquivo selecionar, o usuário deve clicar no botão "Escolher arquivo". Depois disso, a caixa de diálogo do seu sistema operacional será aberta, onde a árvore de pastas será exibida. Após selecionar um arquivo, o navegador fará o download de todas as informações necessárias.Como entendemos que um usuário selecionou um arquivo? Há um evento de "mudança" para isso. Depois que o evento é disparado, podemos voltar para a matriz de arquivos na entrada, onde os dados do arquivo serão gravados.Como ouvimos o evento de mudança? Você pode usar o método addEventListener e trabalhar com ele. Mas trabalhamos com o RxJS, onde qualquer evento pode ser representado como um fluxo:fromEvent(input, 'change').pipe(
map(() => input.files[0])
).subscribe({
next: data => console.log(file)
});
Adicione a função de upload, que fará o upload do arquivo no servidor. Por enquanto, deixe seu corpo vazio:function upload(file) {
console.log(file);
}
A função de upload deve ser chamada após clicar no botão uploadBtn:fromEvent(uploadBtn, 'click').subscribe({
next: () => upload(input.files[0])
});
Mesclar fluxos
Agora, nosso código não é diferente do que escreveríamos usando addEventListener. Sim, funciona, mas se deixarmos assim, perderemos as vantagens que o RxJS nos oferece.O que podemos fazer? Vamos descrever a sequência de etapas para o upload de um arquivo:- Seleção de arquivo
- Clique em uploadBtn
- Extrair arquivo de input.files
- Upload de arquivo
Agora transferimos essa sequência para o código. Mas como combinar fluxos de entrada e uploadBtn? O operador switchMap nos ajudará com isso, o que nos permite projetar um encadeamento em outro:fromEvent(input, 'change').pipe(
switchMap(() => fromEvent(uploadBtn, 'click')),
map(() => input.files[0])
).subscribe({
next: file => upload(file)
});
Este código é muito semelhante à sequência de instruções que descrevemos acima. O usuário seleciona um arquivo, o switchMap é acionado e nós assinamos o uploadBtn. Mas então nada vai acontecer.O switchMap permite apenas que os valores gerados pelo fromEvent (uploadBtn, 'clique') sejam transmitidos para o fluxo externo.Para iniciar o upload de arquivos, é necessário executar a segunda instrução, a saber, clicar em uploadBtn. Em seguida, elaborará o método de mapa, que extrairá o arquivo da matriz, e o método de upload já será chamado na assinatura.A coisa mais interessante aqui é que a sequência de instruções não está quebrada. Para que a função de upload funcione, o evento 'change' deve ser acionado antes.Mas, ainda assim, um problema permaneceu. O usuário pode selecionar um arquivo e desmarcá-lo. E então, quando tentamos fazer upload do arquivo, passamos a função de upload - indefinida. Para evitar essa situação, devemos adicionar uma verificação:fromEvent(input, 'change').pipe(
switchMap(() => fromEvent(uploadBtn, 'click')),
map(() => input.files[0]),
filter(file => !!file)
).subscribe({
next: file => upload(file)
});
Trabalhando com xhr
É hora de implementar o mais difícil - o processo de descarregamento. Vou mostrá-lo em um exemplo de trabalho com xhr, já que o fetch, no momento da redação, não sabe como acompanhar o progresso do upload .Você pode implementar o descarregamento usando qualquer outra biblioteca, por exemplo, axios ou jQuery.ajax.
Como uso a multer no lado do servidor, terei que transferir o arquivo dentro do formulário (a multer aceita dados apenas neste formato). Para isso, escrevi a função createFormData:function createFormData(file) {
const form = new FormData();
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)
});
Vamos descarregar o formulário através do XMLHttpRequest. Precisamos criar uma instância desse objeto e definir métodos de descarga e onerror nele. O primeiro será acionado quando o upload for concluído, o segundo quando o erro ocorrer.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);
}
Agora temos um exemplo de trabalho. Mas contém algumas desvantagens:- Não há como cancelar o download
- Se você clicar no botão uploadBtn n vezes, teremos n conexões paralelas para enviar um arquivo
Isso ocorre porque a função de upload funciona fora do fluxo. Ela mora sozinha. Precisamos corrigi-lo. Vamos fazer com que a função retorne o Observável para nós. Então podemos controlar o upload de arquivos: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();
});
}
Observe a função de seta retornada dentro do observável. Este método será chamado no momento de cancelar a inscrição e cancelar o upload.Coloque a chamada de upload no 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')
});
Agora, se o usuário clicar no botão de upload novamente, a solicitação anterior será cancelada, mas uma nova será criada.Cancelar uma solicitação de clique
Ainda temos o botão calcelBtn. Devemos implementar o cancelamento da solicitação. O operador takeUntil ajudará aqui.takeUntil é traduzido como "tchau". Esse operador pega os valores do fluxo externo e os fornece mais adiante na cadeia. Enquanto o encadeamento interno existir e não gerar nada. Assim que o encadeamento interno gerar um valor, o takeUntil chamará o método de cancelamento de assinatura e cancelará a assinatura do thread externo.Antes de adicionar um operador, precisamos determinar de qual fluxo queremos cancelar a inscrição. Estamos interessados no upload, pois é necessário apenas concluir o upload do arquivo, ou seja, Cancelar inscrição do thread interno:fromEvent(input, 'change').pipe(
switchMap(() => fromEvent(uploadBtn, 'click')),
map(() => input.files[0]),
filter(file => !!file),
map(file => createFormData(file)),
switchMap(data => upload(data).pipe(
takeUntil(fromEvent(cancelBtn, 'click'))
))
).subscribe({
next: () => console.log('File uploaded')
});
Barra de progresso
Resta adicionar a barra de progresso. Para acompanhar o progresso, precisamos definir o método xhr.upload.onprogress. Este método é chamado quando o evento ProgressEvent ocorre. O objeto de evento contém várias propriedades que são úteis para nós:- lengthComputable - se true, sabemos o tamanho total do arquivo (no nosso caso, sempre true)
- total - número total de bytes
- carregado - o número de bytes enviados
Faça alterações na função de upload: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();
});
}
Agora, o upload cospe o status do upload no fluxo. Resta apenas escrever uma função que altere as propriedades de estilo do elemento progressBar:function setProgressBarWidth(width) {
progressBar.style.width = `${width}%`;
}
fromEvent(input, 'change').pipe(
).subscribe({
next: width => setProgressBarWidth(width)
});

Uma dica rápida: para que seus arquivos não sejam carregados localmente com tanta rapidez, ative a configuração "3G Rápido" ou "3G lento" na guia "Desempenho" nas ferramentas do Chrome.
Trazer à mente
Temos um aplicativo de trabalho completo. Resta adicionar alguns traços. Agora, quando você clica no botão uploadBtn, cancelamos o upload anterior e iniciamos um novo. Mas já temos um botão de cancelamento.Desejo que o botão uploadBtn não responda aos cliques subseqüentes até o upload do arquivo (ou até o cancelamento do upload). O que pode ser feito?Você pode travar o atributo desativar até que o processo de upload seja concluído. Mas há outra opção - o operador escapeMap. Esta instrução ignorará novos valores do encadeamento externo até que o encadeamento interno seja concluído. Substitua switchMap por escapeMap:exhaustMap(data => upload(data).pipe(
takeUntil(fromEvent(cancelBtn, 'click'))
))
E agora podemos considerar nossa inscrição completa. Um pouco de refatoração e obtenha a versão 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();
});
}
Eu postei minha versão aqui .Angular e HttpClient
Se você trabalha com Angular, não precisa usar o xhr diretamente. Angular tem um serviço HttpClient. Este serviço pode acompanhar o progresso do carregamento / descarregamento, para isso basta passar os seguintes parâmetros para o método post:- reportProgress: true - obtenha informações de upload / download
- observe: "eventos" - indicam que queremos receber HttpEvents do fluxo
Veja como será o método de upload no 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)
);
}
}
A instrução de filtro filtra apenas os eventos de upload. Outros eventos não nos interessam. Além disso, trazemos o evento para o HttpProgressEvent para acessar as propriedades carregadas e totais. Consideramos a porcentagem.O HttpClient é apenas um invólucro no xhr que nos salva de um clichê e facilita o trabalho com o HTTP.Um exemplo de aplicativo no Angular pode ser encontrado aqui .Conclusão
O RxJS é uma ferramenta muito poderosa nas mãos do desenvolvedor. Em seu arsenal, há um enorme conjunto de operadores para todas as ocasiões. Infelizmente, por causa disso, o limiar de entrada nessa tecnologia é bastante alto. E, muitas vezes, as pessoas, sem saber, começam a escrever suas "bicicletas", o que dificulta a manutenção do código.Portanto, gostaria de desejar a todos os leitores que não fiquem parados e não tenham medo de experimentar. Aprenda RxJS. De repente, você encontra um operador que pode transformar 10 linhas de código em uma. Ou isso ajuda a tornar o código um pouco mais claro.Boa sorte