49
votes

async / await dans Angular `ngOnInit`

J'évalue actuellement les avantages et les inconvénients de remplacer Angular's resp. RxJSâ € ™ Observable avec simple Promise que je puisse utiliser async et await et obtenir un style de code plus intuitive.

Un de nos scénarios typiques: charger des données dans ngOnInit . En utilisant Observables , nous faisons:

ngOnInit(): void;

Quand je retourne une Promise de getData() place, et que j'utilise async et await , cela devient:

async ngOnInit () {
  const data = await this.service.getData();
  this.data = this.modifyMyData(data);
}

Maintenant, évidemment, Angular ne «saura» pas que ngOnInit est devenu async . Je pense que ce n'est pas un problème: mon application fonctionne toujours comme avant. Mais quand je regarde l'interface OnInit , la fonction n'est évidemment pas déclarée de manière à suggérer qu'elle peut être déclarée async :

ngOnInit () {
  this.service.getData().subscribe(data => {
    this.data = this.modifyMyData(data);
  });
}

Donc - en fin de compte: est-ce raisonnable ce que je fais ici? Ou vais-je rencontrer des problèmes imprévus?


5 commentaires

Selon ce commentaire dans le numéro 17420 : "ce n'est pas un problème pour quelqu'un d'utiliser async ngOnInit , c'est juste une pratique de codage maladroite / déconseillée."


