Obtenga su Angular 2: Actualización desde 1.5

Publicado: 2022-03-11

Comencé queriendo escribir una guía paso a paso para actualizar una aplicación de Angular 1.5 a Angular 2, antes de que mi editor me informara cortésmente que necesitaba un artículo en lugar de una novela. Después de mucha deliberación, acepté que necesitaba comenzar con una encuesta amplia de los cambios en Angular 2, abordando todos los puntos cubiertos en el artículo Getting Past Hello World in Angular 2 de Jason Aden. …UPS. Continúe y léalo para obtener una descripción general de las nuevas funciones de Angular 2, pero para un enfoque práctico, mantenga su navegador aquí.

Quiero que esto se convierta en una serie que eventualmente abarque todo el proceso de actualización de nuestra aplicación de demostración a Angular 2. Sin embargo, por ahora, comencemos con un solo servicio. Hagamos un recorrido serpenteante por el código y responderé cualquier pregunta que pueda tener, como….

'OH NO POR QUE TODO ES TAN DIFERENTE'

Angular: la forma antigua

Si es como yo, la guía de inicio rápido de Angular 2 podría haber sido la primera vez que vio TypeScript. Rápidamente, según su propio sitio web, TypeScript es "un superconjunto escrito de JavaScript que se compila en JavaScript simple". Instala el transpiler (similar a Babel o Traceur) y termina con un lenguaje mágico que admite funciones de lenguaje ES2015 y ES2016, así como escritura fuerte.

Puede que le resulte tranquilizador saber que ninguna de estas configuraciones arcanas es estrictamente necesaria. No es terriblemente difícil escribir código Angular 2 en JavaScript simple y antiguo, aunque no creo que valga la pena hacerlo. Es bueno reconocer un territorio familiar, pero gran parte de lo nuevo y emocionante de Angular 2 es su nueva forma de pensar en lugar de su nueva arquitectura.

Esta publicación explica la actualización de un servicio a Angular 2 desde 1.5.

Lo nuevo y emocionante de Angular 2 es su nueva forma de pensar en lugar de su nueva arquitectura.
Pío

Entonces, veamos este servicio que actualicé de Angular 1.5 a 2.0.0-beta.17. Es un servicio Angular 1.x bastante estándar, con solo un par de características interesantes que traté de señalar en los comentarios. Es un poco más complicado que su aplicación estándar de juguetes, pero todo lo que realmente hace es consultar a Zilyo, una API disponible gratuitamente que agrega listados de proveedores de alquiler como Airbnb. Lo siento, es bastante código.

zilyo.servicio.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);

El problema de esta aplicación en particular es que muestra los resultados en un mapa. Otros servicios manejan varias páginas de resultados mediante la implementación de paginación o perezosos scrollers, lo que les permite recuperar una página ordenada de resultados a la vez. Sin embargo, queremos mostrar todos los resultados dentro del área de búsqueda y queremos que aparezcan tan pronto como regresen del servidor en lugar de aparecer repentinamente una vez que se cargan todas las páginas. Además, queremos mostrar actualizaciones de progreso al usuario para que tenga una idea de lo que está sucediendo.

Relacionado: La guía vital para entrevistas con AngularJS

Para lograr esto en Angular 1.5, recurrimos a las devoluciones de llamada. Las promesas nos llevan a la mitad del camino, como puede ver en el contenedor $q.all que activa la devolución de llamada onCompleted , pero las cosas aún se complican bastante.

Luego traemos lodash para crear todas las solicitudes de página para nosotros, y cada solicitud es responsable de ejecutar la devolución de llamada onFetchPage para asegurarnos de que se agregue al mapa tan pronto como esté disponible. Pero eso se complica. Como puede ver en los comentarios, me perdí en mi propia lógica y no pude entender qué se devolvía a qué promesa y cuándo.

La pulcritud general del código sufre aún más (mucho más de lo estrictamente necesario), porque una vez que me confundo, solo desciende en espiral desde allí. Dilo conmigo, por favor...

'TIENE QUE HABER UNA MEJOR MANERA'

Angular 2: una nueva forma de pensar

Hay una manera mejor, y te la mostraré. No voy a dedicar demasiado tiempo a los conceptos de ES6 (también conocido como ES2015), porque hay lugares mucho mejores para aprender sobre esas cosas, y si necesita un punto de partida, ES6-Features.org tiene una buena descripción general. de todas las nuevas funciones divertidas. Considere este código AngularJS 2 actualizado:

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! Repasemos esto línea por línea. Nuevamente, el transpiler de TypeScript nos permite usar cualquier característica de ES6 que queramos porque convierte todo a JavaScript estándar.

