利用IOC模式,可以解决什么问题?

前言

在面向对象编程的代码开发过程中,我们可能会遇到这么一种情况:我们使用的目标类,依赖其它的类。这时候,我们通常就得手动引入这些类并创建实例。

typescript 复制代码
import B from "./modelB";
import C from "./modelC";

class A {
    b = new B();
    c = new C();
    constructor(){}
}

当涉及的类不多的时候,手动创建依赖类,感觉也还好,但是当涉及的类比较多,甚至依赖的类,内部又依赖其它的类时,这时如果仍然手动一一创建,可能我们就会有觉得这样不太方便的感受了。针对这个问题,利用IOC模式,就可以很好地得到解决。

概述

控制反转

IOC,全称 Inversion of Control,翻译过来就是 控制反转,它也是设计模式的一种。

借用上面的例子来帮助理解控制反转:在上面的例子里,我们手动把一个个依赖类创建并赋值给目标类的各个属性,我们可以把这个过程理解为"控制正转";那么,控制反转,就是我们不需要手动去执行"创建依赖类并赋值给目标类的属性"这一操作,因为利用控制反转机制,会自动帮我们完成这个操作。

依赖注入

依赖注入(Dependency Injection),是实现控制反转的主要方式之一。思路就是:利用一个容器,把所有的依赖类都存储起来。当我们需要获取某个类的实例时,不再手动去 new 出来,而是从容器中获取,从容器中获取,会自动帮我们完成创建实例,以及实例内部所依赖的类的创建等一系列操作。

举个例子:例如,我们有一个名为 ApiController 的类,内部依赖名为 UserService 的类,按通常的方式,我们大概率会这样写:

typescript 复制代码
import { UserService } from '../services';

class ApiController {
  user: new UserService();
  constructor(){}

  getList(){}

  getDetail(){}

  addData(){}
}

export default ApiController;

通过依赖注入的方式,我们可以这样写:

typescript 复制代码
import { Inject, Injectable } from '../../utils/decorator';
import { UserService } from '../services';


@Injectable()
class ApiController {
  @Inject()
  user: UserService;
  constructor(){}

  getList(){}

  getDetail(){}

  addData(){}
}

export default ApiController;

利用装饰器,应用@Injectable()的类,会被保存到容器里,应用 @Inject() 的类成员,会被容器自动初始化。即,

typescript 复制代码
@Inject()
user: UserService;

相当于

typescript 复制代码
user = new UserService();

当我们需要用到 ApiController 的时候,就从容器里获取:

typescript 复制代码
import ApiController from './controller/index';
import { IocContainer } from '../utils/container';

const apiController = IocContainer.get(ApiController);

export default apiController;

例子

我们来亲自动手,利用IOC模式,实现这样的一个例子:

定义一个请求控制器类 ApiController ,请求的根路径是 /user,由 @Controller("/user") 装饰器指定。

定义一个请求的子路径 list,由 @Get("list") 装饰器指定。此时整体的路径为 /user/list,请求方式为 GET 方式。

定义一个请求的子路径 detail,由 @Get("detail") 装饰器指定。此时整体的路径为 /user/detail,请求方式为 GET

定义一个请求的子路径 add,由 @Post("add") 装饰器指定。此时整理的路径为 /user/add,请求方式为 POST

typescript 复制代码
import { Controller, Get, Post, Inject, Injectable } from '../../utils/decorator';
import { UserService } from '../services';


@Injectable()
@Controller("/user")
class ApiController {
  @Inject()
  user: UserService;
  constructor(){}

  @Get("list")
  getList(){
    return this.user.list();
  }

  @Get("detail")
  getDetail(){
    return this.user.detail();
  }

  @Post("add")
  addData(){
    return this.user.add();
  }
}

export default ApiController;

其中,访问 GET /uer/list 时,会执行 getList 方法返回数据;访问 GET /user/detail 会执行 getDetail 方法返回数据;访问 POST /user/add 时会执行 addData 方法返回数据:

实现

思路

实现的思路是这样的,跟使用 Map 数据结构差不多:

  1. 定义容器:定义一个IOC容器,用于存放所有需要自动创建以及注入所需依赖的类,类的存储以及获取统一由容器处理。

  2. 存储:利用装饰器以及反射元数据,把关键信息添加到需要存放的类,类属性以及类方法上。这些信息在类被存放到容器内时作为键名使用。

  3. 获取:获取容器里面存放的类时,需要通过容器来获取,容器会自动实例化并处理好其中的依赖关系,最终把实例返回。其中,会利用步骤2中的关键信息来获取目标类。

装饰器

实现依赖注入,需要用到装饰器,这里简单讲解下,会用到的装饰器的用法。对装饰器感兴趣的朋友们,可以去搜搜相关的文章学习学习下。

种类

装饰器以@开头的方式使用函数,用于对被装饰的目标进行修改等操作。一共有5种,分别是:

  1. 类装饰器
  2. 属性装饰器
  3. 参数装饰器
  4. 方法装饰器
  5. 访问符装饰器
typescript 复制代码
@ClassDecorator  // 类装饰器
class Test{
  @PropertyDecorator  // 属性装饰器
  test: number;

  constructor(){}

  @MethdoDecorator  // 方法装饰器
  say(@ParamsDecorator() /* 参数装饰器 */ txt: string){

  }

  @DescriptorDecorator // 访问符装饰器
  get value(){

  }

  set value(){

  }

}

我们会用到的装饰器分别是:类装饰器,属性装饰器以及方法装饰器。

类装饰器

类装饰器函数作用在类上,我们可以对类进行修改属性,添加方法等操作。

类装饰器函数,会接收一个参数,那就是被装饰的类: 例如,我们定义一个名为 AddMethod 的装饰器函数,给类添加一个静态函数:

typescript 复制代码
@AddMethod
class Test{
  constructor(){}
}

/**
 * 类装饰器函数
 * @param target 被装饰的类
 */
function AddMethod(target: any){
  console.log("[AddMethod] ==> ", arguments);
  target.testFn = (txt: string) => console.log("[Test] ===> ", txt);
}

(Test as any).testFn("hhh")

可以看到,我们利用类装饰函数 Addmethod 方法,给类 Test 添加了一个名为 testFn 的静态方法,处理过后,调用 Test.testFn 方法,根据输出的测试语句可知,函数正常执行,说明装饰效果达到了。

这就是装饰器的普通用法,一般来说,为了使装饰器函数可以更好地扩展,我们一般以把装饰器函数作为一个函数的返回值的方式来使用:

typescript 复制代码
/**
 * 类装饰器函数
 * @param target 被装饰的类
 */
function AddMethod(desc?: string): ClassDecorator{
  return function(target: any){
    console.log("[AddMethod] ==> ", arguments);
    target.desc = desc;
    target.testFn = (txt: string) => console.log("[Test] ===> ", txt);
  }
}

效果是一样的:


通过这样的使用方式,我们可以把一些信息通过外部传入,最终添加到类上面去。

属性装饰器

属性装饰器函数,用法跟类装饰器函数一样,或者说,所有的装饰器函数用法基本都是一致的,只是不同装饰器的函数,接收到的参数个数以及参数本身的内容不同罢了。

属性装饰器函数,会接收到2个参数,第一个参数是类的原型, 第二个参数是被装饰的属性的名称。

可以看到,我们给类的 test 属性赋予了初始值为 hhh,测试语句也按预期输出了初始值。打印的 arguments 虽然显示有3个参数,但第三个参数值为 undefined,所以根本用不到。

那么,如何证明,第一个参数的值是类的原型,而不是类本身呢?结合原型链的相关知识,我们可以利用打印类名的方式来测试验证下。思路就是:如果有一个类,不妨名为 Person,那么访问 Person.name 的结果就是 Person 字符串,而访问 Person.prototype.name 的结果是 undefined,因为类的原型的constructor属性,指向类本身,所以 Person.prototyoe.constructor.name 结果也会是 Person 字符串:

那么,我们分别在属性装饰函数内打印 target.name 以及 target.constructor.name,如果后者输出结果为 Test 字符串,并且前者输出为 undefined 字符串,那么就说明,第一个参数 target 就是类的原型:

可以看到,target.name 输出为 undefinedtarget.constructor.name 输出为类名 Test,说明属性装饰器函数的第一个参数值是类的原型。

方法装饰器

方法装饰器函数,接收的参数有三个,第一个是类的原型,第二个是方面的名字,第三个是方法的访问描述符。

typescript 复制代码
class Test{
  test: string;
  constructor(){}

  @MethodDesc()
  add(){
    
  }
}


function MethodDesc(): MethodDecorator{
  return function(target: any, property: any, descriptor: any){
    console.log("[MethodDesc] ===> ", arguments);
  }
}

第三个参数 descriptor 里的 value 属性,就是方法本身。利用装饰器,我们可以实现在方法执行前后做一些操作,例如控制加载动画的显示等:

typescript 复制代码
class Test{
  test: string;
  constructor(){}

  @MethodDesc()
  add(){
   console.log("函数执行...."); 
  }
}

new Test().add();

function MethodDesc(): MethodDecorator{
  return function(target: any, property: any, descriptor: any){
    const rawMethod = descriptor.value;
    descriptor.value = function(...args: any[]){
      console.log("loading动画显示");
      const res = rawMethod.call(this, ...args);
      console.log("loading动画隐藏");
      return res;
    }
  }
}

反射元数据

概念

反射元数据,需要用到 reflect-metadata 包。使用 reflect-metadata 会为顶级对象 Reflect 添加一些用于定义元数据的方法。例如:

  1. Reflect.defineMetadata
  2. Reflect.getMetadata

defineMetadata 用于定义元数据到类或者类成员上,getMetadata 就是获取元数据。

使用方式

安装

首先,需要下载安装 reflect-metadata:

node 复制代码
npm install reflect-metadata

然后,需要在 tsconfig.json 文件里把 experimentalDecoratorsemitDecoratorMetadata 都设置为 true

最后,在入口文件里引入 reflect-metadata 即可:

这样,我们就可以看到,Reflect 对象上多了一些操作元数据的方法:

元数据的存放与获取

定义元数据,需要用到 Reflect.defineMetadta API,它接收4个参数。第一个是元数据的键名,第二个是元数据本身,第三个是存放的目标,第四个是可能用到的存放目标的属性。

typescript 复制代码
/* Define a unique metadata entry on the target.
* @param metadataKey --- A key used to store and retrieve metadata.
* @param metadataValue --- A value that contains attached metadata.
* @param target --- The target object on which to define metadata.
* @param propertyKey --- The property key for the target.
*/
function Reflect.defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol): void

例如:

存放元数据到类成员上:

metadata:key 为键名,把元数据 metadata:value 定义到测试类 Testtest 属性上:

typescript 复制代码
class Test{
  test: string;
  constructor(){}
}

Reflect.defineMetadata("metadata:key", "metadata:value", Test, "test");

获取的时候,通过键名 metadata:key 获取,同时指定为从 Testtest 属性里获取:

typescript 复制代码
console.log("[Test] ==> ", Reflect.getMetadata("metadata:key", Test, "test"));

结果为:

存放数据到类上面:

直接把范围只指定到类的维度即可:

typescript 复制代码
class Test{
  test: string;
  constructor(){}
}

Reflect.defineMetadata("metadata:key", "metadata:newValue", Test);

获取的方式同样把范围指定到类的维度:

typescript 复制代码
console.log("[Test] ==> ", Reflect.getMetadata("metadata:key", Test));

结果:

元数据的存储以及获取就是这么简单,接下来我们实现IOC容器。

IOC容器

结构

前面我们提到,IOC容器用于存放需要自动实例化的类,而且类的获取与存放由容器统一处理。那么,我们就使用一个类来作为IOC容器,不妨名为 IocContainer

typescript 复制代码
export class IocContainer {
  private constructor(){}
}

对于需要存放的类,那么我们就使用 Map 结构来存放,不妨名为 servicesMap

typescript 复制代码
type ClassStructor = new (...args: any[]) => any;
type ServiceKey = string | ClassStructor;

export class IocContainer {
  // IOC类映射
  static servicesMap: Map<ServiceKey, ClassStructor> = new Map();
  private constructor(){}
}

存放类的事情完成了,接下来就处理类的属性。因为一个类的属性可能依赖另一个类,即依赖的注入。那么我们需要记录,哪个类的哪个属性,需要依赖哪个类。这里我们以 类名 + 分隔符 + 属性名 的方式作为键名,把所依赖的类存放起来。像这样:

说明 名为 ApiController 的类,它的 user 属性依赖名为 UserService 的类。

所以IOC容器内需要另外一个结构存放这些信息,我们也是使用 Map 结构来存放,不妨名为 propertyKeysMap

typescript 复制代码
type ClassStructor = new (...args: any[]) => any;
type ServiceKey = string | ClassStructor;

