Comment créer un service singleton dans Angular 2?

142

J'ai lu que l'injection lors du démarrage devrait avoir tous les enfants partager la même instance, mais mes composants principaux et d'en-tête (l'application principale comprend le composant d'en-tête et la sortie de routeur) reçoivent chacun une instance distincte de mes services.

J'ai un FacebookService que j'utilise pour passer des appels vers l'API javascript facebook et un UserService qui utilise le FacebookService. Voici mon bootstrap:

bootstrap(MainAppComponent, [ROUTER_PROVIDERS, UserService, FacebookService]);

D'après ma journalisation, il semble que l'appel de démarrage se termine, puis je vois le FacebookService puis UserService en cours de création avant que le code de chacun des constructeurs ne s'exécute, le MainAppComponent, le HeaderComponent et le DefaultComponent:

entrez la description de l'image ici

Jason Goemaat
la source
8
Vous êtes sûr que vous avez pas ajouté UserServiceet FacebookServiceà providersnulle part ailleurs?
Günter Zöchbauer

Réponses:

132

Jason a tout à fait raison! Cela est dû au fonctionnement de l'injection de dépendances. Il est basé sur des injecteurs hiérarchiques.

Il existe plusieurs injecteurs dans une application Angular2:

  • Le root que vous configurez lors du démarrage de votre application
  • Un injecteur par composant. Si vous utilisez un composant dans un autre. L'injecteur de composants est un enfant du composant parent. Le composant d'application (celui que vous spécifiez lors du boostrapping de votre application) a l'injecteur racine comme parent).

Quand Angular2 essaie d'injecter quelque chose dans le constructeur du composant:

  • Il examine l'injecteur associé au composant. S'il en existe un, il l'utilisera pour obtenir l'instance correspondante. Cette instance est créée paresseusement et est un singleton pour cet injecteur.
  • S'il n'y a pas de fournisseur à ce niveau, il examinera l'injecteur parent (et ainsi de suite).

Donc, si vous souhaitez avoir un singleton pour l'ensemble de l'application, vous devez définir le fournisseur au niveau de l'injecteur racine ou de l'injecteur de composant d'application.

Mais Angular2 examinera l'arborescence des injecteurs par le bas. Cela signifie que le fournisseur au niveau le plus bas sera utilisé et la portée de l'instance associée sera ce niveau.

Voir cette question pour plus de détails:

Thierry Templier
la source
1
Merci, cela explique bien. C'était juste contre-intuitif pour moi parce que cela rompt un peu avec le paradigme des composants autonomes d'angular 2. Alors disons que je crée une bibliothèque de composants pour Facebook, mais je veux qu'ils utilisent tous un service singleton. Peut-être qu'il y a un composant pour afficher la photo de profil de l'utilisateur connecté, et un autre pour publier. L'application qui utilise ces composants doit inclure le service Facebook en tant que fournisseur même si elle n'utilise pas le service lui-même? Je pourrais simplement retourner un service avec une méthode 'getInstance ()' qui gère le singleton du service réel ...
Jason Goemaat
@tThierryTemplier Comment ferais-je le contraire, j'ai une classe de service commune que je veux injecter dans plusieurs composants mais instancier une nouvelle instance à chaque fois (les options des fournisseurs / directives sont censées être obsolètes et supprimées dans la prochaine version)
Boban Stojanovski
Désolé d'être si stupide mais je ne sais pas comment créer un service singleton, pouvez-vous expliquer plus en détail?
gyozo kudor
Donc, pour travailler avec une seule instance d'un service, doit-il être déclaré comme fournisseur dans app.module.ts ou dans app.component.ts?
user1767316
Déclarer chaque service dans app.module.ts uniquement, a fait le travail pour moi.
user1767316
142

Mise à jour (Angular 6 +)

La méthode recommandée pour créer un service singleton a changé. Il est maintenant recommandé de spécifier dans le @Injectabledécorateur sur le service qu'il doit être fourni dans la 'racine'. Cela a beaucoup de sens pour moi et il n'est plus du tout nécessaire de lister tous les services fournis dans vos modules. Vous importez simplement les services lorsque vous en avez besoin et ils s'enregistrent au bon endroit. Vous pouvez également spécifier un module afin qu'il ne soit fourni que si le module est importé.

@Injectable({
  providedIn: 'root',
})
export class ApiService {
}

Mise à jour (Angular 2)

Avec NgModule, la façon de le faire maintenant, je pense, est de créer un 'CoreModule' avec votre classe de service à l'intérieur et de répertorier le service dans les fournisseurs du module. Ensuite, vous importez le module de base dans votre module d'application principal qui fournira une instance à tous les enfants qui demandent cette classe dans leurs constructeurs:

CoreModule.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ApiService } from './api.service';

@NgModule({
    imports: [
        CommonModule
    ],
    exports: [ // components that we want to make available
    ],
    declarations: [ // components for use in THIS module
    ],
    providers: [ // singleton services
        ApiService,
    ]
})
export class CoreModule { }

AppModule.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';

@NgModule({
    declarations: [ AppComponent ],
    imports: [
        CommonModule,
        CoreModule // will provide ApiService
    ],
    providers: [],
    bootstrap: [ AppComponent ]
})
export class AppModule { }

Réponse originale

Si vous répertoriez un fournisseur dans bootstrap(), vous n'avez pas besoin de le répertorier dans votre décorateur de composants:

import { ApiService } from '../core/api-service';

@Component({
    selector: 'main-app',
    templateUrl: '/views/main-app.html',
    // DO NOT LIST PROVIDERS HERE IF THEY ARE IN bootstrap()!
    // (unless you want a new instance)
    //providers: [ApiService]
})
export class MainAppComponent {
    constructor(private api: ApiService) {}
}

En fait, lister votre classe dans «fournisseurs» crée une nouvelle instance de celle-ci, si un composant parent la répertorie déjà, les enfants n'en ont pas besoin, et s'ils le font, ils obtiendront une nouvelle instance.

Jason Goemaat
la source
1
À partir de maintenant (janvier 2017), vous listeriez le service sous [providers]dans votre fichier de module, pas dans bootstrap(), non?
Jason Swett
5
Pourquoi ne pas mettre ApiServicedans AppModule« s providers, et éviter ainsi la nécessité d' une CoreModule? (Je ne suis pas sûr que ce soit ce que suggère @JasonSwett.)
Jan Aagaard
1
@JasonGoemaat Pouvez-vous ajouter l'exemple comment vous l'utilisez dans le composant? Nous pourrions importer le ApiServicedessus, mais pourquoi se donner la peine de le mettre dans le tableau des fournisseurs du CoreModule, puis importer celui-ci dans app.module ... il n'a pas encore cliqué pour moi.
hogan
Ainsi, placer le service sur le fournisseur de module fournira un singleton pour ce module. Et mettre le service sur les fournisseurs de composants créera une nouvelle instance pour chaque instance du composant? Est-ce correct?
BrunoLM
@BrunoLM J'ai créé une application de test pour montrer ce qui se passe. Il est intéressant de noter que même si elle TestServiceest spécifiée à la fois dans les modules principaux et dans les modules d'application, les instances ne sont pas créées pour les modules car elles sont fournies par le composant de sorte que angulaire ne monte jamais aussi haut dans l'arborescence des injecteurs. Donc, si vous fournissez un service dans votre module et que vous ne l'utilisez jamais, une instance ne semble pas être créée.
Jason Goemaat
24

Je sais qu'angular a des injecteurs hiérarchiques comme l'a dit Thierry.

Mais j'ai une autre option ici au cas où vous trouveriez un cas d'utilisation où vous ne voudriez pas vraiment l'injecter au parent.

Nous pouvons y parvenir en créant une instance du service, et en fournissant toujours cela.

import { provide, Injectable } from '@angular/core';
import { Http } from '@angular/core'; //Dummy example of dependencies

@Injectable()
export class YourService {
  private static instance: YourService = null;

  // Return the instance of the service
  public static getInstance(http: Http): YourService {
    if (YourService.instance === null) {
       YourService.instance = new YourService(http);
    }
    return YourService.instance;
  }

  constructor(private http: Http) {}
}

export const YOUR_SERVICE_PROVIDER = [
  provide(YourService, {
    deps: [Http],
    useFactory: (http: Http): YourService => {
      return YourService.getInstance(http);
    }
  })
];

Et puis sur votre composant, vous utilisez votre méthode de fourniture personnalisée.

@Component({
  providers: [YOUR_SERVICE_PROVIDER]
})

Et vous devriez avoir un service singleton sans dépendre des injecteurs hiérarchiques.

Je ne dis pas que c'est une meilleure façon, c'est juste au cas où quelqu'un aurait un problème où les injecteurs hiérarchiques ne sont pas possibles.

