在Angular注入器层次结构中重写依赖关系

377 阅读6分钟

简介

Angular框架可以直接确定依赖性在应用程序中的流动方向,从而使调试工作无缝地进行。

Angular允许通过父组件的注入器提供的依赖性在其子组件中共享,方法是将它们注入到子组件的构造器中。

为了更好地理解这一点,让我们考虑一下父-子依赖注入的实际方法。

要继续学习本教程,你应该有。

  • Node.js v10.x
  • 对Angular的了解
  • 对TypeScript的了解

开始学习

运行下面的命令来创建一个新的Angular应用程序。

ng new phone-book

CD进入电话簿目录,在终端运行下面的命令,为我们的应用程序的正确结构创建一个模块。

ng generate module contacts

创建app/contacts/contact.model.ts

//app/contacts/contact.model.ts
export interface Contact {
  id: number;
  name: string;
  phone_no: string;
}

上面的片段使用了TypeScript接口。它创建了一个模型来验证将从产品的服务中返回的数据。

运行下面的命令来创建一个联系服务。

ng generate service contacts/contact

更新contact.service.ts

//app/contacts/contact.service.ts
import { Injectable } from '@angular/core';
import { Contact } from './contact.model';
@Injectable({
  providedIn: 'root'
})
export class ContactService {
  constructor() { }
  getContacts(): Contact[] {
    return [
      { id: 1, name: 'Peter', phone_no: '09033940948' },
      { id: 2, name: 'Sam', phone_no: '07033945948'},
      { id: 3, name: 'Bryce', phone_no: '08033740948' },
      { id: 4, name: 'Lee', phone_no: '090339409321' },
      { id: 5, name: 'Albert', phone_no: '09066894948'  }
    ];
  }
}

providedIn 属性为该服务创建了一个提供者。在这种情况下,providedIn: 'root' 指定Angular应该在根注入器中提供服务(即让它在整个应用程序中可用)。

现在,ContactService 可以被注入到我们应用程序的任何地方。

更新contact-list.component.ts

//app/contacts/contact-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Contact } from '../contact.model';
import { ContactService } from '../contact.service';
@Component({
  selector: 'app-contact-list',
  templateUrl: './contact-list.component.html',
  styleUrls: ['./contact-list.component.css'],
  providers: [ContactService]
})
export class ContactListComponent implements OnInit {
  contacts: Contact[];
  constructor(private contactService: ContactService) { }
  ngOnInit(): void {
    this.contacts = this.contactService.getContacts();
  }
}

上面的片段使用组件构造函数中的new关键字实例化了ContactService 私有属性,在ngOnInit 方法中调用contactServicegetContacts 方法,并将返回值分配给contacts 属性。

更新contact-list.component.html

//app/contacts/contact-list.component.html
<h3>My Contact List</h3>
<ul>
  <li *ngFor="let contact of contacts">
    {{contact.name}}
  </li>
</ul>
<app-recent-contact></app-recent-contact>

上述代码段使用ngFor 指令来显示联系人列表,也接受RecentContact 作为其直接子组件。

更新app/contacts/recent-contact.component.ts

//app/contacts/recent-contact.component.ts
import { Component, OnInit } from '@angular/core';
import { ContactService } from '../contact.service';
import { Contact } from '../contact.model';
@Component({
  selector: 'app-recent-contact',
  templateUrl: './recent-contact.component.html',
  styleUrls: ['./recent-contact.component.css']
})
export class RecentContactComponent implements OnInit {
  contacts: Contact[];
  constructor(private contactService: ContactService) { }
  ngOnInit(): void {
    this.contacts = this.contactService.getContacts();
  }
}

RecentContactComponent ,作为ContactListComponent 的直接子组件,即使没有通过@component 装饰器的providers 属性实际提供,也可以访问ContactListComponent 提供的依赖关系。

更新app/contacts/recent-contact.component.html

//app/contacts/recent-contact.component.html
<h3>My Recent Contacts</h3>
<ul>
  <li *ngFor="let contact of contacts | slice:0:3">
    {{contact.name}}
  </li>
</ul>

在这里,我们将SlicePipe 应用到ngFor 语句中,只显示最后三个联系人。

使用ng serve 命令运行应用程序,我们应该在浏览器中呈现所有联系人和最近联系人的列表。

覆盖Angular中的依赖关系

在Angular中重写依赖关系需要两个关键属性。

  • provide - 指向你想覆盖的依赖关系
  • useClass - 指向新的依赖关系,它将覆盖providers 属性中的现有依赖关系。

它遵循一个简单的格式。

@Component({
 providers: [
  { provide: Service, useClass : FakeService }
 ]
})

何时覆盖依赖关系

在某些情况下,您希望覆盖您的构造的默认解析。

这些是一些典型的情况。

  • 编写单元测试时覆盖一个提供者
  • 在不对依赖关系进行修改的情况下为其添加一个独特的功能

