Obtenez votre Angular 2 sur: mise à niveau à partir de 1.5

Publié: 2022-03-11

J'ai commencé par vouloir écrire un guide étape par étape pour mettre à niveau une application d'Angular 1.5 vers Angular 2, avant d'être poliment informé par mon éditrice qu'elle avait besoin d'un article plutôt que d'un roman. Après de longues délibérations, j'ai accepté que je devais commencer par une vaste enquête sur les changements dans Angular 2, en abordant tous les points abordés dans l'article Getting Past Hello World in Angular 2 de Jason Aden. …Oups. Allez-y et lisez-le pour avoir un aperçu des nouvelles fonctionnalités d'Angular 2, mais pour une approche pratique, gardez votre navigateur ici.

Je veux que cela devienne une série qui englobe à terme l'ensemble du processus de mise à niveau de notre application de démonstration vers Angular 2. Pour l'instant, cependant, commençons par un seul service. Faisons une promenade sinueuse à travers le code et je répondrai à toutes les questions que vous pourriez avoir, telles que….

"OH NON POURQUOI TOUT EST-IL SI DIFFÉRENT"

Angulaire : l'ancienne méthode

Si vous êtes comme moi, le guide de démarrage rapide Angular 2 a peut-être été la première fois que vous avez regardé TypeScript. Très rapidement, selon son propre site Web, TypeScript est "un sur-ensemble typé de JavaScript qui se compile en JavaScript brut". Vous installez le transpiler (similaire à Babel ou Traceur) et vous vous retrouvez avec un langage magique qui prend en charge les fonctionnalités de langage ES2015 et ES2016 ainsi qu'un typage fort.

Vous trouverez peut-être rassurant de savoir qu'aucune de ces configurations obscures n'est strictement nécessaire. Il n'est pas très difficile d'écrire du code Angular 2 en JavaScript, bien que je ne pense pas que cela en vaille la peine. C'est bien de reconnaître un territoire familier, mais une grande partie de ce qui est nouveau et excitant avec Angular 2 est sa nouvelle façon de penser plutôt que sa nouvelle architecture.

Cet article décrit la mise à niveau d'un service vers Angular 2 à partir de 1.5.

Ce qui est nouveau et passionnant avec Angular 2, c'est sa nouvelle façon de penser plutôt que sa nouvelle architecture.
Tweeter

Regardons donc ce service que j'ai mis à jour d'Angular 1.5 à 2.0.0-beta.17. C'est un service Angular 1.x assez standard, avec seulement quelques fonctionnalités intéressantes que j'ai essayé de noter dans les commentaires. C'est un peu plus compliqué que votre application de jouet standard, mais tout ce qu'il fait, c'est interroger Zilyo, une API disponible gratuitement qui regroupe les annonces de fournisseurs de location comme Airbnb. Désolé, c'est un peu de code.

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);

L'inconvénient de cette application particulière est qu'elle affiche les résultats sur une carte. D'autres services gèrent plusieurs pages de résultats en mettant en œuvre une pagination ou des défileurs paresseux, ce qui leur permet de récupérer une page de résultats soignée à la fois. Cependant, nous voulons afficher tous les résultats dans la zone de recherche, et nous voulons qu'ils apparaissent dès qu'ils reviennent du serveur plutôt que d'apparaître soudainement une fois que toutes les pages sont chargées. De plus, nous souhaitons afficher les mises à jour de progression à l'utilisateur afin qu'il ait une idée de ce qui se passe.

En relation: Le guide vital des entretiens AngularJS

Afin d'accomplir cela dans Angular 1.5, nous avons recours à des rappels. Les promesses nous amènent à mi-chemin, comme vous pouvez le voir dans le wrapper $q.all qui déclenche le rappel onCompleted , mais les choses deviennent encore assez compliquées.

Ensuite, nous apportons lodash pour créer toutes les demandes de page pour nous, et chaque demande est responsable de l'exécution du rappel onFetchPage pour s'assurer qu'elle est ajoutée à la carte dès qu'elle est disponible. Mais cela se complique. Comme vous pouvez le voir dans les commentaires, je me suis perdu dans ma propre logique et je n'ai pas pu comprendre ce qui était retourné à quelle promesse quand.

La netteté globale du code en souffre encore plus (bien plus que ce qui est strictement nécessaire), car une fois que je deviens confus, il ne fait que dégringoler à partir de là. Dites-le avec moi, s'il vous plaît...

'IL DOIT Y AVOIR UNE MEILLEURE FAÇON'

Angular 2 : une nouvelle façon de penser

Il existe un meilleur moyen, et je vais vous le montrer. Je ne vais pas passer trop de temps sur les concepts ES6 (alias ES2015), car il existe de bien meilleurs endroits pour en savoir plus sur ce sujet, et si vous avez besoin d'un point de départ, ES6-Features.org a un bon aperçu de toutes les nouvelles fonctionnalités amusantes. Considérez ce code AngularJS 2 mis à jour :

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); } }

Frais! Passons en revue cette ligne par ligne. Encore une fois, le transpileur TypeScript nous permet d'utiliser toutes les fonctionnalités ES6 que nous voulons car il convertit tout en JavaScript vanille.

Les instructions d' import au début utilisent simplement ES6 pour charger les modules dont nous avons besoin. Étant donné que je fais la plupart de mes développements dans ES5 (c'est-à-dire JavaScript normal), je dois admettre que c'est un peu ennuyeux de devoir soudainement commencer à lister tous les objets que je prévois d'utiliser.

Cependant, gardez à l'esprit que TypeScript transpile tout vers JavaScript et utilise secrètement SystemJS pour gérer le chargement des modules. Les dépendances sont toutes chargées de manière asynchrone et il est (prétendument) capable de regrouper votre code de manière à supprimer les symboles que vous n'avez pas importés. De plus, tout cela prend en charge la "minification agressive", ce qui semble très douloureux. Ces déclarations d'importation sont un petit prix à payer pour éviter de faire face à tout ce bruit.

Les instructions d'importation dans Angular font beaucoup dans les coulisses.

Les déclarations d'importation sont un petit prix à payer pour ce qui se passe dans les coulisses.

Quoi qu'il en soit, à part le chargement de fonctionnalités sélectives à partir d'Angular 2 lui-même, faites particulièrement attention à la ligne import {Observable} from 'rxjs/Observable'; . RxJS est une bibliothèque de programmation réactive époustouflante et folle qui fournit une partie de l'infrastructure sous-jacente à Angular 2. Nous en entendrons certainement parler plus tard.

Nous arrivons maintenant à @Injectable() .

Pour être honnête, je ne suis toujours pas sûr de ce que cela fait, mais la beauté de la programmation déclarative est que nous n'avons pas toujours besoin de comprendre les détails. C'est ce qu'on appelle un décorateur, qui est une construction TypeScript sophistiquée capable d'appliquer des propriétés à la classe (ou à un autre objet) qui la suit. Dans ce cas, @Injectable() enseigne à notre service comment être injecté dans un composant. La meilleure démonstration vient directement de la bouche du cheval, mais elle est assez longue alors voici un aperçu de son apparence dans notre AppComponent :

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

La prochaine étape est la définition de la classe elle-même. Il est précédé d'une déclaration d' export , ce qui signifie, vous l'avez deviné, que nous pouvons import notre service dans un autre fichier. En pratique, nous importerons notre service dans notre composant AppComponent , comme ci-dessus.

@Injectable() enseigne à notre service comment être injecté dans un composant.

@Injectable() enseigne à notre service comment être injecté dans un composant.

Juste après se trouve le constructeur, où vous pouvez voir une véritable injection de dépendance en action. Le constructor(private http:Http) {} ajoute une variable d'instance privée nommée http que TypeScript reconnaît comme par magie comme une instance du service Http. Le point va à TypeScript !

Après cela, ce ne sont que quelques variables d'instance d'apparence normale et une fonction utilitaire avant d'en venir à la vraie viande et aux pommes de terre, la fonction get . Ici, nous voyons Http en action. Cela ressemble beaucoup à l'approche basée sur les promesses d'Angular 1, mais sous le capot, c'est beaucoup plus cool. Être construit sur RxJS signifie que nous obtenons quelques gros avantages par rapport aux promesses :

  • Nous pouvons annuler l' Observable si nous ne nous soucions plus de la réponse. Cela peut être le cas si nous construisons un champ de saisie semi-automatique et que nous ne nous soucions plus des résultats pour "ca" une fois qu'ils ont saisi "cat".
  • L' Observable peut émettre plusieurs valeurs et l'abonné sera appelé à plusieurs reprises pour les consommer au fur et à mesure de leur production.

Le premier est excellent dans de nombreuses circonstances, mais c'est le second sur lequel nous nous concentrons dans notre nouveau service. Passons en revue la fonction get ligne par ligne :

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

Cela ressemble assez à l'appel HTTP basé sur la promesse que vous verriez dans Angular 1. Dans ce cas, nous envoyons les paramètres de requête pour obtenir un décompte de tous les résultats correspondants.

 .map(this.extractData)

Une fois l'appel AJAX renvoyé, il enverra la réponse dans le flux. La méthode map est conceptuellement similaire à la fonction map d'un tableau, mais elle se comporte également comme la méthode then d'une promesse car elle attend que tout ce qui se passe en amont se termine, indépendamment de la synchronicité ou de l'asynchronicité. Dans ce cas, il accepte simplement l'objet de réponse et extrait les données JSON à transmettre en aval. Maintenant nous avons:

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

Nous avons encore un rappel gênant que nous devons glisser là-dedans. Vous voyez, tout n'est pas magique, mais nous pouvons traiter onCountResults dès le retour de l'appel AJAX, le tout sans quitter notre flux. Ce n'est pas si mal. Quant à la ligne suivante :

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

Oh oh, peux-tu le sentir? Un silence subtil s'est emparé de la foule qui regardait, et vous pouvez dire que quelque chose de majeur est sur le point de se produire. Qu'est-ce que cette ligne signifie même? La partie droite n'est pas si folle. Il crée une plage RxJS, que je considère comme un tableau enveloppé Observable glorifié. Si results.totalPages est égal à 5, vous vous retrouvez avec quelque chose comme Observable.of([1,2,3,4,5]) .

flatMap est, attendez-le, une combinaison de flatten et map . Il y a une excellente vidéo expliquant le concept sur Egghead.io, mais ma stratégie consiste à considérer chaque Observable comme un tableau. Observable.range crée son propre wrapper, nous laissant avec le tableau à 2 dimensions [[1,2,3,4,5]] . flatMap aplatit le tableau externe, nous laissant avec [1,2,3,4,5] , puis map simplement sur le tableau, en passant les valeurs en aval une à la fois. Donc cette ligne accepte un entier ( totalPages ) et le convertit en un flux d'entiers de 1 à totalPages . Cela peut sembler peu, mais c'est tout ce dont nous avons besoin pour mettre en place.

LE PRESTIGE

Je voulais vraiment mettre cela sur une seule ligne pour augmenter son impact, mais je suppose que vous ne pouvez pas tous les gagner. Ici, nous voyons ce qui arrive au flux d'entiers que nous avons mis en place sur la dernière ligne. Ils entrent dans cette étape un par un, puis sont ajoutés à la requête en tant que paramètre de page avant d'être finalement regroupés dans une toute nouvelle requête AJAX et envoyés pour récupérer une page de résultats. Voici ce code :

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

Si totalPages était de 5, nous construisons 5 requêtes GET et les envoyons toutes simultanément. flatMap s'abonne à chaque nouvel Observable , donc lorsque les requêtes reviennent (dans n'importe quel ordre), elles sont déballées et chaque réponse (comme une page de résultats) est poussée en aval une à la fois.

Regardons comment tout cela fonctionne sous un autre angle. À partir de notre demande de « comptage » d'origine, nous trouvons le nombre total de pages de résultats. Nous créons une nouvelle requête AJAX pour chaque page, et peu importe quand elles reviennent (ou dans quel ordre), elles sont poussées dans le flux dès qu'elles sont prêtes. Tout ce que notre composant doit faire est de s'abonner à l'Observable renvoyé par notre méthode get , et il recevra chaque page, l'une après l'autre, le tout à partir d'un seul flux. Prends ça, promis.

Chaque réponse est poussée en aval une par une.

Le composant recevra chaque page, l'une après l'autre, le tout à partir d'un seul flux.

C'est un peu anti-climactique après ça:

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

Au fur et à mesure que chaque objet de réponse arrive de flatMap , son JSON est extrait de la même manière que la réponse de la requête count. Ajouté à la fin, il y a l'opérateur catch , qui aide à illustrer le fonctionnement de la gestion des erreurs RxJS basée sur les flux. C'est assez similaire au paradigme try/catch traditionnel, sauf que l'objet Observable fonctionne également pour la gestion des erreurs asynchrones.

Chaque fois qu'une erreur est rencontrée, il court vers l'aval, sautant les opérateurs passés jusqu'à ce qu'il rencontre un gestionnaire d'erreurs. Dans notre cas, la méthode handleError renvoie l'erreur, ce qui nous permet de l'intercepter dans le service mais aussi de laisser l'abonné fournir son propre rappel onError qui se déclenche encore plus en aval. La gestion des erreurs nous montre que nous n'avons pas pleinement profité de notre flux, même avec toutes les choses intéressantes que nous avons déjà accomplies. Il est trivial d'ajouter un opérateur de retry après nos requêtes HTTP, qui réessaye une requête individuelle si elle renvoie une erreur. À titre préventif, nous pourrions également ajouter un opérateur entre le générateur range et les requêtes, en ajoutant une certaine forme de limitation de débit afin de ne pas spammer le serveur avec trop de requêtes à la fois.

Connexe : embauchez les 3 % de développeurs AngularJS indépendants les plus performants.

Récapitulatif : Apprendre Angular 2, ce n'est pas seulement un nouveau framework

Apprendre Angular 2, c'est plus comme rencontrer une toute nouvelle famille, et certaines de leurs relations sont compliquées. J'espère avoir réussi à démontrer que ces relations ont évolué pour une raison, et qu'il y a beaucoup à gagner en respectant la dynamique qui existe au sein de cet écosystème. J'espère que vous avez également apprécié cet article, car j'ai à peine effleuré la surface, et il y a beaucoup plus à dire sur ce sujet.

En relation: Tous les avantages, pas de tracas: un didacticiel angulaire 9