• The front end also needs to understand the decoupling idea: from object-oriented to interface oriented
  • The front end also needs to understand the decoupling idea: from interface oriented to IoC container
  • The front end also needs to understand the decoupling idea: from the IoC container to the DI implementation

From the previous “Decoupling Ideas for the Front End” series, we learned about IoC ideas and specific technical implementations. Today we introduce a lightweight IoC framework, InversifyJS, which provides a very complete implementation of dependency injection (DI) and can be directly used for the development of front-end applications.

For further details, please refer to the InversifyJS Chinese document translated by Jeff Tian.

Then use InversifyJS to practice the IoC idea. According to the thought of interface oriented programming and dependency inversion principle, we need to abstract the requirement interface first. Here we create 1 soldier type TSoldier and 2 weapon types TGun and TKnife:

// file interfaces.ts

interface TSoldier {
    useGun(victim: string) :void,
    useKnife(victim: string) :void,}interface TWeapon {
    start(victim: string) :void
}

interface TGun extends TWeapon {
    fire(victim: string) :void
}

interface TKnife extends TWeapon {
    cut(victim: string) :void
}
Copy the code

To get the module we want from the IoC container, we need to specify some specific identifiers as keys so that the container can identify which module you want. This also means that each module has a unique identifier. If you are smart enough, you will think of using the Symbol type as the identifier:

// file types.ts

const TYPES = {
    Ak47: Symbol.for("Ak47"),
    Ak48: Symbol.for("Ak48"),
    TuLong: Symbol.for("TuLong"),
    SupermanA: Symbol.for("SupermanA"),
    SupermanB: Symbol.for("SupermanB"),}export { TYPES }
Copy the code

The following is the procedure for registering modules into the IoC container and registering dependencies between modules. InversifyJS uses dependency injection (DI) to implement the registration process elegantly. There are two very important decorator functions Injectable and Inject. The former registers module information with the IoC container, while the latter establishes dependencies between modules through property injection (or constructor injection).

// file entities.ts

import { injectable, inject } from "inversify";
import "reflect-metadata";
import { Weapon, ThrowableWeapon, Warrior } from "./interfaces"
import { TYPES } from "./types";


@injectable(a)class Ak47 implements TGun {
    public fire(victim: string){
        console.log('Ak47 is firing ' + victim)
    }
    public start(victim: string) {
        this.fire(victim)
    }
}

@injectable(a)class Ak48 implements TGun {
    public fire(victim: string){
        console.log('Ak48 is firing ' + victim)
    }
    public start(victim: string) {
        this.fire(victim)
    }
}

@injectable(a)class TuLong implements TKnife {
    public cut(victim: string){
        console.log('knife is cutting ' + victim)
    }
    public start(victim: string) {
        this.cut(victim)
    }
}

@injectable(a)class SupermanA implements TSoldier {
    @inject(TYPES.Ak47) private _gun: Ak47
    @inject(TYPES.TuLong) private _knife: TuLong
    
    public useGun(victim: string) { 
        this._gun.start(victim)
    }
    public useKnife(victim: string) { 
        this._knife.start(victim)
    }
}

@injectable(a)class SupermanB implements TSoldier {
    @inject(TYPES.Ak48) private _gun: Ak48
    @inject(TYPES.TuLong) private _knife: TuLong
    
    public useGun() { 
        this._gun.start()
    }
    public useKnife() { 
        this._knife.start()
    }
}

export { Ak47, TuLong, SupermanA, SupermanB }
Copy the code

The source code for these two decorator functions is very simple, so we can take a look at the implementation:

import * as ERRORS_MSGS from ".. /constants/error_msgs";
import * as METADATA_KEY from ".. /constants/metadata_keys";


function injectable() {
    return function (target) {
        if (Reflect.hasOwnMetadata(METADATA_KEY.PARAM_TYPES, target)) {
            throw new Error(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);
        }
        var types = Reflect.getMetadata(METADATA_KEY.DESIGN_PARAM_TYPES, target) || [];
        
        Reflect.defineMetadata(METADATA_KEY.PARAM_TYPES, types, target);
        return target;
    };
}

function inject(serviceIdentifier) {
    return function (target, targetKey, index) {
        if (serviceIdentifier === undefined) {
            throw new Error(UNDEFINED_INJECT_ANNOTATION(target.name));
        }
        var metadata = new Metadata(METADATA_KEY.INJECT_TAG, serviceIdentifier);
        
        if (typeof index === "number") {
            tagParameter(target, targetKey, index, metadata);
        }
        else{ tagProperty(target, targetKey, metadata); }}; }export { injectable, inject }