@ConnorsFan J'ai lu exactement ce problème avant d'ouvrir mon message :-) (j'aurais dû le lier). Je ne sais toujours pas si «maladroit» et «non recommandé» ont des raisons objectives, ou si l'équipe Angular veut juste pousser vers le style réactif?


Voici une autre bonne lecture à ce sujet.


@qqilihq - Bonjour, je cherche à faire cette conversion en ce moment. Avez-vous continué et étiez-vous satisfait du résultat? Quelque problème que ce soit...?


@LeeGunn C'est moyen. Je l'ai utilisé ici et là, mais dans l'ensemble très avec parcimonie. La raison (a) étant la gestion des erreurs (expliquée en détail ci-dessous par @Reactgular), (b) le code est un peu moins «imbriqué» (pas de rappels), mais le gain est assez petit, et (c) nous travaillons avec plusieurs observables «réels» dans la base de code (qui se mettent à jour continuellement) - là, await n'aidera pas, et nous nous retrouverions avec deux façons incohérentes (ou confondre les nouveaux membres de l'équipe).


5 Réponses :


36
votes

Ce n'est pas différent de ce que vous aviez auparavant. ngOnInit renverra une promesse et l'appelant ignorera cette promesse. Cela signifie que l'appelant n'attendra pas que tout dans votre méthode se termine avant de continuer. Dans ce cas précis, cela signifie que la vue finira d'être configurée et que la vue pourra être lancée avant que ces this.data soient définies.

C'est la même situation que vous aviez auparavant. L'appelant n'attendrait pas la fin de vos abonnements et lancerait éventuellement l'application avant que ces this.data n'aient été this.data . Si votre vue repose sur des data vous avez probablement une sorte de configuration ngIf pour vous empêcher d'y accéder.

Personnellement, je ne vois pas cela comme une pratique maladroite ou mauvaise tant que vous êtes conscient des implications. Cependant, les ngIf peuvent être fastidieux (ils seraient nécessaires dans les deux cas). Je suis personnellement passé à l'utilisation de résolveurs de routes là où cela a du sens pour éviter cette situation. Les données sont chargées avant la fin de la navigation et je peux savoir que les données sont disponibles avant que la vue ne soit chargée.


1 commentaires

Merci d'avoir mentionné les résolveurs d'itinéraire. Je pense que c'est une meilleure approche de ce problème qui se produit très souvent.



2
votes

Vous pouvez utiliser la fonction rxjs of .

of(this.service.getData());

Convertit la promesse en une séquence observable.


2 commentaires

Merci. Mais mon objectif était d'éviter Observable en faveur de la syntaxe d' await (j'ai développé une forte aversion contre la syntaxe basée sur le rappel, à l'époque :-)).


@qqilihq Je suis d'accord, Promise était moche, puis nous avons await et async et async heureux, maintenant nous avons Observable et nous sommes de retour à moche mais plus utile que Promise .



1
votes

Si getData () renvoie un observable, vous pouvez le changer en promesse et résoudre la promesse avec take (1).

import { take } from 'rxjs/operators';

async ngOnInit(): Promise<any> {
  const data = await this.service.getData().pipe(take(1)).toPromise();
  this.data = this.modifyMyData(data);
}


1 commentaires

async ngOnInit (): Promise <any> a fait l'affaire pour moi.



20
votes

Maintenant, évidemment, Angular ne «saura» pas que ngOnInit est devenu asynchrone. Je pense que ce n'est pas un problème: mon application fonctionne toujours comme avant.

Sémantiquement, il compilera bien et fonctionnera comme prévu, mais la commodité d'écrire async / wait un coût de gestion des erreurs, et je pense que cela devrait être évité.

Regardons ce qui se passe.

Que se passe-t-il lorsqu'une promesse est rejetée:

core.js:6014 ERROR Error: Uncaught (in promise): [object Undefined]
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at :4200/app-module.js:21450:30 [angular]
    at Object.onInvokeTask (core.js:39680) [angular]
    at timer (zone-evergreen.js:2650) [<root>]

Ce qui précède génère la trace de pile suivante:

    public ngOnInit() {
        const p = new Promise((resolver, reject) => {
            setTimeout(() => reject(), 1000);
        });
    }

Nous pouvons clairement voir que l'erreur non ngOnInit été déclenchée par un ngOnInit et également voir quel fichier de code source trouver la ligne de code incriminée.

Que se passe-t-il lorsque nous utilisons async/wait qui est rejeté:

core.js:6014 ERROR Error: Uncaught (in promise):
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at rejected (tslib.es6.js:71) [angular]
    at Object.onInvoke (core.js:39699) [angular]
    at :4200/polyfills.js:4090:36 [angular]
    at Object.onInvokeTask (core.js:39680) [angular]
    at drainMicroTaskQueue (zone-evergreen.js:559) [<root>]

Ce qui précède génère la trace de pile suivante:

    public async ngOnInit() {
        const p = await new Promise((resolver, reject) => reject());
    }

Qu'est-il arrivé? Nous n'avons aucune idée, car la trace de la pile est en dehors du composant.

Néanmoins, vous pourriez être tenté d'utiliser les promesses et simplement éviter d'utiliser async / wait . Voyons donc ce qui se passe si une promesse est rejetée après un setTimeout() .

core.js:6014 ERROR Error: Uncaught (in promise): -1
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at new ZoneAwarePromise (zone-evergreen.js:876) [angular]
    at ExampleComponent.ngOnInit (example.component.ts:44) [angular]
    .....

Nous obtiendrons la trace de pile suivante:

public ngOnInit() {
    const p = new Promise((resolver, reject) => reject(-1));
}

Encore une fois, nous avons perdu le contexte ici et nous ne savons pas où aller pour corriger le bogue.

Les observables souffrent des mêmes effets secondaires de la gestion des erreurs, mais généralement les messages d'erreur sont de meilleure qualité. Si quelqu'un utilise throwError(new Error()) l'objet Error contiendra une trace de pile, et si vous utilisez le HttpModule l'objet Error est généralement un objet de réponse Http qui vous informe de la demande.

Donc la morale de l'histoire ici: attrapez vos erreurs, utilisez des observables quand vous le pouvez et n'utilisez pas async ngOnInit() , car cela reviendra vous hanter comme un bogue difficile à trouver et à corriger.


3 commentaires

Bonne explication.


La partie manquante ici est à quoi ressemblerait le stacktrace lorsque l'erreur se produit dans l' Observable sont-ils meilleurs?


@Peter la plupart des observables qui génèrent une erreur proviennent généralement de HttpClient et sont asynchrones. Ainsi, lorsque l'erreur est lancée, le stacktrace proviendra de l'extérieur de votre code source. Vous pouvez utiliser l'opérateur catchError() pour gérer les erreurs, et si vous voulez juste l'aide de stacktrace, catchError(err => throwError(err)) renverrait l'erreur et ajouterait votre code source à la pile d'appels.



1
votes

J'ai utilisé try catch dans le ngOnInit ():

async ngOnInit() {      
   try {           
       const user = await userService.getUser();
    } catch (error) {           
        console.error(error);       
    }    
} 

Ensuite, vous obtenez une erreur plus descriptive et vous pouvez trouver où se trouve le bogue


0 commentaires