Typescript IOC控制反转 101 (2): 基於reflect-metadata的IOC容器

865 阅读8分钟

(接下来的内容主要是ioc实践方面, 如果是一般中小企前端工程师, 对这方面没什么兴趣, 看完第一篇就足够了)

在上一篇文章Typescript IOC控制反转 101 (1): 写个简单的IOC容器中, 我们简单做了一个ioc容器, 功能已经实现了。但有没有更好的实践方案?

反射

说正题前, 先了解一些前置知识, 先了解一下反射, 这是很重要的概念, 工业级的IOC框架 (如 Spring) 就是基于反射实现, Java里有句话叫 接口加反射, 啥都能干, 可想而知, 反射有多重要。那什么是反射? google一下反射, 看到这句话:

In object-oriented programming languages such as Java, reflection allows inspection of classes, interfaces, fields and methods at runtime without knowing the names of the interfaces, fields, methods at compile time. It also allows instantiation of new objects and invocation of methods.

翻译过来就是: 在面向对象语言中, 如java, 反射可以在运行期间, 做到类型, 接口, 属性和方法的检查, 不需要知道在编译期间知道接口, 属性和方法的名称。 它也可以实例化对象, 和调用方法。

如果在其他OOP语言, 会经常使用反射, IOC, ORM等就是基于反射实现的。但在js/ts里, 很少会直接使用反射 (Reflect)的api, 为什么? 我的答案是因为js是一门动态语言, 它的对象操作就像是操作哈希表 (事实上, 在v8中, 对象就是类似哈希表的结构), 在对象里, 属性和方法的赋值增删改没什么太大区别, 就是键值对 (也因此在js中很少用Map这个数据结构, 因为对象已经能实现它的功能), 键值对结构即使在静态语言中,本身就可以做到动态操作。当然这也意味着js的一系列对象操作是不安全的 (例如经常出现的undefined问题), 这是另一个话题啦。

既然如此, 为什么我们要了解反射? 因为我们要操作装饰器 (decorator)

装饰器

装饰器是一个在其他常见的oop语言 (如 java, C#)中一个烂大街的特性, 在ioc框架中大量使用, 但在js中依然是实验性质的存在 (所以我在文章一开始说一般中小企前端工程师没必要看, 因为这特性在生产环境用不上)。装饰器是什么? 看代码是最容易理解:

@sealed
class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
}

就是在代码加了一些装饰器, 然后可以利用反射对加上注解的类, 属性和方法做一些操作, 这样做有什么好处?就是可以做到只需加入一些装饰器, 就能加强代码的功能。

写一个demo来看看吧。先创建一个项目, 引入依赖:

npm install reflect-metadata

Reflect Metadata 是ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。

然后要在tsconfig.js做些配置:


    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "lib": ["ES6", "DOM"],                                       /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
    "emitDecoratorMetadata": true,                      /* Emit design-type metadata for decorated declarations in source files. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
     "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
     "types": ["reflect-metadata"],                                   /* Specify type package names to be included without being referenced in a source file. */
import 'reflect-metadata'

@Reflect.metadata('inClass', 'A')
class Test {
  @Reflect.metadata('inMethod', 'B')
  public hello(): string {
    return 'hello world';
  }
}

console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'

可以直接用Reflect.metadata作为装饰器, 然后利用Reflect.getMetadata就可以获取相应的值。

也可以自己写一个装饰器:

import 'reflect-metadata';

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'a'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'b'

由于本文主要是IOC容器, 装饰器只作简单介绍, 详情可参考:

Typescript官网

深入理解 TypeScript

写一个装饰器的简单应用

我们希望写一个函数, 调用它, 可以根据装饰器信息创建一个类, 效果如下:

// Person.ts

@Injectable()
export class Person {

  @InjectValue("Tom")
  name: string

  @InjectValue(12)
  age: number

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}


//demo.ts

import { Person } from "./Person";

const person: Person = getBean('Person') as Person;

console.log("名字是",person.name); // 名字是Tom
console.log("年龄是",person.age);  // 年龄是12

首先要先写好装饰器函数, 用来给类赋值:

// decorator.ts

import 'reflect-metadata'

/**
 * 标记该类可注入属性
 * @returns
 */
export function Injectable(): ClassDecorator {
  return target => {
    Reflect.defineMetadata("injectable", true, target);
  }
}

/**
 * 为属性赋值
 * @param value 
 * @returns 
 */
export function InjectValue(value: unknown) {
  return Reflect.metadata("injectValue", value);
}

然后我们要有一个函数, 用来存放要初始化的类:

import 'reflect-metadata';
import { Person } from './Person';

type beanType = Record<string, Function>;

const beans:beanType = {};

saveBean(Person);

export function saveBean(target: Function) {
  const name = target.name;
  beans[name] = target;
}

然后还得有一个函数, 用来获取装饰器 @InjectValue赋的值:

export function getMetaValue(target: Object, propertyKey: string) {
  const metaValue = Reflect.getMetadata("injectValue", target, propertyKey);
  return metaValue;
}

最后我们要创建一个函数, 可以用来实例化对象:

export function createBean(beanName: string) {
  // 获取saveBean存放的类
  const Bean = beans[beanName];

  if (!Bean) {
    throw new Error("没有对应的类, 无法创建");
  }

  // 判断是否可以注入属性
  const isInjectable = Reflect.getMetadata("injectable", Bean) as boolean;

  if (!isInjectable) {
    return Bean;
  }

  // 利用反射, 创建实例
  const result = Reflect.construct(Bean, []);

  Object.keys(result).forEach(key => {
    // 获取装饰器InjectValue赋的值
    const propertyValue = getMetaValue(result, key);

    if (propertyValue !== undefined || propertyValue !== null) {
      Reflect.defineProperty(result, key, {
        value: propertyValue
      });
    } else {
      const value = Reflect.get(Bean, key);
      Reflect.defineProperty(result, key, {
        value
      });
    }
  })

  return result;
}

写个测试吧:

import { Person } from "./Person";
import { createBean } from "./reflectUtils";

const person: Person = createBean('Person') as Person;

console.log("名字是",person.name); // 名字是Tom
console.log("年龄是",person.age);  // 年龄是12

有点东西, 这不就是我们之前做的IOC容器装饰器版吗!?不过现在 @InjectValue的值是硬编码, 我们可以再完善一下。

完善装饰器版IOC容器

写代码之前, 先整理一下我们想要的需求:

  1. @InjectValue不要硬编码, 要可以从配置文件中获取
  2. 可以注入实例对象
  3. 以上需求完成后, 可以直接用函数调用, 就像之前一样

期望实现效果如下:

// Person.ts
export interface Person {
  name: string,
  age: number,
  introduce: () => string
}

----

// Father.ts

import { Person } from './Person';

@Injectable()
export class Father implements Person {

  @InjectValue("${father.name}")
  name: string;

  @InjectValue("${father.age}")
  age: number;


  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  introduce() {
    return `My name is ${this.name}. I am ${this.age} years old`;
  }
}

----

// Student.ts

import { Person } from './Person';

@Injectable()
export class Student implements Person {

  @InjectValue("${student.name}")
  name: string;

  @InjectValue("${student.age}")
  age: number;

  @Inject("father")
  father: Person;

  constructor(name: string, age: number, father: Person) {
    this.name = name;
    this.age = age;
    this.father = father;
  }

  introduce() {
    return `My name is ${this.name}. I am ${this.age} years old. My father's name is ${this.father.name}.`;
  }
}

----

// container.ts

{
  "father": {
    "name": "Thomas",
    "age": 50
  },
  "student": {
    "name": "Tom",
    "age": 16
  }
}

----

// container.ts

import * as beanConfig from './beanConfig.json';
import { Father } from './Father';
import { Student } from './Student';

const beanFactory = beanFactoryConfig(beanConfig);
beanFactory.add(Student);
beanFactory.add(Father, "father");

export const getBean = beanFactory.build();


----

// demo.ts

import { getBean } from "./container";
import { Person } from "./Person";

const student = getBean("Student") as Person;

// My name is Tom. I am 16 years old. My father's name is Thomas.
console.log(student.introduce());

接下来要开始写代码了, 也是同样套路, 先把装饰器函数完成:

// decorator.ts

import 'reflect-metadata'

/**
 * 标记该类可注入属性
 * @returns
 */
export function Injectable(): ClassDecorator {
  return target => {
    Reflect.defineMetadata("injectable", true, target);
  }
}

/**
 * 为属性赋值
 * @param value 
 * @returns 
 */
export function InjectValue(value: unknown) {
  return Reflect.metadata("injectValue", value);
}

/**
 * 
 * @param value 属性注入实例
 * @returns 
 */
export function Inject(value: unknown) {
  return Reflect.metadata("inject", value);
}

处理容器部分前, 先写下实用函数:

import 'reflect-metadata';

/**
 * 获取InjectValue的值
 * @param target 
 * @param valueName 
 * @param config  json配置文件
 */
export function parseInjectValue(target: Object, property: string, config?: unknown) {
  const injectValue = Reflect.getMetadata("injectValue", target, property);

  const regPattern = /\$\{(.*)\}/;
  let result;

  if (regPattern.test(injectValue)) {
    const jsonProperty = RegExp.$1.trim();
    const jsonConfig = JSON.parse(config as string);

    const propertyArr = jsonProperty.split(".");

    propertyArr.forEach(item => {
      result = jsonConfig[item];

      if (!result) {
        throw new Error("无法获取配置中的值, 请检查属性名是否正确")
      }
    })
  } else {
    result = injectValue;
  }

  return result as unknown;
}

