soraping / any-source

blog及源码阅读

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

typescript依赖注入实践

soraping opened this issue · comments

之前写过的一篇关于《前端IOC 的简单实践》,基于本人是一个前端,设计模式的基础知识都不完备,所以内容不是太严谨,正在学习中!
文章中提到了一个关键词:依赖注入。

有小伙伴跟我提说在真实项目中如何使用的,我知道 angular 就是借鉴 springioc ,奈何我没有用过,下面呢就来说说我之前在nodejs项目上的一些实践。

去年,我贴了一个 nodejs 的简易web框架-- lenneth,基于 koa2 搞的,简单说就是用装饰器模仿 spring 来写 nodejs 的注解(说注解习惯了,就不说装饰器了),看下示例:

import {
  Controller,
  Autowired,
  Post,
  Get,
  RequestBody,
  PathVariable,
  Response,
  TResponse,
  UseBefore,
  Description
} from "lenneth";
import { UserService } from "../services";
import { IUserInfo } from "../interface";
import { UserAuth, RuleAuth } from "../middleware";

@Controller("/user")
export class UserController {
  @Autowired()
  userService: UserService;

  @Post("/add")
  @Description("添加会员")
  @UseBefore(UserAuth, RuleAuth)
  async addUser(
    @RequestBody() user: IUserInfo,
    @Response() response: TResponse
  ) {
    response.body = this.userService.addUser(user);
  }

  @Get("/detail/:userId")
  @UseBefore(UserAuth)
  @Description("查询会员")
  async getUser(
    @PathVariable("userId") userId: string,
    @Response() response: TResponse
  ) {
    response.body = this.userService.getUserInfo(userId);
  }
}

看到这些注解,是不是很眼熟,就是从 spring 抄来的,具体介绍可以去项目里看看,下面来重点介绍实现 Autowired 注解的过程,也就是依赖注入的实践。

看上面的实例,这个项目依赖了一个 UserService 类,在这个 UserController 这个方法中会用到这个依赖类的某个方法。

依赖注入:

@Autowired()
userService: UserService;

使用:

this.userService.addUser(user);

来看下 Autowired 注解的实现:

import { Metadata } from "@common";
import { descriptorOf, getClassName } from "@utils";

/**
 * 注入service,类属性修饰器
 * @param params 实例化参数
 */
export const Autowired = (params: any = ""): Function => {
  return (target: any, propertyKey: string) => {
    // 获取该属性的类型
    let typeClass = Metadata.getType(target, propertyKey);
    const descriptor = descriptorOf(target, propertyKey) || {
      writable: true,
      configurable: true
    };
    // 实例化修饰类
    descriptor.value = params ? new typeClass(params) : new typeClass();
    Reflect.defineProperty(
      (target && target.prototype) || target,
      propertyKey,
      descriptor
    );
  };
};

解读这段实现之前,先引出了另一个概念--反射,就是在运行时动态获取一个对象的一切信息,包括方法/属性等等,特点在于动态类型反推导。

Reflect 是ES6新增的api,本身提供了不少静态方法,不过要使用还需要引入 reflect-metadata 这个库,为了使编译器在设计时将元数据序列化传给修饰器。

通过反射能获得系统提供的metadataKey信息:

  • design:type 修饰目标对象的类型;
  • design:paramtypes 修饰目标对象方法的参数类型;
  • design:returntype 修饰目标对象方法返回值的类型;

来看下案例:

import "reflect-metadata";

const validate = () => {
  return (target: any, propertyKey: string) => {
    // 修饰目标对象的类型
    let type = Reflect.getMetadata("design:type", target, propertyKey);
    // 修饰目标的参数类型
    let paramTypes = Reflect.getMetadata(
      "design:paramtypes",
      target,
      propertyKey
    );
    // 修饰目标的返回值类型
    let returnType = Reflect.getMetadata(
      "design:returntype",
      target,
      propertyKey
    );
    // 所有能通过反射获取的元数据类型key
    let allKeys = Reflect.getMetadataKeys(target, propertyKey);
    console.log("type", type);
    console.log("paramTypes", paramTypes);
    console.log("returnType", returnType);
    console.log("allKeys", allKeys);
  };
};

class Person {
  private name: string;

  @validate()
  getInfo(tags: string): string {
    return `your name is ${this.name}, tags is ${tags}`;
  }
}

控制台展示:

type function Function() { [native code] }
paramTypes [ [Function: String] ]
returnType function String() { [native code] }
allKeys [ 'design:returntype', 'design:paramtypes', 'design:type' ]

特别注意:design:returntype 依赖于所修饰方法的是否显式定义类型了,如果没有定义类型,那就会默认返回 undefined

我们也可以自定义 metadataKey,即在相应的类上定义自定义的元数据。

const service = () => {
  return (target: any) => {
    // 自定义元数据,key 为 ServiceDecorator
    Reflect.defineMetadata("ServiceDecorator", "your personal value", target);
  };
};

@service()
class Person {
  private name: string;
}

// 在合适的位置获取之前定义的元数据
// your personal value
console.log(Reflect.getMetadata("ServiceDecorator", Person));

自此,有了这个知识,在看上面的 Autowired 代码是不是简单的多了。

Autowired 注解的本质是一个属性修饰器,主要是考虑到会有参数传入,所以就写了一个高阶函数。修饰器本身就不做介绍了,可以看下阮一峰老师的es6教程。

在方法内部,先获取了被修饰对象的类型,转换如下:

let typeClass = Reflect.getMetadata("design:type", target, propertyKey);

这个 metadataKey 是系统提供的 design:type,获取被修饰对象的类型。

@Autowired()
userService: UserService;

那这个 typeClass 的值就是 UserService

// 获取指定对象属性的描述对象
const descriptor = Reflect.getOwnPropertyDescriptor(target, propertyKey) || {
      writable: true,
      configurable: true
    };

这里就是获取 UserControlleruserService 属性的描述对象,那这个值有什么用呢?

Reflect.getOwnPropertyDescriptor 方法其实等同于 Object.getOwnPropertyDescriptor ,它会返回一个object:

{
    value: "value",
    writable: true,
    enumerable: true,
    configurable: true
}

返回的四个字段中value就是这个属性的值,我们只要修改这个value字段,就可以实现注入了。

descriptor.value = params ? new typeClass(params) : new typeClass();
Reflect.defineProperty(
  (target && target.prototype) || target,
  propertyKey,
  descriptor
);

所以,最后修改了这个属性的描述对象的值,使它指向了所返回类型的实例对象,再重新定义这个属性的描述对象,这样编译后,userService 这个被修饰的属性就是UserService 的实例对象,能够访问到UserService内的属性方法了。

如此,就实现了 Autowired 注解的功能了。

完整示例:

const Autowired = (params: any = ""): Function => {
  return (target: any, propertyKey: string) => {
    // 获取该属性的类型
    let typeClass = Reflect.getMetadata("design:type", target, propertyKey);
    const descriptor = Reflect.getOwnPropertyDescriptor(
      target,
      propertyKey
    ) || {
      writable: true,
      configurable: true
    };
    // 实例化修饰类
    descriptor.value = params ? new typeClass(params) : new typeClass();
    Reflect.defineProperty(
      (target && target.prototype) || target,
      propertyKey,
      descriptor
    );
  };
};

class UserService {
  getUserById(id: string) {
    return `user id is ${id}`;
  }
}

class Person {
  @Autowired()
  private userService: UserService;
  
  getUserInfo(id: string) {
    console.log(this.userService.getUserById(id));
  }
}

// user id is 12
console.log(new Person().getUserInfo("12"));

Reflect.defineProperty(
(target && target.prototype) || target,
propertyKey,
descriptor
);

(target && target.prototype) || target,
这里为啥要这么判断呢?

很久没有写js了,看着都有点陌生了。。。
这段判断是我加的,一般情况下只有 target 便就够了,我也是通过各种调试发现,真正需要的是 target .prototype ,具体当时为什么这么做我还真忘记了,你也可以dubug,断点试下,打印上下文参数,应该不难发现