Come posso creare un servizio singleton in Angular 2?


142

Ho letto che l'iniezione al bootstrap dovrebbe far sì che tutti i bambini condividano la stessa istanza, ma i miei componenti principale e header (l'app principale include componente header e router-outlet) ottengono ciascuno un'istanza separata dei miei servizi.

Ho un servizio Facebook che utilizzo per effettuare chiamate all'API javascript di Facebook e un servizio utente che utilizza il servizio Facebook. Ecco il mio bootstrap:

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

Dalla mia registrazione sembra che la chiamata bootstrap sia terminata, quindi vedo FacebookService e UserService che vengono creati prima dell'esecuzione del codice in ciascuno dei costruttori, MainAppComponent, HeaderComponent e DefaultComponent:

inserisci qui la descrizione dell'immagine


8
Stai sicuro che avete , non aggiunto UserServicee FacebookServicedi providersaltrove?
Günter Zöchbauer,

Risposte:


132

Jason ha perfettamente ragione! È causato dal modo in cui funziona l'iniezione di dipendenza. Si basa su iniettori gerarchici.

Esistono diversi iniettori all'interno di un'applicazione Angular2:

  • Quello principale che si configura all'avvio dell'applicazione
  • Un iniettore per componente. Se si utilizza un componente all'interno di un altro. L'iniettore componente è un figlio del componente principale uno. Il componente dell'applicazione (quello che specifichi quando boostra la tua applicazione) ha l'iniettore radice come genitore).

Quando Angular2 tenta di iniettare qualcosa nel costruttore del componente:

  • Esamina l'iniettore associato al componente. Se ce n'è uno corrispondente, lo utilizzerà per ottenere l'istanza corrispondente. Questa istanza è pigramente creata ed è un singleton per questo iniettore.
  • Se non esiste un provider a questo livello, esaminerà l'iniettore principale (e così via).

Quindi, se si desidera avere un singleton per l'intera applicazione, è necessario che il provider sia definito a livello dell'iniettore principale o dell'iniettore del componente dell'applicazione.

Ma Angular2 guarderà l'albero dell'iniettore dal basso. Ciò significa che verrà utilizzato il provider al livello più basso e l'ambito dell'istanza associata sarà questo livello.

Vedi questa domanda per maggiori dettagli:


1
Grazie, questo lo spiega bene. Per me è stato controintuitivo perché questo in qualche modo si rompe con il paradigma del componente autonomo di Angular 2. Quindi diciamo che sto creando una libreria di componenti per Facebook, ma voglio che tutti utilizzino un servizio singleton. Forse c'è un componente per mostrare l'immagine del profilo dell'utente che ha effettuato l'accesso e un altro per pubblicare. L'app che utilizza tali componenti deve includere il servizio Facebook come fornitore anche se non utilizza il servizio stesso? Potrei semplicemente restituire un servizio con un metodo 'getInstance ()' che gestisce il singleton del servizio reale ...
Jason Goemaat

@tThierryTemplier Come farei il contrario, ho una classe di servizio comune che voglio iniettare in più componenti ma istanziare una nuova istanza ogni volta (le opzioni di provider / direttive dovrebbero essere deprecate e rimosse nella prossima versione)
Boban Stojanovski

Scusa se sono così stupido ma non è chiaro per me come si crea un servizio singleton, puoi spiegarlo in modo più dettagliato?
Gyozo Kudor,

Quindi, per funzionare con una singola istanza di un servizio, dovrebbe essere dichiarato come provider in app.module.ts o in app.component.ts?
user1767316,

Dichiarare ogni servizio solo in app.module.ts, ha fatto il lavoro per me.
user1767316,

142

Aggiornamento (Angolare 6 +)

Il modo consigliato per creare un servizio singleton è cambiato. Si consiglia ora di specificare nel @Injectabledecoratore sul servizio che dovrebbe essere fornito in 'root'. Questo ha molto senso per me e non è più necessario elencare tutti i servizi forniti nei moduli. Basta importare i servizi quando ne hai bisogno e si registrano nel posto giusto. È inoltre possibile specificare un modulo in modo che venga fornito solo se il modulo viene importato.

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

Aggiornamento (Angolare 2)

Con NgModule, il modo per farlo ora penso sia creare un 'CoreModule' con la tua classe di servizio al suo interno ed elencare il servizio nei provider del modulo. Quindi importa il modulo principale nel modulo dell'app principale che fornirà un'istanza a tutti i bambini che richiedono quella classe nei loro costruttori:

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 { }

Risposta originale

