第5章 Nest.js精进-IOC控制反转

在本章,会继续精进Nest.js,首先会学习IOC控制反转,这是一种设计思想,把对象的创建和依赖关系交给容器管理,而不是手动创建和管理。

  • 传统方式:手动创建new A(),new B()。
  • IOC方式:需求由容器解决。

现在我有A类和B类两个类,A类中有getName()实例方法,我若想在B类中调用A类的getName()实例方法,那么传统方式需要我们在B类中去手动实例化A类,然后再从实例对象中调用getName()实例方法。

ts 复制代码
class A {
  getName() {
    return 'A'
  }
}

class B {
  constructor() {
    // 手动实例化
    let a = new A()
    console.log(a.getName())
  }
}

那么,在B类中手动实例化A类并调用对应实例方法的行为是会导致两个类产生高度耦合的关系,假设我再次手动实例化B类,会导致依赖关系混乱与维护困难,例如无法对A进行模拟或者替换;每个A实例状态都独立,难以统一管理;还有存在内存泄漏等风险。

IOC控制反转就是为了解决上述的高度耦合问题,即对A类与B类之间的关系进行解耦。

首先我们需要创建一个Container类,在Container类中存放所有实例化的操作,然后其余类如果需要相关类的实例化从而调用对应的方法,就由Container类来分配。

ts 复制代码
class A {
  getName() {
    return 'A依赖注入生效'
  }
}

class B {

}

class Container{
  // 存储所有需要的实例化操作
}

那么,在Container类(容器)中要如何存储实例化操作?这需要依赖第三方库reflect-metadata,通过npm命令下载。

ts 复制代码
npm install reflect-metadata

当我们在该文件中导入第三方库reflect-metadata之后,可以使用Reflect.getMetadata()静态方法。需要说明的是,Reflect标准内置对象中并没有getMetadata()静态方法,因此这个静态方法不是原生,而是来自第三方库reflect-metadata。

ts 复制代码
import 'reflect-metadata'
class Container {
  private providers = new Map<any, any>()

  get(clsz: new (...args: any[]) => any) {
    const paramsTypes = Reflect.getMetadata('design:paramtypes', clsz)
  }
}

直接使用Reflect.getMetadata()静态方法是没有效果的,我们还要在需要依赖的类上,添加一个@Injectable()装饰器。

装饰器是一个特殊的函数,使用方式是放在类的头上。例如:现在我创建一个Injectable函数,然后使用在A类上。

ts 复制代码
// 装饰器本质上是一个函数
const Injectable = () => {
  return (target: any) => {
    console.log('Injectable装饰器读取到:',target)
    return target
  }
}
// 使用 @ 符号应用装饰器
@Injectable()
class A {
  getName() {
    return 'A依赖注入生效'
  }
}

装饰器的执行时机是在编译时,即运行文件的时候就自动调用,因此当我执行index.ts文件时,Injectable函数就会调用,并且打印出读取到的A类信息,如图5-1所示。使用装饰器的好处是什么?是可以在不破坏A类代码本身的结构基础上,对A类进行额外的拓展。

图5-1 装饰器的基础使用

当我们所使用的Injectable函数处于以下两种情况之一时,会被称为装饰器:

(1)被用于装饰器模式,即可以附加到类、方法、属性等上,以修改或增强其行为。

(2)或者该函数符合装饰器的调用签名,即它接受一个目标对象(根据装饰的对象类型不同,参数也不同)并可能返回一个新的描述符或修改后的目标。

而Injectable装饰器既然可以读取到A类,那么就可以在A类的原型链上添加新的实例方法。例如以下操作:使用Injectable装饰器对A类添加一个getName2的实例方法并调用执行。

ts 复制代码
const Injectable = () => {
  return (target: any) => {
    target.prototype.getName2 = () => {
      return 'getName2'
    }
    console.log(target.prototype.getName2())
    return target
  }
}

@Injectable()
class A {
  getName() {
    return 'A依赖注入生效'
  }
}

装饰器的方法使用如图5-2所示,可以在装饰器上添加方法并使用,若需要参数,还可以从@Injectable()的实参位置传入。

图5-2 装饰器的方法使用

但装饰器存在一个问题,若Injectable装饰器的方法和A类中的方法名称一样,就会出现Injectable装饰器方法覆盖A类实例方法的问题。因为装饰器在编译时执行,则先执行装饰器代码实现原型链覆盖并执行getName()方法,而后调用实例对象a的getName()方法,因此执行两次装饰器中的getName()方法。

ts 复制代码
const Injectable = () => {
  return (target: any) => {
    target.prototype.getName = () => {
      return 'getName2'
    }
    console.log(target.prototype.getName())
    return target
  }
}

@Injectable()
class A {
  getName() {
    return 'A依赖注入生效'
  }
}

const a = new A()
console.log(a.getName())
// getName2
// getName2

为了解决装饰器覆盖原方法的问题,TypeScript推出了元编程的概念,它希望我们避免直接修改原型。解决方案是使用元数据反射(Reflect Metadata)来实现元编程。而元数据反射的底层确实使用了WeakMap来存储元数据,这样可以避免内存泄漏(弱引用),并且不会直接影响对象的原型链。

那么什么是元数据反射?元数据反射是一种在对象上添加元数据(关于数据的数据)的方式,这些元数据不会直接成为对象的一部分,而是存储在独立的WeakMap中。这样,我们可以给对象附加额外的信息,而不改变其结构。

那么如何使用元数据反射?即通过Reflect.getMetadata('design:paramtypes', clazz)静态方法(需要安装第三方库)。

首先要说明Reflect.getMetadata('design:paramtypes', clazz)静态方法有两个参数,其中参数1有三个内置选项:

(1)design:paramtypes 获取构造函数的参数类型。

(2)design:returntype 获取构造函数的返回类型。

(3)design:type 获取构造函数的类型。

第1个参数是一个固定的字符串,字符串可以有以上三种内置选项,或者自定义也可以。

第2个参数是目标类,我们要读取哪个哪个类的元数据。因此说如果不在目标类上使用装饰器的话,Reflect.getMetadata()静态方法就没有目标类可以读取元数据,自然是无法生效的。

我们来使用一下元数据反射,以下要做的需求:

  • 通过元数据反射,在B类中使用A类的实例方法。
ts 复制代码
// tsconfig.json中打开这两个选项,从而实现Reflect.getMetadata()静态方法的使用
"experimentalDecorators": true
"emitDecoratorMetadata": true

思路是很清晰的,我们通过design:paramtypes获取构造函数的参数类型来实现。步骤如下:

(1)在A类与B类使用装饰器,而装饰器执行时,TypeScript会生成元数据。

(2)在容器中读取类的构造函数参数类型信息(拿到类本身)。

(3)拿到类之后,通过类创建实例对象完成了依赖注入,塞到Map的键值对里,键是被读取的类,值是被读取类中的构造函数参数类型的实例对象。

在B类的构造函数中设置一个参数,该参数的类型是A类,装饰器被应用,TypeScript编译器为被装饰B类生成元数据,我们通过Reflect.getMetadata()静态方法配合第一个参数design:paramtypes可以从B类读取到构造函数的参数a的类型A类,当装饰器读取到A类后,通过A类创建实例对象instance,然后和B类一起存到providers中。

ts 复制代码
// 完整IOC控制反转实现
import 'reflect-metadata'
const Injectable = () => {
  return (target: any) => {
    return target
  }
}

@Injectable()
class A {
  getName() {
    return 'A依赖注入生效'
  }
}

@Injectable()
class B {
  constructor(private a: A) {
    console.log(this.a.getName())
  }
}

class Container {
  private providers = new Map<any, any>()

  get(clsz: new (...args: any[]) => any) {
    const parmsTypes = Reflect.getMetadata('design:paramtypes', clsz) || []
    // 递归创建实例对象,完成了依赖注入
    const instance = new clsz(...parmsTypes.map((type: any) => this.get(type)))
    this.providers.set(clsz, instance)
    return instance
  }
}

const container = new Container()
// 多次请求B类,会直接从providers中返回缓存的实例
container.get(B)

其实当我们完成递归创建实例对象之后,B类就能够调用A类的getName()实例方法了。因为做到了B实例的a属性现在指向A实例。

ts 复制代码
// 当调用 container.get(B) 时:

// 1. 创建 A 实例
const aInstance = new A();
// aInstance 是 A 类的一个具体实例,有 getName() 方法

// 2. 创建 B 实例并注入
const bInstance = new B(aInstance);  // aInstance 作为参数传递

// 3. B 构造函数内部执行
class B {
  // TypeScript 语法糖展开为:
  // constructor(a: A) {
  //   this.a = a;  // this.a 被赋值为传入的 aInstance
  // }
  
  // 所以实际上:
  // this.a = aInstance
  
  // 因此:
  // this.a.getName() 等于 aInstance.getName()
}

通过@Injectable()装饰器,TypeScript在编译时为B类记录了构造函数参数类型(A类)。在运行时,依赖注入容器读取这些元数据,递归创建A类的实例,并将其注入到B类的构造函数中。因此,B类的实例可以通过 this.a.getName() 访问A类的实例方法。

后续将实例对象放入providers(Map数据结构)中是利用Map结构的键唯一特性,当我们第一次请求B类时,容器会创建B类的实例,并且发现B类 依赖A类,于是先创建或获取A类的实例,然后将B类和其实例存入providers。下次再请求B类时,就直接从providers中返回缓存的实例,而不会再次创建。

接下来,我们来优化一下容器注入,假如有多个不同类(B类、B1类、B2类、B3类)同时注入了A类,那么这会不断的重复注入到Map中,因为Map结构只保证Key唯一。这就没有必要,因此我们可以在最前面加一个判断,如果在Map中已经存在注入的类(例如A类),那么我们就直接返回注入的类就行。

ts 复制代码
class Container {
  private providers = new Map()

  get(clsz: new (...args: any[]) => any) {
    // 优化:防止注入重复
    if (this.providers.has(clsz)) {
      return this.providers.get(clsz)
    }
    const parmsTypes = Reflect.getMetadata('design:paramtypes', clsz) || []
    const instance = new clsz(...parmsTypes.map((type: any) => this.get(type)))
    this.providers.set(clsz, instance)
    return instance
  }
}

以上就是IOC控制反转实现的全过程,最核心和精彩的代码就以下这条:完成了拆解高耦合依赖的工作。

ts 复制代码
const instance = new clsz(...parmsTypes.map((type: any) => this.get(type)))
相关推荐
LV技术派1 天前
适合很多公司和团队的 AI Coding 落地范式(二)
前端·aigc·ai编程
IT_陈寒1 天前
Redis性能翻倍的5个冷门技巧:从每秒10万到20万的实战优化之路
前端·人工智能·后端
ss2731 天前
高版本node启动RuoYi-Vue若依前端ruoyi-ui
前端·javascript·vue.js
饼干,1 天前
模拟试卷2
前端·javascript·easyui
南雨北斗1 天前
js 严格模式
前端
聪明的Levi1 天前
FRONT END REVIEW
前端·css·html
仙人掌一号1 天前
React 白屏机制原理分析[共1500字,阅读时长8min]
前端·javascript·面试
sophie旭1 天前
Suspense+React.lazy--组件渲染如何暂停 → 等待 → 恢复
前端·javascript·react.js
我的div丢了肿么办1 天前
js中worker的详细讲解
前端·javascript·vue.js