¿Cómo creo un servicio singleton en Angular 2?

142

He leído que la inyección cuando el bootstrapping debería hacer que todos los niños compartan la misma instancia, pero mis componentes principal y de encabezado (la aplicación principal incluye el componente de encabezado y la salida del enrutador) obtienen cada uno una instancia separada de mis servicios.

Tengo un FacebookService que utilizo para realizar llamadas a la API de Facebook JavaScript y un UserService que utiliza el FacebookService. Aquí está mi bootstrap:

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

Desde mi registro, parece que finaliza la llamada de arranque, luego veo que se crea FacebookService y luego UserService antes de que se ejecute el código en cada uno de los constructores, MainAppComponent, HeaderComponent y DefaultComponent:

ingrese la descripción de la imagen aquí

Jason Goemaat
fuente
8
¿Estás seguro de que no has agregado UserServicey FacebookServiceen providersningún otro lugar?
Günter Zöchbauer

Respuestas:

132

Jason tiene toda la razón! Es causado por la forma en que funciona la inyección de dependencia. Se basa en inyectores jerárquicos.

Hay varios inyectores dentro de una aplicación Angular2:

  • La raíz que configura al iniciar su aplicación
  • Un inyector por componente. Si usa un componente dentro de otro. El inyector de componentes es hijo del componente principal. El componente de la aplicación (el que especifique al reiniciar su aplicación) tiene el inyector raíz como primario.

Cuando Angular2 intenta inyectar algo en el constructor de componentes:

  • Se ve en el inyector asociado con el componente. Si hay una coincidente, la usará para obtener la instancia correspondiente. Esta instancia se crea perezosamente y es un singleton para este inyector.
  • Si no hay un proveedor en este nivel, mirará el inyector principal (y así sucesivamente).

Por lo tanto, si desea tener un singleton para toda la aplicación, debe tener el proveedor definido a nivel del inyector raíz o del inyector del componente de la aplicación.

Pero Angular2 mirará el árbol del inyector desde la parte inferior. Esto significa que se utilizará el proveedor en el nivel más bajo y el alcance de la instancia asociada será este nivel.

Vea esta pregunta para más detalles:

Thierry Templier
fuente
1
Gracias, eso lo explica bien. Para mí fue simplemente contra-intuitivo porque eso rompe un poco con el paradigma del componente autónomo de angular 2. Digamos que estoy haciendo una biblioteca de componentes para Facebook, pero quiero que todos usen un servicio singleton. Tal vez hay un componente para mostrar la imagen de perfil del usuario conectado y otro para publicar. ¿La aplicación que usa esos componentes tiene que incluir el servicio de Facebook como proveedor incluso si no usa el servicio en sí? Podría devolver un servicio con un método 'getInstance ()' que gestiona el singleton del servicio real ...
Jason Goemaat
@tThierryTemplier ¿Cómo haría lo contrario? Tengo una clase de servicio común que quiero inyectar en varios componentes pero instanciar una nueva instancia cada vez (se supone que las opciones de proveedores / directivas están en desuso y se eliminan en la próxima versión)
Boban Stojanovski
Perdón por ser tan estúpido, pero no está claro para mí cómo se crea un servicio único, ¿puede explicarlo con más detalle?
gyozo kudor
Entonces, para trabajar con una sola instancia de un servicio, ¿debería declararse como proveedor en app.module.ts o en app.component.ts?
user1767316
Declarar cada servicio solo en app.module.ts, hizo el trabajo por mí.
user1767316
142

Actualización (Angular 6 +)

La forma recomendada de crear un servicio singleton ha cambiado. Ahora se recomienda especificar en el @Injectabledecorador del servicio que se debe proporcionar en la 'raíz'. Esto tiene mucho sentido para mí y ya no es necesario enumerar todos los servicios proporcionados en sus módulos. Solo importa los servicios cuando los necesita y se registran en el lugar adecuado. También puede especificar un módulo para que solo se proporcione si se importa el módulo.

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

Actualización (Angular 2)

Con NgModule, creo que la forma de hacerlo ahora es crear un 'CoreModule' con su clase de servicio y enumerar el servicio en los proveedores del módulo. Luego importa el módulo central en el módulo de la aplicación principal que proporcionará la única instancia a los niños que soliciten esa clase en sus constructores:

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

Respuesta original

Si incluye un proveedor bootstrap(), no necesita incluirlo en su decorador de componentes:

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

De hecho, la inclusión de su clase en 'proveedores' crea una nueva instancia de la misma, si algún componente principal ya lo incluye, los hijos no necesitan hacerlo y, si lo hacen, obtendrán una nueva instancia.

Jason Goemaat
fuente
1
A partir de ahora (enero de 2017), enumeraría el servicio [providers]en su archivo de módulo, no en bootstrap(), ¿verdad?
Jason Swett
55
¿Por qué no poner ApiServiceen AppModule's providers, y así evitar la necesidad de CoreModule? (No estoy seguro de que esto sea lo que sugiere @JasonSwett).
Jan Aagaard
1
@JasonGoemaat ¿Puedes agregar el ejemplo de cómo lo usas en el componente? Podríamos importarlo ApiServiceen la parte superior, pero ¿por qué molestarse en ponerlo en la matriz de proveedores de CoreModule y luego importarlo en app.module ... todavía no hizo clic para mí?
Hogan
Por lo tanto, poner el servicio en el proveedor del módulo proporcionará un singleton para ese módulo. ¿Y poner el servicio en los proveedores de componentes creará una nueva instancia para cada instancia del componente? ¿Está bien?
BrunoLM
@BrunoLM Creé una aplicación de prueba para ayudar a mostrar lo que está sucediendo. Es interesante que, aunque TestServicese especifica en los módulos principales y de la aplicación, las instancias no se crean para los módulos porque es proporcionado por el componente, por lo que angular nunca llega tan alto en el árbol del inyector. Entonces, si proporciona un servicio en su módulo y nunca lo usa, parece que no se crea una instancia.
Jason Goemaat
24

Sé que angular tiene inyectores jerárquicos, como dijo Thierry.

Pero tengo otra opción aquí en caso de que encuentre un caso de uso en el que realmente no desee inyectarlo en el padre.

Podemos lograr eso mediante la creación de una instancia del servicio, y en proporcionar siempre devolver eso.

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

Y luego, en su componente, utiliza su método de suministro personalizado.

@Component({
  providers: [YOUR_SERVICE_PROVIDER]
})

Y debe tener un servicio único sin depender de los inyectores jerárquicos.

No digo que esta sea una mejor manera, es solo en caso de que alguien tenga un problema donde los inyectores jerárquicos no sean posibles.

Joel Almeida
fuente
1
¿Debería SHARE_SERVICE_PROVIDERestar YOUR_SERVICE_PROVIDERen el componente? También supongo que la importación del archivo de servicio es necesaria como normal y el constructor seguirá teniendo un parámetro de tipo 'YourService', ¿verdad? Creo que me gusta, le permite garantizar un singleton y no tener que asegurarse de que el servicio se brinde en la jerarquía. También permite que los componentes individuales obtengan su propia copia simplemente enumerando el servicio en providerslugar del proveedor único, ¿verdad?
Jason Goemaat
@JasonGoemaat tienes razón. Editado eso. Exactamente, lo haces exactamente de la misma manera en el constructor y en los proveedores de ese componente que agregas YOUR_SERVICE_PROVIDER. Sí, todos los componentes obtendrán la misma instancia simplemente agregándola en los proveedores.
Joel Almeida
En el momento en que un desarrollador usa esto, deberían preguntarse a sí mismos "¿por qué estoy usando Angular si no voy a usar los mecanismos que Angular proporciona para esta situación?" Proporcionar el servicio a nivel de aplicación debería ser suficiente para cada situación dentro del contexto de Angular.
RyNo
1
+1 Aunque esta es una forma de crear servicios únicos, sirve muy bien como una forma de crear un servicio multitono simplemente cambiando la instancepropiedad en un mapa de instancias de valores clave
Precastic
1
@RyNo Puedo imaginar una aplicación que no requiera un servicio para cada ruta, o un módulo reutilizable que quiera una instancia estática y quiera usar la misma instancia con cualquier otro módulo que la use. Tal vez algo que crea una conexión de socket web al servidor y procesa mensajes. Tal vez solo unas pocas rutas en la aplicación desearían usarla, por lo que no es necesario crear una instancia de servicio y una conexión de socket web cuando la aplicación se inicia si no es necesaria. Puede programar alrededor de eso para que los componentes 'inicien' el servicio en todas partes donde se use, pero los singletons a pedido serían útiles.
Jason Goemaat
16

La sintaxis ha sido modificada. Mira este enlace

Las dependencias son singletons dentro del alcance de un inyector. En el siguiente ejemplo, se comparte una sola instancia de HeroService entre HeroesComponent y sus hijos HeroListComponent.

