依赖注入是一种设计模式,在很多编程语言中都可以看到,如Java,C#.
这里,我们要解释跟依赖注入有关的三个概念:
- 依赖(dependency)
- 注入(injection)
- 注入器(injector)
在一个应用中,一个对象实例(消费者)会在其内部使用其他对象实例,完成业务逻辑。这些被使用的对象实例,称为依赖
。把依赖传递给消费者代码的过程,称为注入
。如果在注入
过程中,借助了第三方代码,这些代码称为注入器
或注入者
。使用注入器
的原因,是因为消费者代码通常只知道依赖
的接口,而对如何初始化一个依赖
知之甚少。
依赖注入设计模式解决的主要问题是代码的耦合。例如,Angular 组件的职责是把数据渲染到页面上。那么如何获得数据呢?一种方法是,把需要的数据在组件类的代码中写好,也就是使用静态数据;另一种方法是把获得数据的业务逻辑委托给某个服务(依赖),在组件类的代码中,调用该服务,获得需要的数据。
第一个方法的明显缺陷就是违背了软件的单一职责的设计原则。下面,我们介绍如何使用依赖注入的设计模式,实现第二个方法。
创建数据服务
我们在前面的文章中,介绍了如何在组件模板展示一个图书列表。图书列表的数据是放置在主组件类(AppComponent)中的静态数据:
export class AppComponent {
…
books: Book[] = [
{id: 1, name: '《三体》', price: 50.00},
{id: 2, name: '《黑暗森林》', price: 40.00},
{id: 3, name: '《死神永生》', price: 60.00},
{id: 4, name: '《超新星纪元》', price: 35.00}
];
…
}
现在,我们就创建一个服务,返回这些数据。
- 使用 Angular CLI 命令创建一个图书服务。在
src/app
路径下,运行下面的命令:
ng generate service books/book
命令输出:
CREATE src/app/books/book.service.spec.ts (347 bytes)
CREATE src/app/books/book.service.ts (133 bytes)
文件结构如下所示:
服务类是一个普通的 TypeScript 类,附加了 @Injectable
装饰器。
import { Injectable } from '@angular/core’;
@Injectable({
providedIn: ‘root’
})
export class BookService {
constructor() { }
}
如果一个类附加了 @Injectable
装饰器,就表示是 Angular 的服务,并且可以注入到 Angular 的组件或其他服务中。
与组件一样,服务也必须进行注册,才能使用。@Injectable
装饰器的 providedIn
属性值是一个对象,表示服务的注入器。 上面代码中的 root
,表示根注入器,也是默认注入器。
- 在 BookService 类中,创建一个方法,返回图书列表数据。
getBooks(): Book[] {
return [
{id: 1, name: '《三体》', price: 50.00},
{id: 2, name: '《黑暗森林》', price: 40.00},
{id: 3, name: '《死神永生》', price: 60.00},
{id: 4, name: '《超新星纪元》', price: 35.00}
];
}
使用数据服务
BookService
已经可以为我们提供图书数据,下面就把这些图书展示到应用的首页上。
- 在
BookListComponent
类中,声明books
属性和bookService
属性。
books: Book[];
private bookService: BookService;
- 在
BookListComponent
类的构造函数中,创建BookService
的一个实例对象,为bookService
属性赋值。
constructor() {
this.bookService = new BookService();
}
- 在
BookListComponent
类的初始化方法中,获取图书数据。
ngOnInit(): void {
this.books = this.bookService.getBooks();
}
- 修改
book-list.component.html
文件,展示图书列表数据。
<ul>
<li *ngFor="let book of books">{{book.name}}</li>
</ul>
- 修改
app.component.html
文件,删除文件中的全部内容,只保留下面一行代码。
<app-book-list></app-book-list>
- 运行应用,打开浏览器,访问
http://localhost:4200/
,页面显示如下:
注入数据服务
细心的你一定发现了上面代码中存在的问题。在 BookListComponent
类的构造函数中,我们手工创建了 BookService
,并没有使用依赖注入设计模式。接下来,我们就对代码进行调整,使用 Angular 提供的依赖注入机制,把 BookService
注入到 BookListComponent
中。
- 在
BookListComponent
类中,删除bookService
属性。
books: Book[];
- 在
BookListComponent
类中,修改构造函数,把需要注入的服务,作为构造函数的参数传入。
constructor(private bookService: BookService) {}
现在,组件不需要再进行服务类的实例化工作。在初始化方法使用服务实例对象之前,已经在构造函数中对服务类进行了实例化。
依赖注入机制
在 Angular 应用中,除了根注入器,模块和组件都提供了注入器。注入器也按层次结构进行组织。当一个组件需要一个依赖时,应用会进行一个两阶段当搜索过程。
- 阶段1:应用会搜索消费者组件的所有上级组件(直接的和间接的)。如果找到了匹配的依赖,停止搜索,创建依赖的对象实例,返回给消费者组件。如果没有找到匹配的依赖,则进行阶段2。
- 阶段2:应用会搜索所有上级模块的注入器,包括应用的根注入器。如果找到了匹配的依赖,停止搜索,创建依赖的对象实例,返回给消费者组件。如果没有找到匹配的依赖,则返回一个错误。
搜索过程如图所示: