angular-服务和依赖注入


服务和依赖注入#

1 准备服务#

@Injectable()
export class DiscountService {
...

2 准备依赖组件#

import { Component, Input } from '@angular/core';
import { DiscountService } from './discount.service';

@Component({
  selector: 'paDiscountDisplay',
  template: `
    <div class="bg-info text-white p-2">
      The Discount: {{ discounter.discount }}
    </div>
  `,
})
export class PaDiscountDisplayComponent {
  constructor(public discounter: DiscountService) {}
}

3 注册服务#

@NgModule({
  declarations: [
    PaAttrDirective,
    ProductComponent,
    ToggleViewComponent,
    ProductFormComponent,
    ProductTableComponent,
    PaDiscountDisplayComponent,
    PaDiscountEditorComponent,
  ], //列出应用的类(组件、指令和管道),告诉Angular哪些类构成了这个模块。
  imports: [BrowserModule, FormsModule, ReactiveFormsModule, FormsModule],
  providers: [DiscountService], //提供服务
  bootstrap: [ProductComponent], //根组件
})
export class AppModule {}

如果PaDiscountDisplayComponent 被标记为独立组件(standalone),而独立组件不能在 NgModuledeclarations 数组中声明。独立组件应该通过 imports 数组导入。

首选:在应用的根级别使用 providedIn#

这种方式是在 Angular 6 引入的,用于在应用的根级别自动提供服务。这意味着你不需要在任何模块的 providers 数组中手动添加这个服务。Angular 会在应用启动时自动将这个服务添加到根注入器中,使得服务在整个应用范围内可用。

在 Angular 中,providedIn 属性可以接受几个不同的值,这些值决定了服务(Service)的提供方式和作用域。以下是 providedIn 可以接受的值及其含义:

1. 'root'#

  • 含义:服务在应用的根注入器中提供。这意味着服务在整个应用范围内可用,并且只会被实例化一次。

  • 用途:适用于大多数全局服务,这些服务需要在应用的任何地方都可以访问,例如 HTTP 服务、状态管理服务等。

  • 示例

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

2. 'platform'#

  • 含义:服务在平台注入器中提供。平台注入器是 Angular 应用的最顶层注入器,适用于需要在多个应用实例之间共享的服务。

  • 用途:适用于需要在多个 Angular 应用实例之间共享的服务,例如日志服务、配置服务等。

  • 示例

    @Injectable({
      providedIn: 'platform'
    })
    export class PlatformService {
      constructor() {}
    }
    

3. 'any'#

  • 含义:服务在每个组件或指令的注入器中提供。这意味着每次使用该服务的组件或指令都会创建一个新的服务实例。

  • 用途:适用于需要在每个组件或指令中独立使用的服务,例如表单验证服务、局部状态管理服务等。

  • 示例

    @Injectable({
      providedIn: 'any'
    })
    export class LocalService {
      constructor() {}
    }
    

4. 模块类(Module Class)#

  • 含义:服务在指定的模块注入器中提供。这意味着服务只在该模块及其子模块中可用。

  • 用途:适用于需要在特定模块及其子模块中使用的服务,例如某个功能模块的专用服务。

  • 示例

    @Injectable({
      providedIn: MyModule
    })
    export class ModuleService {
      constructor() {}
    }
    

5. null#

  • 含义:服务不会自动提供,需要在模块的 providers 数组中手动提供。

  • 用途:适用于需要在多个模块中以不同方式提供的服务,或者需要延迟加载的服务。

  • 示例

    @Injectable({
      providedIn: null
    })
    export class ManualService {
      constructor() {}
    }
    

    在模块中提供服务:

    @NgModule({
      declarations: [
        // 组件列表
      ],
      imports: [
        // 模块列表
      ],
      providers: [ManualService],  // 在这里提供服务
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

总结#

  • 'root':全局提供,整个应用范围内可用。
  • 'platform':平台提供,多个应用实例之间共享。
  • 'any':每个组件或指令独立提供。
  • 模块类:在指定模块及其子模块中提供。
  • null:不自动提供,需要手动在模块中提供。

选择合适的 providedIn 值取决于你的具体需求和应用结构。

使用服务提供程序#

准备创建一个log服务

log.service.ts

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

export enum LogLevel {
  DEBUG,
  INFO,
  ERROR,
}

@Injectable()
export class LogService {
  minmumLevel: LogLevel = LogLevel.INFO;

  logInfoMessage(message: string) {
    this.logMessage(LogLevel.INFO, message);
  }

  logDebugMessage(message: string) {
    this.logMessage(LogLevel.DEBUG, message);
  }

  logErrorMessage(message: string) {
    this.logMessage(LogLevel.ERROR, message);
  }

  logMessage(level: LogLevel, message: string) {
    if (level >= this.minmumLevel) {
      console.log(`Message: (${LogLevel[level]}): ${message}`);
    }
  }
}

discount.service.ts 更新

import { Injectable } from '@angular/core';
import { LogService } from './log.service';

@Injectable()
export class DiscountService {
  private discountValue: number = 10;

  public get discount(): number {
    return this.discountValue;
  }

  public set discount(value: number) {
    this.discountValue = value || 0;
  }
  constructor(private logger: LogService) {}

  public applyDiscount(price: number): number {
    this.logger.logInfoMessage(
      `Discount ${this.discount} applied to price ${price}`
    );
    return Math.max(price - this.discountValue, 5);
  }
}

这个时候,因为没有注册服务

main.ts:7

NullInjectorError: R3InjectorError(AppModule)[DiscountService -> LogService -> LogService -> LogService]:

NullInjectorError: No provider for LogService!

at Object.DiscountService_Factory [as factory] (discount.service.ts:5:29)

最简单的方法是添加到app.module.ts 的 providers

为什么要这么执行?毕竟 Angular 第一次使用 LogService 对象的时候才需要创建一个 LogService 对象。

类提供程序#

上面的图就是类提供程序(类注册服务)。

还有一种写法

providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    { provide: LogService, useClass: LogService },
  ], //提供服务
  • provide 属性持有作为消费依赖值的令牌的键。
  • 第二个属性是一个提供者定义对象,它告诉注入器如何创建依赖值。提供者定义可以是以下几种之一:
    • useClass —— 此选项告诉 Angular DI 在注入依赖时实例化一个提供的类。
    • useExisting —— 允许你将某个令牌作为别名并引用任何现有的令牌。
    • useFactory —— 允许你定义一个构建依赖的函数。
    • useValue —— 提供一个应该用作依赖的静态值。
  • 可选属性:multi解析多个对象依赖
属性 描述
provide 此属性持有作为消费依赖值的令牌的键,是注入器查找依赖的标识符(任何对象都可以作为令牌)
useClass 此选项告诉 Angular DI 在注入依赖时实例化一个提供的类。当使用 useClass 时,注入器会创建该类的实例并将其作为依赖值
useExisting 允许你将某个令牌作为别名并引用任何现有的令牌,可用于将一个依赖项作为另一个依赖项的别名,以便它们在注入器中被视为相同的依赖
useFactory 允许你定义一个构建依赖的函数,通过该函数的执行结果来提供依赖值。可以在函数中进行复杂的逻辑操作,比如根据不同的条件创建不同的实例或进行其他计算
useValue 提供一个应该用作依赖的静态值,适用于提供常量、配置对象或已有的实例,注入器会直接使用该静态值作为依赖
multi 当为 true 时,注入器会返回一个实例数组。这对于允许多个提供者分布在多个文件中为一个公共令牌提供配置信息很有用;当为 false 时,注入器将仅返回一个单一的实例,而不是数组

理解令牌 - provide#

字符串令牌 - provide: 'logger'#

任何对象都可以作为令牌,下面以字符串(而不是类)作为令牌

{ provide: 'logger', useClass: LogService }

angular 对类的令牌会自动匹配,其他令牌需要帮助。

discount.service.ts

@Injectable()
export class DiscountService {
  private discountValue: number = 10;
  ...
  constructor(@Inject('logger') private logger: LogService) {}
  ...

@Inject装饰器被用于构造函数的参数,用来指定提供程序令牌用于解析依赖。

Opaque 不透明令牌 - InjectionToken 对象#

我们可能需要注入一个配置对象、一个函数或者一些不是类的实例的数据,此时InjectionToken就派上用场。

log.service.ts

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

export const LOG_SERVICE = new InjectionToken('logger');

export enum LogLevel {
  DEBUG,
  INFO,
  ERROR,
}

@Injectable()
export class LogService {
...

app.module.ts

{ provide: LOG_SERVICE, useClass: LogService }

discount.service.ts

@Injectable()
export class DiscountService {
  private discountValue: number = 10;
  ...
  constructor(@Inject(LOG_SERVICE) private logger: LogService) {}
  ...

useClass 属性#

使用必须小心,最常用的方法是使用不同子类

下面创建一个子类SpecialLogService

import { Injectable, InjectionToken } from '@angular/core';
...

@Injectable()
export class LogService {
  ...
}

@Injectable()
export class SpecialLogService extends LogService {
  constructor() {
    super();
    this.minmumLevel = LogLevel.DEBUG;
  }
  override logMessage(level: LogLevel, message: string) {
    if (level >= this.minmumLevel) {
      console.log(`Special Message: (${LogLevel[level]}): ${message}`);
    }
  }
}

app.module.ts

providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    { provide: LOG_SERVICE, useClass: SpecialLogService },
  ], //提供服务

multi - 解析多个对象的依赖#

传递一个数组,可以多个类提供程序配置为使用同一个令牌,并将 multi 设置为 true

app.module.ts

  providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    { provide: LOG_SERVICE, useClass: LogService, multi: true },
    { provide: LOG_SERVICE, useClass: SpecialLogService, multi: true },
  ], //提供服务

discount.service.ts

import { Inject, Injectable } from '@angular/core';
import { LOG_SERVICE, LogLevel, LogService } from './log.service';

@Injectable()
export class DiscountService {
  private discountValue: number = 10;
  private logger: LogService;
  ...
  constructor(@Inject(LOG_SERVICE) private loggers: LogService[]) {
    this.logger = loggers.find((l) => l.minmumLevel == LogLevel.DEBUG);
  }

  public applyDiscount(price: number): number {
    this.logger.logInfoMessage(
      `Discount ${this.discount} applied to price ${price}`
    );
    return Math.max(price - this.discountValue, 5);
  }
}

useValue - 使用值提供程序#

它允许你提供一个应该用作依赖的静态值。这个静态值可以是任何类型,包括对象、函数、字符串、数字、布尔值等。

app.module.ts

let logger = new LogService();
logger.minmumLevel = LogLevel.DEBUG;

@NgModule({
 providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    { provide: LogService, useValue: logger },

discount.service.ts

@Injectable()
export class DiscountService {
...

  constructor(private logger: LogService) {}
...

下面的单独例子是注入字符串、数字或布尔值

@NgModule({
  providers: [
    { provide: 'apiUrl', useValue: 'http://example.com/api' },
    { provide: 'maxRetries', useValue: 3 },
    { provide: 'isProduction', useValue: false }
  ]
})
export class AppModule {}

useFactory - 使用工厂提供程序#

属性 描述
provide 此属性持有作为消费依赖值的令牌的键,是注入器查找依赖的标识符(任何对象都可以作为令牌)
useFactory 允许你定义一个构建依赖的函数,通过该函数的执行结果来提供依赖值。可以在函数中进行复杂的逻辑操作,比如根据不同的条件创建不同的实例或进行其他计算
deps 提供一个令牌组成的数组,令牌被解析后,将被传递给 useFactory 属性指定的函数
multi 当为 true 时,注入器会返回一个实例数组。这对于允许多个提供者分布在多个文件中为一个公共令牌提供配置信息很有用;当为 false 时,注入器将仅返回一个单一的实例,而不是数组

app.module.ts

  providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    {
      provide: LOG_LEVEL,
      useValue: LogLevel.DEBUG,
    },
    {
      provide: LogService,// 提供一个注入令牌 LOG_LEVEL
      deps: [LOG_LEVEL],// 依赖项列表,这里表示 LogService 的实例化需要依赖 LOG_LEVEL 的值
      useFactory: (level) => {
        let logger = new LogService();
        logger.minmumLevel = level;// 将传入的 level(来自 LOG_LEVEL)设置为 LogService 的最小日志级别
        return logger;
      },
    },
  ], //提供服务

useExisiting - 使用已有服务提供程序#

第一种 - 替换服务实现而不改变依赖关系

假设你已经有很多地方依赖于 ServiceA,但你想在某些情况下使用 ServiceB 来代替 ServiceA,同时又不想修改所有依赖 ServiceA 的代码,你可以使用 useExisting

providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    {
      provide: LOG_LEVEL,
      useValue: LogLevel.INFO,
    },
    {
      provide: 'debuglevel',
      useExisting: LOG_LEVEL,
    },
    {
      provide: LogService,
      deps: ['debuglevel'],
      useFactory: (level) => {
        let logger = new LogService();
        logger.minmumLevel = level;
        return logger;
      },
    },
  ], //提供服务

第二种 - 服务别名
useExisting 可以用来为一个服务创建别名,在某些情况下,你可能想要使用不同的名称来引用同一个服务。例如,你可能有一个服务 DataService,但在不同的上下文中,你想将其称为 UserDataServiceProductDataService,但它们实际上是同一个服务实例。

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

@Injectable()
export class DataService {
  getData(): string[] {
    return ['Data 1', 'Data 2'];
  }
}

@NgModule({
  providers: [
    DataService,
    { provide: 'UserDataService', useExisting: DataService },
    { provide: 'ProductDataService', useExisting: DataService }
  ]
})
export class AppModule {}

本地提供程序 - ElementInjector#

前面都是Angular模块(@NgModule)级别的注入,可以在组件(@Component)级别拥有一个提供程序,单独解析依赖服务。

ElementInjector

Angular 会为每个 DOM 元素隐式创建 ElementInjector

@Component() 装饰器中使用其 providersviewProviders 属性提供服务时,会配置一个 ElementInjector

属性 描述
providers 用于在组件及其所有子组件的注入器中提供服务。这意味着在该组件及其后代组件中都可以注入由 providers 提供的服务。当一个服务在 providers 中声明时,它会在整个组件树中被共享,除非有另一个组件在其自己的 providers 中声明了相同的服务,此时会创建一个新的服务实例。
viewProviders 仅在组件及其视图子组件(通过模板引用的子组件)的注入器中提供服务,而不包括内容子组件(通过 <ng-content> 投影的子组件)。

例子 transform 管道当大于30的价格需要输出消息,会淹没在所有消息里。

discount.pipe.ts

@Pipe({
  name: 'discount',
  pure: false, //当 pure 设置为 false 时,检测变化时重新执行
})
export class DiscountPipe implements PipeTransform {
  constructor(private discout: DiscountService, private logger: LogService) {}
  transform(privce: number): number {
    if (privce > 30) {
      this.logger.logInfoMessage(`Large price discouted: ${privce}`);
    }
    return this.discout.applyDiscount(privce);
  }
}

这个时候修改LOG_LEVEL默认值为ERROR。所有消息都消失了。

app.module.ts

providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    {
      provide: LOG_LEVEL,
      useValue: LogLevel.ERROR,
    },
    {
      provide: 'debuglevel',
      useExisting: LOG_LEVEL,
    },
    {
      provide: LogService,
      deps: ['debuglevel'],
      useFactory: (level) => {
        let logger = new LogService();
        logger.minmumLevel = level;
        return logger;
      },
    },
  ], //提供服务

如果这个时候 ProductTableComponent 组件注入LogService 服务,使用的单独的提供程序

productTable.component.ts

@Component({
  selector: 'paProductTable',
  templateUrl: './productTable.component.html',
  styleUrls: ['./productTable.component.css'],
  providers: [LogService],
})
export class ProductTableComponent implements OnInit, AfterViewInit {
...

viewProviders - 子视图创建#

创建一个指令

valueDisplay.directive.ts

import { Directive, HostBinding, Inject, InjectionToken } from '@angular/core';

export const VALUE_SERVICE = new InjectionToken('value_service');

@Directive({ selector: '[paDisplayValue]' })
export class PaDisplayValueDirective {
  constructor(@Inject(VALUE_SERVICE) serviceValue: string) {
    this.elementContent = serviceValue;
  }

  @HostBinding('textContent')
  elementContent: string;
}

app.module.ts

providers: [
    DiscountService,
    SimpleDataSource,
    Model,
    LogService,
    {
      provide: VALUE_SERVICE,
      useValue: 'Apples',
    },

template.component.html

<div class="row m-2">
  <div class="col-4 p-2">
    <paProductForm> [ng-content]: <span paDisplayValue></span> </paProductForm>
  </div>
  <div class="col-8 p-2">
    <!-- <paProductTable [model]="model"></paProductTable> -->
    <paToggleView>
      <paProductTable></paProductTable>
    </paToggleView>
  </div>
</div>

productForm.component.html

下面是 ng-content

<div class="bg-info text-white m-2 p-2">
  View Child Value: <span paDisplayValue></span>
</div>
<div class="bg-info text-white m-2 p-2">
  Content Child Value: <ng-content></ng-content>
</div>

下面使用viewProviders

productForm.component.ts

@Component({
  selector: 'paProductForm',
  templateUrl: './productForm.component.html',
  styleUrls: ['./productForm.component.css'],
  viewProviders: [{ provide: VALUE_SERVICE, useValue: 'Orange' }],
})
export class ProductFormComponent implements OnInit {

控制依赖解析#

装饰器 描述
@Host 限制依赖注入的搜索范围到宿主元素及其直接子元素。当使用 @Host 时,注入器将不会在组件树的更高级别查找服务,而是仅在宿主元素及其直接子元素的范围内查找。这样可以确保服务的查找范围被限制在当前组件及其直接子组件,避免了意外地使用来自组件树更高层次的服务实例。
@Optional 用于标记一个依赖项为可选的。如果注入器无法找到该服务,不会抛出错误,而是将该服务的注入值设置为 null。这在某些情况下非常有用,例如当服务不是必需的,或者服务可能不存在时,使用 @Optional 可以防止应用程序因服务未找到而崩溃。
@SkipSelf 指示注入器跳过当前组件,从父组件开始查找服务。当使用 @SkipSelf 时,注入器会首先尝试在父组件中查找服务,而不是当前组件。这对于某些特定的依赖注入场景很有用,例如,当希望使用父组件的服务实例而不是当前组件的服务实例时。

@Host - 限制查找范围#

valueDisplay.directive.ts

import {
  Directive,
  Host,
  HostBinding,
  Inject,
  InjectionToken,
  Optional,
} from '@angular/core';

export const VALUE_SERVICE = new InjectionToken('value_service');

@Directive({ selector: '[paDisplayValue]' })
export class PaDisplayValueDirective {
  constructor(@Inject(VALUE_SERVICE) @Host() @Optional() serviceValue: string) {
    this.elementContent = serviceValue || 'No Value';
  }

  @HostBinding('textContent')
  elementContent: string;
}

@Host() 的查找范围

  • @Host() 装饰器用于限制依赖注入的范围,确保只能从宿主元素及其祖先元素中进行注入。
  • 宿主元素是指指令或组件所附加的那个 DOM 元素。
  1. template.component.html 中的 <paProductForm> 组件:

    <paProductForm> [ng-content]: <span paDisplayValue></span> </paProductForm>
    
  2. productForm.component.html 中的内容投影部分:

    <div class="bg-info text-white m-2 p-2">
      Content Child Value: <ng-content></ng-content>
    </div>
    

在这个场景中:

template.component.ts 没有注入 VALUE_SERVICE

@Optional 一般与 @Host 一起使用,

@Optional() - 如果找不到服务,不会抛出错误#

  • 当指定的依赖项不存在时,不会抛出错误,而是将依赖项设置为 nullundefined
  • 这使得代码更加健壮,能够在某些情况下处理缺失的依赖项。

@SelfSkip - 指示注入器跳过当前组件,从父组件开始查找服务#

productForm.component.ts

@Component({
  selector: 'paProductForm',
  templateUrl: './productForm.component.html',
  styleUrls: ['./productForm.component.css'],
  viewProviders: [{ provide: VALUE_SERVICE, useValue: 'Orange' }],
})
export class ProductFormComponent implements OnInit {
  newProduct: Product = new Product();

  constructor(
    private model: Model,
    @Inject(VALUE_SERVICE) @SkipSelf() private serviceValue: string
  ) {
    console.log('Service Value:' + serviceValue);
  }

这里上面注入的是Orange,但是下面console的是 Apple. 因为从父级开始查找。

作者:【唐】三三

出处:https://www.cnblogs.com/tangge/p/18671072

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   【唐】三三  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
历史上的今天:
2018-01-14 Visual Studio(2017/2019/2022) 插件推荐
2011-01-14 ie兼容问题
2011-01-14 Div+CSS布局居中
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示