前言
在面向对象编程的代码开发过程中,我们可能会遇到这么一种情况:我们使用的目标类,依赖其它的类。这时候,我们通常就得手动引入这些类并创建实例。
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
数据结构差不多:
-
定义容器:定义一个IOC容器,用于存放所有需要自动创建以及注入所需依赖的类,类的存储以及获取统一由容器处理。
-
存储:利用装饰器以及反射元数据,把关键信息添加到需要存放的类,类属性以及类方法上。这些信息在类被存放到容器内时作为键名使用。
-
获取:获取容器里面存放的类时,需要通过容器来获取,容器会自动实例化并处理好其中的依赖关系,最终把实例返回。其中,会利用步骤2中的关键信息来获取目标类。
装饰器
实现依赖注入,需要用到装饰器,这里简单讲解下,会用到的装饰器的用法。对装饰器感兴趣的朋友们,可以去搜搜相关的文章学习学习下。
种类
装饰器以@
开头的方式使用函数,用于对被装饰的目标进行修改等操作。一共有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
输出为 undefined
,target.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
添加一些用于定义元数据的方法。例如:
Reflect.defineMetadata
Reflect.getMetadata
defineMetadata
用于定义元数据到类或者类成员上,getMetadata
就是获取元数据。
使用方式
安装
首先,需要下载安装 reflect-metadata
:
node
npm install reflect-metadata
然后,需要在 tsconfig.json
文件里把 experimentalDecorators
和 emitDecoratorMetadata
都设置为 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
定义到测试类 Test
的 test
属性上:
typescript
class Test{
test: string;
constructor(){}
}
Reflect.defineMetadata("metadata:key", "metadata:value", Test, "test");
获取的时候,通过键名 metadata:key
获取,同时指定为从 Test
的 test
属性里获取:
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容器还负责类的获取与存放,所以还需要定义两个方法,分别用于获取和存放,不妨名为 set
和 get
:
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个步骤进行:
- 获取构造函数
- 创建实例
- 初始化实例属性
- 返回实例
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);
的作用是,获取被装饰属性的类型。例如,我们把 ApiController
的 user
的属性类型,定义为 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);
}
}
该函数会被柯里化,然后用于生成下面提到的 Get
和 Post
装饰函数。
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
的服务类,内部分别有 list
,detail
和 add
方法。使用 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
属性以及 getList
,getDetail
和 addData
函数。
使用 Controller
和 Injectable
装饰器来装饰,作用分别是定义请求的根路径以及把 ApiController
类存放到IOC容器内。
user
属性类型为 UserService
类,并且被 Inject
装饰器装饰。说明 ApiController
类的 user
属性,依赖的类是 UserService
。这些信息会以键名为 ApiController:user
,值为 UserService
类的方式,存放到IOC容器的 propertyKeysMap
里。
getList
,getDetail
和 addData
函数,被 Get
或 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;
完整请求路径的拼接
上面我们在 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
以及各子路径 list
,detail
和 add
拼接成了完整的请求路径,以及各方法对应的请求方式也是正确的。
http服务搭建
为了我们最终能通过API访问的方式执行对应的方法,得到数据。我们需要简单搭建一个 http
服务。
首先,下载 http
包以及 @types/node
包,后者主要是给 ts 提供参数支持等功能。
接下来,引入 http
并创建服务:
补充下每次请求的处理逻辑:从 req
请求对象中获取请求的路径以及方式,遍历路由信息 routeInfo
,当遍历到请求路径以及方式都一致时,执行当前遍历到的路由信息子项里的方法,获取数据并返回:
最后,把服务运行起来,进行验证:
可以看到,结果跟预想一致。说明验证结果没有问题。
总结
利用装饰器以及反射元数据,把关键信息添加到类以及类的属性或方法上。后续再根据这些信息去存放,获取类。就实现了依赖注入。
借助IOC模式,我们可以把创建类实例的操作,交给容器帮我们执行。这样我们就可以把更多的精力投放到代码的具体实现上,而无需手动去维护创建各依赖实例等操作。一定程度上提高了开发的效率。