Joël Almeida
la source
1
Devrait-il SHARE_SERVICE_PROVIDERêtre YOUR_SERVICE_PROVIDERdans le composant? De plus, je suppose que l'importation du fichier de service est nécessaire comme d'habitude et que le constructeur aura toujours un paramètre de type «YourService», non? J'aime cela, je pense, vous permet de garantir un singleton et de ne pas avoir à vous assurer que le service est fourni dans la hiérarchie. Cela permet également aux composants individuels d'obtenir leur propre copie en répertoriant simplement le service au providerslieu du fournisseur singleton, n'est-ce pas?
Jason Goemaat
@JasonGoemaat vous avez raison. Modifié cela. Exactement, vous le faites exactement de la même manière dans le constructeur et sur les fournisseurs de ce composant que vous ajoutez YOUR_SERVICE_PROVIDER. Oui, tous les composants obtiendront la même instance en l'ajoutant simplement dans les fournisseurs.
Joel Almeida
Au moment où un développeur utilise cela, il devrait se demander "pourquoi est-ce que j'utilise même Angular si je ne vais pas utiliser les mécanismes fournis par Angular pour cette situation?" Fournir le service au niveau de l'application devrait être suffisant pour chaque situation dans le contexte d'Angular.
RyNo
1
+1 Bien que ce soit un moyen de créer des services singleton, il sert très bien de moyen de créer un service multiton en changeant simplement la instancepropriété en une carte d'instances clé-valeur
Precastic
1
@RyNo Je peux imaginer une application qui ne nécessite pas de service pour chaque route, ou un module réutilisable qui veut une instance statique et veut utiliser la même instance avec tous les autres modules qui l'utilisent. Peut-être quelque chose qui crée une connexion Web socket avec le serveur et traite les messages. Peut-être que seules quelques routes dans l'application voudraient l'utiliser, donc pas besoin de créer une instance de service et une connexion de socket Web lorsque l'application démarre si elle n'est pas nécessaire. Vous pouvez programmer autour de cela pour que les composants «initient» le service partout où il est utilisé, mais des singletons à la demande seraient utiles.
Jason Goemaat
16

La syntaxe a été modifiée. Vérifiez ce lien

Les dépendances sont des singletons dans le cadre d'un injecteur. Dans l'exemple ci-dessous, une seule instance de HeroService est partagée entre HeroesComponent et ses enfants HeroListComponent.

Étape 1. Créez une classe singleton avec le décorateur @Injectable

@Injectable()
export class HeroService {
  getHeroes() { return HEROES;  }
}

Étape 2. Injecter dans le constructeur

export class HeroListComponent { 
  constructor(heroService: HeroService) {
    this.heroes = heroService.getHeroes();
  }

Étape 3. Enregistrer le fournisseur

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing,
    HttpModule,
    JsonpModule
  ],
  declarations: [
    AppComponent,
    HeroesComponent,
    routedComponents
  ],
  providers: [
    HeroService
  ],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule { }
Manish Jain
la source
et si ma Injectableclasse n'est pas un service et contient juste des staticchaînes pour une utilisation globale?
Syed Ali Taqi
2
comme ces fournisseurs: [{provide: 'API_URL', useValue: ' coolapi.com '}]
Whisher
7

L'ajout d'un @Injectabledécorateur au service ET son enregistrement en tant que fournisseur dans le module racine en fera un singleton.

développeur
la source
Dites-moi simplement si je le comprends. Si je fais ce que tu as dit, d'accord, ce sera un singleton. Si, en plus de cela, le service est également un fournisseur dans un autre module, ce ne sera plus un singleton, non? À cause de la hiérarchie.
heringer
1
Et n'enregistrez pas le fournisseur dans @Component décorateur des pages.
Laura
@Laura. Dois-je toujours l'importer dans des composants qui utilisent réellement le service?
Mark
@Mark Oui, vous devez l'importer, puis il vous suffit de le déclarer de la manière constructorsuivante: import { SomeService } from '../../services/some/some'; @Component({ selector: 'page-selector', templateUrl: 'page.html', }) export class SomePage { constructor( public someService: SomeService ) { }
Laura
6

cela semble bien fonctionner pour moi

@Injectable()
export class MyStaticService {
  static instance: MyStaticService;

  constructor() {
    return MyStaticService.instance = MyStaticService.instance || this;
  }
}
Capitaine Harlock
la source
8
J'appellerais cela un anti-pattern Angular2. Fournissez le service correctement et Angular2 injectera toujours la même instance. Voir aussi stackoverflow.com/questions/12755539/…
Günter Zöchbauer
3
@ Günter Zöchbauer, pourrait s'il vous plaît donner quelques conseils concernant "Fournissez le service correctement et Angular2 injectera toujours la même instance." ? Parce que ce n'est pas clair et que je n'ai pas pu trouver d'informations utiles sur Google.
nowiko
Je viens de publier cette réponse qui pourrait vous aider avec votre question stackoverflow.com/a/38781447/217408 (voir aussi le lien ici)
Günter Zöchbauer
2
C'est parfait. Vous devez utiliser propre injection angulaires dépendance, mais il n'y a pas de mal à utiliser ce modèle pour être absolument certain que votre service est un singleton quand vous attendez qu'il soit. Cela permet potentiellement de gagner beaucoup de temps à la recherche de bogues simplement parce que vous injectez le même service à deux endroits différents.
PaulMolloy
J'ai utilisé ce modèle pour vérifier que le problème auquel je faisais face était dû au fait que le service n'était pas unique
hr-tis
5

