Как создать одноэлементную службу в Angular 2?


148

Я читал, что инъекция при начальной загрузке должна иметь все дочерние элементы, использующие один и тот же экземпляр, но каждый из моих основных компонентов и компонентов заголовка (основное приложение включает компонент заголовка и маршрутизатор) получает отдельный экземпляр моих служб.

У меня есть FacebookService, который я использую для звонков в facebook javascript api и UserService, который использует FacebookService. Вот мой бутстрап:

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

Судя по моему журналу, похоже, что вызов начальной загрузки завершается, затем я вижу, что FacebookService, затем UserService создается до того, как код в каждом из конструкторов запускается, MainAppComponent, HeaderComponent и DefaultComponent:

введите описание изображения здесь


8
Вы уверены, что еще не добавили UserServiceи ни FacebookServiceк providersчему?
Günter Zöchbauer

Ответы:


132

Джейсон совершенно прав! Это вызвано тем, как работает внедрение зависимостей. Он основан на иерархических инжекторах.

В приложении Angular2 есть несколько инжекторов:

  • Корневой, который вы настраиваете при загрузке вашего приложения
  • Инжектор на компонент. Если вы используете компонент внутри другого. Инжектор компонентов является потомком родительского компонента. Компонент приложения (тот, который вы указываете при ускорении вашего приложения) имеет корневой инжектор в качестве родительского).

Когда Angular2 пытается что-то внедрить в конструктор компонента:

  • Он смотрит в инжектор, связанный с компонентом. Если есть подходящий, он будет использовать его для получения соответствующего экземпляра. Этот экземпляр создается лениво и является синглтоном для этого инжектора.
  • Если на этом уровне нет провайдера, он будет смотреть на родительский инжектор (и так далее).

Итак, если вы хотите иметь синглтон для всего приложения, вам необходимо определить поставщика либо на уровне корневого инжектора, либо на уровне инжектора компонентов приложения.

Но Angular2 будет смотреть на дерево инжекторов снизу. Это означает, что будет использоваться поставщик на самом низком уровне, и область действия связанного экземпляра будет на этом уровне.

См. Этот вопрос для получения более подробной информации:


1
Спасибо, это хорошо объясняет. Для меня это было просто нелогично, потому что это как бы противоречит парадигме автономных компонентов angular 2. Допустим, я создаю библиотеку компонентов для Facebook, но я хочу, чтобы все они использовали одноэлементную службу. Может быть, есть компонент для отображения изображения профиля вошедшего в систему пользователя и еще один для публикации. Приложение, которое использует эти компоненты, должно включать службу Facebook в качестве поставщика, даже если оно не использует саму службу? Я мог бы просто вернуть службу с помощью метода getInstance (), который управляет синглтоном реальной службы ...
Джейсон Гоэмаат,

@tThierryTemplier Как бы я поступил наоборот, у меня есть общий класс обслуживания, который я хочу внедрить в несколько компонентов, но каждый раз создавать новый экземпляр (предполагается, что параметры поставщиков / директив устарели и будут удалены в следующем выпуске)
Бобан Стояновски,

Извините за такую ​​глупость, но мне непонятно, как вы создаете одноэлементную службу, не могли бы вы объяснить более подробно?
gyozo kudor 05

Итак, чтобы работать с одним экземпляром службы, следует ли его объявить как поставщик в app.module.ts или app.component.ts?
user1767316

Объявление каждой службы только в app.module.ts сделало мою работу за меня.
user1767316

148

Обновление (Angular 6+)

Изменился рекомендуемый способ создания одноэлементной службы . Теперь рекомендуется указать в @Injectableдекораторе службы, что она должна находиться в «корне». Для меня это имеет большой смысл, и больше нет необходимости перечислять все предоставляемые услуги в ваших модулях. Вы просто импортируете услуги, когда они вам нужны, и они регистрируются в нужном месте. Вы также можете указать модуль, чтобы он предоставлялся только в том случае, если модуль импортирован.

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

