如何在Angular 2中创建单例服务?


142

我已经读过,引导时的注入应该让所有子节点共享同一个实例,但是我的主要和标头组件(主应用程序包括标头组件和路由器出口)都分别获得了我的服务的单独实例。

我有一个FacebookService和一个使用FacebookService的UserService,可以用来调用facebook javascript api。这是我的引导程序:

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

从我的记录看来,引导调用完成了,然后我看到在运行每个构造函数中的代码(MainAppComponent,HeaderComponent和DefaultComponent)之前先创建了FacebookService,然后创建了UserService:

在此处输入图片说明


8
您确定尚未添加UserService和添加FacebookServiceproviders其他任何地方?
昆特·绍赫鲍尔(GünterZöchbauer)

Answers:


132

杰森完全正确!这是由依赖项注入的工作方式引起的。它基于分层注射器。

Angular2应用程序中有几个注入器:

  • 在引导应用程序时配置的根目录
  • 每个组件有一个注射器。如果在另一个组件内部使用组件。组件注入器是父组件一个的子代。应用程序组件(在增强应用程序时指定的组件)将根注入器作为父组件。

当Angular2尝试在组件构造函数中注入一些东西时:

  • 它查看与组件关联的注射器。如果有匹配的实例,它将使用它来获取相应的实例。该实例是延迟创建的,并且是此注入器的单例。
  • 如果此级别上没有提供者,它将查看父注入器(依此类推)。

因此,如果要为整个应用程序设置一个单例,则需要在根注入器或应用程序组件注入器的级别定义提供程序。

但是Angular2将从底部看进样器树。这意味着将使用最低级别的提供者,并且关联实例的范围将为该级别。

有关更多详细信息,请参见此问题:


1
谢谢,这很好地解释了。这对我来说是违反直觉的,因为这有点违反了angular 2的独立组件范例。假设我正在为Facebook创建一个组件库,但我希望它们都使用单例服务。也许有一个组件可以显示已登录用户的个人资料图片,另一个可以发布。使用这些组件的应用程序必须包括Facebook服务作为提供程序,即使它本身不使用服务也是如此?我可以使用管理实际服务单例的'getInstance()'方法返回服务...
Jason Goemaat

@tThierryTemplier我该怎么做,我有一个公共服务类,我想注入多个组件,但是每次实例化一个新实例(在下一个发行版中应该弃用提供程序/指令选项)
Boban Stojanovski

很抱歉如此愚蠢,但我不清楚您如何创建单例服务,您能否详细解释一下?
gyozo kudor

因此,要使用服务的单个实例,应在app.module.ts或app.component.ts中将其声明为provider?
user1767316

仅在app.module.ts中声明每个服务,对我来说就是成功的。
user1767316

142

更新(Angular 6 +)

创建单例服务的推荐方法已更改。现在建议@Injectable在服务的装饰器中指定应在“根”中提供它。这对我来说很有意义,并且不再需要在您的模块中列出所有提供的服务。您只需在需要时导入服务,然后它们就会在适当的位置进行注册。您也可以指定一个模块,以便仅在导入模块时才提供。

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

更新(角度2)

我认为,使用NgModule的方法是在其中创建一个带有您的服务类的'CoreModule',并在模块的提供程序中列出该服务。然后,将核心模块导入到主应用程序模块中,该模块将为在其构造函数中请求该类的所有子级提供一个实例:

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

原始答案

如果您在中列出了提供程序bootstrap(),则无需在组件装饰器中列出它们:

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

实际上,将您的类列出在“提供者”中会创建一个新的实例,如果任何父组件已经列出了它,则子级不需要这样做,如果他们这样做了,他们将获得一个新实例。


1
截至目前(2017年1月),您将服务列[providers]在模块文件中,而不是中bootstrap(),对吗?
杰森·斯威特

5
为什么不把ApiServiceAppModuleproviders,从而避免了需要CoreModule?(不确定这是@JasonSwett的建议。)
Jan Aagaard

1
@JasonGoemaat可以添加示例如何在组件中使用它吗?我们可以ApiService在顶部导入,但是为什么还要费心将其放入CoreModule的providers数组中,然后将其导入到app.module中……它并没有点击我。
hogan

因此,将服务放在模块提供程序上将为该模块提供单例。并将服务放在组件提供程序上会为组件的每个实例创建一个新实例吗?那正确吗?
BrunoLM

@BrunoLM我创建了一个测试应用程序来帮助显示正在发生的事情。有趣的是,即使TestService在核心模块和应用模块中都指定了实例,也不会为模块创建实例,因为实例是由组件提供的,因此角度永远不会比注入器树高。因此,如果您在模块中提供服务而从不使用它,则似乎不会创建实例。
杰森·古玛

