Angular 依赖注入

一、什么是依赖注入

控制反转(IoC)

控制反转的概念最早在2004年由Martin Fowler提出,是针对面向对象设计不断复杂化而提出的一种设计原则,是利用面向对象编程法则来降低应用耦合的设计模式。

IoC强调的是对代码引用的控制权由调用方转移到了外部容器,在运行是通过某种方式注入进来,实现了控制反转,这大大降低了程序之间的耦合度。依赖注入是最常用的一种实现IoC的方式,另一种是依赖查找

依赖注入(Dependency Injection)

当然,按照惯例我们应该举个例子, 哦对,我们主要说明的是依赖注入,依赖查找请自行查阅资料。

假设我们有一个能做汉堡的设备(HRobot),需要用肉(meat)和一些沙拉(salad)作为原料,我们可以这样实现:

export class HRobot {
    public meat: Meat;
    public salad: Salad;
    constructor() {
        this.meat = new Meat();
        this.salad = new Salad();
    }
    cook() {}
}

看一下好像没有什么问题,可能你已经发现,我们的原材料都是放在机器里面的,如果我们想吃别的口味的汉堡恐怕就要去乡村基了。
为了可以吃到别的口味的汉堡,我们不得不改造一下我们的HRobot:

export class HRobot {
    public meat: Meat;
    public salad: Salad;
    constructor(public meat: Meat, public salad: Salad) {
        this.meat = meat;
        this.salad = salad;
    }
    cook() {}
}

现在,只要要直接给它meat和salad就好了,我们的HRobot()并不需要知道给它的是什么样的meat

let hRobot = new HRobot(new Meat(), new Salad());

比如,我们想吃鸡肉汉堡,只需要给它一块鸡肉就好:

class Chicken extends Meat {
    meat = 'chiken';
}

let cRobot = new HRobot(new Chicken(), new Salad());

感觉还不错,我们再也不会为了吃一个鸡肉汉堡大费周章的去改造一台机器,这太不可思议了。

我可能想到了,你还是懒得弄块鸡肉给它,这时候可以使用工厂函数:

export class HRobotFactory {
    createHRobot() {
        let robot = new HRobot(this.createMeat(), this.createSalad());
    }
    
    createMeat() {
        return new Meat();
    }
    
    creatSalad() {
        return new Salad();
    }
}

现在有了工厂,就有源源不断的汉堡可以吃了,开不开心,惊不惊喜?
好吧,没有最懒,只有更懒,连工厂都懒得管理我也是无话可说,幸运的是我们有Angular提供的依赖注入框架,它可以让你伸手就有汉堡吃!

二、 Angular依赖注入

在介绍Angular依赖注入之前,先来理一下三个概念:

  • 注入器(Injector):就想制造工厂,提供了一系列的接口,用于创建依赖对象的实例。
  • 提供商(Provider):用于配置注入器,注入器通过它来创建被依赖对象的实例,Provider把令牌(Token)映射到工厂方法,被依赖的对象就是通过这个方法创建的。
  • 依赖(Denpendence):指定了被依赖对象的类型,注入器会根据此类型创建对应的对象。

说了半天到底是什么样的?


依赖注入简单图示

用代码示例如下:

var injector = new Injector(...);
var robot = injector.get(HRobot);
robot.cook();

Injector()的实现如下:

import { ReflecttiveInjector } form '@angular/core';

var injector = ReflectiveInjector.resolveAndCreat([
    {provide: HRobot, useClass: HRobot},
    {provide: Meat, useClass: Meat},
    {provide: Salad, useClass: Salad}
]);

还有注入器是这样知道知道初始化HRobot需要依赖MeatSalad:

export class Robot {
    //...
    consructor(public meat: Meat, public salad: Salad) {}
    //...
}

当然,看了头大是应该的,因为上面的东西压根就不需要自己动手写,Angular的依赖注入框架已经自动帮我们完成了(注入器的生成和调用)。

1. 在组件中注入服务
Angular在底层做了大量的初始化工作,这极大地降低了我们使用依赖注入的成本,现在要完成依赖注入,我们只需要三步:

  • 通过import导入被依赖的对象服务
  • 在组件中配置注入器。在启动组件时,Angular会读取@Component装饰器里的providers元数据,它是一个数组,配置了该组件需要使用的所有依赖,Angular的依赖注入框架会根据这个列表去创建对应的示例。
  • 在组件构造函数中声明需要注入的依赖。注入器会根据构造函数上的声明,在组件初始化时通过第二步中的providers元数据配置依赖,为构造函数提供对应的依赖服务,最终完成依赖注入。

例子来了:

// app.component.ts
//...
// 1. 导入被依赖对象的服务
import { MyService } from './my-service/my-service.service';

@Component({
    //...
    // 2. 在组件中配置注入器
    providers: [
        MyService
    ]
    //...
})

export class AppComponent {
    // 3. 在构造函数中声明需要注入的依赖
    constructor(private myService: MyService) {}
}

2. 在服务中注入服务
除了组件依赖服务,服务间依的相互调用也很寒常见。例如我们想给我们的汉堡机器人加上一个计数器,来记录它的生产状况,但是计数器又依靠电源来工作,我们就可以用一个服务来实现:

