前端也要懂的IOC

IOC(inversion of control) 是什麼?

望文生義即「反向控制」,它是一種設計思想,大體意思就是把對象控制的全部權交給別人(容器)html

有了反轉,那麼什麼是正轉呢?

看如下代碼,便是自身應用程序主動去獲取依賴對象,而且本身建立對象前端


// 常見的依賴
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理念,解決對象間的解耦。git

IOC是如何解決耦合嚴重的問題的呢

提供了一個container容器來管理,它是依賴注入設計模式的體現,如下代碼就使得C和A、B沒有的強耦合關係,直接經過container容器來管控github

// 使用 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容器的最簡僞代碼:web

class Container {
    //存放每一個文件暴露的類和類名
    classObjs = {}
    get(Module) {
        let obj = new Module()
        const properties = Object.getOwnPropertyNames(obj);
        for(const p of properties) {
            if(!obj[p]) {
                if(!this.classObjs[p]) {
                    obj[p] = this.get(this.classObjs[p])
                }
            }
        }
        return obj
    }
}複製代碼

可是業界實現的方式主要是經過裝飾器 decorator 和 reflect-metadata來實現的,接下來就聊聊這二者是如何配合實現依賴注入(DI)的。注: DI是IOC的一種實現方式。spring

裝飾器

裝飾器是一種函數,是在代碼編譯的時候對類的行爲進行修改,好比:typescript


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]) 簡化版實現以下:bash

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上就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'
 * }]
 *
 */複製代碼

最後你會發現不少spring後端框架的一些思想,其實都慢慢的被運用到了前端,好比如今比較流行的web框架Midway

參考

深刻理解typescript

解讀IOC框架invertisifyJS

Midway

相關文章
相關標籤/搜索