Copy the code

Here we come across a few slightly unfamiliar functions: reflect.hasownMetadata, reflect.definemetadata, and reflect.getMetadata. We all know that the Reflect global object, like the Proxy object, is a new API provided by ES6 for manipulating objects. Reflect Metadata is a proposal in ES7 that adds and reads meta information for classes and class attributes at declaration time. And the reason why Metadata is used in this way to save relevant information, can be understood to avoid the intrusion and pollution of the target module. Reflect Metadata uses the preservation of key pairs of meta information and is essentially a WeakMap mapping, but with more design in the data structure. InversifyJS dependency injection is implemented by combining Reflect Metadata with the decorator function.

Reflect object

The main purpose of the Reflect Object is to put some methods of the Object that are clearly internal to the language (such as Object.defineProperty) on the Reflect Object. At this stage, some methods are deployed on both Object and Reflect objects, and future new methods will only be deployed on Reflect objects. That is, from the Reflect object you can get the methods inside the language. Reflect also makes Object operations functional.

Reflect Metadata

Reflect Metadata-related methods are not standard apis, but rather extended capabilities provided by the introduced Reflect-Metadata library. Metadata, also known as “meta information,” is usually additional information that needs to be hidden inside a program that is not related to business logic. TypeScript already supports it in version 1.5+ :

  • Installation:npm i reflect-metadata --save
  • Configuration tsconfig. Json:emitDecoratorMetadata: true

Reflect Metadata’s API can be used on classes or class attributes:

  • throughReflect.getMetadata("design:type", target, key)The decorator function can get the attribute type
  • throughReflect.getMetadata("design:paramtypes", target, key)You can get function parameter types
  • throughReflect.getMetadata("design:returntype", target, key)You can get the return value type
  • throughReflect.defineMetadata("keyName", "value", target, key)You can customize the metadataKey and value and retrieve its value when appropriate

The Injectable decorator assigns meta information about target with key Design: Paramtypes to meta information about inversify: Paramtypes with key inversify: Paramtypes, which can be consumed during module instantiation.

The inject decorator instantiates a metinformation object Metadata based on the passed identifier (i.e., the types defined in the previous article), and then instantiates the parameter Metadata based on the type of the parameter (when the decorator is used as a parameter decorator, the third parameter index is the sequential index of the parameter in the function parameter, which is of numeric type. Otherwise, the decorator is used as an attribute decorator. To call different handlers to hold the meta information.

Injectable and Inject are used to store meta information. IOC’s instance management capability still relies on the Container class.

The Inject decorator tells the IoC container that the attribute of the high-level module needs to be assigned to an instance of the low-level module corresponding to the identifier, but the IoC container does not yet know which module an identifier needs to map. Therefore, we also need to bind identifiers to specific modules, so that the IoC container can map relevant modules through identifiers, so that subsequent calls can get exactly the required modules through identifiers:

// file inversify.config.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { TSoldier, TGun, TKnife } from "./interfaces";
import { Ak47, TuLong, SupermanA, SupermanB } from "./entities";


const container = new Container()

container.bind<TGun>(TYPES.Ak47).to(Ak47)
container.bind<TGun>(TYPES.Ak48).to(Ak48)
container.bind<TKnife>(TYPES.TuLong).to(TuLong)
container.bind<TSoldier>(TYPES.SupermanA).to(SupermanA)
container.bind<TSoldier>(TYPES.SupermanB).to(SupermanB)

export { container }
Copy the code

Finally, we run the code and perfectly resolve the dependencies between modules through the IoC container of InversifyJS, and accurately output the results:

import { container } from "./inversify.config";
import { TYPES } from "./types";
import { TSoldier } from "./interfaces";


const supermanA = container.get<TSoldier>(TYPES.SupermanA)
const supermanB = container.get<TSoldier>(TYPES.SupermanB)

supermanA.useGun('dog')  // Ak47 is firing dog
supermanB.useGun('dog')  // Ak48 is firing dog
supermanA.useKnife('pig')  // knife is cutting pig
supermanB.useKnife('pig')  // knife is cutting pig
Copy the code

Front end small white, the article inevitably has fallacy, asks everybody big guy light spray!