// power.service.ts

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

@Injectable()
export class PowerService {
    // power come from here..
}


// count.service.ts

import { Injectable } from '@angular/core';
import { PowerService } from './power/power.service';

@Injectable()
export class CountService {
    constructor(private power: PoowerService) {}
}

// app.component.ts   这里是当前组件,其实模块中的注入也一样,后面讲到
//...
providers: [
    CountService,
    PowerService
]

这里需要注意的是@Injectable装饰器是非必须的,因为只有一个服务依赖其他服务的时候才必须需要使用@Injectable显式装饰,来表示这个服务需要依赖,所以我们的PowerService并不是必须加上@Injectable装饰器的,可是,Angular官方推荐是否依赖其他服务,都应该使用@Injectable来装饰服务。

3. 在模块中注入服务
在模块中注册服务和在组件中注册服务的方法是一样的,只是在模块中注入的服务在整个组件中都是可用的。

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule
  ],
  providers: [CountService, PowerService],
  bootstrap: [AppComponent]
})
export class AppModule { }

与在组件中注入不同的是,在Angular应用启动的时候,它好首先加载这个模块需要的所有依赖,,此时会生成一个全局的根注入器,由该依赖创建的依赖注入对象会再整个应用中可见,并共享一个实例。
Angular没有模块级作用域这个概念,只有应用程序级作用域和组件级作用域,这种设计主要是考虑模块的扩展性,一个应用通常由多个模块合并和成,在@NgModule中注册的服务,默认在整个应用中可用。

下面说两种特殊情况

  • 假设在两个模块中使用同样的Token注入了同一个服务,并且这两个模块先后导入到了根组件中:
// ...
@NgModule({
imports: [
    AModule,
    BModule
]
// ...
})

那么后面导入的模块中的服务会覆盖前面导入模块中的服务,也就是说BModule中的服务会覆盖AModule中的服务,即使是在AModule中注入的服务,同样使用的是BMoudle中提供的实例。

  • 还是假设两个模块同样使用同一个Token注入了同一个服务,但是BModule模块是导入在AModule模块中的:
// a.module.ts
// ...
@NgModule({
    imports: [BModule]
})

那么这种情况下两个模块使用的都是AModule中注入的服务。可以推断出在根模块中注入的服务是拥有最高优先级的,你可以在任何地方放心使用。

三、Provider

1. Provider的理解
Provider是有必要单独提出来一节的,上面第二节中我们其实只是简单的使用了其中一种的provider下面来详细说一下Provider
Angular中,Provider描述了注入器(Injector)如何初始化令牌(Token)所对应的依赖服务。Provider一个运行时的依赖,注入器依靠它来创建服务对象的实例。
比如我们上面用到的例子:

// ...
@Component({
    //...
    // 2. 在组件中配置注入器
    providers: [
        MyService
    ]
    //...
})

实际上它的完整形式应该是这样的:

@Component({
    //...
    // 2. 在组件中配置注入器
    providers: [
        {provide: MyService, useClass: MyService}
    ]
    //...
})

所以说我们上面只使用了一种provider: 类Provider(ClassProvider)。

2. Provider注册方式