Se elenchi un fornitore bootstrap(), non è necessario elencarlo nel decoratore dei componenti:

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

In effetti, elencare la tua classe in "provider" ne crea una nuova istanza, se qualche componente genitore la elenca già allora i figli non ne hanno bisogno e se lo fanno otterranno una nuova istanza.


1
A partire da ora (gennaio 2017), avresti elencato il servizio [providers]nel file del tuo modulo, non in bootstrap(), giusto?
Jason Swett l'

5
Perché non mettere ApiServicein AppModule's providers, e quindi evitare la necessità di CoreModule? (Non sono sicuro che cosa stia suggerendo @JasonSwett.)
Jan Aagaard,

1
@JasonGoemaat Puoi aggiungere l'esempio come lo usi nel componente? Potremmo importare ApiServicein alto, ma perché poi preoccuparsi di metterlo nella matrice dei provider del CoreModule e quindi importare quello in app.module ... non ha ancora cliccato per me.
hogan,

Quindi, mettendo il servizio sul provider del modulo fornirà un singleton per quel modulo. E mettere il servizio sui provider di componenti creerà una nuova istanza per ogni istanza del componente? È giusto?
BrunoLM,

@BrunoLM Ho creato un'app di prova per aiutare a mostrare cosa sta succedendo. È interessante notare che, sebbene TestServicesia specificato in entrambi i moduli core e app, le istanze non vengono create per i moduli perché sono fornite dal componente in modo che l'angolo non raggiunga mai così in alto l'albero dell'iniettore. Quindi se fornisci un servizio nel tuo modulo e non lo usi mai, un'istanza non sembra essere creata.
Jason Goemaat,

24

So che angular ha iniettori gerarchici come ha detto Thierry.

Ma ho un'altra opzione qui nel caso in cui trovi un caso d'uso in cui non vuoi davvero iniettarlo dal genitore.

Possiamo ottenerlo creando un'istanza del servizio e, a condizione che, lo restituisca sempre.

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

E poi sul tuo componente usi il tuo metodo di fornitura personalizzato.

@Component({
  providers: [YOUR_SERVICE_PROVIDER]
})

E dovresti avere un servizio singleton senza dipendere dagli iniettori gerarchici.

Non sto dicendo che questo è un modo migliore, è solo nel caso in cui qualcuno abbia un problema in cui gli iniettori gerarchici non sono possibili.


1
Dovrebbe SHARE_SERVICE_PROVIDERessere YOUR_SERVICE_PROVIDERnel componente? Inoltre suppongo che l'importazione del file di servizio sia necessaria come di consueto e il costruttore disporrà comunque di un parametro di tipo "YourService", giusto? Mi piace questo, penso, ti permette di garantire un singleton e non devi assicurarti che il servizio sia fornito nella gerarchia. Inoltre, consente ai singoli componenti di ottenere la propria copia semplicemente elencando il servizio al providersposto del provider singleton, giusto?
Jason Goemaat,

@JasonGoemaat hai ragione. Modificato quello. Esatto, lo fai esattamente allo stesso modo nel costruttore e sui fornitori di quel componente che aggiungi YOUR_SERVICE_PROVIDER. Sì, tutti i componenti otterranno la stessa istanza semplicemente aggiungendola ai provider.
Joel Almeida,

Nel momento in cui uno sviluppatore lo usa, dovrebbero chiedersi "perché sto usando Angular se non ho intenzione di usare i meccanismi che Angular prevede per questa situazione?" La fornitura del servizio a livello di applicazione dovrebbe essere sufficiente per ogni situazione nel contesto di Angular.
RyNo

1
+1 Anche se questo è un modo per creare servizi singleton, serve molto bene come modo di creare un servizio multiton semplicemente cambiando la instanceproprietà in una mappa di valori-chiave delle istanze
Precastic

1
@RyNo Posso immaginare un'app che non richiede un servizio per ogni route o un modulo riutilizzabile che desidera un'istanza statica e desidera utilizzare la stessa istanza con qualsiasi altro modulo che la utilizza. Forse qualcosa che crea una connessione socket Web al server ed elabora i messaggi. Forse solo alcune rotte nell'app vorrebbero utilizzarlo, quindi non è necessario creare un'istanza del servizio e una connessione socket Web all'avvio dell'app se non è necessaria. È possibile programmare in modo che i componenti "inizino" il servizio ovunque venga utilizzato, ma su richiesta sarebbe utile un singolo.
Jason Goemaat,

16

La sintassi è stata modificata. Controlla questo link