24

我知道,像Thierry所说的,Angular具有分层喷射器。

但是在这里我还有另一种选择,以防万一您找到了一个您真的不想将其注入父级的用例。

我们可以通过创建服务实例来实现这一点,并在提供时始终返回该实例。

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

然后在您的组件上使用自定义的提供方法。

@Component({
  providers: [YOUR_SERVICE_PROVIDER]
})

而且,您应该具有单例服务,而不必依赖于分层注入器。

我并不是说这是一种更好的方法,以防万一有人遇到无法使用分层注入器的问题。


1
应该SHARE_SERVICE_PROVIDERYOUR_SERVICE_PROVIDER在组件?我还假设需要像正常情况一样导入服务文件,并且构造函数仍将具有'YourService'类型的参数,对吗?我认为,我喜欢这样,您可以保证单身,而不必确保服务是按层次结构提供的。它还仅通过列出服务providers而不是单例提供程序就可以使各个组件获得自己的副本,对吗?
杰森·古玛

@JasonGoemaat你是对的。编辑。确实,您在添加的组件的构造函数和提供程序中以完全相同的方式进行操作YOUR_SERVICE_PROVIDER。是的,只需将其添加到提供程序中,所有组件都将获得相同的实例。
乔尔·阿尔梅达

在开发人员使用此功能时,他们应该问自己:“如果我不打算使用Angular为这种情况提供的机制,我为什么还要使用Angular?” 在Angular上下文中的每种情况下,在应用程序级别提供服务应足够。
RyNo

1
+1尽管这是一种创建单例服务的方法,但只需将属性更改为实例的键值映射,它就可以很好地用作创建例服务的方法instance
Precastic

1
@RyNo我可以想象一个不需要为每个路由提供服务的应用程序,或者是一个可重用的模块,它需要一个静态实例,并希望与使用该实例的任何其他模块使用同一实例。也许可以创建到服务器的Web套接字连接并处理消息。也许应用程序中只有几个路由会想要使用它,所以不需要时不需要在应用程序启动时创建服务实例和Web套接字连接。您可以围绕它进行编程,以使组件在使用该服务的任何地方都能“初始化”该服务,但是按需单例将很有用。
詹森·古玛

16

语法已更改。检查此链接

依赖关系是注入器范围内的单例。在下面的示例中,单个HeroService实例在HeroesComponent及其HeroListComponent子级之间共享。

步骤1.使用@Injectable装饰器创建单例类

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

步骤2.注入构造函数

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

步骤3.注册提供商

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

如果我的Injectable课程不是服务,仅包含static供全局使用的字符串怎么办?
Syed Ali Taqi

2
像这样的提供者:[{provide:'API_URL',useValue:' coolapi.com '}]
Whisher

11

A singleton service是一项应用程序中仅存在一个实例的服务。

(2)种方法可为您的应用程序提供单例服务。

  1. 使用该providedIn财产,或

  2. 直接在AppModule应用程序中提供模块

使用providerIn

从Angular 6.0开始,创建单例服务的首选方法是providedIn将root 设置为服务的@Injectable()装饰器。这告诉Angular在应用程序根目录中提供服务。

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

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

NgModule提供者数组

在使用6.0之前的Angular版本构建的应用程序中,服务注册为NgModule提供程序数组,如下所示:

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

如果这NgModule是root AppModule,则UserService将是一个单例,并且在整个应用程序中都可用。尽管您可能会看到它是用这种方式编码的,但从Angular 6.0开始,最好在服务本身上使用装饰器的providedIn属性,@Injectable()因为这样会使服务树状地摇动。


7

添加@Injectable装饰到服务,注册为在根模块提供商将使一个单身。


告诉我是否理解。如果我按照您说的去做,好吧,这将是一个单身人士。如果除此之外,该服务也是另一个模块中的提供者,它将不再是单例,对吗?由于层次结构。
heringer

1
并且不要在页面的@Component装饰器中注册提供程序。
劳拉

@劳拉 我是否仍将其导入实际使用该服务的组件中?
标记

