实现方式
参考

nest 中 IoC & DI 的实现

July 05, 2020

nest 是一款构建高效,可扩展的 Node.js 服务器端应用程序的框架。 nest 里高度使用了控制反转IoC的设计思想。for example

import { Injectable } from '@nestjs/common'
import { Cat } from './interfaces/cat.interface'

// 定义一个依赖
@Injectable()
export class CatsService {
  private readonly cats: Cat[] = []

  create(cat: Cat) {
    this.cats.push(cat)
  }

  findAll(): Cat[] {
    return this.cats
  }
}

@Controller('cats')
export class CatsController {
  // 依赖注入
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto)
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll()
  }
}

上述源码来自 nestjs 官方文档,可以看到在 nest 通过在类构造器中实现依赖注入的

@Controller('cats')
export class CatsController {
  // 依赖注入
  constructor(private catsService: CatsService) {}
}

constructor中只是声明了catsService: CatsService, 并没有给catsService实例化,那么 nestjs 是怎么知道当前类需要哪些依赖,寻找并注入的呢?

实现方式

nestjs 是用 ts 写的,当开启 emitDecoratorMetadata, 编译选项后(配置如下)

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

如果源码引入了 reflect-metadata 这个包,那么在运行时,ts 编译器会将类型信息通过 reflect-metadata 注入, 出处参考 typescript issue

import 'reflect-metadata'

class Point {
  x: number
  y: number
}

class Line {
  private _p0: Point

  @validate
  set p0(value: Point) {
    this._p0 = value
  }
  get p0() {
    return this._p0
  }
}

function validate<T>(
  target: any,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<T>
) {
  let set = descriptor.set
  descriptor.set = function(value: T) {
    let type = Reflect.getMetadata('design:type', target, propertyKey)
    if (!(value instanceof type)) {
      throw new TypeError('Invalid type.')
    }
    set.call(target, value)
  }
}

等价于

class Line {
  private _p0: Point

  @validate
  @Reflect.metadata('design:type', Point)
  set p0(value: Point) {
    this._p0 = value
  }
  get p0() {
    return this._p0
  }
}

@Reflect.metadata('design:type', Point) 会在 Reflect 中给 Line.p0 设置一个 key 为design:type 的元信息,具体内容可以参考文档reflect-metadata,类似的还有 design:paramtypes,design:returntype

nest 就是通过上述方式,在实例化时根据 design:type 获取到在constructor中参数的 class 类型,并根据 class 类型实现依赖注入。一个简单实现

import 'reflect-metadata'
type Construct = { new (...args: any): any }

// 全局IoC容器
const IoCPool: Set<Construct> = new Set()

// 加入到IoC容器
function Injectable(target: Construct) {
  IoCPool.add(target)
}

function DI(target: Construct): Construct {
  const dependenciesCls = Reflect.getMetadata('design:paramtypes', target)
  const dependenciesInstance = dependenciesCls.map((d: Construct) => {
    if (!IoCPool.has(d)) {
      throw new Error(`unresigned dependence ${d.name}`)
    }
    // 递归注入
    return d.length ? DI(d) : new d()
  })

  class F {
    constructor() {
      // 注入依赖实例
      return new target(...dependenciesInstance)
    }
  }

  return F
}

@Injectable
class A {
  say() {
    console.log('A')
  }
}

@DI
class B {
  constructor(private a: A) {}
  say() {
    this.a.say()
    console.log('B')
  }
}

// @ts-ignore
let b = new B()
b.say()
// A B

参考


feiker 少年起而行之