Setembro de 2016

Volume 31 – Número 9

Framework Reativo – Criar Páginas Web Assíncronas Habilitadas para AJAX com Extensões Reativas

Por Peter Vogel

Em um artigo anterior, discuti como o padrão de observador pode ser usado para gerenciar tarefas de longa duração (msdn.com/magazine/mt707526). Ao fim do artigo, mostrei como as Extensões Reativas da Microsoft (Rx) fornecem um mecanismo simples para o gerenciamento de uma sequência de eventos de um processo de longa duração em um aplicativo do Windows.

No entanto, o uso de Rx apenas para monitorar uma sequência de eventos de uma tarefa de longa duração não tira o máximo proveito da tecnologia. A vantagem do uso de Rx é que é possível integrar de forma assíncrona qualquer processo baseado em eventos a qualquer outro processo. Neste artigo, por exemplo, vou usar Rx para fazer chamadas assíncronas para um serviço Web por meio do clique de botão em uma página da Web (o clique de um botão é, de fato, uma sequência de um evento). Para usar Rx no ambiente Web do lado do cliente, usarei Rx para JavaScript (RxJS).

O recurso Rx fornece um modo padrão para abstrair diversos cenários e manipulá-los usando uma interface fluente semelhante ao LINQ, que permite compor aplicativos com base em blocos de construção mais simples. Rx permite integrar eventos da interface do usuário ao processamento de back-end e, ao mesmo tempo, mantê-los separados. Com Rx, você pode reescrever a interface do usuário sem ter que fazer alterações correspondentes no back-end (e vice-versa).

O RxJS também dá suporte a uma separação clara entre o HTML e seu código, fornecendo de fato vinculação de dados sem exigir marcação HTML especial. O RxJS também se baseia em tecnologias de cliente existentes (jQuery, por exemplo). O recurso Rx tem outro benefício: todas as implementações Rx costumam parecer muito semelhantes. O código RxJS neste artigo é muito parecido com o código do Microsoft .NET Framework que escrevi em meu artigo anterior. Você pode aproveitar em qualquer ambiente Rx as habilidades que desenvolveu em um ambiente de Rx específico.

Introdução ao RxJS

Para alcançar seus objetivos, o RxJS abstrai as partes de um aplicativo em dois grupos. Os membros do primeiro grupo são observáveis: basicamente, qualquer item que dispara um evento. O RxJS fornece um conjunto avançado de operadores para a criação de observáveis. Os observáveis incluem qualquer item que pode ser definido para parecer disparar um evento (o Rx pode converter matrizes em origens de eventos, por exemplo). Os operadores de Rx também permitem filtrar e transformar a saída de eventos. Pode ser útil considerar um observável como um pipeline de processamento para uma origem de evento.

Os membros do segundo grupo são os observadores, que aceitam resultados dos observáveis e fornecem processamento para três notificações de um observável: um novo evento (com os dados associados), um erro ou uma sequência de fim de evento. No RxJS, um observador pode ser um objeto com funções para lidar com uma ou mais das três notificações ou apenas um conjunto de funções, uma para cada notificação.

Para vincular esses dois grupos, um observável inscreve um ou mais observadores em seu pipeline.

Você pode adicionar o RxJS a seu projeto por meio do NuGet (na biblioteca do NuGet, procure RxJS-All). Apenas um aviso: Quando adicionei o RxJS pela primeira vez a um projeto configurado com TypeScript, o NuGet Manager perguntou se eu também queria os arquivos de definição de TypeScript relevantes. Quando cliquei em Sim, os arquivos foram adicionados e recebi cerca de 400 erros de "definição duplicada". Deixei de aceitar essa opção.

Além disso, há várias bibliotecas de suporte de Rx que fornecem plug-ins úteis para RxJS. Por exemplo, RxJS-DOM (disponível por meio do NuGet como RxJS-Bridges-HTML) fornece integração a eventos no DOM do cliente e a jQuery. Essa biblioteca é essencial para a criação de aplicativos da Web dinâmicos com RxJS.

