Angular Provider 之 useFactory

Posted Angular完全开发手册

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Angular Provider 之 useFactory相关的知识,希望对你有一定的参考价值。

我们可以很容易的编写一个 service ,并将其通过DI注入到Component 或别的 service 中去。

 
   
   
 
  1. // a.service.ts

  2. @Injectable({})

  3. class ServiceA {

  4.    ...

  5. }

  6. // a.module.ts

  7. @NgModule({

  8.    ...

  9.    providers:[ServiceA]

  10. })

  11. class AModule{}

  12. //

但是如果我们想注入的东西并不是一个 service ,该怎么做?看一个简单的例子:

 
   
   
 
  1. @Injectable({})

  2. class ServiceB{

  3.    constructor(private config:ServiceBConfig){}

  4.    log(message:string){

  5.        if(this.config.isProd){

  6.            return;

  7.        }

  8.        window.console.log(message);

  9.    }

  10. }

这个 service 需要在构造函数中传入一个 config,config 是一个对象,对象中的属性决定了service中 log 方法的逻辑:如果目前是生产环境,则不在控制台输出信息,可能是出于安全性的考虑。

 
   
   
 
  1. interface ServiceBConfig{

  2.    isProd:boolean;

  3. }

很明显,config 只是一个对象字面量,并不是一个可注入的 service。如果直接运行项目,会报错,因为 angular 不知道从哪里得到这个 config 对象,用以实例化 ServiceB。

这时候我们就需要用到 provider 的另一种模式: useFactory。

 
   
   
 
  1. // b.module.ts

  2. const config:ServiceBConfig = {

  3.    isProd: false

  4. }

  5. @NgModule({

  6.    ...

  7.    providers:[

  8.        {

  9.            provide: ServiceB,

  10.            useFactory: serviceBFactory(config),

  11.        }

  12.    ]

  13. })

  14. const serviceBFactory =

  15.      (config:ServiceBConfig) =>

  16.        () => new ServiceB(config);

userFactory 的值必须为一个返回 ServiceB 实例的函数。因为我们需要接收一个 config 来实例化 ServiceB,所以这里使用了高阶函数。serviceBFacotry(config) 执行后返回的正是 useFactory 需要的函数:

 
   
   
 
  1. ()=> new ServiceB(config)

这样,我们就可以很灵活的根据环境来实例化所需要的service。

我们仍然像以前一样使用它:

 
   
   
 
  1. @Component({...})

  2. export class ComponentB{

  3.    constructor(private serviceB:ServiceB){}

  4. }

但是在 ng build 时,控制台会有警告:

 
   
   
 
  1. Can't resolve all parameters for ServiceB in ... This will become an error in Angular ...

这是因为 angular 在 build 时会去检查所有声明了@Injectable 的类的 constructor 需要的参数。Angular 要在程序用到这个类的实例时对其进行实例化。如果参数也是可注入的,Angular 自己可以找到。但是 config 并不是,所以 Angular 会给出警告,因为如果找不到,程序运行时就会出错。

但是我们自己写的程序,很清楚的知道,config 参数我们是用 ServiceBFactory 传进去的。如何避免这个警告呢?很简单,去掉 ServiceB 的 @Injectable 声明:

 
   
   
 
  1. //@Injectable()

  2. export class ServiceB{

  3.    ...

  4. }

再进行build,发现警告消失了。

去掉了@Injectable ,我的 serviceB 还可以被注入么?

实际上,当你使用依赖注入时,Angular 寻找的只是一个 InjectionToken:

 
   
   
 
  1. // b.component.ts

  2. ...

  3. constructor(private serviceB:ServiceB){}

  4. ...

它找的是 ServiceB 这个 token ,并不是 ServiceB这个类,我们平时在 providers 数组中声明的 provider,其实相当于

 
   
   
 
  1. @NgModule({

  2.    ...

  3.    //providers:[ServiceA]

  4.    // 相当于

  5.    providers:[

  6.       {

  7.        provider: ServiceA, // angular 会创建一个名称一样的 token

  8.        useFactory: (...args)=> new ServiceA(...args),

  9.        deps:[...]

  10.      }

  11.    ]

  12. })

angular 会自动生成一个和类名一样的token。并生成一个 factory 函数,函数的参数就是constructor 中的参数。 参数的值是地 deps数组中指定的其它可注入类。

如果我们的 service C 依赖了另一个可注入类:

 
   
   
 
  1. // b.service.ts

  2. export class ServiceC{

  3.    constructor(private userService:UserService){}

  4. }

那么实际上 angular 会将基转换为

 
   
   
 
  1. @NgModule({

  2.    ...

  3.    providers:[

  4.        {

  5.            provide: ServiceC,

  6.            useFactory: (userService:UserService)=>new ServiceC(userService),

  7.            deps:[UserService]

  8.           }

  9.    ]

  10. })

需要注意的是 UserService 必须声明为可注入的

 
   
   
 
  1. @Injectable()

  2. export class UserService{...}

所以,DI 使用时,在构造函数中声明的是 token,而使用的,是这个token对应的实例。

我们也可以偷梁换柱:

 
   
   
 
  1. @NgModule({

  2.    ...

  3.    providers:[

  4.        {

  5.            provide: ServiceC,

  6.            useFactory: ()=>new ServiceA(),

  7.            deps:[]

  8.           }

  9.    ]

  10. })

现在,当我们在组件中注入 ServiceC 时,实际得到的是 ServiceA的实例。因为 ServiceA 的 constructor 中没有依赖,所以我们不需要在 factory 函数中声明参数,deps数组也是空的,可以不写。

当然,在实际项目中,我们不会这么使用。因为当你使用 ServiceC时,会创建一个新的 ServiceA 实例,而服务应该(大部分情况) 是单例的。如果真要给 ServiceA 起个别名,应该使用 useExisting

 
   
   
 
  1. ...

  2. {provide: ServiceC, useExisting: ServiceA}

这样, angular 会在注入 ServiceC 时,寻找已经存在的 ServiceA 的实例,而不是再创建一个。

依赖注入的知识点很多,今天先说这么多。代码是基于 angular5的,angular6中又有一些改变,后面慢慢写。如果有什么问题,欢迎入群交流。


以上是关于Angular Provider 之 useFactory的主要内容,如果未能解决你的问题,请参考以下文章

angular源码剖析之Provider系列--CacheFactoryProvider

Angular 2-7 中的 PROVIDER、INJECTOR 和 SERVICE 有啥区别?

在angular中,provider,怎么使用

AngularJS 笔记之创建服务方式比较 : factory vs service vs provider 。

Angular Multi Provider 依赖顺序

Angular2 + typescript + jspm:没有 Http 提供者( App -> Provider -> Http )