Paso 1. Crea una clase singleton con el decorador @Injectable

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

Paso 2. Inyectar en el constructor

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

Paso 3. Registrar proveedor

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing,
    HttpModule,
    JsonpModule
  ],
  declarations: [
    AppComponent,
    HeroesComponent,
    routedComponents
  ],
  providers: [
    HeroService
  ],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule { }
Manish Jain
fuente
¿Qué Injectablepasa si mi clase no es un servicio y solo contiene staticcadenas para uso global?
Syed Ali Taqi
2
como estos proveedores: [{proporcionar: 'API_URL', useValue: ' coolapi.com '}]
Whisher
7

Agregar @Injectabledecorador al Servicio Y registrarlo como proveedor en el Módulo raíz lo convertirá en un singleton.

bresleveloper
fuente
Solo dime si lo estoy entendiendo. Si hago lo que dijiste, será un singleton. Si, además de eso, el servicio también es un proveedor en otro módulo, ya no será un singleton, ¿verdad? Por la jerarquía.
heringer
1
Y no registre al proveedor en el decorador @Component de las páginas.
Laura
@Laura. ¿Todavía lo importo a componentes que realmente usan el servicio?
Mark
@ Mark Sí, debe importarlo, y luego solo debe declararlo de la constructorsiguiente manera: import { SomeService } from '../../services/some/some'; @Component({ selector: 'page-selector', templateUrl: 'page.html', }) export class SomePage { constructor( public someService: SomeService ) { }
Laura
6

esto parece estar funcionando bien para mí

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

  constructor() {
    return MyStaticService.instance = MyStaticService.instance || this;
  }
}
Capitán Harlock
fuente
8
Yo llamaría a esto un antipatrón Angular2. Proporcione el servicio correctamente y Angular2 siempre inyectará la misma instancia. Ver también stackoverflow.com/questions/12755539/…
Günter Zöchbauer
3
@ Günter Zöchbauer, podría dar algunos consejos sobre "Proporcionar el servicio correctamente y Angular2 siempre inyectará la misma instancia". ? Porque no está claro y no pude encontrar ninguna información útil buscando en Google.
nowiko
Acabo de publicar esta respuesta que podría ayudar con su pregunta stackoverflow.com/a/38781447/217408 (vea también el enlace allí)
Günter Zöchbauer
2
Esto es perfecto. Usted debe utilizar angulares de inyección de dependencia propia, pero no hay daño al usar este patrón para estar absolutamente seguro de que su servicio es un conjunto unitario cuando se espera que sea. Potencialmente ahorra mucho tiempo buscando errores solo porque inyecta el mismo servicio en dos lugares diferentes.
PaulMolloy
He utilizado este modelo para determinar que el problema que estaba enfrentando era debido al servicio no ser Singleton
HR-tis
5

Aquí hay un ejemplo de trabajo con Angular versión 2.3. Simplemente llame al constructor del servicio de manera similar a este constructor (private _userService: UserService). Y creará un singleton para la aplicación.

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
fuente
4

UNA singleton service es un servicio para el que solo existe una instancia en una aplicación.

Hay (2) formas de proporcionar un servicio singleton para su aplicación.

  1. usar la providedInpropiedad, o

  2. proporcionar el módulo directamente en AppModulela aplicación

Usando provideIn

Comenzando con Angular 6.0, la forma preferida de crear un servicio singleton es establecer la providedInraíz en el @Injectable()decorador del servicio . Esto le dice a Angular que proporcione el servicio en la raíz de la aplicación.

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

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

Matriz de proveedores de NgModule

En las aplicaciones creadas con versiones angulares anteriores a 6.0, los servicios son matrices de proveedores de NgModule registradas de la siguiente manera:

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

Si esta NgModulefuera la raíz AppModule, UserService sería un singleton y estaría disponible en toda la aplicación. Aunque puede verlo codificado de esta manera, es preferible utilizar la providedInpropiedad del @Injectable()decorador en el servicio en sí mismo a partir de Angular 6.0, ya que hace que sus servicios sean inestables.

AbolfazlR
fuente
3

Puedes usar useValueen proveedores

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