Le dipendenze sono singleton nell'ambito di un iniettore. Nell'esempio seguente, una singola istanza di HeroService è condivisa tra i componenti HeroesComponent e HeroListComponent.

Passaggio 1. Creare una classe singleton con @Injectable decorator

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

Passaggio 2. Iniettare nel costruttore

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

Passaggio 3. Registrare il provider

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing,
    HttpModule,
    JsonpModule
  ],
  declarations: [
    AppComponent,
    HeroesComponent,
    routedComponents
  ],
  providers: [
    HeroService
  ],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule { }

cosa succede se la mia Injectableclasse non è un servizio e contiene solo staticstringhe per l'uso globale?
Syed Ali Taqi,

2
come questo provider: [{fornire: 'API_URL', useValue: ' coolapi.com '}]
Whisher

7

L'aggiunta di @Injectabledecoratore al servizio e la sua registrazione come fornitore nel modulo radice lo renderà un singleton.


Dimmi solo se lo capisco. Se faccio quello che hai detto, ok, sarà un singleton. Se, oltre a ciò, il servizio è anche un provider in un altro modulo, non sarà più un singleton, giusto? A causa della gerarchia.
Heringer,

1
E non registrare il provider in @Component decorator delle pagine.
Laura,

@Laura. Devo ancora importarlo in componenti che utilizzano effettivamente il servizio?
Segna il

@Mark Sì, è necessario importarlo, quindi è necessario dichiararlo in constructorquesto modo: import { SomeService } from '../../services/some/some'; @Component({ selector: 'page-selector', templateUrl: 'page.html', }) export class SomePage { constructor( public someService: SomeService ) { }
Laura

6

questo sembra funzionare bene per me

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

  constructor() {
    return MyStaticService.instance = MyStaticService.instance || this;
  }
}

8
Lo definirei un anti-pattern Angular2. Fornire il servizio correttamente e Angular2 inietterà sempre la stessa istanza. Vedere anche stackoverflow.com/questions/12755539/...
Günter Zöchbauer

3
@ Günter Zöchbauer, potresti dare qualche consiglio in merito a "Fornire il servizio correttamente e Angular2 inietterà sempre la stessa istanza". ? Perché non è chiaro e non sono riuscito a trovare alcuna informazione utile su Google.
nowiko,

Ho appena pubblicato questa risposta che potrebbe aiutarti con la tua domanda stackoverflow.com/a/38781447/217408 (vedi anche il link lì)
Günter Zöchbauer,

2
Questo è perfetto. Si consiglia di utilizzare angolari propria iniezione di dipendenza, ma non c'è nulla di male a utilizzare questo modello per essere assolutamente certi che il vostro servizio è un Singleton quando ci si aspetta che sia. Risparmia potenzialmente molto tempo a caccia di bug solo perché inietti lo stesso servizio in due luoghi diversi.
PaulMolloy,

Ho usato questo schema per accertare che il problema che stavo affrontando era dovuto al fatto che il servizio non era singleton
hr-tis

5

Ecco un esempio funzionante con Angular versione 2.3. Chiama semplicemente il costruttore del servizio come questo costruttore (private _userService: UserService). E creerà un singleton per l'app.

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

4

A singleton serviceè un servizio per il quale esiste solo un'istanza in un'app.

Esistono (2) modi per fornire un servizio singleton per la tua applicazione.

  1. utilizzare la providedInproprietà o

  2. fornire il modulo direttamente AppModulenell'applicazione

Usando fornitoIn

A partire da Angular 6.0, il modo preferito per creare un servizio singleton è impostare il providedInroot sul @Injectable()decoratore del servizio . Ciò indica ad Angular di fornire il servizio nella radice dell'applicazione.

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

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

Array di provider NgModule

Nelle app create con versioni angolari precedenti alla 6.0, i servizi sono array di provider NgModule registrati come segue:

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

Se questo NgModulefosse il root AppModule, UserService sarebbe un singleton e disponibile in tutta l'app. Anche se puoi vederlo codificato in questo modo, è preferibile utilizzare la providedInproprietà del @Injectable()decoratore sul servizio stesso a partire da Angular 6.0 in quanto rende i tuoi servizi agitabili.


3

È possibile utilizzare useValuenei provider

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

@NgModule({
...
  providers: [ { provide: MyService, useValue: new MyService() } ],
...
})

5
useValuenon è correlato a singleton. Il valore d'uso è solo quello di passare un valore invece di un Type( useClass) che DI chiama newo useFactorydove viene passata una funzione che restituisce il valore quando viene chiamato da DI. DI angolare mantiene automaticamente una singola istanza per provider. Forniscilo una sola volta e avrai un singleton. Siamo spiacenti, devo effettuare il
downgrade del voto