Assim como a maioria dos plug-ins JavaScript para Rx, RxJS-DOM vincula seus novos recursos ao objeto Rx que é central para a biblioteca do RxJS. RxJS-DOM adiciona uma propriedade DOM ao objeto Rx que tem várias funções úteis, inclusive várias que fazem a associação com funções semelhantes a jQuery. Por exemplo, para fazer uma chamada AJAX usando RxJS-DOM para recuperar objetos JSON, você usaria este código:

return Rx.DOM.getJSON("...url...")

Para usar Rx e RxJS-DOM, bastam estas duas marcas de script:

<script src="~/Scripts/rx.all.js"></script>
<script src="~/Scripts/rx.dom.js"></script>

Usar rx.all.js e rx.dom.js é uma solução excessiva, pois eles incluem toda a funcionalidade de ambas as bibliotecas. Felizmente, a funcionalidade de Rx e RxJS-DOM está dividida em várias bibliotecas mais leves. Assim, você pode incorporar apenas as bibliotecas que contêm a funcionalidade de que precisa para sua página (para qualquer operador, a documentação no GitHub indicará a qual biblioteca o operador pertence).

Integrar um Serviço RESTful

Um cenário típico em um aplicativo JavaScript é quando o usuário clica em um botão para recuperar um resultado de um serviço Web, fazendo uma chamada assíncrona ao serviço. A primeira etapa para criar uma solução RxJS é converter o evento de clique no botão em um observável, o que a integração de RxJS/DOM/jQuery facilita. Por exemplo, este código usa jQuery para recuperar uma referência a um elemento no formulário com a id getButton e cria um observável com base no evento de clique desse elemento:

var getCust = $('#getButton').get(0);
var getCustObsvble = Rx.DOM.fromEvent(getCust, "click");

A função fromEvent permite criar um observável com base em qualquer evento de qualquer elemento. No entanto, RxJS contém vários atalhos para eventos mais "populares", inclusive o evento de clique. Por exemplo, eu também poderia ter usado este código para criar um observável com base no evento de clique de um botão:

var getCustObsvble = Rx.DOM.click(getCust);

Posso criar um pipeline de processamento mais avançado para este evento que simplifique o processamento no aplicativo. Por exemplo, eu poderia evitar um cenário em que o usuário clica no botão duas vezes em sucessão rápida (muito rapidamente, por exemplo, para que eu desabilite o botão e impeça que o usuário faça isso). Em vez de escrever código de tempo limite, lido com isso adicionando a função debounce ao pipeline, especificando que só quero ver cliques com pelo menos dois segundos entre eles:

var getCustObsvble = Rx.DOM.click(getCust).debounce(2000);

Também posso executar processamento no evento usando flatMapLatest, , por exemplo. Isso permite incorporar uma função de transformação ao pipeline (algo como LINQ SelectMany). A função base (flatMap) executa uma transformação em cada evento em uma sequência. A função flatMapLatest vai um passo além e lida com um problema típico do processamento assíncrono: flatMapLatest cancela o processamento assíncrono anterior se a transformação é invocada uma segunda vez e uma solicitação assíncrona ainda está pendente.

Com flatMapLatest, se um usuário clicar no botão duas vezes (e com mais de dois segundos de diferença), se flatMapLatest ainda estiver transformando o evento anterior, flatMapLatest cancelará o evento anterior. Isso elimina a necessidade de escrever código para lidar com o cancelamento de processamento assíncrono.

A primeira etapa para usar flatMapLatest é criar a função de transformação. Para fazer isso, incluo em uma função a chamada getJSON mostrada anteriormente. Como a função está vinculada a um clique de botão, não preciso de dados da página (de fato, a função mal se qualifica como uma "transformação", pois ignora as entradas do evento).

Aqui está uma função que faz uma solicitação a um serviço de API Web, usando jQuery para recuperar a Id do cliente de um elemento na página:

function getCustomer()
{
  return Rx.DOM.getJSON("api/customerorders/customerbyid/"
    + $("custId").val());
}

Com RxJS, não é preciso fornecer retornos de chamada para a solicitação. Os resultados da chamada são passados automaticamente através dos observáveis para os observadores.

Para integrar a transformação à cadeia de processamento, chamo flatMapLatest do observador, passando uma referência para a função:

var getCustObsvble = Rx.DOM.click(getCust).debounce(2000).flatMapLatest(getCustomer);

Agora devo inscrever funções de processamento no observável. Posso atribuir até três funções na inscrição: uma função para lidar com a notificação quando um novo evento é recebido, uma para relatar notificações de erro e uma para lidar com uma notificação enviada ao fim da sequência de eventos. Como o pipeline de processamento de evento de clique é projetado especificamente para produzir uma sequência de um evento, não preciso fornecer uma função de "fim de sequência".

O código resultante para atribuir as duas funções necessárias pode ter esta aparência:

var getCustObsvble.subscribe(
  c   => $("#custName").val(c.FirstName),
  err => $("Message").text("Unable to retrieve Customer: "
    + err.description)
);

Se achasse que esse conjunto de funções poderia ser útil em outros lugares (ou apenas para simplificar o código de inscrição), eu poderia criar um objeto observador. Um objeto observador tem as mesmas funções que usei antes, atribuídas a propriedades nomeadas como OnNext, onError e onComplete. Como não preciso lidar com onComplete, o objeto observador seria assim:

var custObservr = {
  onNext:  c   => $("#custName").val(c.FirstName),
  onError: err => $("#Message").text("Unable to retrieve Customer: "
     + err.description)
};

Com um observador independente, o código que o observável usa para inscrever o observador se torna mais simples:

var getCustObsvble.subscribe(custObservr);

Reunindo tudo, só preciso de uma função ready para a página que recupera o botão, anexa o pipeline que transforma o botão em um observável muito sofisticado e inscreve um observador no pipeline. Como RxJS implementa uma interface fluente, eu poderia fazer isso com apenas duas linhas de código: uma linha de código jQuery para recuperar o elemento de botão e uma linha de código RxJS para criar o pipeline e inscrever o observador. No entanto, dividir a criação do pipeline RxJS e do código de inscrição em duas linhas tornará a função ready mais fácil de ler para o próximo programador.

A versão fim da função ready seria semelhante a:

$(function () {
  var getCust = $('#getButton').get(0);
  var getCustObsvble =
    Rx.DOM.click(getCust).debounce(2000).flatMapLatest(getCustomer);
  getCustObsvble.subscribe(custObservr);
});

Como bônus, o processo que integra o evento de clique, a chamada ao serviço Web e a exibição de dados é executado de forma assíncrona. Com RxJS, evito todos os detalhes desagradáveis para fazer com que isso funcione.

Abstrair sequências de eventos

Abstraindo o processo (e fornecendo um conjunto avançado de operadores), Rx faz com que muitas coisas fiquem parecidas. Isso permite fazer alterações significativas no processo, sem ter que fazer alterações estruturais significativas no código.

Por exemplo, como RxJS-DOM trata um observável com um evento (um clique de botão) de maneira bastante semelhante a um observável que gera uma sequência contínua de eventos (como mousemove), posso fazer uma alteração significativa na interface do usuário sem ter que fazer muitas alterações no código. Por exemplo, eu poderia decidir que, em vez de o usuário disparar a solicitação de serviço Web com um clique de botão, vou recuperar os dados do cliente assim que o usuário digitar a Id do cliente.

A primeira coisa que precisa mudar é o elemento cujos eventos vou observar. Neste caso, isso significa mudar do botão na página para a caixa de texto na página que contém a Id do cliente (também mudarei o nome da variável que contém o elemento):

var getCustId = $('#custId').get(0);

Eu poderia transformar qualquer quantidade de eventos para a caixa de texto em um observável. Por exemplo, se eu usasse o evento de desfoque na caixa de texto, faria a chamada ao serviço Web quando o usuário saísse da caixa de texto. No entanto, quero que tudo funcione de forma mais dinâmica. Opto por recuperar o objeto de cliente assim que o usuário digita caracteres "suficientes" na caixa de texto. Isso significa mudar para o evento keyup, o que gera uma sequência de eventos: um para cada pressionamento de tecla.

Essa alteração no pipeline tem esta aparência:

var getCustObsvble = Rx.DOM.keyup(getCustId)

À medida que o usuário digita caracteres, a função de transformação poderia acabar sendo chamada várias vezes. De fato, se o usuário digitar mais rápido do que consigo recuperar objetos de Cliente, acabarei tendo várias solicitações empilhadas. Eu poderia usar flatMapLatest para limpar as solicitações, mas há uma solução melhor: Em meu sistema de Gerenciamento de Pedidos de Clientes, a Id do cliente sempre tem quatro caracteres. Como resultado, não há motivo para chamar a função de transformação, exceto quando há exatamente quatro caracteres na caixa de texto (e, só para constar, como o pipeline agora está obtendo a Id do cliente e retornando um objeto de Cliente completo, a função de transformação agora está realmente fazendo uma "transformação").

Para implementar essa condição, basta adicionar a função de filtro de Rx ao pipeline. A função de filtro funciona como uma cláusula LINQ Where: Deve ser passada por outra função (a função selector) que contém um teste e retorna um valor Booliano com base nos dados associados ao último evento. Apenas os eventos que passarem no teste serão passados ao observador inscrito. A função de filtro receberá automaticamente o objeto de evento mais recente na sequência. Assim, no teste da função selector, posso usar a propriedade de destino do objeto de evento para recuperar o valor atual da caixa de texto e verificar o comprimento desse valor.

Mais uma alteração no pipeline: Removerei a função debounce, pois é difícil ver qual vantagem ela fornece nessa nova interface do usuário. Ainda assim, após uma alteração significativa nas interações da interface do usuário, o código revisado que cria o observável e inscreve os observadores ainda é estruturalmente idêntico à versão anterior:

var getCustObsvble = Rx.DOM.keyup(getCustId)
                       .filter(e => e.target.value.length == 4)
                       .flatMapLatest(getCustomer);
getCustObsvble.subscribe(custObservr);

E, claro, não tenho que fazer alterações nas funções custObservr: Elas estão isoladas das alterações na interface do usuário.

Posso fazer mais uma alteração opcional, desta vez na função de transformação. Sempre são passados três parâmetros para a função de transformação. Eu os ignorava quando a origem do evento era um botão. O primeiro parâmetro passado para a função de transformação flatMapLatest é o objeto do evento que está sendo observado. Posso aproveitar esse objeto de evento para eliminar o código jQuery que recuperou a Id do cliente e, em vez disso, recuperar o valor da caixa de texto do objeto de evento. Essa alteração torna o código menos vinculado, pois a função de transformação não está mais ligada a um elemento específico na página.

A nova função de transformação é semelhante a:

function getCustomer(e)
{
  return Rx.DOM.getJSON("api/customerorders/customerbyid/"
    + e.target.value);
}

Essa é a vantagem da abstração Rx: para mudar de um elemento que produz um único evento para um elemento que produz uma sequência de eventos, basta alterar uma única linha de código que compõe o pipeline (e tenho a oportunidade de melhorar a função de transformação). De todas as alterações que fiz, a que tem maior probabilidade de causar problemas é renomear a variável que contém o elemento de entrada. Essa alteração também enfatiza que, assim como no LINQ, o segredo para o sucesso com Rx é familiarizar-se com os operadores disponíveis.

Abstrair chamadas de serviços Web

Embora a abstração de Rx faça com que as alterações na interface do usuário sejam muito parecidas, uma boa abstração também deve fazer o mesmo no processamento de back-end. Por exemplo, e se a página for revisada para recuperar não só dados do cliente, mas também seus pedidos de venda? Para tornar tudo mais interessante, vou pressupor que não haja uma propriedade de navegação para recuperar os pedidos de vendas como parte do objeto do cliente, e terei que fazer uma segunda chamada.

Com Rx, primeiro preciso criar uma segunda função de transformação que converte uma Id do cliente em um conjunto de solicitações de clientes:

function getCustomerOrders(e) {
  return Rx.DOM.getJSON("customerorders/ordersbycustomerid/"
    + e.target.value);
}

Em seguida, preciso criar um segundo observável que usa essa transformação (o código é muito parecido com o código que cria o observável de cliente):

var getOrdersObsvble = Rx.DOM.keyup(getCustId)
       .filter(e => e.target.value.length == 4)
       .flatMapLatest(getCustomerOrders);

Nesse ponto, pode-se esperar que a combinação e a coordenação dos observáveis exija muito trabalho. Porém, como Rx faz com que todos os observáveis sejam muito parecidos, é possível combinar a saída de vários observáveis em uma única sequência, que pode ser manuseada por um único observador (que é relativamente simples).

Por exemplo, se houvesse vários observáveis para recuperar pedidos de várias origens, eu poderia usar a função de mesclagem de Rx para reunir todos os pedidos em uma única sequência, na qual inscreveria um único observador. Por exemplo, o código a seguir reúne observáveis que recuperam solicitações atuais (getCurrentOrdersObsvble) e pedidos postados (getPostedOrdersObsvle). Em seguida, ele passa a sequência resultante para um único observador, chamado allOrdersObservr:

Rx.Observable.merge(getCurrentOrdersObsvble, getPostedOrdersObsvble)
             .subscribe(allOrdersObservr);

Em meu caso, quero fazer algo mais interessante: combinar um observável que recupera um objeto do cliente a um observável que recupera todos os pedidos desse cliente. Felizmente, RxJS tem um operador para isso: combineLatest. O operador combineLatest aceita dois observáveis e uma função de processamento. A função de processamento que você passa para combineLatest recebe o último resultado de cada sequência e permite especificar como os resultados devem ser reunidos. Em meu caso, combineLatest fornece mais funcionalidade do que preciso, pois tenho apenas um resultado de cada observável: o objeto do cliente de um observável e todos os pedidos do cliente do outro.

No código a seguir, adiciono uma nova propriedade (chamada Orders) ao objeto do cliente do primeiro observável e incluo o resultado do segundo observável nessa propriedade. Finalmente, posso inscrever um observador chamado CustOrdersObsrvr para lidar com o novo objeto de Cliente+Pedidos:

Rx.Observable.combineLatest(getCustObsvble, getOrdersObsvble,
                           (c, ords) => {c.Orders = ord; return c;})
             .subscribe(CustOrdersObsrvr);

Agora custOrdersObservr pode trabalhar com o novo objeto que criei:

var custOrdersObservr = {
  onNext: co => {
    // ...Code to update the page with customer and orders data...               
  },
  onError: err => $("#Message").text("Unable to retrieve Customer and Orders: "
    + err.description)
};

Conclusão

Abstraindo os componentes de um aplicativo como apenas dois tipos de objetos (observáveis e observadores) e fornecendo um conjunto avançado de operadores para gerenciar e transformar os resultados de observáveis, RxJS fornece uma maneira altamente flexível (e sustentável) de criar aplicativos Web.

Naturalmente, com o uso de uma biblioteca poderosa que abstrai processos complexos em código simples, sempre há um risco: quando o aplicativo faz algo inesperado, a depuração de um problema pode ser um pesadelo, pois você não pode ver as linhas individuais de código que a abstração eliminou. Claro, você não tem mais que escrever todo esse código e (presumivelmente) a camada de abstração terá menos bugs do que o código que você tenha escrito. É a escolha de sempre entre potência e visibilidade.

Optando pelo RxJS, o benefício líquido é que você pode fazer alterações significativas no aplicativo sem ter que fazer grandes alterações no código.


Peter Vogel é arquiteto de sistemas e diretor da PH&V Information Services. A PH&V oferece consultoria de pilha completa, desde o design da UX usando a modelagem de objetos até o design de banco de dados. Entre em contato com ele pelo email peter.vogel@phvis.com.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Stephen Cleary, James McCaffrey e Dave Sexton
Stephen Cleary trabalha com multithreading e programação assíncrona há 16 anos e usa o suporte assíncrono no Microsoft .NET Framework desde a primeira visualização da tecnologia da comunidade. Ele é autor de "Concurrency in C# Cookbook" (O'Reilly Media, 2014). Seu site, incluindo seu blog, é stephencleary.com.

Entre em contato com o Dr. James McCaffrey trabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com o Dr. McCaffrey pelo email jammc@microsoft.com.