Las declaraciones de import al principio simplemente usan ES6 para cargar los módulos que necesitamos. Dado que hago la mayor parte de mi desarrollo en ES5 (también conocido como JavaScript normal), debo admitir que es un poco molesto tener que comenzar a enumerar de repente todos los objetos que planeo usar.

Sin embargo, tenga en cuenta que TypeScript está transpilando todo a JavaScript y está usando SystemJS en secreto para manejar la carga de módulos. Todas las dependencias se cargan de forma asíncrona y (supuestamente) es capaz de agrupar su código de una manera que elimina los símbolos que no ha importado. Además, todo admite la "minificación agresiva", que suena muy doloroso. Esas declaraciones de importación son un pequeño precio a pagar para evitar lidiar con todo ese ruido.

Las declaraciones de importación en Angular hacen mucho detrás de escena.

Las declaraciones de importación son un pequeño precio a pagar por lo que sucede detrás de escena.

De todos modos, además de cargar funciones selectivas desde Angular 2, preste especial atención a la import {Observable} from 'rxjs/Observable'; . RxJS es una biblioteca de programación reactiva alucinante y genial que proporciona parte de la infraestructura subyacente de Angular 2. Definitivamente tendremos noticias de ella más adelante.

Ahora llegamos a @Injectable() .

Todavía no estoy totalmente seguro de qué hace eso para ser honesto, pero la belleza de la programación declarativa es que no siempre necesitamos entender los detalles. Se llama decorador, que es una elegante construcción de TypeScript capaz de aplicar propiedades a la clase (u otro objeto) que le sigue. En este caso, @Injectable() enseña a nuestro servicio cómo inyectarse en un componente. La mejor demostración viene directamente de la boca del caballo, pero es bastante larga, así que aquí hay un adelanto de cómo se ve en nuestro AppComponent:

 @Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })

El siguiente paso es la propia definición de la clase. Tiene una declaración de export antes, lo que significa que, lo adivinó, podemos import nuestro servicio a otro archivo. En la práctica, importaremos nuestro servicio a nuestro componente AppComponent , como se indicó anteriormente.

@Injectable() le enseña a nuestro servicio cómo inyectarse en un componente.

@Injectable() le enseña a nuestro servicio cómo inyectarse en un componente.

Inmediatamente después está el constructor, donde puedes ver una inyección de dependencia real en acción. El constructor(private http:Http) {} agrega una variable de instancia privada llamada http que TypeScript reconoce mágicamente como una instancia del servicio Http. ¡El punto va a TypeScript!

Después de eso, son solo algunas variables de instancia de aspecto regular y una función de utilidad antes de llegar a la verdadera carne y papas, la función de get . Aquí vemos Http en acción. Se parece mucho al enfoque basado en promesas de Angular 1, pero bajo el capó es mucho más genial. Estar construido sobre RxJS significa que obtenemos un par de grandes ventajas sobre las promesas:

  • Podemos cancelar el Observable si ya no nos importa la respuesta. Este podría ser el caso si estamos creando un campo de autocompletado de escritura anticipada y ya no nos preocupamos por los resultados de "ca" una vez que hayan ingresado "cat".
  • El Observable puede emitir múltiples valores y se llamará al suscriptor una y otra vez para consumirlos a medida que se producen.

El primero es excelente en muchas circunstancias, pero es el segundo en el que nos estamos enfocando en nuestro nuevo servicio. Repasemos la función get línea por línea:

 return this.http.get(this._countUrl, { search: this.parameterize(params) })

Se ve bastante similar a la llamada HTTP basada en promesas que vería en Angular 1. En este caso, enviamos los parámetros de consulta para obtener un recuento de todos los resultados coincidentes.

 .map(this.extractData)

Una vez que regrese la llamada AJAX, enviará la respuesta a la corriente. El map de métodos es conceptualmente similar a la función de map de una matriz, pero también se comporta como el método then de una promesa porque espera a que se complete lo que estaba sucediendo aguas arriba, independientemente de la sincronicidad o asincronía. En este caso, simplemente acepta el objeto de respuesta y extrae los datos JSON para pasarlos en sentido descendente. Ahora tenemos:

 .map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })

Todavía tenemos una devolución de llamada incómoda que debemos deslizar allí. Vea, no todo es magia, pero podemos procesar onCountResults tan pronto como regrese la llamada AJAX, todo sin salir de nuestra transmisión. Eso no es tan malo. En cuanto a la siguiente línea:

.flatMap(resultados => Observable.range(1, resultados.totalPages))

Oh, oh, ¿puedes sentirlo? Un silencio sutil se ha apoderado de la multitud que miraba, y se puede decir que algo importante está por suceder. ¿Qué significa esta línea? La parte de la mano derecha no es tan loca. Crea un rango RxJS, que considero como una matriz envuelta en Observable glorificada. Si results.totalPages es igual a 5, terminas con algo como Observable.of([1,2,3,4,5]) .

flatMap es, espera, una combinación de flatten y map . Hay un gran video que explica el concepto en Egghead.io, pero mi estrategia es pensar en cada Observable como una matriz. Observable.range crea su propio envoltorio, dejándonos con la matriz bidimensional [[1,2,3,4,5]] . flatMap aplana la matriz externa, dejándonos con [1,2,3,4,5] , luego map simplemente mapea sobre la matriz, pasando los valores aguas abajo uno a la vez. Entonces, esta línea acepta un número entero ( totalPages ) y lo convierte en un flujo de números enteros desde 1 hasta totalPages . Puede que no parezca mucho, pero eso es todo lo que necesitamos configurar.

EL PRESTIGIO

Tenía muchas ganas de poner esto en una línea para aumentar su impacto, pero supongo que no puedes ganarlos todos. Aquí vemos lo que sucede con el flujo de enteros que configuramos en la última línea. Fluyen en este paso uno por uno, luego se agregan a la consulta como un parámetro de página antes de finalmente empaquetarse en una nueva solicitud AJAX y enviarse para obtener una página de resultados. Aquí está ese código:

 .flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })

Si totalPages fuera 5, construimos 5 solicitudes GET y las enviamos todas simultáneamente. flatMap suscribe a cada nuevo Observable , por lo que cuando las solicitudes regresan (en cualquier orden) se desenvuelven y cada respuesta (como una página de resultados) se envía hacia abajo una a la vez.

Veamos cómo funciona todo esto desde otro ángulo. A partir de nuestra solicitud de "recuento" de origen, encontramos el número total de páginas de resultados. Creamos una nueva solicitud AJAX para cada página, y no importa cuándo regresan (o en qué orden), se envían a la transmisión tan pronto como están listas. Todo lo que nuestro componente necesita hacer es suscribirse al Observable devuelto por nuestro método get , y recibirá cada página, una tras otra, todo desde una sola transmisión. Toma eso, promesas.

Cada respuesta se empuja río abajo una a la vez.

El componente recibirá cada página, una tras otra, todo desde una sola transmisión.

Todo es un poco anti-climático después de eso:

 .map(this.extractData).catch(this.handleError);

A medida que llega cada objeto de respuesta de flatMap , su JSON se extrae de la misma manera que la respuesta de la solicitud de conteo. Agregado al final está el operador catch , que ayuda a ilustrar cómo funciona el manejo de errores RxJS basado en secuencias. Es bastante similar al paradigma tradicional de prueba/captura, excepto que el objeto Observable también funciona para el manejo asincrónico de errores.

Cada vez que se encuentra un error, se ejecuta en sentido descendente, omitiendo operadores anteriores hasta que encuentra un controlador de errores. En nuestro caso, el método handleError vuelve a generar el error, lo que nos permite interceptarlo dentro del servicio, pero también permitir que el suscriptor proporcione su propia devolución de llamada onError que se activa aún más aguas abajo. El manejo de errores nos muestra que no hemos aprovechado al máximo nuestra transmisión, incluso con todas las cosas geniales que ya hemos logrado. Es trivial agregar un operador de retry después de nuestras solicitudes HTTP, que vuelve a intentar una solicitud individual si devuelve un error. Como medida preventiva, también podríamos agregar un operador entre el generador de range y las solicitudes, agregando alguna forma de limitación de velocidad para que no enviemos spam al servidor con demasiadas solicitudes a la vez.

Relacionado: Contrate al 3% superior de los desarrolladores independientes de AngularJS.

Resumen: aprender Angular 2 no se trata solo de un nuevo marco

Aprender Angular 2 es más como conocer a una familia completamente nueva, y algunas de sus relaciones son complicadas. Espero haber logrado demostrar que estas relaciones evolucionaron por una razón, y hay mucho que ganar al respetar la dinámica que existe dentro de este ecosistema. Espero que hayas disfrutado este artículo también, porque apenas he arañado la superficie y hay mucho más que decir sobre este tema.

Relacionado: Todas las ventajas, sin complicaciones: un tutorial de Angular 9