3

Da Angular @ 6, puoi avere providedInin un Injectable.

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

}

Controlla i documenti qui

Esistono due modi per rendere un servizio un singleton in angolare:

  1. Dichiarare che il servizio deve essere fornito nella radice dell'applicazione.
  2. Includere il servizio in AppModule o in un modulo importato solo da AppModule.

A partire da Angular 6.0, il modo preferito per creare un servizio singleton è specificare sul servizio che deve essere fornito nella radice dell'applicazione. Questo viene fatto impostando fornitoIn a root sul decoratore @Injectable del servizio:


Questo va bene, ma potresti anche avere problemi imprevisti con le variabili non presenti che potrebbero essere risolti dichiarando alcuni elementi public static.
cjbarth,

2

Dichiarare il proprio servizio come fornitore solo in app.module.ts.

Ha fatto il lavoro per me.

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

quindi o istanziarlo utilizzando un parametro privato del costruttore:

constructor(private topicService: TopicService) { }

o poiché se il tuo servizio viene utilizzato da HTML, l'opzione -prod reclamerà:

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

aggiungi un membro per il tuo servizio e riempilo con l'istanza ricevuta nel costruttore:

export class SomeComponent {
  topicService: TopicService;
  constructor(private topicService: TopicService) { 
    this.topicService= topicService;
  }
}

0
  1. Se si desidera rendere il servizio singleton a livello dell'applicazione, è necessario definirlo in app.module.ts

    provider: [MyApplicationService] (puoi definire lo stesso anche nel modulo figlio per renderlo specifico per quel modulo)

    • Non aggiungere questo servizio nel provider che crea un'istanza per quel componente che interrompe il concetto singleton, basta iniettare attraverso il costruttore.
  2. Se si desidera definire il servizio singleton a livello di componente creare un servizio, aggiungere quel servizio in app.module.ts e aggiungere l'array provider all'interno del componente specifico come mostrato nello snipet di seguito.

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

  3. Angular 6 offre un nuovo modo per aggiungere servizi a livello di applicazione. Invece di aggiungere una classe di servizio all'array provider [] in AppModule, è possibile impostare la seguente configurazione in @Injectable ():

    Classe di esportazione @Injectable ({paidIn: 'root'}) MyService {...}

La "nuova sintassi" offre tuttavia un vantaggio: i servizi possono essere caricati pigramente da Angular (dietro le quinte) e il codice ridondante può essere rimosso automaticamente. Ciò può portare a prestazioni e velocità di caricamento migliori, anche se ciò si applica solo a servizi e app più grandi in generale.


0

Oltre alle risposte eccellenti di cui sopra, potrebbe esserci qualcos'altro che manca se le cose nel tuo singleton non si comportano ancora come singleton. Mi sono imbattuto nel problema quando ho chiamato una funzione pubblica sul singleton e ho scoperto che stava usando le variabili sbagliate. Si scopre che il problema era che thisnon è garantito che fosse associato al singleton per qualsiasi funzione pubblica nel singleton. Questo può essere corretto seguendo i consigli qui , in questo modo:

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

In alternativa, puoi semplicemente dichiarare i membri della classe come public staticinvece di public, quindi il contesto non avrà importanza, ma dovrai accedervi come SubscriptableService.onServiceRequested$invece di usare l'iniezione di dipendenza e accedervi tramite this.subscriptableService.onServiceRequested$.


0

Servizi per genitori e figli

Ho avuto problemi con un servizio di genitore e suo figlio utilizzando istanze diverse. Per forzare l'utilizzo di un'istanza, puoi alias il genitore con riferimento al figlio nei provider del modulo dell'app. Il genitore non sarà in grado di accedere alle proprietà del figlio, ma la stessa istanza verrà utilizzata per entrambi i servizi. https://angular.io/guide/dependency-injection-providers#aliased-class-providers

app.module.ts

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

Servizi utilizzati da componenti al di fuori dell'ambito dei moduli dell'app

Durante la creazione di una libreria composta da un componente e un servizio, ho riscontrato un problema in cui venivano create due istanze. Uno dal mio progetto angolare e uno dal componente all'interno della mia biblioteca. La correzione:

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>

Intendevi rispondere alla tua domanda? In tal caso, puoi separare la risposta in una risposta formale su StackOverflow, tagliandola / incollandola nel campo Risposta dopo aver pubblicato la domanda.
Ryanwebjackson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.