简体   繁体   English

无需在 Angular 7 中注入即可创建单例服务

[英]Create a singleton service without needing to inject it in Angular 7

Situation情况

I've been trying to find a way where I can instantiate a service that will purely sit "in the background" and listen to events (and do stuff) - which I would like to be created when the app initializes, and be forgotten about.我一直在尝试找到一种方法,我可以实例化一个纯粹位于“后台”并侦听事件(并做一些事情)的服务——我希望在应用程序初始化时创建它,然后被遗忘.

Unfortunately, I would need to use dependency injection into a component, for the service to be instantiated - most paths I take lead to using the AppComponent 's constructor.不幸的是,我需要在组件中使用依赖注入,以便实例化服务 - 我采用的大多数路径都导致使用AppComponent的构造函数。

I won't be directly interacting with the service though (calling methods/properties), and want to keep it out of other components/services which don't have anything directly to do with it.我不会直接与服务交互(调用方法/属性),并希望将它与其他没有直接关系的组件/服务分开。


The service服务

The service and the logic in it is pretty straightforward.服务和其中的逻辑非常简单。 My service is based on a Dynamic page titles in Angular 2 tutorial.我的服务基于Angular 2教程中的动态页面标题

The service will listen to NavigationEnd events from the Router , grab the ActivatedRoute , and then use the route's data to set the page title.该服务将侦听来自Router NavigationEnd事件,获取ActivatedRoute ,然后使用该路由的数据来设置页面标题。

Unlike the example in the tutorial, I've created my own service instead of putting the logic within the AppComponent ;与教程中的示例不同,我创建了自己的服务,而不是将逻辑放在AppComponent I want to keep my separation of concerns tip-top.我想让我的关注点分离处于最佳状态。

page-title.service.ts:页面标题.service.ts:

import { Injectable } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { filter, map, mergeMap } from 'rxjs/operators';

@Injectable()
export class PageTitleService {

  constructor(
    router: Router,
    activatedRoute: ActivatedRoute,
    titleService: Title
  ) {
    router.events
      .pipe(
        filter((event) => event instanceof NavigationEnd),
        map(() => activatedRoute),
        map((route) => {
          while (route.firstChild) {
            route = route.firstChild;
          }

          return route;
        }),
        filter((route) => route.outlet === 'primary'),
        mergeMap((route) => route.data)
      )
      .subscribe((routeData) => titleService.setTitle(routeData['title']));
  }

}

Obviously, the service itself will rely on dependency injection to use the Router , ActivatedRoute , and Title services.显然,服务本身将依赖依赖注入来使用RouterActivatedRouteTitle服务。


The problem问题

The only way I currently know to instantiate this service is to use dependency injection into another component/service.我目前知道的实例化此服务的唯一方法是使用依赖注入到另一个组件/服务中。

Eg in app.component.ts :例如在app.component.ts

export class AppComponent implements OnInit {

  constructor(
    pageTitleService: PageTitleService, // inject the service to instantiate it
    // ... other services
  ) { }

  ngOnInit() {
    // do stuff with other services, but not the pageTitleService
  }

}

The problem is, I want to avoid doing this if at all possible.问题是,如果可能的话,我想避免这样做。


Question

Is it possible to instantiate the service somewhere other than a component/service?是否可以在组件/服务以外的其他地方实例化服务?


Possibility?可能性?

I do have an app-load.module.ts , which does some upfront initialization, before the rest of the app is loaded:我确实有一个app-load.module.ts ,它会在加载应用程序的其余部分之前进行一些前期初始化:

import { APP_INITIALIZER, NgModule } from '@angular/core';

import { OrganisationService } from './core/organisation/organisation.service';

export function initApp(organisationService: OrganisationService) {
  return () =>
    organisationService
      .initialize()
      .then(() => window.document.documentElement.classList.remove('app-loading'));
}

@NgModule({
  imports: [],
  declarations: [],
  providers: [
    OrganisationService,
    { provide: APP_INITIALIZER, useFactory: initApp, deps: [OrganisationService], multi: true }
  ]
})
export class AppLoadModule { }

Could I perhaps instantiate the PageTitleService in here, somewhere?我可以在这里的某个地方实例化PageTitleService吗?

Or, is there a better place/way to do it?或者,有没有更好的地方/方法来做到这一点?

Thanks in advance.提前致谢。

Just an observation why injecting in a component (App component) would not be such a bad idea:只是观察为什么注入组件(App 组件)并不是一个坏主意:

  1. Showing title in the browser window is a Application requirement and app component is actually the container (or root) your application.在浏览器窗口中显示标题是应用程序的要求,而应用程序组件实际上是应用程序的容器(或根)。
  2. So can't we say that it is a concern of application (app component) to have the title updated promptly?那么我们不能说及时更新标题是应用程序(应用程序组件)的关注点吗?
  3. A service as i understand can be seen as a helper and App component can use this service to update the title我理解的服务可以看作是一个助手,App 组件可以使用这个服务来更新标题
  4. You can have a init method in the service which will be called from app component so that it can start listening to router event and update the title.您可以在服务中有一个init方法,该方法将从应用程序组件中调用,以便它可以开始侦听路由器事件并更新标题。 This makes it very imperative, explicit too and you can move calling this init method to some other component if required.这使得它非常必要,也很明确,如果需要,您可以将调用此 init 方法移动到其他组件。

Solution解决方案

What you need is a Class , which you can use to update page title [or some other stuff], but it should not be injectable.你需要的是一个Class ,你可以用它来更新页面标题[或其他一些东西],但它不应该是可注入的。

You can define a plain JS Class Not as @injectable .您可以定义一个普通的 JS Class Not as @injectable You can then use the APP_INITIALIZER to instantiate the class manually.然后您可以使用APP_INITIALIZER手动实例化该类。 The instance thus created will not be accessible using DI, but can be saved and shared by using global/static variable.这样创建的实例将无法使用 DI 访问,但可以使用全局/静态变量进行保存和共享。

Here is the sample stackblitz .这是示例stackblitz I have only logged to console, you can update as needed.我只登录到控制台,您可以根据需要进行更新。

TitleService Class that updates title using updateTitle method. TitleService使用updateTitle方法更新标题的类。
PageTitleService Class that will subscribe to router and calls TitleService.updateTitle . PageTitleService将订阅路由器并调用TitleService.updateTitle
pageTitleServiceInit factory function to manually instantiate PageTitleService . pageTitleServiceInit工厂函数来手动实例化PageTitleService

Code代码

import { APP_INITIALIZER, Injectable, NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";

import { AppComponent } from "./app.component";
import { HelloComponent } from "./hello/hello.component";
import {
  ActivatedRoute,
  NavigationEnd,
  Router,
  RouterModule
} from "@angular/router";
import { RandomComponent } from "./random/random.component";
import { filter } from "rxjs/operators";

// ================ TitleService ======================
@Injectable()
export class TitleService {
  updateTitle(title: string) {
    console.log("new title: ", title);
  }
}

// ================ Initalizer Factory ======================
function pageTitleServiceInit(router, route, titleService) {
  return () => {
    new PageTitleService(router, route, titleService);
  };
}

// ================ PageTitleService ======================
export class PageTitleService {
  constructor(
    router: Router,
    activatedRoute: ActivatedRoute,
    titleService: TitleService
  ) {
    router.events
      .pipe(filter(event => event instanceof NavigationEnd))
      .subscribe((x: any) => {
        console.log(activatedRoute);
        titleService.updateTitle(x.url);
      });
  }
}

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot([
      { path: "random", component: RandomComponent },
      { path: "hello", component: HelloComponent }
    ])
  ],
  declarations: [AppComponent, HelloComponent, RandomComponent],
  bootstrap: [AppComponent],
  providers: [
    TitleService,
    {
      provide: APP_INITIALIZER,
      useFactory: pageTitleServiceInit,
      deps: [Router, ActivatedRoute, TitleService],
      multi: true
    }
  ]
})
export class AppModule {}

Use APP_INITIALIZER or APP_BOOTSTRAP_LISTENER .使用APP_INITIALIZERAPP_BOOTSTRAP_LISTENER More details you can find here .您可以在此处找到更多详细信息。

For use Router in APP_INITIALIZER set initialNavigation option to true .要在APP_INITIALIZER使用Router ,请将initialNavigation选项设置为true Or use APP_BOOTSTRAP_LISTENER (but you skip first navigation in this case)或者使用APP_BOOTSTRAP_LISTENER (但在这种情况下您跳过第一个导航)

for add initalizer you need add provider for it要添加初始化程序,您需要为其添加提供程序

{
  provide: APP_INITIALIZER,
  useFactory: (router) => yourFunction,
  multi: true,
  deps: [Router]
}

I think there are a few options possible.我认为有几种可能的选择。

  1. Simply create an instance yourself.只需自己创建一个实例。 An service is noting more than a regular class.服务不仅仅是一个普通的课程。 You can create an instance yourself new PageTitleService(router, activatedRoute, pageService) However, this requires you to pass an instance of the objects defined in the constructor.您可以自己创建一个实例new PageTitleService(router, activatedRoute, pageService)但是,这需要您传递构造函数中定义的对象的实例。
  2. Use the injector.使用注射器。 You can call injector.get(PageTitleService) to get an instance.你可以调用injector.get(PageTitleService)来获取一个实例。 Unfortunately injector is also a service which should be inejcted in an component or service.不幸的是,注入器也是一种应该在组件或服务中注入的服务。

In your case, you are going to need dependency injection.在您的情况下,您将需要依赖注入。 You could try option 1 and also manually create instances of router and activatedRoute, but they might have their own dependencies.您可以尝试选项 1 并手动创建路由器和激活路由的实例,但它们可能有自己的依赖项。 And their dependencies might also have dependencies.他们的依赖也可能有依赖。

You could create a regular Singleton which has some kind of init function that does al the initial work.您可以创建一个常规的 Singleton,它具有某种 init 函数来完成初始工作。 Something like this:像这样的东西:

class Singleton
{
  private constructor()
  {
    console.log("hello");
  }

  private hasStarted = false;

  private static _singelton :Singleton
  static get(): Singleton{
    return Singleton._singleton || (Singleton._singleton = new Singleton())
  }

  pubic init(router: Router, activatedRoute: ActivatedRoute, titleService: Title): void{
    if(!hasStarted){
      //do whatever you want
      hasStarted = true;
    }
  }
}

In your appcomponent:在您的应用组件中:

constructor(router: Router, activatedRoute: ActivatedRoute, titleService: Title){
  Singelton.getInstance().init(router: Router, activatedRoute: ActivatedRoute, titleService: Title);
}

This way there will only be one instance of your singleton an everyting in the init will only be executed once.这样一来,你的单例就只有一个实例,init 中的每一个都只会执行一次。

Note: nothing is realy private in typescipt.注意:typescipt 中没有什么是真正私有的。 Something like this is still possible.这样的事情还是有可能的。

const singleton = Singelton.getInstance();
singelton['hasStarted'] = false;
singleton.init(...);

Solution解决方案

Instead of injecting the service via your app.component.ts , you could inject the service via the app.module.ts (or via your app-load.module.ts )您可以通过app.module.ts (或通过app-load.module.ts )注入服务,而不是通过app.component.ts注入服务

Eg:例如:

@NgModule({
  ...
})
export class AppModule {
  constructor(pageTitleService: PageTitleService) {}
}

Is it possible for you to create a title.ts in your root folder and do:您是否可以在根文件夹中创建title.ts并执行以下操作:

export const titleConfig = {
  title: '',
}

and access that object from whenever you need it, and change it in your router并随时访问该对象,并在路由器中更改它

import { titleConfig } from 'src/app/title';
// ...more magic goes here

export class PageTitleService {

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private titleService: Title
  ) {
    this.router.events
      .filter((event) => event instanceof NavigationEnd)
      .map(() => this.activatedRoute)
      .map((route) => {
        while (route.firstChild) {
          route = route.firstChild;
        }

        return route;
      })
      .filter((route) => route.outlet === 'primary')
      .mergeMap((route) => route.data)
      .subscribe((routeData) => titleConfig.title = routeData['title']); // change here
  }

}

if you are using ChangeDetectionStrategy.Onpush then you can do a { ...routerData['title'] } or add a setTitle method to the object.如果您正在使用ChangeDetectionStrategy.Onpush那么您可以执行{ ...routerData['title'] }或向对象添加setTitle方法。 I'm not sure.我不知道。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM