公众号:@ 搞事的前端
今天看啥  ›  专栏  ›  饿了么新餐饮前端团队

前端也要懂的IOC

饿了么新餐饮前端团队  · 掘金  ·  · 2019-11-22 07:55

文章预览

阅读 527

前端也要懂的IOC

IOC(inversion of control) 是什么?

望文生义即“反向控制”,它是一种设计思想,大致意思就是把对象控制的所有权交给别人(容器)

有了反转,那么什么是正转呢?

看以下代码,即是自身应用程序主动去获取依赖对象,并且自己创建对象


// 常见的依赖
import {A} from './A';
import {B} from './B';

class C {
  constructor() {
    this.a = new A();
    this.b = new B(this.a);
  }
}复制代码

那为什么要使用IOC呢?

我们看上面的代码发现A被B和C依赖,这种依赖关系随这着应用的增大,越来越复杂,耦合度也越来越高。所以有人提出了IOC理念,解决对象间的解耦。

IOC是如何解决耦合严重的问题的呢

提供了一个container容器来管理,它是依赖注入设计模式的体现,以下代码就使得C和A、B没有的强耦合关系,直接通过container容器来管控

// 使用 IoC
import {Container} from 'injection';
import {A} from './A';
import {B} from './B';
const container = new Container();
container.bind(A);
container.bind(B);

class C {
    A:B
  constructor() {
    this.a = container.get('a');
    this.b = container.get('b');
  }
}复制代码


那么IOC容器里主要做了哪些事?

  • 类的实例化
  • 查找对象的依赖关系

以下是实现IOC容器的最简伪代码:

class Container {
    //存放每个文件暴露的类和类名
    classObjs = {}
    get(Module) {
        let obj = new Module()
        const properties = Object.getOwnPropertyNames(obj);
        for(const prop of properties) {
            if(!obj[p]) {
                if(!this.classObjs[p]) {
                    obj[p] = this.get(this.classObjs[p])
                }
            }
        }
        return obj
    }
}复制代码

但是业界实现的方式主要是通过装饰器 decorator 和 reflect-metadata来实现的,接下来就聊聊这两者是如何配合实现依赖注入(DI)的。注: DI是IOC的一种实现方式。

装饰器

装饰器是一种函数,是在代码编译的时候对类的行为进行修改,比如:


function helloWord(target: any) {
  console.log('hello Word!');
}

@helloWord
class HelloWordClass {
}

//tsc编译后
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function helloWord(target) {
    console.log('hello Word!');
}
let HelloWordClass = class HelloWordClass {
};
HelloWordClass = __decorate([
    helloWord
], HelloWordClass);复制代码

装饰器主要有这几种: 类装饰器,方法、属性装饰器、参数装饰器。当装饰器运行的时候,函数会接收三个参数:target, key ,descriptor, 装饰器,target、key、descriptor 有所不同,详细请看文档

Reflect-Metadata

Reflect Metadata 是 ES7 的一个提案, 它本质是一个WeakMap对象,数据结构如下:

WeakMap {
  target: Map {
    propertyKey: Map {
      metadataKey: metadataValue
    }
  }
}复制代码

所以 Reflect.defineMetadata(metadataKey, metadataValue, target[, propertyKey]) 简化版实现如下:

const weakMap = new WeakMap()
const defineMetadata = (metadataKey, metadataValue, target, propertyKey) => {
  const metadataMap = new Map();
  metadataMap.set(metadataKey, metadataValue)
  const targetMap = new Map();
  targetMap.set(propertyKey, metadataMap)
  weakMap.set(target, targetMap)
}复制代码


装饰器与Reflect-Metadata结合实现依赖注入

Reflect-Metadata一般结合着decorators一起用,为类和类属性添加元数据。

基于Typescript的依赖注入就是通过这两者结合来实现的。


type Constructor<T = any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {};

class OtherService {
  a = 1;
}

@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1复制代码


通过以下编译后的代码发现,Typescriopt 通过__decorate将OtherService注入到了TestService类里面,然后通过new target(...args)OtherService赋值到实例属性上


var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
const Injectable = () => target => { };
class OtherService {
    constructor() {
        this.a = 1;
    }
}
let TestService = class TestService {
    constructor(otherService) {
        this.otherService = otherService;
    }
    testMethod() {
        console.log(this.otherService.a);
    }
};

TestService = __decorate([
    Injectable(),
    __metadata("design:paramtypes", [OtherService])
], TestService);

const Factory = (target) => {
    // 获取所有注入的服务
    const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
    const args = providers.map((provider) => new provider());
    return new target(...args);
};
Factory(TestService).testMethod(); // 1复制代码

装饰器与Reflect-Metadata结合实现@Controller/@Get

我们在后端的框架里看到很多这种注解的写法,其实也是这样实现的

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {}
}复制代码

首先我们先利用自定义metaKey生成装饰器

const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');复制代码

然后在装饰器里通过Reflect.getMetadata获取到刚刚存入(Reflect.defineMetadata)的元数据,

最后在将这些元数据重组生成一个map数据结构。

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);
  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];
    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    }
  })
};复制代码

有了以上的方法,通过以下调用,再将生成的Routes绑定到koa或者midway上就ok了

Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'
mapRoute(new SomeClass());
/**
 * [{
 *    route: '/a',
 *    method: 'GET',
 *    fn: someGetMethod() { ... },
 *    methodName: 'someGetMethod'
 *  },{
 *    route: '/b',
 *    method: 'POST',
 *    fn: somePostMethod() { ... },
 *    methodName: 'somePostMethod'
 * }]
 *
 */复制代码


参考

深入理解typescript

解读IOC框架invertisifyJS

Midway


………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览