5 choses que j'aimerais savoir quand j'ai commencé à utiliser Angular

Modern Angular est un cadre puissant avec de nombreuses fonctionnalités, avec lequel à première vue des concepts et des mécanismes complexes viennent. Cela est particulièrement visible pour ceux qui viennent de commencer à travailler à la fois dans le front-end en principe, et avec Angular en particulier.


J'ai également rencontré le même problème lorsque je suis arrivé à Tinkoff au poste de développeur frontend junior il y a environ deux ans et que j'ai plongé dans le monde d'Angular. Par conséquent, je vous propose une courte histoire sur cinq choses, dont la compréhension faciliterait considérablement mon travail au début.



Injection de dépendance (DI)


Au début, je suis allé dans le composant et j'ai vu qu'il y avait des arguments dans le constructeur de classe. J'ai fait une petite analyse du travail des méthodes de classe, et il est devenu clair que ce sont des dépendances externes. Mais comment sont-ils entrés en classe? Où a été appelé le constructeur?


Je suggère immédiatement de comprendre un exemple, mais pour cela nous avons besoin d'un cours. Si en JavaScript "normal", la POO est présente avec certains "hacks", alors avec ES6 il y a une "vraie" syntaxe. Angular utilise TypeScript dès la sortie de la boîte, dans laquelle la syntaxe est à peu près la même. Par conséquent, je propose de l'utiliser davantage.


Imaginez qu'il existe une classe JokerService dans notre application qui gère les blagues. La méthode getJokes() renvoie une liste de blagues. Supposons que nous l'utilisions à trois endroits. Comment obtenir des blagues à trois endroits différents dans le code? Il existe plusieurs façons:


  1. Créez une instance de la classe à chaque endroit. Mais pourquoi devons-nous obstruer la mémoire et créer autant de services identiques? Et s'il y a 100 sièges?
  2. Rendez la méthode statique et récupérez les données à l'aide de JokerService.getJokes ().
  3. Mettez en œuvre l'un des modèles de conception. Si nous avons besoin que le service soit un pour toute l'application, alors ce sera Singleton. Mais pour cela, vous devez écrire une nouvelle logique dans la classe.

Nous avons donc trois options tout à fait fonctionnelles. Le premier ne nous conviendra pas - dans ce cas, il est inefficace. Nous ne voulons pas créer de copies supplémentaires, car elles seront complètement identiques. Il reste deux options.


Compliquons la tâche pour comprendre quelle méthode nous convient le mieux. Supposons qu'en troisième place, nous ayons besoin pour une raison quelconque de créer notre propre service avec certains paramètres. Il peut s'agir d'un auteur spécifique, de la durée de la blague, de la langue, etc. Que ferons-nous alors?


Dans le cas de la méthode statique, vous devrez passer les paramètres à chaque appel, car la classe est commune à tous les endroits. Autrement dit, dans chaque appel à getJokes() nous passerons tous les paramètres uniques à cet endroit. Bien sûr, il vaut mieux les passer lorsque vous instanciez puis appelez simplement la méthode getJokes() .


Il s'avère que la deuxième option ne nous conviendra pas non plus: elle nous fera toujours dupliquer beaucoup de code à chaque endroit. Il ne reste que Singleton, qui devra à nouveau mettre à jour la logique, mais avec des variantes. Mais comment comprendre de quelle option nous avons besoin?


Si vous pensiez que vous pouvez simplement créer un objet et utiliser la clé pour prendre le service souhaité, je peux vous féliciter: vous venez de réaliser comment l' injection de dépendance fonctionne en général. Mais allons un peu plus loin.


Pour vous assurer qu'un mécanisme est nécessaire pour nous aider à obtenir les bonnes instances, imaginez que JokerService a besoin de deux autres services, dont l'un est facultatif, et le second devrait donner un résultat spécial à un certain endroit. Ce n'est pas difficile.


Injection de dépendance angulaire


Comme l'indique la documentation , DI est un modèle de conception important pour une application. Angular a son propre framework de dépendance, qui est utilisé dans Angular lui-même pour augmenter l'efficacité et la modularité.