@NgModule({
...
  providers: [ { provide: MyService, useValue: new MyService() } ],
...
})
Roman Rhrn Nesterov
fuente
55
useValueno está relacionado con singleton. Usevalue es solo para pasar un valor en lugar de un Type( useClass) que DI llama newo useFactorydonde se pasa una función que devuelve el valor cuando DI lo llama. Angular DI mantiene una única instancia por proveedor automáticamente. Solo proporcione una vez y tendrá un singleton. Lo siento, tengo que votar a favor porque es información no válida: - /
Günter Zöchbauer
3

Desde Angular @ 6, puede tener providedInen un Injectable.

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

}

Consulta los documentos aquí

Hay dos formas de hacer que un servicio sea un singleton en Angular:

  1. Declare que el servicio se debe proporcionar en la raíz de la aplicación.
  2. Incluya el servicio en AppModule o en un módulo que solo importa AppModule.

Comenzando con Angular 6.0, la forma preferida de crear servicios singleton es especificar en el servicio que debe proporcionarse en la raíz de la aplicación. Esto se hace configurando provideIn para rootear en el decorador @Injectable del servicio:

sabithpocker
fuente
Esto es bueno, pero también puede tener problemas inesperados con variables que no están allí que podrían resolverse declarando algunos elementos public static.
cjbarth
2

Simplemente declare su servicio como proveedor solo en app.module.ts.

Hizo el trabajo por mí.

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

luego instanciarlo usando un parámetro privado del constructor:

constructor(private topicService: TopicService) { }

o ya que si su servicio se usa desde html, la opción -prod reclamará:

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

agregue un miembro para su servicio y llénelo con la instancia recibida en el constructor:

export class SomeComponent {
  topicService: TopicService;
  constructor(private topicService: TopicService) { 
    this.topicService= topicService;
  }
}
usuario1767316
fuente
0
  1. Si desea hacer que el servicio sea único a nivel de aplicación, debe definirlo en app.module.ts

    proveedores: [MyApplicationService] (también puede definir lo mismo en el módulo secundario para que sea específico de ese módulo)

    • No agregue este servicio en el proveedor que crea una instancia para ese componente que rompe el concepto singleton, solo inyecte a través del constructor.
  2. Si desea definir el servicio singleton a nivel de componente, cree el servicio, agregue ese servicio en app.module.ts y agregue la matriz de proveedores dentro del componente específico como se muestra en el siguiente fragmento.

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

  3. Angular 6 proporciona una nueva forma de agregar servicio a nivel de aplicación. En lugar de agregar una clase de servicio a la matriz de proveedores [] en AppModule, puede establecer la siguiente configuración en @Injectable ():

    @Injectable ({shownIn: 'root'}) clase de exportación MyService {...}

Sin embargo, la "nueva sintaxis" ofrece una ventaja: Angular (detrás de escena) puede cargar los servicios con pereza y eliminar automáticamente el código redundante. Esto puede conducir a un mejor rendimiento y velocidad de carga, aunque esto realmente solo se aplica a servicios y aplicaciones más grandes en general.

Vijay Barot
fuente
0

Además de las excelentes respuestas anteriores, puede faltar algo más si las cosas en su singleton aún no se comportan como un singleton. Me encontré con el problema cuando llamé a una función pública en el singleton y descubrí que estaba usando las variables incorrectas. Resulta que el problema era thisque no se garantiza que esté vinculado al singleton para ninguna función pública en el singleton. Esto se puede corregir siguiendo los consejos aquí , así:

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

Alternativamente, puede simplemente declarar los miembros de la clase como en public staticlugar de public, luego el contexto no importará, pero tendrá que acceder a ellos como en SubscriptableService.onServiceRequested$lugar de usar la inyección de dependencia y acceder a ellos a través de this.subscriptableService.onServiceRequested$.

cjbarth
fuente
0

Servicios para padres e hijos.

Estaba teniendo problemas con un servicio para padres y su hijo usando diferentes instancias. Para forzar el uso de una instancia, puede alias al padre con referencia al hijo en los proveedores del módulo de la aplicación. El padre no podrá acceder a las propiedades del niño, pero se usará la misma instancia para ambos servicios. 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}
]

Servicios utilizados por componentes fuera del alcance de los módulos de su aplicación

Al crear una biblioteca que consta de un componente y un servicio, me encontré con un problema en el que se crearían dos instancias. Uno por mi proyecto angular y otro por el componente dentro de mi biblioteca. La solución:

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
fuente
¿Querías responder a tu propia pregunta? Si es así, puede separar la respuesta en una Respuesta formal en StackOverflow, cortándola / pegándola en el campo Respuesta después de que se publique la Pregunta.
ryanwebjackson