@Mark是的,您需要导入它,然后只需要constructor像这样声明它: import { SomeService } from '../../services/some/some'; @Component({ selector: 'page-selector', templateUrl: 'page.html', }) export class SomePage { constructor( public someService: SomeService ) { }
Laura

6

这似乎对我来说很好

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

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

8
我将其称为Angular2反模式。正确提供服务,Angular2将始终注入相同的实例。另请参见stackoverflow.com/questions/12755539/...
君特Zöchbauer

3
@GünterZöchbauer,请提供一些有关“正确提供服务,Angular2将始终注入相同实例”的建议。?因为不清楚,我无法通过谷歌搜索找到任何有用的信息。
nowiko '16

我只是张贴了这个答案与你的问题可能会帮助stackoverflow.com/a/38781447/217408(见链接有)
君特Zöchbauer

2
太棒了。您应该使用角度自己的依赖项注入,但是使用此模式绝对可以确保您期望的服务是单例的,这没有什么害处。仅仅因为您在两个不同的地方注入了相同的服务,潜在地节省了寻找错误的大量时间。
PaulMolloy

我使用这种模式来确定我面临的问题是由于服务不是单例
hr-tis

5

这是Angular 2.3版的工作示例。只需像这样的构造函数(private _userService:UserService)那样直接调用服务的构造函数即可。它将为应用创建一个单例。

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

3

您可以useValue在提供程序中使用

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

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

5
useValue与单例无关。Usevalue只是传递一个值,而不是传递DI调用的TypeuseClassnewuseFactory传递被DI调用时返回该值的函数的位置。Angular DI自动为每个提供程序维护一个实例。仅提供一次,您将拥有一个单身人士。对不起,我不得不downvote,因为它只是无效信息: - /
君特Zöchbauer

3

在Angular @ 6中,您可以providedIn使用Injectable

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

}

这里检查文档

在Angular中有两种使服务成为单例的方法:

  1. 声明应在应用程序根目录中提供服务。
  2. 将服务包含在AppModule或仅由AppModule导入的模块中。

从Angular 6.0开始,创建单例服务的首选方法是在服务上指定应在应用程序根目录中提供的服务。这是通过将providerIn设置为服务的@Injectable装饰器的根目录来完成的:


这样做很好,但是您可能还会遇到意想不到的问题,因为变量不存在,可以通过声明一些项目来解决public static
cjbarth

2

仅在app.module.ts中声明您的服务为提供者。

它为我做了工作。

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

然后使用构造函数private参数实例化它:

constructor(private topicService: TopicService) { }

或者由于如果您的服务是通过html使用的,则-prod选项将声明:

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

为您的服务添加一个成员,并用构造函数中收到的实例填充它:

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

0
  1. 如果要在应用程序级别使服务单例,则应在app.module.ts中定义它

    provider:[MyApplicationService](您也可以在子模块中定义相同的名称,以使其特定于该模块)

    • 不要在提供程序中为该组件创建实例的提供程序中添加该服务,该实例会破坏单例概念,只需通过构造函数注入即可。
  2. 如果要在组件级别定义单例服务,请创建服务,将其添加到app.module.ts中,并在特定组件内添加provider数组,如下面的代码所示。

    @Component({选择器:'app-root',templateUrl:'./test.component.html',styleUrls:['./test.component.scss'],提供者:[TestMyService]})

  3. Angular 6提供了在应用程序级别添加服务的新方法。您可以在@Injectable()中设置以下配置,而不是向AppModule中的provider []数组添加服务类:

    @Injectable({providedIn:'root'})导出类MyService {...}

但是,“新语法”确实提供了一个优势:Angular可以在后台(延迟)加载服务,并且可以自动删除冗余代码。这可以带来更好的性能和加载速度-尽管实际上这仅适用于更大的服务和应用程序。


0

除了上述出色的答案之外,如果您单身人士中的事物仍然没有表现为单身人士,那么可能还会缺少一些其他内容。在单例上调用公共函数并发现它使用了错误的变量时,我遇到了这个问题。事实证明,问题this在于对于单例中的任何公共功能,不能保证将其绑定到单例中。可以按照此处的建议进行纠正,如下所示:

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

或者,您可以简单地将类成员声明为,public static而不是public上下文,这无关紧要,但是您必须像访问它们一样,SubscriptableService.onServiceRequested$而不是使用依赖注入并通过来访问它们this.subscriptableService.onServiceRequested$


0

父母子女服务

我在使用不同实例的父服务及其子服务时遇到麻烦。要强制使用一个实例,您可以在应用程序模块提供程序中引用该子实例作为父实例的别名。父级将无法访问子级的属性,但是两个服务将使用相同的实例。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}
]

应用模块范围之外的组件所使用的服务

创建由组件和服务组成的库时,我遇到了一个问题,即将创建两个实例。一个是我的Angular项目,另一个是我的库中的组件。解决方法:

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>

您是想回答自己的问题吗?如果是这样,您可以在发布问题后将其剪切/粘贴到“答案”字段中,从而将答案分离为StackOverflow上的正式答案。
ryanwebjackson
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.