关于javascript:依赖注入的那些事

前言

1、管制反转:Inversion of Control,简称IoC

2、依赖注入:Dependency Injection,简称DI,依赖注入实际上是管制反转概念的一种实现模式。一种打消类之间依赖关系的设计模式。

假如咱们有一个类Human,要实例一个Human,咱们须要实例一个类Clothes,而实例化衣服Clothes,咱们又须要实例化Cloth,实例化纽扣等等。

当需要达到肯定简单的水平时,咱们不能为了一个人穿衣服去从布从纽扣开始从头实现,最好能把所有的需要放到一个工厂或者仓库,咱们须要什么间接从工厂的仓库外面间接拿(与间接封装成一个Clothes类,咱们须要时再去new有什么区别? — 如果去new实例的话,那类与类之间还是存在着依赖关系)

这个时候就须要依赖注入了,咱们实现一个IoC容器(仓库),而后须要衣服就从仓库外面间接拿实例好的衣服给人作为属性穿上去。

将所有的类与类之间的依赖关系交给IoC容器去治理,之后真正应用的是IoC容器中已存在依赖关系的类的实例(并非再去new新的实例,因而并不会影响类源代码逻辑,集体猜测:若前面须要扩大需要能够依据源类再创立别的容器)

IoC是一种很好的解耦合思维,在开发中,IoC意味着你设计好的对象交给容器管制,而不是应用传统的形式,在对象外部间接管制。在软件开发中有很好的作用。

基于ts中装璜器的应用

类装璜器

类装璜器表达式会在运行时当作函数被调用,类的构造函数作为其惟一的参数

能够用来监督,批改或替换类定义

批改类的结构器:

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
  console.log('装璜器已执行');
  return class extends constructor {
    newProperty = "new property";
    hello = "override";
  }
}
@classDecorator
class Greeter {
  property = "property";
  hello: string;
  constructor(m: string) {
      this.hello = m;
      console.log(this.hello); 
  }
}
console.log(new Greeter("world"));
// 装璜器已执行
// world
// Greeter {
//   property: 'property',
//   hello: 'override',
//   newProperty: 'new property'
// }

程序运行程序
1、为类Greeter增加装璜内容(加在原来类的构造函数前面,因而能起到重写构造函数属性的成果)
2、实例化类;

批改类的某个办法:

function cheating(target: any) {
  target.prototype.hit = function(rival: Somebody) {
    const hitDamage: number = 100;
    console.log(`${this.name}对${rival.name}造成一次挫伤: ${hitDamage}`);
  }
}
class Somebody {
  speed: number = 10;
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  hit(rival: Somebody) {
    const hitDamage: number = 10;
    console.log(`${this.name}对${rival.name}造成一次挫伤:${hitDamage}`);
  }
}
@cheating
class SBody extends Somebody{
}
const s0 = new Somebody('小红0');
const s1 = new SBody('小红1');
const rival = new Somebody('小明');
s0.hit(rival); // 小红0对小明造成一次挫伤:10
s1.hit(rival); // 小红1对小明造成一次挫伤: 100

属性装璜器

当要装璜的属性为实例属性时(没有static润饰),装璜的是该类原型上的属性

function logProperty(params: any) {
  // target--->类的原型对象;attr--->装璜的属性名
  return function (target: any, attr: any) {
    target[attr] = params;
  }
}
 
class HttpClient1 {
  @logProperty('http://www.baidu1.com')
  url: any | undefined;
  constructor() {
  }
  getUrl() {
    console.log(this.url);
  }
}
class HttpClient2 {
  @logProperty('http://www.baidu22.com')
  url: any | undefined = 'http://www.baidu2.com';
  constructor() {
  }
  getUrl() {
    console.log(this.url);
  }
}
let http1 = new HttpClient1();
http1.getUrl(); // http://www.baidu1.com  HttpClient1类中url没有初始化,因而获取的是原型链上的值

let http2 = new HttpClient2();
http2.getUrl(); // http://www.baidu2.com 获取的是HttpClient2中url初始化的值(先确定本身对象中是否有这个值,没有的话再去原型链上找,再没有就返回undefined)

当要装璜的属性为动态属性时(static润饰),装璜的是该类上的动态属性

function logProperty(params: any) {
  // target--->类;attr--->装璜的属性名
  return function (target: any, attr: any) {
    target[attr] = params;
  }
}
 
class HttpClient1 {
  @logProperty('http://www.baidu1.com')
  static url: any | undefined;
  constructor() {
  }
  getUrl() {
    console.log(HttpClient1.url);
  }
}
class HttpClient2 {
  @logProperty('http://www.baidu22.com')
  static url: any | undefined = 'http://www.baidu2.com';
  constructor() {
  }
  getUrl() {
    console.log(HttpClient2.url);
  }
}
console.log(HttpClient1.url); // http://www.baidu1.com
console.log(HttpClient2.url); // http://www.baidu22.com

程序运行程序:
1、为属性执行装璜器函数(给原型上对应属性赋值 || 给类动态属性赋值 || 其余逻辑操作);
2、实例化类;

参数装璜器

// 参数装璜器 承受3个参数
// target: Object —— 被装璜的参数所在的办法的类
// methodName: string | symbol —— 办法名
// paramsIndex: number —— 办法中参数的索引值
// 在结构器中的参数应用
function logParams(params: any) {
  return function (target: any, methodName: any, paramsIndex: any) {
    console.log(1, params);
    console.log(2, target);
    console.log(3, methodName);
    console.log(4, paramsIndex);
  }
}
class HttpClient {
  public url: any | undefined;
  constructor() {
  }
  getData(@logParams('uuid') uuid: any) {
    console.log(uuid);
  }
}
let http = new HttpClient();
http.getData(123456);
// 1 uuid
// 2 HttpClient {}
// 3 getData
// 4 0
// 123456

reflect-metadata

当想要给类中的某个属性或类构造函数中某个参数设置元数据时,个别采纳reflect-metadata这种形式(暂未找到其余形式,原生js如何给一个对象属性设置元数据???,通过反射来获取类属性下面的批注

import 'reflect-metadata';
class Post {
  @Reflect.metadata('role', 'admin')
  haha: {haha?: string; test?: number}
}
const metadata = Reflect.getMetadata('role', Post.prototype, 'haha');
console.log(metadata);  // admin

1、能够用于给类中一些属性设置一些元数据。元数据指的是表述货色时用的数据。
2、Reflect.metadata:能够用于给某个类中的某个属性值增加一个元数据(键值对)
3、Reflect.getMetadata:获取某个类中某个属性值的某个元数据(并不会烦扰到原来的属性值)

import "reflect-metadata";
// 用来记录某个属性的元数据
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
  // Reflect.metadata返回一个函数 (target: Object, propertyKey: string | symbol): void
  return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
  // 获取类中propertyKey的某个元数据(批注)
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class Greeter {
  @format("Hello, %s")
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    let formatString = getFormat(this, "greeting");
    return formatString.replace("%s", this.greeting);
  }
}
const test = new Greeter('syz').greet();
console.log(test); // Hello, syz

reflect-metadata参考1
reflect-metadata参考2

程序调试技巧

1、应用VS Code配合插件Code Runner(鼠标框选程序右键Run Code即可),因为程序应用的是typescript,应用Code Runner运行须要在全局装置ts-nodenpm install ts-node -g)和 typescriptnpm install typescript -g
在文件同级目录下配置一下tsconfig.json文件即可

{
  "compileOnSave": false,
  "buildOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "module": "commonjs",
    "moduleResolution": "node",
    "target": "ES6",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": true,
    "removeComments": false,
    "sourceMap": false,
    "inlineSourceMap": true,
    "noEmitHelpers": false,
    "declaration": true,
  }
}

2、备注tsc xxx.js可将ts文件编译为js文件,能够查看编译后的js代码,可能更容易了解!

繁难DI实现

波及到类装璜器 & 属性装璜器的应用

思路流程

1、实现一个IoC容器 Injector ,并实例化一个根容器rootInjector(用于寄存各个依赖的工厂容器)
2、实现一个依赖注入办法Injectable(...)(用于将各个依赖类注入根容器)
3、实现基于注解的属性注入办法Inject(...)(将类须要用到的依赖从根容器取出来并注入到类中,若根容器不存在则创立此依赖)

import 'reflect-metadata';
// 工厂外面的各种操作
export class Injector {
  private readonly providerMap: Map<any, any> = new Map();
  private readonly instanceMap: Map<any, any> = new Map();
  public setProvider(key: any, value: any): void {
    if (!this.providerMap.has(key)) {
      this.providerMap.set(key, value);
    }
  }
  public getProvider(key: any): any {
    return this.providerMap.get(key);
  }
  public setInstance(key: any, value: any): void {
    if (!this.instanceMap.has(key)) this.instanceMap.set(key, value);
  }
  public getInstance(key: any): any {
    if (this.instanceMap.has(key)) return this.instanceMap.get(key);
    return null;
  }
  public setValue(key: any, value: any): void {
    if (!this.instanceMap.has(key)) this.instanceMap.set(key, value);
  }
}
// 示意根注入器(用于寄存各个依赖的根容器)
export const rootInjector = new Injector();
// 将类注入到工厂中 类装璜器返回一个值,它会应用提供的构造函数来替换原来类的申明
export function Injectable(): (_constructor: any) => any {
  return function (_constructor: any): any {
    rootInjector.setProvider(_constructor, _constructor);
    return _constructor;
  };
}
// 将依赖注入到生产者
export function Inject(): (_constructor: any, propertyName: string) => any {
  return function (_constructor: any, propertyName: string): any {
    // 获取属性定义时的类型
    const  propertyType: any = Reflect.getMetadata('design:type', _constructor, propertyName);
    const injector: Injector = rootInjector;
    let providerInsntance = injector.getInstance(propertyType);
    if (!providerInsntance) {
      const providerClass = injector.getProvider(propertyType);
      providerInsntance = new providerClass();
      injector.setInstance(propertyType, providerInsntance);
    }
    _constructor[propertyName] = providerInsntance;
  };
}
@Injectable()
class Cloth {
  name: string = '麻布';
}
@Injectable()
class Clothes {
  // 为类Clothes注入类Cloth 之后类Clothes就领有了应用类Cloth的能力
  @Inject()
  cloth: Cloth;
  clotheName: string;
  constructor() {
    this.cloth = this.cloth;
    this.clotheName = this.clotheName;
  }
  updateName(name: string) {
    this.clotheName = name;
  }
}
class Human1 {
  @Inject() 
  clothes: Clothes;
  name: string;
  constructor(name: string) {
    this.clothes = this.clothes;
    this.name = name;
  }
  update(name: string) {
    this.clothes.updateName(name);
  }
}
// 单例:用于数据状态的保护(一个变 所有变)
const pepe1 = new Human1('syz');
console.log(pepe1);
// Human1 {
//   clothes: Clothes { cloth: Cloth { name: '麻布' }, clotheName: undefined }
// }
pepe1.update('耐克');
console.log(pepe1);
// Human1 {
//   clothes: Clothes { cloth: Cloth { name: '麻布' }, clotheName: '耐克' }
// }

繁难DI参考

仿Angular中的DI

service依赖注入,应用单例模式,当前单个或多个组件须要应用这个服务时,都是共享同一个实例(因而可用于状态共享),极大的缩小了内存资源的损耗(失常状况每次应用类都得实例化的)

在某个注入器的范畴内,服务是单例的。也就是说,在指定的注入器中最多只有某个服务的最多一个实例。
利用只有一个根注入器。在root或AppModule级提供UserService意味着它注册到了根注入器上。在整个利用中只有一个UserService实例,每个要求注入UserService的类都会失去这一个服务实例,除非你在子注入器中配置了另一个提供者

1、如何将依赖从容器中取出来呢 — 调用inject()办法会返回类的实例(Reflect.construct(...)用于实例化,接着调用了类的构造函数,将对应的依赖实例作为结构函数参数传进去)
2、写在类中constructor()中的参数都是要注入依赖的。一般类型,例如stringnumber不能写进去,否则报错

相干技术:
1、类装璜器,配合reflect-metadata
2、参数装璜器,配合reflect-metadata

代码实现

1、实现Injectable()类装璜器,用于标记类是否可被注入容器中

import { Type } from './type';
import 'reflect-metadata';
const INJECTABLE_METADATA_KEY = Symbol('INJECTABLE_KEY');
// 必须被这个装璜器润饰的Class才阐明是能够被注入的 (示意Class本人抵赖是可被注入的)
export function Injectable() {
  return function(target: any) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    return target;
  }
}

// 查看Class是否可被注入
export function isInjectable<T>(target: Type<T>) {
  return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true;
}

2、实现Inject()参数装璜器,用于将value值注入类中

import { Token } from './provider';
import 'reflect-metadata';
const INJECT_METADATA_KEY = Symbol('INJECT_KEY');
// 参数装璜器 承受3个参数
// target: Object —— 被装璜的参数所在的办法的类
// methodName: string | symbol —— 办法名
// parameterIndex: number —— 办法中参数的索引值
// 在结构器中的参数应用
export function Inject(token: Token<any>) {
  return function(target: any, _: string | symbol, index: number) {
    Reflect.defineMetadata(INJECT_METADATA_KEY, token,target,`index-${index}`);
    return target;
  };
}
export function getInjectionToken(target: any, index: number) {
  return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined;
}

3、实现addProvider(...)办法,用于将所有须要注入到容器的所有类/值增加到`
providers变量中,后续inject(…)将从providers`取值

4、实现inject(...)办法,将类注入容器并返回实例(应用Reflect.construct办法)

export class Container {

  providers = new Map<Token<any>, Provider<any>>();
  instances = new Map<Token<any>, any>();
  
  addProvider<T>(provider: Provider<T>) {
    this.assertInjectableIfClassProvider(provider);
    // 反复注入同个类的话 貌似是以前面的为准???
    this.providers.set(provider.provide, provider);
  }
  inject<T>(type: Token<T>): T {    
    let provider = this.providers.get(type);
    let instance = this.instances.get(type);
    if(provider === undefined && !(type instanceof InjectionToken)) {
      provider = { provide: type, useClass: type };
      this.assertInjectableIfClassProvider(provider);
    }
    // 容器中是否已存在实例 存在的话间接返回原有的实例 共享变量
    if(instance) {
      return instance;
    }
    return this.injectWithProvider(type, provider);
  }
  private assertInjectableIfClassProvider<T>(provider: Provider<T>) {
    if(isClassProvider(provider) && !isInjectable(provider.useClass)) {
      throw new Error(
        `Cannot provide ${this.getTokenName(provider.provide)} using class ${this.getTokenName( provider.useClass )}, 
        ${this.getTokenName(provider.useClass)} isn't injectable`
      );
    }
  }
  private getTokenName<T>(token: Token<T>) {
    return token instanceof InjectionToken ? token.injectionIdentifier : token.name
  } 
  private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {
    let instance;
    if (provider === undefined) {
      throw new Error(`No provider for type ${this.getTokenName(type)}`);
    }
    if (isClassProvider(provider)) {
      instance = this.injectClass(provider as ClassProvider<T>);
    } else if (isValueProvider(provider)) {
      instance =  this.injectValue(provider as ValueProvider<T>);
    } else {
      instance = this.injectFactory(provider as FactoryProvider<T>);
    }
    this.addInstances(type, instance);
    return instance;
  }
  // 要害 在实例化服务类时 须要结构该服务类依赖的独享(即在构造函数中注入的依赖)
  private injectClass<T>(classProvider: ClassProvider<T>): T {
    const target = classProvider.useClass;
    const params = this.getInjectedParams(target);
    // 这里的作用 相似于 new target(...params)
    return Reflect.construct(target, params);
  }
  private injectValue<T>(valueProvider: ValueProvider<T>): T {
    return valueProvider.useValue;
  }
  private injectFactory<T>(factoryProvider: FactoryProvider<T>): T {
    return factoryProvider.useFactory();
  }
  // 用于获取类构造函数中申明的依赖对象 重点&难点
  private getInjectedParams<T>(target: Type<T>) {
    // 获取参数的类型 "design:paramtypes" 用于润饰指标对象办法的参数类型
    const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (InjectableParam | undefined)[];
    if(argTypes === undefined) {
      return [];
    }
    return argTypes.map((argTypes, index) => {
      if(argTypes === undefined) {
        throw new Error (
          `Injection error. Recursive dependency detected in constructor for type ${target.name} with parameter at index ${index}`
        );
      }
      const overrideToken = getInjectionToken(target, index);
      const actualToken = overrideToken === undefined ? argTypes : overrideToken;
      let provider = this.providers.get(actualToken);
      // 递归调用 一层接一层
      return this.injectWithProvider(actualToken, provider);
    });
  }
  // 已注入容器的实例增加到变量instances
  private addInstances<T>(type: Token<T>, instance: any) {
    this.instances.set(type, instance);
  }
}

4、调用demo

import { Container } from './container';
import { Injectable } from './Injectable';
import { Inject } from './Inject';
import { InjectionToken } from './provider';
const API_URL = new InjectionToken('apiUrl');
@Injectable()
class HttpClient{
  get() {
    console.log('get');
  }
}
@Injectable()
class HttpService {
  constructor(
    // 应用这种形式是如何将HttpClient注入的 应用Reflect.getMetadata('design:paramtypes', target)获取target类的参数类型信息
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string,
  ) { }
  test() {
    console.log(this.apiUrl);
    this.httpClient.get();
  }
}
const container = new Container();
// addProvider 这一步就相当于angular中在module中的providers增加Service
container.addProvider({
  provide: API_URL,
  useValue: 'https://www.666.com/'
});
container.addProvider({provide: HttpService, useClass: HttpService });
container.addProvider({provide: HttpClient, useClass: HttpClient });
const httpService = container.inject(HttpService);
httpService.test();

// https://www.666.com/
//get

关键点

1、Reflect.getMetadata('design:paramtypes', value):获取类结构函数参数的类型

2、Reflect.construct(data, params):实例化类

3、类中依赖如果存在多层采纳递归注入形式

残缺代码 查看

疑难

1、一种打消类之间依赖关系的设计模式,具体是如何体现的?貌似还是存在依赖,只是封装起来了而已?
解答:将所有的类与类之间的依赖关系交给IoC容器去治理,之后真正应用的是IoC容器中已存在依赖关系的类的实例(并非再去创立新的实例,因而并不会影响类源代码逻辑,集体猜测:若前面须要扩大需要能够依据源类再创立别的容器);

2、angular中同个Service在多个中央注入,获取依赖时如何确定要获取哪一个值?
解答:如果是在同一个注入容器反复注入,依照注入的先后顺序,前面注入的会重写后面注入的;

3、在constructprivate httpClient: HttpClient是如何将HttpClient注入依赖的?
解答:应用Reflect.getMetadata('design:paramtypes', target)获取target类的参数类型信息;

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理