上面提到我只使用了其中一种注册方式,那么下面介绍Angular中提供的四中常见的注册方式:

  • 类Provider(ClassProvider
  • 值Provider(ValueProvider
  • 别名Provider(ExistingProvider
  • 工厂Provider(FactoryProvider

1. 类Provider
Provider 基于令牌(Token)指定依赖项,这种方式可是让依赖被动态指定为其他不同的具体实现,只要接口不变,对于使用方就是透明的。比如数据渲染服务(Render),Render服务对上层提供的接口是固定的,倒是底层的渲染方式可以不同:
```ts
var inject = Injector.resolveAndCreate([
{provide: Render, useClass: DomRender}
//{provide: Render, useClass: DomRender} // canvas 渲染方式
//{provide: Render, useClass: DomRender} // 服务的想染方式
])

// 调用方不用做任何修改
class AppComponent {
    construtor(private render: Render) {}
}
```

2. 值Provider
由于依赖的对象并不一定都是类,也可以是字符串、常量、对象等其他数据类型的,这可以方便用在全局变量、系统相关参数配置场景中。在创建Provider对象的时候,只需要使用useValue就可以声明一个值Provider
```ts
let freeMan = {
freeJob: boolen;
live: () => {return 'do something u cant do'}
};

@Component({
    // ...
    providers: [
        {provide: 'someone', useValue: freeMan}
    ]
})
```

3. 别名Provider
有了别名Provider,我们就可以在一个Provider中配置多个令牌(Token),其对于的对象指向同一个实例,从而实现了多个依赖、一个对象实例的作用:

    // ...
    providers: [
        {provider: Power1, useClass: PowerService},
        {provider: Power2, useClass: PowerService}
    ]
    // ...

仔细想想,这样对吗?
显然是不对的,如果两个都使用了useClass那么按照令牌,将会创建两个不同的实例出来,那么应该怎么实现两个令牌同一个实例呢?答案是使用useExistiong:

    // ...
    providers: [
        {provider: Power1, useClass: PowerService},
        {provider: Power2, useExisting: PowerService}
    ]
    // ...
    ```
**4. 工厂`Provider`**
    工厂`Provider`允许我们根据不同的条件来实例化不同的服务,比如,我们在开发环境需要打印日志,但是在实际部署的时候可能并不需要打印这些东西,那么我们总不可能去找到整个应用中所有的`console.log()`这样的方法吧,这个时候我们可以使用工厂`provider`来帮我们处理,我们只需要在工厂`provider`中设定一个条件,使其能够根据条件返回实例化我们需要的服务就可以了。为了实现这样的功能我们可以在根模块中这样注入:
    ```ts
    // app.module.ts
    @NgModule({
    // ...
    providers: [
        HeroService,
        ConsoleService,
        {
            provide: LoggerService, 
            useFactory: (consoleService) => {
                return new LoggerService(true, consoleService);
            },
            deps: [ConsoleService]
        }
    ],
    bootstrap: [AppComponent]
    })
    export class AppModule { }

哦哦,那两个服务是这样写的:
   ```ts
    // console.service.ts
    // ...
    export class ConsoleService {
        log(message) {
            console.log(`ConsoleService: ${message}`);
        }
    }
      
    // logger.service.ts
    // ...
    export class LoggerService {
        constructor(private enable: boolean, 
            consoleService: ConsoleService
        ) { }
    
        log(message: string) {
            if (this.enable) {
                console.log(`LoggerService: ${message}`);
            }
        }
    }

然后在组件构造函数中写上需要的服务就好。

四、限定方式的依赖注入

想象一场景,你应用中的某个服务的provider被当做无效代码删掉了,那么你的应用可能就会出问题。还好这个问题早在设计的时候就已经考虑到了,我们可以使用Angular提供的@Optional@Host装饰器来解决这个问题。
Optional可以兼容依赖不存在的情况,提高系统的健壮性;@Host可以限定查找规则,明确实例化的位置,避免一些莫名的共享对象问题。

@Optional
借助@Optional就可以实现可选注入:

// app.component.ts
// ...
import { Optional } from '@angular/core';
constructor(@Optional() private logger: LoggerService) {
    if (this.logger) {
        this.logger.log('i am choosed');
    }
}

像例子中的那样只需要在宿主组件(Host Component)的构造函数中增加@Optional装饰器即可。
需要注意的是,上面例子中的LoggerService并不是不存在,只是并没有根据providers元数据中配置被实例化出来。

@Host
Angular中依赖查找的规则是按照注入器从当前组件向父组件查找,直到找到要注入的依赖位置,如果找不到就会报错。我们可以使用Angular提供的@Host装饰器来解决 这个问题。
宿主组件如果一个组件注入了依赖项,那么这个组件就是这个依赖的宿主组件;如果这个组件通过<ng-content>被嵌入到了父组件,那这个父组件就是这个依赖的宿主组件。

  1. 宿主组件是当前组件
    我们给组件构造函数加上@Host装饰器:
    // ...
    @Component({
        selector: 'parent',
        template: `
            <h1>这里是父组件</h1>
        `
    })
    constructor(
        @Host()
        logger: LoggerService) {}
        // 加上@Host之后会报错,因为我们并没有在这个组件中注入LoggerService
        
        // 但是我们可以加上@Optional来避免报错
        //@Host()
        //@Optional()
        //logger: LoggerService) {}
    )
    
  2. 宿主组件是父组件
    我们修改一下上面的组件为父组件:
    // parent.component.ts
    // ...
    @Component({
        selector: 'parent',
        template: `
            <h1>这里是父组件</h1>
            <ng-content></content>
        `
        // 在父组件中注入 LoggerService
        providers: [LoggerService] 
    })
    constructor() {}
    
    增加一个子组件:
    // child.component.ts
    // ...
    @Component({
        selector: 'child',
        template: `
            <h1>这里是子组件</h1>
        `
    })
    constructor(
        @Host()
        @Optional()
        logger: LoggerService) 
    ){}
    
    当然<parent>标签中应该这样写:
    <parent>
        <child></child>
    </parent>
    
    因为此时宿主组件是父组件,所以我们在父组件中注入LoggerService Angular注入器会自动向上查找,找到ParentComponet中的配置,从而完成注入。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 1.背景介绍 以前原始社会,我们需要斧子,然后由于还没有社会分工,只能自己打磨一把来使用,对应在程序上是我们需要一...
    LMmx阅读 298评论 0 2
  • 1.依赖注入(DI) DI是一个设计模式,处理组件如何获取依赖。angular中 injector子系统专门负责创...
    梧桐叶_阅读 1,346评论 1 6
  • 本文作为学习的感悟记录,阅读本文之前需要熟读相关的官方文档。 declearable(components, di...
    csRyan阅读 1,368评论 0 1
  • Angular算是将后端开发工程化引入前端的先驱之一,而Dependency injection依赖注入(后面简称...
    点融黑帮阅读 2,219评论 0 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139