export class IocContainer {
  // IOC类映射
  static servicesMap: Map<ServiceKey, ClassStructor> = new Map();
  // 实例属性映射
  static propertyKeysMap: Map<string, ClassStructor> = new Map();
 private constructor(){}
}

由于IOC容器还负责类的获取与存放,所以还需要定义两个方法,分别用于获取和存放,不妨名为 setget

typescript 复制代码
type ClassStructor = new (...args: any[]) => any;
type ServiceKey = string | ClassStructor;

export class IocContainer {
  // IOC类映射
  static servicesMap: Map<ServiceKey, ClassStructor> = new Map();
  // 实例属性映射
  static propertyKeysMap: Map<string, ClassStructor> = new Map();
  private constructor(){}
 
   /**
   * 实例化IOC类
   * @param key 键名
   */
  static get(key: ServiceKey){}

  /**
   * 存放IOC类
   * @param key 键名
   * @param value 值
   */
  static set(key: ServiceKey, value: ClassStructor){}

}

IOC容器的结构就确定了,接下来我们分别实现存放以及获取类的逻辑。

存储

存储的逻辑很简单,直接往 servicesMap 里塞即可:

typescript 复制代码
type ClassStructor = new (...args: any[]) => any;
type ServiceKey = string | ClassStructor;

export class IocContainer {
  // ......

  /**
   * 存放IOC类
   * @param key 键名
   * @param value 值
   */
  static set(key: ServiceKey, value: ClassStructor){
    IocContainer.servicesMap.set(key, value);
  }

}

获取

获取的逻辑稍微复杂一些,因为获取一个类,需要把它实例化,并且处理内部属性的依赖情况。我们分为以下4个步骤进行:

  1. 获取构造函数
  2. 创建实例
  3. 初始化实例属性
  4. 返回实例
typescript 复制代码
export class IocContainer {
  // ......

  /**
   * 实例化IOC类
   * @param key 键名
   */
  static get(key: ServiceKey){
     // 1. 获取构造函数
     // 2. 创建实例
     // 3. 初始化实例属性
     // 4. 返回实例
  }

}

获取构造函数

即从 servicesMap 中获取对应的类即可,同时需要判断下这个类是否不在容器内:

typescript 复制代码
export class IocContainer {
  // ......

  /**
   * 实例化IOC类
   * @param key 键名
   */
  static get(key: ServiceKey){
    // 1. 获取构造函数
    const Ctor = IocContainer.servicesMap.get(key);
    if(!Ctor) return undefined;
    // 2. 创建实例
    // 3. 初始化实例属性
    // 4. 返回实例
  }

}

创建实例

当获取的类存在时,直接先进行实例化:

typescript 复制代码
export class IocContainer {
  // ......

  /**
   * 实例化IOC类
   * @param key 键名
   */
  static get(key: ServiceKey){
    // 1. 获取构造函数
    const Ctor = IocContainer.servicesMap.get(key);
    if(!Ctor) return undefined;
    // 2. 创建实例
    const inst = new Ctor();
    // 3. 初始化实例属性
    // 4. 返回实例
  }

}

初始化实例属性

初始化实例的属性时,不能简单的只是从 servicesMap 里获取属性依赖的类,然后实例化。需要考虑到,依赖的类内部的属性,可能也依赖其它类,即存在嵌套依赖的情况。这时候就需要递归调用自身来获取实例,思路是这样的:

首先,因为存放属性对应的依赖时,键名是 类名 + 分隔符 + 属性名。是这样的形式:

所以,如果需要从 propertyKeysMap 里获取到值,那么我们就需要知道类名以及属性名。遗憾的是,目前我们只知道类名这一信息,那么我们要如何知道,该类存在哪些属性,以及这些属性都依赖哪些类呢?咋一看,好像没办法。但我们可以这样想:propertyKeysMap 里的 key 里面,肯定含有已知的类名,那么我们就遍历 propertyKeysMap ,利用分隔符拆分键名,这样我们可以得到键名里隐含的类名以及属性名。此时如果键名里的类名与我们已知的类名相同的话,那么,我们就可以知道,键名里的属性名,就是我们已知的类名里的属性,此时键名对应的值,就是属性依赖的类。文字描述不够清晰,上代码辅助理解:

typescript 复制代码
export class IocContainer {
  // ......