Обновление (Angular 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 г.) вы бы указали службу [providers]в файле модуля, а не в нем bootstrap(), верно?
Джейсон Светт

5
Почему бы не поставить ApiServiceв AppModule«с providers, и таким образом избежать необходимости CoreModule? (Не уверен , что это то , что предполагает @JasonSwett.)
Jan Aagaard

1
@JasonGoemaat Можете ли вы добавить пример того, как вы используете его в компоненте? Мы могли бы импортировать ApiServiceвверху, но зачем вообще вообще нужно помещать его в массив поставщиков CoreModule, а затем импортировать его в app.module ... для меня это еще не сработало.
Хоган

Таким образом, предоставление услуги поставщику модуля предоставит синглтон для этого модуля. И размещение службы у поставщиков компонентов создаст новый экземпляр для каждого экземпляра компонента? Это правильно?
BrunoLM

@BrunoLM Я создал тестовое приложение, чтобы показать, что происходит. Интересно, что хотя TestServiceэто и указано в модулях ядра и приложения, экземпляры не создаются для модулей, потому что они предоставляются компонентом, поэтому angular никогда не поднимается так высоко в дереве инжекторов. Поэтому, если вы предоставляете услугу в своем модуле и никогда не используете ее, экземпляр не создается.
Джейсон Гоэмаат

24

Я знаю, что у 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_PROVIDERбыть YOUR_SERVICE_PROVIDERв компоненте? Также я предполагаю, что импорт служебного файла необходим как обычно, а в конструкторе все еще будет параметр типа YourService, верно? Мне это нравится, я думаю, это позволяет вам гарантировать одноэлементность и не нужно убедиться, что услуга предоставляется вверх по иерархии. Это также позволяет отдельным компонентам получать свою собственную копию, просто перечисляя службу providersвместо поставщика синглтонов, верно?
Джейсон Гоэмаат

@JasonGoemaat, вы правы. Отредактировал это. Точно так же вы делаете это в конструкторе и в поставщиках добавляемого вами компонента YOUR_SERVICE_PROVIDER. Да, все компоненты получат один и тот же экземпляр, просто добавив его в провайдеры.
Joel Almeida

Когда разработчик использует это, он должен спросить себя: «Почему я вообще использую Angular, если я не собираюсь использовать механизмы, которые Angular предоставляет для этой ситуации?» Предоставления услуги на уровне приложения должно быть достаточно для любой ситуации в контексте Angular.
RyNo

1
+1 Хотя это способ создания одноэлементных служб, он отлично подходит для создания многотонной службы, просто изменяя instanceсвойство в карту экземпляров ключ-значение
Precastic

1
@RyNo Я могу представить себе приложение, которому не требуется служба для каждого маршрута, или многоразовый модуль, которому нужен статический экземпляр и который хочет использовать тот же экземпляр с любыми другими модулями, которые его используют. Может быть, что-то, что создает соединение с сервером через веб-сокет и обрабатывает сообщения. Возможно, только несколько маршрутов в приложении захотят его использовать, поэтому нет необходимости создавать экземпляр службы и соединение с веб-сокетом при запуске приложения, если оно не требуется. Вы можете запрограммировать это так, чтобы компоненты «запускали» службу везде, где она используется, но по запросу будут полезны синглтоны.
Джейсон Гоэмаат

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строки для глобального использования?
Сайед Али Таки

2
вроде этого провайдеры: [{provide: 'API_URL', useValue: ' coolapi.com '}]
Whisher,

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 ) { }
Лаура

6

мне кажется, это хорошо работает

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

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

9
Я бы назвал это антипаттерном Angular2. Правильно предоставьте услугу, и Angular2 всегда будет внедрять один и тот же экземпляр. См. Также stackoverflow.com/questions/12755539/…
Günter Zöchbauer 01

3
@ Günter Zöchbauer, пожалуйста, дайте совет относительно «Правильно предоставьте услугу, и Angular2 всегда будет внедрять один и тот же экземпляр». ? Потому что это неясно, и я не смог найти никакой полезной информации в Google.
nowiko 05

Я только что опубликовал этот ответ, который может помочь с вашим вопросом stackoverflow.com/a/38781447/217408 (см. Также ссылку там)
Günter Zöchbauer

2
Это потрясающе. Вы должны использовать собственную инъекцию зависимостей angular, но нет ничего плохого в использовании этого шаблона, чтобы быть абсолютно уверенным, что ваша служба является синглтоном, когда вы этого ожидаете. Потенциально экономит время на поиск ошибок просто потому, что вы внедряете один и тот же сервис в двух разных местах.
PaulMolloy

Я использовал этот шаблон, чтобы убедиться, что проблема, с которой я столкнулся,
связана

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 - это просто передать значение вместо Type( useClass), которое вызывает DI newили useFactoryгде передается функция, которая возвращает значение при вызове DI. Angular DI автоматически поддерживает один экземпляр для каждого поставщика. Только предоставьте его один раз, и у вас будет синглтон. Извините, я должен проголосовать против, потому что это просто неверная информация: - /
Günter Zöchbauer

3

Из Angular @ 6 вы можете иметь providedIn файл Injectable.

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

}

Проверьте документы здесь

Есть два способа сделать сервис синглтоном в Angular:

  1. Объявите, что услуга должна быть предоставлена ​​в корне приложения.
  2. Включите службу в AppModule или в модуль, который импортируется только AppModule.

Начиная с Angular 6.0, предпочтительным способом создания одноэлементных служб является указание службы, которая должна быть предоставлена ​​в корне приложения. Для этого нужно установить для providedIn значение root в декораторе службы @Injectable:


Это хорошо, но у вас также могут возникнуть неожиданные проблемы с отсутствием переменных, которые можно решить, объявив некоторые элементы public static.
cjbarth 02

2

Просто объявите свою услугу поставщиком только в app.module.ts.

Это сработало для меня.

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

затем либо создайте его, используя закрытый параметр конструктора:

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

1

А singleton service - это служба, для которой в приложении существует только один экземпляр.

Есть (2) способа предоставить одноэлементную службу для вашего приложения.

  1. использовать providedInсвойство, или

  2. предоставить модуль прямо в AppModuleприложении

Использование providedIn

Начиная с Angular 6.0, предпочтительным способом создания одноэлементной службы является установка providedInroot в @Injectable()декораторе службы . Это говорит Angular предоставить службу в корне приложения.

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

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

Массив поставщиков NgModule

В приложениях, созданных с использованием версий Angular до 6.0, службы зарегистрированы как массивы поставщиков NgModule следующим образом:

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

Если бы это NgModuleбыл корень AppModule, UserService был бы одноэлементным и доступным во всем приложении. Хотя вы можете видеть, что это закодировано таким образом, использование providedInсвойства @Injectable()декоратора в самой службе предпочтительнее, начиная с Angular 6.0, поскольку это делает ваши службы древовидными.


0
  1. Если вы хотите сделать одноэлементный сервис на уровне приложения, вы должны определить его в app.module.ts

    провайдеры: [MyApplicationService] (вы можете определить то же самое в дочернем модуле, чтобы сделать его специфичным для этого модуля)

    • Не добавляйте эту службу в поставщик, который создает экземпляр для этого компонента, который нарушает концепцию синглтона, просто введите через конструктор.
  2. Если вы хотите определить одноэлементную службу на уровне компонента, добавьте эту службу в app.module.ts и добавьте массив поставщиков внутри определенного компонента, как показано в фрагменте ниже.

    @Component ({селектор: 'app-root', templateUrl: './test.component.html', styleUrls: ['./test.component.scss'], провайдеры: [TestMyService]})

  3. Angular 6 предоставляет новый способ добавления сервиса на уровне приложения. Вместо того, чтобы добавлять класс обслуживания в массив Provider [] в AppModule, вы можете установить следующую конфигурацию в @Injectable ():

    @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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.