Voici un exemple de travail avec Angular version 2.3. Appelez simplement le constructeur du service à la manière de ce constructeur (private _userService: UserService). Et cela créera un singleton pour l'application.

user.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { Subject }    from 'rxjs/Subject';
import { User } from '../object/user';


@Injectable()
export class UserService {
    private userChangedSource;
    public observableEvents;
    loggedUser:User;

    constructor() {
       this.userChangedSource = new Subject<any>();
       this.observableEvents = this.userChangedSource.asObservable();
    }

    userLoggedIn(user:User) {
        this.loggedUser = user;
        this.userChangedSource.next(user);
    }

    ...
}

app.component.ts

import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { UserService } from '../service/user.service';
import { User } from '../object/user';

@Component({
    selector: 'myApp',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
    loggedUser:User;

    constructor(private _userService:UserService) { 
        this._userService.observableEvents.subscribe(user => {
                this.loggedUser = user;
                console.log("event triggered");
        });
    }
    ...
}
David Dehghan
la source
4

A singleton serviceest un service pour lequel une seule instance existe dans une application.

Il existe (2) façons de fournir un service singleton pour votre application.

  1. utiliser la providedInpropriété, ou

  2. fournir le module directement dans le AppModulede l'application

Utilisation de providedIn

À partir d'Angular 6.0, la meilleure façon de créer un service singleton est de définir la providedInracine sur le @Injectable()décorateur du service . Cela indique à Angular de fournir le service à la racine de l'application.

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UserService {
}

Tableau des fournisseurs NgModule

Dans les applications créées avec des versions Angular antérieures à 6.0, les services sont des tableaux de fournisseurs NgModule enregistrés comme suit:

@NgModule({
  ...
  providers: [UserService],
  ...
})

S'il NgModules'agissait de la racine AppModule, le UserService serait un singleton et disponible dans toute l'application. Bien que vous puissiez le voir codé de cette façon, utiliser la providedInpropriété du @Injectable()décorateur sur le service lui-même est préférable à partir d'Angular 6.0 car cela rend vos services transformables en arborescence.

AbolfazlR
la source
3

Vous pouvez utiliser useValuedans les fournisseurs

import { MyService } from './my.service';

@NgModule({
...
  providers: [ { provide: MyService, useValue: new MyService() } ],
...
})
Roman Rhrn Nesterov
la source
5
useValuen'est pas lié au singleton. Usevalue est juste de passer une valeur au lieu d'un Type( useClass) que DI appelle newou useFactoryoù une fonction est passée qui retourne la valeur lorsqu'elle est appelée par DI. Angular DI gère automatiquement une seule instance par fournisseur. Ne le fournissez qu'une seule fois et vous avez un singleton. Désolé, je dois voter contre car ce n'est qu'une information invalide: - /
Günter Zöchbauer
3

Depuis Angular @ 6, vous pouvez avoir providedIndans un fichier Injectable.

@Injectable({
  providedIn: 'root'
})
export class UserService {

}

Consultez les documents ici

Il existe deux façons de faire d'un service un singleton dans Angular:

  1. Déclarez que le service doit être fourni dans la racine de l'application.
  2. Incluez le service dans l'AppModule ou dans un module importé uniquement par l'AppModule.

À partir d'Angular 6.0, la meilleure façon de créer un service singleton est de spécifier sur le service qu'il doit être fourni dans la racine de l'application. Cela se fait en définissant providedIn sur root sur le décorateur @Injectable du service:

sabithpocker
la source
C'est bien, mais vous pouvez également rencontrer des problèmes inattendus avec des variables absentes qui pourraient être résolus en déclarant certains éléments public static.
cjbarth
2

Déclarez simplement votre service en tant que fournisseur dans app.module.ts uniquement.

Cela a fait le travail pour moi.

providers: [Topic1Service,Topic2Service,...,TopicNService],

puis instanciez-le en utilisant un paramètre privé du constructeur:

constructor(private topicService: TopicService) { }

ou puisque si votre service est utilisé à partir de html, l'option -prod réclamera:

Property 'topicService' is private and only accessible within class 'SomeComponent'.

ajoutez un membre pour votre service et remplissez-le avec l'instance reçue dans le constructeur:

export class SomeComponent {
  topicService: TopicService;
  constructor(private topicService: TopicService) { 
    this.topicService= topicService;
  }
}
user1767316
la source
0
  1. Si vous souhaitez rendre le service singleton au niveau de l'application, vous devez le définir dans app.module.ts

    fournisseurs: [MyApplicationService] (vous pouvez également définir la même chose dans le module enfant pour le rendre spécifique à ce module)

    • N'ajoutez pas ce service dans le fournisseur qui crée une instance pour ce composant qui rompt le concept de singleton, injectez simplement via le constructeur.
  2. Si vous souhaitez définir un service singleton au niveau du service de création de composant, ajoutez ce service dans app.module.ts et ajoutez un tableau de fournisseurs à l'intérieur d'un composant spécifique, comme indiqué dans l'extrait ci-dessous.

    @Component ({selector: 'app-root', templateUrl: './test.component.html', styleUrls: ['./test.component.scss'], fournisseurs: [TestMyService]})

  3. Angular 6 fournit une nouvelle façon d'ajouter un service au niveau de l'application. Au lieu d'ajouter une classe de service au tableau des fournisseurs [] dans AppModule, vous pouvez définir la configuration suivante dans @Injectable ():

    @Injectable ({providedIn: 'root'}) classe d'exportation MyService {...}

La "nouvelle syntaxe" offre cependant un avantage: les services peuvent être chargés paresseusement par Angular (dans les coulisses) et le code redondant peut être supprimé automatiquement. Cela peut améliorer les performances et la vitesse de chargement - bien que cela ne s'applique vraiment qu'aux services et applications plus volumineux en général.

Vijay Barot
la source
0

En plus des excellentes réponses ci-dessus, il peut y avoir quelque chose qui manque si les éléments de votre singleton ne se comportent toujours pas comme un singleton. J'ai rencontré le problème en appelant une fonction publique sur le singleton et en constatant qu'elle utilisait les mauvaises variables. Il s'avère que le problème était qu'il n'était thispas garanti d'être lié au singleton pour les fonctions publiques du singleton. Cela peut être corrigé en suivant les conseils ici , comme ceci:

@Injectable({
  providedIn: 'root',
})
export class SubscriptableService {
  public serviceRequested: Subject<ServiceArgs>;
  public onServiceRequested$: Observable<ServiceArgs>;

  constructor() {
    this.serviceRequested = new Subject<ServiceArgs>();
    this.onServiceRequested$ = this.serviceRequested.asObservable();

    // save context so the singleton pattern is respected
    this.requestService = this.requestService.bind(this);
  }

  public requestService(arg: ServiceArgs) {
    this.serviceRequested.next(arg);
  }
}

Alternativement, vous pouvez simplement déclarer les membres de la classe comme public staticau lieu de public, alors le contexte n'aura pas d'importance, mais vous devrez y accéder comme SubscriptableService.onServiceRequested$au lieu d'utiliser l'injection de dépendances et d'y accéder via this.subscriptableService.onServiceRequested$.

cjbarth
la source
0

Services aux parents et aux enfants

J'avais des problèmes avec un service parental et son enfant utilisant des instances différentes. Pour forcer l'utilisation d'une instance, vous pouvez alias le parent en référence à l'enfant dans vos fournisseurs de modules d'application. Le parent ne pourra pas accéder aux propriétés de l'enfant, mais la même instance sera utilisée pour les deux services. https://angular.io/guide/dependency-injection-providers#alias-class-providers

app.module.ts

providers: [
  ChildService,
  // Alias ParentService w/ reference to ChildService
  { provide: ParentService, useExisting: ChildService}
]

Services utilisés par les composants en dehors de la portée des modules de votre application

Lors de la création d'une bibliothèque composée d'un composant et d'un service, j'ai rencontré un problème où deux instances seraient créées. Un par mon projet Angular et un par le composant à l'intérieur de ma bibliothèque. Le correctif:

my-outside.component.ts

@Component({...})
export class MyOutsideComponent {
  @Input() serviceInstance: MyOutsideService;
  ...
}

my-inside.component.ts

  constructor(public myService: MyOutsideService) { }

my-inside.component.hmtl

<app-my-outside [serviceInstance]="myService"></app-my-outside>
Scott VandenToorn
la source
Vouliez-vous répondre à votre propre question? Si tel est le cas, vous pouvez séparer la réponse en une réponse formelle sur StackOverflow, en la coupant / collant dans le champ Réponse une fois la question publiée.
ryanwebjackson