  /**
   * 实例化IOC类
   * @param key 键名
   */
  static get(key: ServiceKey){
    // 1. 获取构造函数
    const Ctor = IocContainer.servicesMap.get(key);
    if(!Ctor) return undefined;
    // 2. 创建实例
    const inst = new Ctor();
    // 3. 初始化实例属性
    const CtorName = Ctor.name;
    IocContainer.propertyKeysMap.forEach((val, key) => {
      const [className, property] = key.split(DELIMITER);
      if(className === CtorName){
        (inst as any)[property] = IocContainer.get(val);
      }
    })
    // 4. 返回实例
  }

}

这样,通过遍历 propertyKeysMap,把所有字符串里包含已知类名的键名都利用分隔符拆分一下,就能知道类名对应实例含有哪些属性,以及属性所依赖的类,最终递归调用 IocContainer.get() 方法初始化对应的依赖类即可。

返回实例

经过前三个步骤,我们以及得到依赖关系处理完成的实例,直接返回即可:

typescript 复制代码
export class IocContainer {
  // ......

  /**
   * 实例化IOC类
   * @param key 键名
   */
  static get(key: ServiceKey){
    // 1. 获取构造函数
    const Ctor = IocContainer.servicesMap.get(key);
    if(!Ctor) return undefined;
    // 2. 创建实例
    const inst = new Ctor();
    // 3. 初始化实例属性
    const CtorName = Ctor.name;
    IocContainer.propertyKeysMap.forEach((val, key) => {
      const [className, property] = key.split(DELIMITER);
      if(className === CtorName){
        (inst as any)[property] = IocContainer.get(val);
      }
    })
    // 4. 返回实例
    return inst;
  }

}

至此,获取依赖类的逻辑就完成了。

辅助常量/函数的定义

我们定义一些常量以及工具函数,用于后续辅助使用。

集中定义一些常量:

typescript 复制代码
/**
 * 常量
 */

// 定义元数据时使用的 key
export enum KEY {
  PATH = "ico:path",
  METHOD = "ioc:method",
  TYPE = "design:type",
};

// 请求方式
export enum REQ_METHOD {
  GET = "GET",
  POST = "POST"
};

// 分隔符
export const DELIMITER = ":";

这里需要额外说一下, KEY.TYPE 对应的字符串 design:type。这个字符串是使用了 reflect-metadata 后,内部已经定义好的字符串,所以不是自定义的。除此之外,其它的常量字符串,都是自定义。

接下来,定义一些工具函数。例如接下来会用到的柯里化函数:

typescript 复制代码
/**
 * 工具函数
 */

/**
 * 函数柯里化
 * @param fn 目标函数
 * @param len 形参个数
 * @param args 参数数组
 * @returns 柯里化后的函数
 */
export function curry(fn: Function, len: number = fn.length, ...args: any[]){
  return function(this: any, ...params: any[]){
    const allParams = [...args, ...params];
    return (allParams.length >= len) ? fn.apply(this, allParams) : curry(fn, len, ...allParams);
  }
}

装饰器函数定义

装饰器函数,用于给类添加元数据。接下来一一讲解并实现

Injectable

Injectable 是类装饰器函数,作用是把被装饰的类放到IOC容器里:

typescript 复制代码
/**
 * 存储IOC类
 * @returns 类装饰器函数
 */
export function Injectable(): ClassDecorator{
  return function(target: any){
    IocContainer.set(target, target);
  }
}

Inject

Inject 是属性装饰器函数,作用是记录哪个类的哪个属性,依赖哪个类:

typescript 复制代码
/**
 * 依赖注入
 * @returns 属性装饰器函数
 */
export function Inject(): PropertyDecorator{
  return function(target: any, property: any){
    const key = `${target.constructor.name}${DELIMITER}${property}`;
    const value = Reflect.getMetadata(KEY.TYPE, target, property);
    IocContainer.propertyKeysMap.set(key, value);
  }
}

这里需要说一下,Reflect.getMetadata(KEY.TYPE, target, property); 的作用是,获取被装饰属性的类型。例如,我们把 ApiControlleruser 的属性类型,定义为 UserService 类:

那么,我们通过 Reflect.getMetadata(KEY.TYPE, target, property); 获取到的,就是它的类型,即 UserService 类:

这就之前在 "IOC容器-获取-初始化实例属性" 里提过的,往 PropertyKeysMap 里存数据时,键名是 类名+分隔符+属性名,值是属性依赖的类。

Controller

Controller是类装饰器函数,作用是把请求的根路径信息添加到类上面:

typescript 复制代码
/**
 * 定义请求的根路径
 * @param path 请求的根路径
 * @returns 类装饰器函数
 */
export function Controller(path: string = ""): ClassDecorator{
  return function(target: any){
    Reflect.defineMetadata(KEY.PATH, path,target);
  }
}

ApiMethod

ApiMethod 是方法装饰器函数,作用是把请求发方式以及请求的子路径信息添加到方法体里面:

typescript 复制代码
/**
 * 定义实例方法对应的请求方式以及请求的子路径
 * @param method 请求的方式
 * @param path 请求的子路径
 * @returns 方法装饰器函数
 */
function ApiMethod(method: string, path?: string): MethodDecorator{
  return function(target: any, property: any, descriptor: any){
    const rawMethod = descriptor.value;
    Reflect.defineMetadata(KEY.METHOD, method, rawMethod);
    Reflect.defineMetadata(KEY.PATH, path ?? "", rawMethod);
  }
}

该函数会被柯里化,然后用于生成下面提到的 GetPost 装饰函数。

Get

Get 是利用 ApiMethod 函数柯里化出来的,填充了第一个表示请求方式的参数为 GET ,即为 GET 请求:

typescript 复制代码
// GET 请求
export const Get = curry(ApiMethod)(REQ_METHOD.GET);

Post

Post 是利用 ApiMethod 函数柯里化出来的,填充了第一个表示请求方式的参数为 Post ,即为 Post 请求:

typescript 复制代码
// POST 请求
export const Post = curry(ApiMethod)(REQ_METHOD.POST);

类的定义

这里我们使用到的类,分为两种,一种称为服务类,另一种称为控制器类。服务类作为依赖被注入到控制器类中,在控制器类中被调用。

服务类

我们定义一个名为 UserService 的服务类,内部分别有 listdetailadd 方法。使用 Injectable 装饰器函数装饰,这样 UserService 类就会被存放到IOC容器内:

typescript 复制代码
/**
 * 服务类
 */
import { Injectable } from '../../utils/decorator';

@Injectable()
export class UserService {
  constructor(){}

  list(){
    return {
      code: 200,
      data: ["list", "list", "list"],
      msg: "success",
    };
  }

  detail(){
    return {
      code: 200,
      data: { name: "JuneRain", desc: "hhhhh"},
      msg: "success",
    };
  }

  add(){
    return {
      code: 200,
      data: [{name: "Joey"}, {name: "Rachel"}],
      msg: "success",
    };
  }
}

控制器类

我们定义一个名为 ApiController 的类,内部分别含有 user 属性以及 getListgetDetailaddData 函数。

使用 ControllerInjectable 装饰器来装饰,作用分别是定义请求的根路径以及把 ApiController 类存放到IOC容器内。

user 属性类型为 UserService 类,并且被 Inject 装饰器装饰。说明 ApiController 类的 user 属性,依赖的类是 UserService。这些信息会以键名为 ApiController:user,值为 UserService 类的方式,存放到IOC容器的 propertyKeysMap 里。

getListgetDetailaddData 函数,被 GetPost 装饰器装饰,作用是在对应的方法体里,添加请求方式以及请求的子路径信息。

typescript 复制代码
/**
 * 控制器
 */


import { Controller, Get, Post, Inject, Injectable } from '../../utils/decorator';
import { UserService } from '../services';


@Injectable()
@Controller("/user")
class ApiController {
  @Inject()
  user: UserService;
  constructor(){}

  @Get("list")
  getList(){
    return this.user.list();
  }

  @Get("detail")
  getDetail(){
    return this.user.detail();
  }

  @Post("add")
  addData(){
    return this.user.add();
  }
}

export default ApiController;

完整请求路径的拼接

上面我们在 ApiController 里添加了请求路径以及请求路径的信息。那么我们接下来就把获取请求方式和路径信息的逻辑实现一下。

不妨把这个获取请求信息的函数名为 parseApiInfo。它接收的参数只有一个,那就是IOC容器初始化出来的实例。

typescript 复制代码
export function parseApiInfo(inst: any){}

我们知道,我们把请求的跟路径添加在了类上面,把请求方法以及请求的子路径信息添加在了方法体上面。那么,相应的,我们获取信息的步骤,也就有两个:

typescript 复制代码
export function parseApiInfo(inst: any){
  // 获取 api 根路径  
  
  // 获取实例各方法对应的 api 子路径以及对应的请求方式

}

首先,如何获取根路径信息。自然是借助 Reflect.getMetadata API,从类里获取:

typescript 复制代码
export function parseApiInfo(inst: any){
  // 获取 api 根路径  
  const instProto = <any>Reflect.getPrototypeOf(inst);
  const rootPath = <string>(Reflect.getMetadata(KEY.PATH, instProto.constructor));
  // 获取实例各方法对应的 api 子路径以及对应的请求方式

}

因为上面说到,提供的参数,是类的实例。利用原型链的知识,可以知道。实例的原型的 constructor 属性指向的就是类本身。所以这里先利用 Reflect.getPrototypeOf 获取实例的原型,接下来获取根路径 rootPath 的时候再指定是从 instProto.constructor ,即类上获取。

根路径获取完了,接下来获取实例的各方法上埋藏的请求方式以及子路径信息。我们知道,类的方法,实际是定义在原型上的,所以我们可以通过 Reflect.ownKeys API从实例的原型上获取到所有的方法名,举个例子:

可以看到,构造函数 constructor 也在数组里,我们没有在上面添加信息,所以需要把它过滤出去。然后再遍历得到的方法名数组,利用方法名获取到方法体,再从方法体上获取我们埋藏的信息,即请求方式以及子路径。这样,根路径,请求方式以及子路径都知道了,我们就可以拼接一下再统一返回一个对象:

typescript 复制代码
  export function parseApiInfo(inst: any){
  // 获取 api 根路径  
  const instProto = <any>Reflect.getPrototypeOf(inst);
  const rootPath = <string>(Reflect.getMetadata(KEY.PATH, instProto.constructor));
  // 获取实例各方法对应的 api 子路径以及对应的请求方式
  const methodNameList = <string[]>(Reflect.ownKeys(instProto).filter((name) => name !== "constructor"));
  const info = methodNameList.map((name) => {
    const rawMethod = instProto[name];
    const reqMethod = Reflect.getMetadata(KEY.METHOD, rawMethod);
    const reqPath = Reflect.getMetadata(KEY.PATH, rawMethod);
    return {
      url: `${rootPath}/${reqPath}`,
      reqMethod,
      rawMethod: rawMethod.bind(inst),
    };
  })
  // 返回信息
  return info;
}

当然了,最后别忘了把路由信息返回出来。

验证

请求信息验证

首先,我们先来验证下,通过IOC容器实例化 ApiController ,得到的实例对象是否正确。

引入 ApiController 以及 IocContainer,创建实例,打印实例,实例的属性以及实例的各方法返回值:


可以看到,实例 `apiControlelr` 对象的 `user` 属性为 `UserService` 的实例对象。各方法的返回值也符合预期,是 `UserService` 里对应方法的返回值。

路由信息验证

我们把IOC容器创建出来的实例对象,传给 parseApiInfo 函数处理,看下返回值是否符合预期:

可以看到,打印的路由信息,已经把根路径 /user 以及各子路径 listdetailadd 拼接成了完整的请求路径,以及各方法对应的请求方式也是正确的。

http服务搭建

为了我们最终能通过API访问的方式执行对应的方法,得到数据。我们需要简单搭建一个 http 服务。

首先,下载 http 包以及 @types/node 包,后者主要是给 ts 提供参数支持等功能。

接下来,引入 http 并创建服务:

补充下每次请求的处理逻辑:从 req 请求对象中获取请求的路径以及方式,遍历路由信息 routeInfo ,当遍历到请求路径以及方式都一致时,执行当前遍历到的路由信息子项里的方法,获取数据并返回:

最后,把服务运行起来,进行验证:

可以看到,结果跟预想一致。说明验证结果没有问题。

总结

利用装饰器以及反射元数据,把关键信息添加到类以及类的属性或方法上。后续再根据这些信息去存放,获取类。就实现了依赖注入。

借助IOC模式,我们可以把创建类实例的操作,交给容器帮我们执行。这样我们就可以把更多的精力投放到代码的具体实现上,而无需手动去维护创建各依赖实例等操作。一定程度上提高了开发的效率。

代码地址

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax