从nest.js中了解IoC和DI的实现

做一个有温度和有干货的技术分享作者 ------ Qborfy

背景

从上一篇《从egg.js转到nest.js》,想对其再深入了解一下,尤其比较好奇Typescript是如何实现IoC和DI,因为在Java的是通过的反射(Spring IoC实现原理)去创建对应的类。因此下文将详细讲解Nest.js中IoC和DI的实现原理。

前置知识

在了解实现原理之前有几个知识概念,需要了解一下:

  • IoC和DI
  • JavaScript的Reflect
  • TypeScript的装饰器

IoC和DI

IoC和DI其实同属于一个技术理念,下面维基百科的介绍:

IoC,控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫"依赖查找"(Dependency Lookup)。

简单的说IoC是一个开发代码的设计原则,DI则是实现这个设计原则的方案。

IoC

从代码层上来讲解IoC,简单的说就是:

  • Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式地用 new 创建 B 的对象。
  • 使用IoC设计原则后,A 的代码只需要定义一个 private 的B对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。
  • IoC将采用依赖注入或依赖查找两种方案去实现

再通俗一点,就是有一个IoC容器管家,负责你开发的代码类的归置,你只管使用代码类,不用管它放在哪里,只需要调用即可。

DI

DI,Dependency Injection,依赖注入

依赖注入是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中 依赖查找是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制

简单的说,就是依赖注入是将需要注入的对象完全交给框架去实现,而依赖查找则是开发者通过框架提供的方法,由自己控制需要注入的时间点。

问题

采用IoC和DI,需要注意的问题是:

  • 循环依赖,就是A依赖B,B依赖A,如何避免这种情况发生,或者框架提供什么样的方案去避免?
  • 如果依赖的类越来越多,会不会导致项目启动速度变慢,因为需要初始化类很多,尤其当遇到一些类初始化可能会错误,但其实是可以忽略的?
  • 初始化类的顺序如何控制,如:A依赖B,需要B实例化后才能实例?

JavaScript的Reflect

Reflect在MDN网站是这么解释的:

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。 其中的一些方法与 Object 相同,尽管二者之间存在某些细微上的差别。

按照前端开发者理解来说,Reflect能解决开发中遇到很多this的代理问题,虽然大部分方案都可以通过其他方式解决,但是Reflect的定义能帮助我们快速实现这些功能。

Reflect符合ES6标准的提供的API有如下几个:

  • Reflect.apply(target, thisArgument, argumentsList),和 Function.prototype.apply(thisArgument, argumentsList) 功能类似,也是调用函数,且允许将函数的this指向thisArgument
  • Reflect.construct(target, argumentsList[, newTarget]),new一个target,且可以将target的this的指向新的newTarget对象
  • Reflect.defineProperty(target, propertyKey, attributes),拦截target对象的操作,和 Object.defineProperty() 类似
  • Reflect.deleteProperty(target, propertyKey),作为函数的delete操作符,相当于执行 delete targetname。
  • Reflect.get(target, propertyKey[, receiver]),获取target的属性值,和target[name]的区别在于可以receiver,可以指定调用属性值的时候this
  • Reflect.getOwnPropertyDescriptor(target, propertyKey),类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined。
  • Reflect.getPrototypeOf(target),返回指定对象的原型(即内部的 \[Prototype] 属性的值)
  • Reflect.has(target, propertyKey),判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同
  • Reflect.ownKeys(target),返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable 影响).
  • Reflect.isExtensible(target), 判断一个对象是否可扩展(即是否能够添加新的属性)
  • Reflect.preventExtensions(target),阻止新属性添加到对象
  • Reflect.set(target, propertyKey, value[, receiver]),将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
  • Reflect.setPrototypeOf(target, prototype),可设置对象的原型,即内部的 \[Prototype] 属性)为另一个对象或 null,利用原型链用来强制给某个对象增加额外方法

当然还有一些没有进入标准,但是在ES7提案的方法Reflect Metadata(Typescript已实现),后面Nest.js已采用的方法,主要有以下几个:

  • Reflect.getMetadata(metadataKey, target, propertyKey), 用于获取某个类的元数据
  • Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);, 用于设置某个类的元数据

简单理解这个api方法,你可以通过Reflect.defineMetadata获取到类或者函数的参数类型,也可以给类或者函数设置元数据再获取,具体代码如下:

TypeScript 复制代码
function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`);
    // other...
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}

TypeScript 的优势了,TypeScript 支持编译时自动添加一些 metadata 数据,如下所示:

  • Reflect.getMetadata("design:type", target, key), 获取target函数类型
  • Reflect.getMetadata("design:paramtypes", target, key), 获取target函数参数类型
  • Reflect.getMetadata("design:returntype", target, key), 获取target函数返回值类型

这个Reflect.getMetadata("design:paramtypes", target, key)基本上就是Nest.js实现Ioc和DI的核心代码。

TypeScript的装饰器

装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。

如何实现一个装饰器呢?

如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。

javascript 复制代码
function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

@color('blue')
function say(){
    ....
}

IoC和DI实现原理

其实在了解完Reflect.getMetadata,我们就大概知道IoC和DI的实现原理,我们以一个@Controller为例, 具体步骤如下:

  • 实现@Controller装饰器工厂,标识待注入的类
  • 实现IoC容器,注册要被依赖注入的类
  • 获取待注入的类其构造函数所需要的参数类型,并实例化,返回待注入的类

具体代码如下:

TypeScript 复制代码
// 实现Controller装饰器
function Controller(path: string){
    return function(target){
        Reflect.defineMetadata('Controller', path, target);
    }
}

// 需要依赖注入的类
class A(){
    say(){
        console.log('aaaaaa');
    }
}

// 引用
@Controller("/api")
class Demo(){
    construtor(a: A ){
        this.a = A;
    }

    say(){
        this.a.say();
    }
}


// 实现Ioc容器和DI依赖注入
class Container {
  provides = new Map()
  
  // 注册要被依赖注入类,形成IoC容器 后续可以做
  addProvide(provider) {
    this.provides.set(provider.name, provider)
  }
  // 注入依赖类
  inject(target) {
      // 获取参数类型
      const paramTypes = Reflect.getMetadata('design:paramtypes', target) || []
      const args = paramTypes.map((type) => {
        return new type() // 简单做一下实例化
      })
      return Reflect.construct(target, args)
  }
}

const container = new Container()
const project = container.inject(Project)

// project就是最终生成返回使用的类

project.say(); // 输出 aaaaaa

所以Nest.js实现IoC和DI的核心实现原理:

  • 通过装饰器给 class 或者对象添加 metadata
  • 运行的时候通过这些元数据来实现依赖的扫描,对象的创建等等功能

当然,还有很多问题没解决,目前只是简单实现了依赖注入,上述IoC的问题还没有解决,由于篇幅较长,所以拆成几篇,放到后续继续研究。

相关推荐
凌云若寒8 分钟前
BarTender官网下载打印机驱动
运维·服务器·前端·网络·产品经理
七夜zippoe11 分钟前
Agent 事件驱动架构:从轮询到事件触发的架构升级
ai·架构·agent·轮询·事件驱动·事件触发
昌原的儿子LEO19 分钟前
嵌入式 Linux I2C 驱动开发核心知识总结:设备树与 platform 架构全解析
linux·驱动开发·架构
zd84510150020 分钟前
基于 HTML 与 Modbus TCP 构建设备仪器上位机:可行性分析与代码结构设计
前端·tcp/ip·html
loulanyue_33 分钟前
不生成文本的AI?新型Jev模型与System 1决策工作流深度解析——读IBM Master Inventor Martin Keen前沿架构
人工智能·架构
Oflycomm33 分钟前
光伏组件级快速关断的PLC通信架构与模组选型分析
架构·嵌入式·plc·光伏·电力线通信
流烟默40 分钟前
Spring Cloud 微服务 Sentinel 热点参数与授权规则集成指南(附架构建议)
spring cloud·微服务·架构·sentinel
Amumu121381 小时前
NodeJs介绍
前端·node.js
咧咧小侠1 小时前
Windows Codex 连接 Chrome 失败:一次 nodeRepl.fetch 报错排查记录
前端·chrome
Eider19981 小时前
大模型输出格式幻觉解决思路
开发语言·前端·javascript