D'une manière générale, l' injection de dépendances est un mécanisme puissant dans lequel une classe reçoit les dépendances nécessaires quelque part à l'extérieur, plutôt que de créer des instances par elle-même.


Ne laissez pas la syntaxe et les fichiers avec l'extension html vous dérouter. Chaque composant dans Angular est un objet JavaScript standard, une instance d'une classe. En termes généraux: lorsque vous insérez un composant dans un modèle, une instance de la classe de composant est créée. Par conséquent, à ce moment, vous pouvez transmettre les dépendances nécessaires au constructeur. Prenons maintenant un exemple:


 @Component({ selector: 'jokes', template: './jokes.template.html', }) export class JokesComponent { private jokes: Observable<IJoke[]>; constructor(private jokerService: JokerService) { this.jokes = this.jokerService.getJokes(); } } 

Dans le constructeur du composant, nous indiquons simplement que nous avons besoin d'un JokerService . Nous ne le créons pas nous-mêmes. S'il y a cinq autres composants qui l'utilisent, ils se référeront tous à la même instance. Tout cela nous permet de gagner du temps, d'éliminer le passe-partout et d'écrire des applications très productives.


Fournisseurs


Et maintenant, je propose de traiter le cas lorsque vous avez besoin d'obtenir différentes instances du service. Tout d'abord, jetez un œil au service lui-même:


 @Injectable({ providedIn: 'root', //   ,   «»  }) export class JokerService { getJokes(): Observable<IJoke[]> { //     } } 

Lorsque le service est un pour l'ensemble de l'application, cette option sera suffisante. Mais que faire si nous avons, disons, deux implémentations de JokerService ? Ou est-ce juste pour une raison quelconque qu'un composant spécifique a besoin de sa propre instance de service? La réponse est simple: provider .


Pour plus de commodité, j'appellerai provider fournisseur et le processus de substitution d'une valeur dans une classe sera vérifié . Ainsi, nous pouvons fournir des services de différentes manières et à différents endroits. Commençons par le dernier. Trois options sont disponibles:


  • Dans l'application entière - spécifiez provideIn: 'root' dans le décorateur de service lui-même.
  • Dans le module - spécifiez le fournisseur dans le décorateur de service en tant que provideIn: JokesModule ou dans le décorateur du module @NgModule providers: [JokerService] .
  • Dans le composant - spécifiez le fournisseur dans le décorateur du composant, comme dans le module.

L'endroit est choisi en fonction de vos besoins. Nous avons compris l'endroit, passons au mécanisme lui-même. Si nous spécifions simplement provideIn: root dans le service, cela équivaudrait à l'entrée suivante dans le module:


 @NgModule({ // ...     providers: [{provide: JokerService, useClass: JokerService}], }) //   

Cela peut être lu quelque chose comme ceci: "Si un JokerService demandé, alors donnez une instance de la classe JokerService» De là, vous pouvez obtenir une instance spécifique de différentes manières:


  • Par jeton - vous devez spécifier un InjectionToken et obtenir un service dessus. Notez que dans les exemples ci-dessous dans provide vous pouvez transmettre le même jeton:


     const JOKER_SERVICE_TOKEN = new InjectionToken<string>('JokerService'); // ...     [{provide: JOKER_SERVICE_TOKEN, useClass: JokerService}]; 

  • Par classe - vous pouvez remplacer la classe. Par exemple, nous demanderons JokerService et donnerons - JokerHappyService :


     [{provide: JokerService, useClass: JokerHappyService}]; 

  • Par valeur - vous pouvez immédiatement renvoyer l'instance souhaitée:


     [{provide: JokerService, useValue: jokerService}]; 

  • Par usine - vous pouvez remplacer la classe par une usine qui créera l'instance souhaitée lors de son accès:


     [{provide: JokerService, useFactory: jokerServiceFactory}]; 


C’est tout. Autrement dit, pour résoudre l'exemple avec une instance spéciale, vous pouvez utiliser l'une des méthodes ci-dessus. Choisissez le plus adapté à vos besoins.


Soit dit en passant, DI fonctionne non seulement pour les services, mais en général pour toute entité que vous obtenez dans le constructeur de composants. Il s'agit d'un mécanisme très puissant qui devrait être utilisé à son plein potentiel.


Un petit résumé


Pour une compréhension complète, je propose de considérer le mécanisme d'injection de dépendance simplifié en angulaire par étapes en utilisant l'exemple de service:


  1. Lors de l'initialisation de l'application, le service a un jeton. Si nous ne l'avons pas spécifié spécifiquement dans le fournisseur, il s'agit de JokerService.
  2. Lorsqu'un service est demandé dans un composant, le mécanisme DI vérifie si le jeton transféré existe.
  3. Si le jeton n'existe pas, DI lancera une erreur. Dans notre cas, le jeton existe et le JokerService s'y trouve.
  4. Lorsque le composant est créé, une instance de JokerService est passée au constructeur comme argument.

Détection de changement


Nous entendons souvent, comme argument pour utiliser des frameworks, quelque chose comme «Le framework fera tout pour vous - plus rapidement et plus efficacement. Vous n'avez besoin de penser à rien. Gérez simplement les données. C'est peut-être vrai avec une application très simple. Mais si vous devez travailler avec des entrées utilisateur et opérer constamment sur des données, il vous suffit de savoir comment fonctionne le processus de détection des modifications et de rendu.


Dans Angular, Change Detection est responsable de la vérification des modifications. À la suite de diverses opérations - modification de la valeur d'une propriété de classe, exécution d'une opération asynchrone, réponse à une requête HTTP, etc. - le processus de vérification commence tout au long de l'arborescence des composants.


Étant donné que l'objectif principal du processus est de comprendre comment restituer un composant, l'essentiel est de vérifier les données utilisées dans les modèles. S'ils sont différents, le modèle est marqué comme «modifié» et sera redessiné.


Zone.js


Comprendre comment Angular assure le suivi des propriétés de classe et des opérations synchrones est assez simple. Mais comment effectue-t-il le suivi asynchrone? La bibliothèque Zone.js, créée par l'un des développeurs Angular, en est responsable.


Voici ce que c'est. Une zone en soi est un «contexte d'exécution», pour le dire franchement, l'endroit et l'état dans lesquels le code est exécuté. Une fois l'opération asynchrone terminée, la fonction de rappel est exécutée dans la même zone où elle a été enregistrée. Angular découvre donc où le changement s'est produit et ce qu'il faut vérifier.


Zone.js remplace par ses implémentations presque toutes les fonctions et méthodes asynchrones natives. Par conséquent, il peut suivre le moment où le callback une fonction asynchrone sera appelé. Autrement dit, Zone indique à Angular quand et où commencer le processus de validation des modifications.


Changer les stratégies de détection


Nous avons compris comment Angular surveille un composant et exécute la vérification des modifications. Imaginez maintenant que vous avez une énorme application avec des dizaines de composants. Et pour chaque clic, chaque opération asynchrone, chaque demande exécutée avec succès, une vérification est lancée sur l'ensemble de l'arborescence des composants. Très probablement, une telle application aura de sérieux problèmes de performances.


Les développeurs angulaires ont réfléchi à cela et nous ont donné la possibilité d'établir une stratégie de détection des changements, dont le bon choix peut augmenter de manière très significative la productivité.


Vous avez le choix entre deux options:


  • Par défaut - comme son nom l'indique, il s'agit de la stratégie par défaut lorsqu'un CD est lancé pour chaque action.
  • OnPush est une stratégie dans laquelle un CD n'est lancé que dans quelques cas:
    • si la valeur de @Input() a changé;
    • si un événement s'est produit à l'intérieur du composant ou de ses descendants;
    • si la vérification a été lancée manuellement;
    • si un nouvel événement arrive dans Async Pipe.

Sur la base de ma propre expérience de développement sur Angular, ainsi que de l'expérience de mes collègues, je peux dire avec certitude qu'il vaut mieux toujours indiquer la stratégie OnPush , sauf si le default vraiment nécessaire. Cela vous donnera plusieurs avantages:


  • Une compréhension claire du fonctionnement du processus CD.
  • Travail @Input() avec les propriétés @Input() .
  • Gain de performance.

Travailler avec @Input()


Comme d'autres frameworks populaires, Angular utilise un flux de données en aval. Le composant accepte les paramètres d'entrée marqués avec le décorateur @Input() . Prenons un exemple:


 interface IJoke { author: string; text: string; } @Component({ selector: 'joke', template: './joke.template.html', }) export class JokeComponent { @Input() joke: IJoke; } 

Supposons qu'il existe un composant décrit ci-dessus qui affiche le texte de la blague et l'auteur. Le problème avec cette écriture est que vous pouvez muter accidentellement ou spécifiquement l'objet transféré. Par exemple, remplacez le texte ou l'auteur.


 setAuthorNameOnly() { const name = this.joke.author.split(' ')[0]; this.joke.author = name; } 

Je constate tout de suite que c'est un mauvais exemple, mais cela montre clairement ce qui pourrait arriver. Pour vous protéger contre de telles erreurs, vous devez rendre les paramètres d'entrée en lecture seule. Grâce à cela, vous comprendrez comment travailler correctement avec les données et créer un CD. Sur cette base, la meilleure façon d'écrire une classe ressemblera à ceci:


 @Component({ selector: 'joke', template: './joke.template.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class JokeComponent { @Input() readonly joke: IJoke; @Output() updateName = new EventEmitter<string>(); setAuthorNameOnly() { const name = this.joke.author.split(' ')[0]; this.updateName.emit(name); } } 

L'approche décrite n'est pas une règle, mais seulement une recommandation. Il existe de nombreuses situations où cette approche sera gênante et inefficace. Au fil du temps, vous apprendrez à comprendre dans quel cas vous pouvez refuser la méthode proposée de travail avec les entrées.


Rxjs


Bien sûr, je peux me tromper, mais il semble que ReactiveX et la programmation réactive en général soient une nouvelle tendance. Angular a succombé à cette tendance (ou l'a peut-être créée) et utilise RxJS par défaut. La logique de base de l'ensemble du framework fonctionne sur cette bibliothèque, il est donc très important de comprendre les principes de la programmation réactive.


Mais qu'est-ce que RxJS? Il combine trois idées que je vais révéler dans un langage assez simple avec quelques omissions:


  • Le modèle «Observateur» est une entité qui produit des événements et un auditeur reçoit des informations sur ces événements.
  • Le modèle d'itérateur - vous permet d'obtenir un accès séquentiel aux éléments d'un objet sans révéler sa structure interne.
  • La programmation fonctionnelle avec des collections est un modèle dans lequel la logique bat en petits composants très simples, chacun ne résolvant qu'un seul problème.

La combinaison de ces modèles nous permet de décrire très simplement des algorithmes complexes à première vue, par exemple:


 private loadUnreadJokes() { this.showLoader(); //   fromEvent(document, 'load') .pipe( switchMap( () => this.http .get('/api/v1/jokes') //   .pipe(map((jokes: any[]) => jokes.filter(joke => joke.unread))), //   ), ) .subscribe( (jokes: any[]) => (this.jokes = jokes), //   error => { /*   */ }, () => this.hideLoader(), //       ); } 

Seulement 18 lignes avec toute la belle indentation. Essayez maintenant de réécrire cet exemple sur Vanilla ou au moins sur jQuery. Près de 100% de cela vous prendra au moins deux fois plus d'espace et ne sera pas aussi expressif. Ici, vous pouvez simplement suivre la ligne avec vos yeux et lire le code comme un livre.


Observable


Comprendre que toutes les données peuvent être représentées sous forme de flux ne vient pas immédiatement. Par conséquent, je propose de passer à une analogie simple. Imaginez un flux est un tableau de données triées par le temps. Par exemple, dans ce mode de réalisation:


 const observable = []; let counter = 0; const intervalId = setInterval(() => { observable.push(counter++); }, 1000); setTimeout(() => { clearInterval(intervalId); }, 6000); 

Nous considérerons que la dernière valeur du tableau est pertinente. Chaque seconde, un nombre sera ajouté au tableau. Comment savoir ailleurs dans l'application qu'un élément a été ajouté au tableau? Dans une situation normale, nous appelons une sorte de callback et mettons à jour la valeur du tableau dessus, puis prenons simplement le dernier élément.


Grâce à une programmation réactive, il n'est pas nécessaire non seulement d'écrire beaucoup de nouvelles logiques, mais aussi de penser à mettre à jour les informations. Cela peut être comparé à un simple écouteur:


 document.addEventListener('click', event => {}); 

Vous pouvez mettre beaucoup d' EventListener dans toute l'application, et ils fonctionneront, sauf, bien sûr, si vous vous occupez de l'opposé à dessein.


La programmation réactive fonctionne également. À un endroit, nous créons simplement un flux de données et y déposons périodiquement de nouvelles valeurs, et à un autre, nous nous abonnons à ce flux et écoutons simplement ces valeurs. Autrement dit, nous apprenons toujours la mise à jour et pouvons la gérer.


Voyons maintenant un exemple réel:


 export class JokesListComponent implements OnInit { jokes$: Observable<IJoke>; authors$ = new Subject<string[]>(); unread$ = new Subject<number>(); constructor(private jokerService: JokerService) {} ngOnInit() { //  ,    subscribe()    this.jokes$ = this.jokerService.getJokes(); this.jokes$.subscribe(jokes => { this.authors$.next(jokes.map(joke => joke.author)); this.unread$.next(jokes.filter(joke => joke.unread).length); }); } } 

Grâce à cette logique, lors de la modification des données en jokes , nous mettons automatiquement à jour les données sur le nombre de blagues non lues et la liste des auteurs. Si vous avez quelques autres composants, dont l'un recueille des statistiques sur le nombre de blagues lues par un auteur, et le second calcule la durée moyenne des blagues, alors les avantages deviennent évidents.


Banc d'essai


Tôt ou tard, le développeur comprend que si le projet n'est pas MVP, vous devez écrire des tests. Et plus les tests seront écrits, plus leur description sera claire et détaillée, plus il sera facile, rapide et fiable d'apporter des modifications et de mettre en œuvre de nouvelles fonctionnalités.


Angular a probablement prévu cela et nous a donné un outil de test puissant. De nombreux développeurs essaient d'abord de maîtriser une sorte de technologie «dès le départ» sans entrer dans la documentation. J'ai fait de même, c'est pourquoi j'ai réalisé assez tard toutes les capacités de test disponibles «prêtes à l'emploi».


Vous pouvez tester n'importe quoi dans Angular, mais si vous avez juste besoin d'instancier et de commencer à appeler des méthodes pour tester une classe ou un service normal, la situation avec le composant est complètement différente.


Comme nous l'avons déjà découvert, grâce aux DI, les dépendances sont prises en dehors du composant. D'une part, cela complique un peu l'ensemble du système, d'autre part, cela nous donne de grandes opportunités pour mettre en place des tests et vérifier de nombreux cas. Je propose de comprendre l'exemple d'un composant:


 @Component({ selector: 'app-joker', template: '<some-dependency></some-dependency>', styleUrls: ['./joker.component.less'], }) export class JokerComponent { constructor( private jokesService: JokesService, @Inject(PARTY_TOKEN) private partyService: PartyService, @Optional() private sleepService: SleepService, ) {} makeNewFriend(): IFriend { if (this.sleepService && this.sleepService.isSleeping) { this.sleepService.wakeUp(); } const joke = this.jokesService.generateNewJoke(); this.partyService.goToParty('Pacha'); this.partyService.toSay(joke.text); const laughingPeople = this.partyService.getPeopleByReaction('laughing'); const girl = laughingPeople.find(human => human.sex === 'female'); const friend = this.partyService.makeFriend(girl); return friend; } } 

Ainsi, dans l'exemple actuel, il existe trois services. L'un est importé de la manière habituelle, un par jeton et un autre service est facultatif. Comment configurer le module de test? Je vais immédiatement montrer la vue terminée:


 beforeEach(async(() => { TestBed.configureTestingModule({ imports: [SomeDependencyModule], declarations: [JokerComponent], //  ,    providers: [{provide: PARTY_TOKEN, useClass: PartyService}], }).compileComponents(); fixture = TestBed.createComponent(JokerComponent); component = fixture.componentInstance; fixture.detectChanges(); //    ,     })); 

TestBed nous permet de faire une simulation complète du module requis. Vous pouvez y connecter n'importe quel service, remplacer des modules, obtenir des instances de classes à partir d'un composant, et bien plus encore. Maintenant que le module est déjà configuré, passons aux possibilités.


Les dépendances inutiles peuvent être évitées


Une application angulaire se compose de modules, qui peuvent inclure d'autres modules, services, directives, etc. Dans le test, nous devons, en fait, recréer le fonctionnement du module. Si dans notre exemple, nous utilisons <some-dependency></some-dependency> dans le modèle, cela signifie que nous devons également importer SomeDependencyModule dans le test. Et s'il y a des addictions là-bas? Donc, ils doivent également être importés.
Si l'application est complexe, il y aura beaucoup de telles dépendances. L'importation de toutes les dépendances entraînera le fait que dans chaque test, l'application entière sera localisée et toutes les méthodes seront appelées. Peut-être que cela ne nous convient pas.


Il existe au moins un moyen de se débarrasser des dépendances nécessaires - il suffit de réécrire le modèle. Supposons que vous ayez des tests de capture d'écran ou des tests d'intégration et qu'il ne soit pas nécessaire de tester l'apparence du composant. Ensuite, il suffit de simplement vérifier les méthodes. Dans ce cas, vous pouvez écrire la configuration comme suit:


 TestBed.configureTestingModule({ declarations: [JokerComponent], providers: [{provide: PARTY_TOKEN, useClass: PartyService}], }) .overrideTemplate(JokerComponent, '') //   ,   .compileComponents(); 

Oui, ce n'est pas pour tout le monde. À l'intérieur de Tinkoff, nous avons convenu de n'utiliser cette approche que dans les cas où il n'est pas nécessaire de vérifier l'affichage du composant. Par exemple, lorsque vous travaillez uniquement avec des données ou lorsque vous communiquez avec la partie. S'il est nécessaire de vérifier comment les données sont transférées vers les composants enfants ou, par exemple, comment les entrées utilisateur sont traitées, cette option ne fonctionnera pas. Si vous avez un tel cas - passez au paragraphe suivant.


Vous pouvez mouiller toutes les dépendances du constructeur


Nous nous sommes déjà familiarisés avec le jeton d'injection , je propose donc de passer immédiatement aux choses sérieuses. Dans l'exemple ci-dessus, j'ai déjà vérifié le service de jeton dans le test. Si vous n'écrivez pas de test d'intégration, cela n'a aucun sens d'appeler les méthodes d'un vrai service, faites simplement une simulation.


ts-mockito , , . Angular « ».


 //    export class MockPartyService extends PartyService { meetFriend(): IFriend { return {} as IFriend; } goToParty() {} toSay(some: string) { console.log(some); } } // ... TestBed.configureTestingModule({ declarations: [JokerComponent, MockComponent], providers: [{provide: PARTY_TOKEN, useClass: MockPartyService}], //    }).compileComponents(); 

C’est tout. .



. , — , — . , :


  • .
  • — , . — .

— . , . — .


Résumé


Angular, . , , «».


, Angular - . HTTP-, , lazy-loading . Angular .

Source: https://habr.com/ru/post/fr480782/


All Articles