终于开始要处理beanFactoryConfig, 传入json配置创建IOC容器。我们知道beanFactoryConfig只是一个工厂函数, 真正要创建的是 BeanFactory实例, 先处理 BeanFactory:

class BeanFactory {
  private container: Record<string | symbol, Function>

  private jsonConfig: object

  constructor(config: unknown) {
    this.jsonConfig = config as object;
    this.container = {};
  }

  add(target: Function, name?: string | symbol) {
    const containerName = name ? name : target.name;
    this.container[containerName] = target;
  }
}

export function beanFactoryConfig(jsonConfig: unknown) {
  return new BeanFactory(jsonConfig as object);
}

最后要完成build方法, 它要返回getBean, 实现原理与之前类似, 只是是多加注入类的处理:

import { parseInjectValue, getInjectObj } from './reflectUtils'

class BeanFactory {
  private container: Record<string | symbol, Function>

  private jsonConfig: object

  constructor(config: unknown) {
    this.jsonConfig = config as object;
    this.container = {};
  }

  add(target: Function, name?: string | symbol) {
    const containerName = name ? name : target.name;
    this.container[containerName] = target;
  }

  build() {
    const getBean = (beanName: string | symbol) => {
      const bean = this.container[beanName];
      if (!bean) {
        throw new Error("没有对应的类, 无法创建");
      }

      if (!this.isInjectable(bean)) {
        return bean;
      }

      // 利用反射, 创建实例
      const result = Reflect.construct(bean, []);
      Object.keys(result).forEach(key => {
        // 获取装饰器InjectValue赋的值
        const injectValue = parseInjectValue(result, key, this.jsonConfig);
        if (injectValue) {
          Reflect.defineProperty(result, key, {
            value: injectValue
          });
        }

        // 获取Inject的值
        const injectName = getInjectObj(result, key);
        if (injectName) {
          const injectedBean = getBean(injectName);
          Reflect.defineProperty(result, key, {
            value: injectedBean
          });
        }

        // 如果没有装饰器情况
        if (!injectValue && !injectName) {
          const value = Reflect.get(bean, key);
          Reflect.defineProperty(result, key, {
            value
          });
        }
      })

      return result as object;
    };

    return getBean;
  }

  private isInjectable(bean: object) {
    const isInjectable = Reflect.getMetadata("injectable", bean) as boolean;
    return isInjectable;
  }
}

export function beanFactoryConfig(jsonConfig: unknown) {
  return new BeanFactory(jsonConfig as object);
}

测试一下代码:

import { getBean } from "./container";
import { Person } from "./Person";

const student = getBean("Student") as Person;

// My name is Tom. I am 16 years old. My father's name is Thomas.
console.log(student.introduce());

总算是完成基于装饰器的IOC容器, 一般如果只是简单讲解ioc原理的, 到了这阶段已经完结, 但那些讲解给出的demo代码通常有一个致命的bug。

循环依赖

想一想, 如果在Father类里添加Student类, 然后注入依赖, 会发生什么? 如果直接调用getBean("Student"), 它会先实例化Student类, 然后实例化Father类, 由于Father里有Student类, 所以上述过程会一直循环, 卡死。

所以我们得要有一个变量去记录依赖实例化的过程, 避免循环依赖, 我们可以用一个Set数据结构来解决:

import { parseInjectValue, getInjectObj } from './reflectUtils'

class BeanFactory {
  private container: Record<string | symbol, Function>

  private jsonConfig: object

  private injectProcess: Set<string | symbol>

  constructor(config: unknown) {
    this.jsonConfig = config as object;
    this.container = {};
    this.injectProcess = new Set();
  }
...
  build() {
    const getBean = (beanName: string | symbol) => {
      const bean = this.container[beanName];
      if (!bean) {
        throw new Error("没有对应的类, 无法创建");
      }

      if (!this.isInjectable(bean)) {
        return bean;
      }

      // 利用反射, 创建实例
      const result = Reflect.construct(bean, []);
      this.injectProcess.add(beanName);
      Object.keys(result).forEach(key => {
...
        if (injectName) {
          if (this.injectProcess.has(injectName)) {
            console.log(this.injectProcess.keys())
            throw new Error("该类在依赖注入过程中已被注入, 存在循环依赖, 请检查依赖注入过程")
          }
...
      this.injectProcess.delete(beanName);

      return result as object;
    };

    return getBean;
  }
}

一路下来, 终于完成了, 如果有了解过inversify的读者, 会发现上述的实现很类似 (我就是看着它的设计来实现)。

源码地址