后者是通过使用extend 关键字从现有依赖关系中创建一个新的依赖关系来实现的。

让我们考虑后一种情况的实际做法,创建一个名为RecentContactService 的新服务,它将扩展ContactService ,并使用slice 数组方法过滤掉数据,而不是使用模板中的管道。

更新recent-contact.service.ts

//app/contacts/recent-contact.service.ts
import { Injectable } from '@angular/core';
import { ContactService } from './contact.service';
import { Contact } from './contact.model';
@Injectable({
  providedIn: 'root'
})
export class RecentContactService extends ContactService {
  constructor() {
    super();
  }
  getContacts(): Contact[] {
    return super.getContacts().slice(Math.max(super.getContacts().length - 3, 0))
  }
}

在这里,getContacts 方法从ContactService 所产生的数组中返回最后三个项目。

现在,我们可以将这个新的服务(依赖关系)添加到providers 的属性中。RecentContactComponent

更新recent-contact.component.ts

//app/contact/recent-contact.component.ts
...
import { RecentContactService } from '../recent-contact.service';
@Component({
  selector: 'app-recent-contact',
  templateUrl: './recent-contact.component.html',
  styleUrls: ['./recent-contact.component.css'],
  providers: [{
    provide: ContactService,
    useClass: RecentContactService
  }]
})
export class RecentContactComponent implements OnInit {
  contacts: Contact[];
  constructor(private recentContactService: RecentContactService) {}
  ngOnInit(): void {
    this.contacts = this.recentContactService.getContacts();
  }
}

在这里,useClass 属性允许Angular用我们的新依赖性(RecentContactService )覆盖我们之前的依赖性(ContactService )。

用字符串值覆盖依赖关系

在我们想要覆盖的依赖是一个字符串的情况下,useValue 语法会很方便。

比如我们的应用程序有一个DBconfig 文件。

//app/db.config.ts
export interface DBConfig {
  name: string;
  version: number;
}
export const databaseSettings: DBConfig = {
  name: 'MongoDB',
  version: 2.0
};

为了使数据库配置能够在我们的应用程序中访问,我们需要提供一个InjectionToken 对象。

//app/db.config.ts
import { InjectionToken } from '@angular/core';
export const DB_CONFIG = new InjectionToken<DBConfig>('db.config');
...

现在,我们的组件可以使用@Inject 装饰器访问数据库配置。

//app/app.component.ts
import { Component, Inject } from '@angular/core';
import { DB_CONFIG, databaseSettings, DBConfig } from './db.config';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [{
    provide: DB_CONFIG,
    useValue: databaseSettings
  }]
})
export class AppComponent {
  name: string;
  version: number;
  constructor(@Inject(DB_CONFIG) config: DBConfig) {
    this.name = config.name;
    this.version = config.version;
  }
}


//app/app.component.html
<h4>This app uses {{name}} Database v. {{version}}</h4>
<app-contact-list></app-contact-list>

在运行时重写依赖性

useFactory 关键字允许Angular决定在运行时向构造中注入什么依赖。

import { RecentContactService } from './recent-contact.service';
import { ContactService } from './contact.service';
export function contactFactory(isRecent: boolean) {
  return () => {
    if (isRecent) {
      return new RecentContactService();
    }
    return new ContactService();
  };
}

上面的片段是一个工厂,根据布尔条件返回RecentContactServiceContactService

现在,我们可以修改RecentContactComponent'的providers 属性,如下所示。

...
 providers: [{
    provide: ContactService,
    useClass: contactFactory(true)
  }]
...

有了这个,我们可以尽可能多地覆盖任何依赖关系。这就更容易了,因为我们只需要把新的依赖关系添加到contactFactory ,调整条件,最后调整新的依赖关系的相应组件的useClass 属性。

在运行时覆盖依赖关系的限制

如果这些依赖中的任何一个在其构造函数中注入其他依赖与之前的实现contactFactory ,Angular将抛出一个错误。

假设RecentContactServiceContactService 都依赖于AuthService 依赖关系,我们将不得不对contactFactory 进行如下调整。

export function contactFactory(isRecent: boolean) {
  return (auth: AuthService) => {
    if (isRecent) {
      return new RecentContactService();
    }
    return new ContactService();
  };
}

RecentContactComponent 然后,我们必须将AuthService 依赖关系添加到providers
对象的deps 属性中,如下所示。

...
 providers: [{
    ...
    deps: [AuthService]
  }]
...

结论

在大规模的Angular应用程序上工作,需要很好地理解依赖注入的工作方式。在大规模的Angular应用中,了解如何以及何时覆盖依赖性是至关重要的,因为它可以帮助你避免应用中不必要的代码重复。

这篇文章的源代码可以在GitHub上找到。

The postOverriding dependencies in the Angular injector hierarchyappeared first onLogRocket Blog.