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

代码地址

相关推荐
祭の5 分钟前
HttpServletRequest和⽤用户登录表单提交
java·服务器·前端
Python私教14 分钟前
大前端的发展过程
前端
Python私教25 分钟前
es6和es5的区别
前端
会飞的哈士奇37 分钟前
Html让两个Dom进行连线 , 可以自定义连接的位置
前端·javascript·html
wandongle1 小时前
Vue3中使用Axios构建高效的请求处理机制
前端·javascript·vue.js
全栈练习生1 小时前
前端监控之sourcemap精准定位和还原错误源码
前端
贩卖纯净水.1 小时前
HTML5和CSS3新增特性
前端·css3·html5
baozhengw1 小时前
uni-app快速入门(八)--常用内置组件(上)
java·前端·uni-app
理想不理想v1 小时前
【经典】 webpack打包流程及原理?
java·前端·javascript·vue.js·webpack·node.js
red润1 小时前
Vue 项目打包后环境变量丢失问题(清除缓存),区分.env和.env.*文件
前端·vue.js·缓存·webpack