Obtenha seu Angular 2 On: Atualizando de 1.5
Publicados: 2022-03-11Comecei querendo escrever um guia passo a passo para atualizar um aplicativo do Angular 1.5 para o Angular 2, antes de ser educadamente informada pelo meu editor de que ela precisava de um artigo em vez de um romance. Depois de muita deliberação, aceitei que precisava começar com uma ampla pesquisa das mudanças no Angular 2, atingindo todos os pontos abordados no artigo de Jason Aden, Getting Past Hello World in Angular 2. …Opa. Vá em frente e leia-o para obter uma visão geral dos novos recursos do Angular 2, mas para uma abordagem prática, mantenha seu navegador aqui.
Eu quero que isso se torne uma série que eventualmente englobe todo o processo de atualização do nosso aplicativo de demonstração para Angular 2. Por enquanto, porém, vamos começar com um único serviço. Vamos dar um passeio sinuoso pelo código e responderei a quaisquer perguntas que você possa ter, como….
Angular: A Velha Maneira
Se você é como eu, o guia de início rápido do Angular 2 pode ter sido a primeira vez que você olhou para o TypeScript. Rapidamente, de acordo com seu próprio site, o TypeScript é “um superconjunto tipado de JavaScript que compila para JavaScript simples”. Você instala o transpilador (semelhante ao Babel ou Traceur) e acaba com uma linguagem mágica que suporta recursos de linguagem ES2015 e ES2016, bem como digitação forte.
Você pode achar reconfortante saber que nenhuma dessas configurações misteriosas é estritamente necessária. Não é muito difícil escrever código Angular 2 em JavaScript simples, embora eu não ache que valha a pena fazê-lo. É bom reconhecer um território familiar, mas muito do que há de novo e empolgante no Angular 2 é sua nova maneira de pensar, e não sua nova arquitetura.
Então vamos dar uma olhada neste serviço que eu atualizei de Angular 1.5 para 2.0.0-beta.17. É um serviço Angular 1.x bastante padrão, com apenas alguns recursos interessantes que tentei observar nos comentários. É um pouco mais complicado do que seu aplicativo de brinquedo padrão, mas tudo o que ele realmente faz é consultar o Zilyo, uma API disponível gratuitamente que agrega anúncios de locadoras como o Airbnb. Desculpe, é um pouco de código.
zilyo.service.js (1.5.5)
'use strict'; function zilyoService($http, $filter, $q) { // it's a singleton, so set up some instance and static variables in the same place var baseUrl = "https://zilyo.p.mashape.com/search"; var countUrl = "https://zilyo.p.mashape.com/count"; var state = { callbacks: {}, params: {} }; // interesting function - send the parameters to the server and ask // how many pages of results there will be, then process them in handleCount function get(params, callbacks) { // set up the state object if (params) { state.params = params; } if (callbacks) { state.callbacks = callbacks; } // get a count of the number of pages of search results return $http.get(countUrl + "?" + parameterize(state.params)) .then(extractData, handleError) .then(handleCount); } // make the factory return { get : get }; // boring function - takes an object of URL query params and stringifies them function parameterize(params) { return Object.keys(params).map(key => `${key}=${params[key]}`).join("&"); } // interesting function - takes the results of the "count" AJAX call and // spins off a call for each results page - notice the unpleasant imperativeness function handleCount(response) { var pages = response.data.result.totalPages; if (typeof state.callbacks.onCountResults === "function") { state.callbacks.onCountResults(response.data); } // request each page var requests = _.times(pages, function (i) { var params = Object.assign({}, { page : i + 1 }, state.params); return fetch(baseUrl, params); }); // and wrap all requests in a promise return $q.all(requests).then(function (response) { if (typeof state.callbacks.onCompleted === "function") { state.callbacks.onCompleted(response); } return response; }); } // interesting function - fetch an individual page of results // notice how a special callback is required because the $q.all wrapper // will only return once ALL pages have been fetched function fetch(url, params) { return $http.get(url + "?" + parameterize(params)).then(function(response) { if (typeof state.callbacks.onFetchPage == "function") { // emit each page as it arrives state.callbacks.onFetchPage(response.data); } return response.data; // took me 15 minutes to realize I needed this }, (response) => console.log(response)); } // boring function - takes the result object and makes sure it's defined function extractData(res) { return res || { }; } // boring function - log errors, provide teaser for greater ambitions function handleError (error) { // In a real world app, we might send the error to remote logging infrastructure var errMsg = error.message || 'Server error'; console.error(errMsg); // log to console instead return errMsg; } } // register the service angular.module('angularZilyoApp').factory('zilyoService', zilyoService);
A ruga neste aplicativo em particular é que ele mostra os resultados em um mapa. Outros serviços lidam com várias páginas de resultados implementando paginação ou scrollers lentos, o que lhes permite recuperar uma página de resultados por vez. No entanto, queremos mostrar todos os resultados na área de pesquisa e queremos que eles apareçam assim que retornarem do servidor, em vez de aparecerem repentinamente quando todas as páginas forem carregadas. Além disso, queremos exibir atualizações de progresso para o usuário para que ele tenha uma ideia do que está acontecendo.
Para conseguir isso no Angular 1.5, recorremos a callbacks. As promessas nos levam até lá, como você pode ver no wrapper $q.all
que aciona o retorno de chamada onCompleted
, mas as coisas ainda ficam bem confusas.
Em seguida, trazemos o lodash para criar todas as solicitações de página para nós, e cada solicitação é responsável por executar o retorno de chamada onFetchPage
para garantir que ele seja adicionado ao mapa assim que estiver disponível. Mas isso fica complicado. Como você pode ver pelos comentários, eu me perdi na minha própria lógica e não consegui entender o que estava sendo retornado para qual promessa e quando.
A limpeza geral do código sofre ainda mais (muito mais do que o estritamente necessário), porque uma vez que fico confuso, ele só desce a partir daí. Diga comigo, por favor...
Angular 2: uma nova maneira de pensar
Existe uma maneira melhor, e eu vou mostrar para você. Não vou gastar muito tempo nos conceitos do ES6 (também conhecido como ES2015), porque existem lugares muito melhores para aprender sobre essas coisas e, se você precisar de um ponto de partida, o ES6-Features.org tem uma boa visão geral de todos os novos recursos divertidos. Considere este código AngularJS 2 atualizado:
zilyo.service.ts (2.0.0-beta.17)
import {Injectable} from 'angular2/core'; import {Http, Response, Headers, RequestOptions} from 'angular2/http'; import {Observable} from 'rxjs/Observable'; import 'rxjs/Rx'; @Injectable() export class ZilyoService { constructor(private http: Http) {} private _searchUrl = "https://zilyo.p.mashape.com/search"; private _countUrl = "https://zilyo.p.mashape.com/count"; private parameterize(params: {}) { return Object.keys(params).map(key => `${key}=${params[key]}`).join("&"); } get(params: {}, onCountResults) { return this.http.get(this._countUrl, { search: this.parameterize(params) }) .map(this.extractData) .map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; }) .flatMap(results => Observable.range(1, results.totalPages)) .flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); }) .map(this.extractData) .catch(this.handleError); } private extractData(res: Response) { if (res.status < 200 || res.status >= 300) { throw new Error('Bad response status: ' + res.status); } let body = res.json(); return body.result || { }; } private handleError (error: any) { // In a real world app, we might send the error to remote logging infrastructure let errMsg = error.message || 'Server error'; console.error(errMsg); // log to console instead return Observable.throw(errMsg); } }
Frio! Vamos percorrer esta linha por linha. Novamente, o transpilador TypeScript nos permite usar quaisquer recursos do ES6 que desejarmos, pois ele converte tudo em JavaScript vanilla.
As instruções de import
no início estão simplesmente usando o ES6 para carregar os módulos que precisamos. Como faço a maior parte do meu desenvolvimento em ES5 (também conhecido como JavaScript regular), devo admitir que é um pouco chato precisar de repente começar a listar todos os objetos que pretendo usar.
No entanto, lembre-se de que o TypeScript está transpilando tudo para JavaScript e está usando secretamente o SystemJS para lidar com o carregamento do módulo. As dependências estão todas sendo carregadas de forma assíncrona e é (supostamente) capaz de agrupar seu código de uma maneira que remove símbolos que você não importou. Além disso, tudo isso suporta “minificação agressiva”, o que soa muito doloroso. Essas declarações de importação são um pequeno preço a pagar para evitar lidar com todo esse barulho.

De qualquer forma, além de carregar recursos seletivos do próprio Angular 2, observe a linha import {Observable} from 'rxjs/Observable';
. O RxJS é uma biblioteca de programação reativa alucinante e muito legal que fornece algumas das infraestruturas subjacentes ao Angular 2. Definitivamente, teremos notícias dele mais tarde.
Agora chegamos a @Injectable()
.
Ainda não tenho certeza do que isso faz para ser honesto, mas a beleza da programação declarativa é que nem sempre precisamos entender os detalhes. Ele é chamado de decorador, que é uma construção sofisticada do TypeScript capaz de aplicar propriedades à classe (ou outro objeto) que o segue. Neste caso, @Injectable()
ensina nosso serviço como ser injetado em um componente. A melhor demonstração vem direto da boca do cavalo, mas é bem longa, então aqui está uma prévia de como fica em nosso AppComponent:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })
Em seguida é a própria definição de classe. Ele tem uma instrução de export
antes dele, o que significa, você adivinhou, que podemos import
nosso serviço para outro arquivo. Na prática, estaremos importando nosso serviço para nosso componente AppComponent
, como acima.
Logo depois disso está o construtor, onde você pode ver alguma injeção de dependência real em ação. O constructor(private http:Http) {}
adiciona uma variável de instância privada chamada http
que o TypeScript reconhece magicamente como uma instância do serviço Http. O ponto vai para o TypeScript!
Depois disso, são apenas algumas variáveis de instância de aparência regular e uma função de utilidade antes de chegarmos à carne e às batatas reais, a função get
. Aqui vemos o Http
em ação. Parece muito com a abordagem baseada em promessas do Angular 1, mas sob o capô é muito mais legal. Ser construído em RxJS significa que temos algumas grandes vantagens sobre as promessas:
- Podemos cancelar o
Observable
se não nos importarmos mais com a resposta. Esse pode ser o caso se estivermos criando um campo de preenchimento automático de digitação antecipada e não nos preocuparmos mais com os resultados de “ca” depois que eles inserirem “cat”. - O
Observable
pode emitir vários valores e o assinante será chamado repetidamente para consumi-los à medida que são produzidos.
O primeiro é ótimo em muitas circunstâncias, mas é no segundo que estamos nos concentrando em nosso novo serviço. Vamos passar pela função get
linha por linha:
return this.http.get(this._countUrl, { search: this.parameterize(params) })
Parece bastante semelhante à chamada HTTP baseada em promessa que você veria no Angular 1. Nesse caso, estamos enviando os parâmetros de consulta para obter uma contagem de todos os resultados correspondentes.
.map(this.extractData)
Assim que a chamada AJAX retornar, ela enviará a resposta pelo fluxo. O método map
é conceitualmente semelhante à função map
de um array, mas também se comporta como o método then
de uma promessa porque espera que o que esteja acontecendo upstream seja concluído, independentemente da sincronicidade ou assincronicidade. Nesse caso, ele simplesmente aceita o objeto de resposta e provoca os dados JSON para passar downstream. Agora temos:
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })
Ainda temos um retorno de chamada estranho que precisamos inserir lá. Veja, nem tudo é mágica, mas podemos processar onCountResults
assim que a chamada AJAX retornar, tudo sem sair do nosso stream. Isso não é tão ruim. Quanto à próxima linha:
.flatMap(results => Observable.range(1, results.totalPages))
Uh oh, você pode sentir isso? Um silêncio sutil tomou conta da multidão, e você pode dizer que algo importante está prestes a acontecer. O que essa linha significa mesmo? A parte da direita não é tão louca. Ele cria um intervalo RxJS, que eu considero um array envolto em Observable
glorificado. Se results.totalPages
for igual a 5, você terá algo como Observable.of([1,2,3,4,5])
.
flatMap
é, espere por isso, uma combinação de flatten
e map
. Há um ótimo vídeo explicando o conceito no Egghead.io, mas minha estratégia é pensar em cada Observable
como um array. Observable.range
cria seu próprio wrapper, deixando-nos com o array bidimensional [[1,2,3,4,5]]
. flatMap
nivela o array externo, deixando-nos com [1,2,3,4,5]
, então map
simplesmente mapeia sobre o array, passando os valores downstream um de cada vez. Portanto, esta linha aceita um inteiro ( totalPages
) e o converte em um fluxo de inteiros de 1 a totalPages
. Pode não parecer muito, mas é tudo o que precisamos para configurar.
Eu realmente queria colocar isso em uma linha para aumentar seu impacto, mas acho que você não pode ganhar todas. Aqui vemos o que acontece com o fluxo de inteiros que configuramos na última linha. Eles fluem para esta etapa um por um e, em seguida, são adicionados à consulta como um parâmetro de página antes de finalmente serem empacotados em uma nova solicitação AJAX e enviados para buscar uma página de resultados. Aqui está esse código:
.flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })
Se totalPages
for 5, construímos 5 solicitações GET e enviamos todas simultaneamente. flatMap
se inscreve em cada novo Observable
, portanto, quando as solicitações retornam (em qualquer ordem), elas são desempacotadas e cada resposta (como uma página de resultados) é enviada para baixo, uma de cada vez.
Vamos ver como tudo isso funciona de outro ângulo. A partir de nossa solicitação de “contagem” de origem, encontramos o número total de páginas de resultados. Criamos uma nova solicitação AJAX para cada página e, não importa quando elas retornam (ou em que ordem), elas são enviadas para o fluxo assim que estiverem prontas. Tudo o que nosso componente precisa fazer é se inscrever no Observable retornado pelo nosso método get
, e ele receberá cada página, uma após a outra, todas de um único fluxo. Tome isso, promessas.
É tudo um pouco anticlimático depois disso:
.map(this.extractData).catch(this.handleError);
À medida que cada objeto de resposta chega do flatMap
, seu JSON é extraído da mesma maneira que a resposta da solicitação de contagem. No final, há o operador catch
, que ajuda a ilustrar como funciona o tratamento de erros RxJS baseado em fluxo. É bastante semelhante ao paradigma tradicional try/catch, exceto que o objeto Observable
também funciona para tratamento de erros assíncrono.
Sempre que um erro é encontrado, ele corre a jusante, pulando os operadores anteriores até encontrar um manipulador de erros. No nosso caso, o método handleError
lança novamente o erro, permitindo-nos interceptá-lo dentro do serviço, mas também para permitir que o assinante forneça seu próprio retorno de chamada onError
que é acionado ainda mais a jusante. O tratamento de erros nos mostra que não aproveitamos ao máximo nossa transmissão, mesmo com todas as coisas legais que já realizamos. É trivial adicionar um operador de retry
após nossas solicitações HTTP, que repete uma solicitação individual se retornar um erro. Como medida preventiva, também poderíamos adicionar um operador entre o gerador de range
e as solicitações, adicionando alguma forma de limitação de taxa para não enviar spam ao servidor com muitas solicitações de uma só vez.
Recapitulação: Aprender Angular 2 não é apenas uma nova estrutura
Aprender Angular 2 é mais como conhecer uma família totalmente nova, e alguns de seus relacionamentos são complicados. Espero ter conseguido demonstrar que esses relacionamentos evoluíram por um motivo, e há muito a ganhar respeitando a dinâmica que existe dentro desse ecossistema. Espero que você tenha gostado deste artigo também, porque eu mal arranhei a superfície, e há muito mais a dizer sobre este assunto.