200 LOC의 Typescript 종속 주입

대상 프로그래밍에서 가장 흔히 볼 수 있는 모델 중 하나는 의존항 주입과inversion of control principle, (IOC)이다.IOC 용기는 통상적으로 기능이 풍부하고 복잡한 야수이기 때문에 경험이 풍부한 프로그래머라도 쓰러질 수 있다.그것들은 의존 관계를 가진 유형 집합을 사용하는데, 당신이 어떤 실례를 필요로 할 때, 그것들은 자동으로 당신을 위해 실례를 연결할 수 있다.
AngularNestJs 프레임워크와 모듈 시스템에서 Typescript 컨테이너를 보았을 수 있습니다.또는 Inversify 와 같은 독립된 용기를 사용하고 있을 수도 있습니다.
프로그래밍 개념의 수수께끼를 푸는 가장 좋은 방법 중 하나는 스스로 구축하는 것이다. 따라서 본고는 점차적으로 가장 작은 장난감 용기를 구축하고자 한다.근데 일단은...

역사 속성


2014년 프레임워크 전쟁에서 일부 구글 엔지니어들이 문제에 부딪혔다.그들이 Angular 2를 구축하고 있는 언어 Typescript에 치명적인 결함이 있다는 것을 깨달았을 때, 그들은 Angular 2를 개발하고 있다.이것은 일종의 거래 파괴자이기 때문에 그들은 구글 엔지니어가 되어 이런 상황에서 한 것이다.그들은 새로운 언어를 발명했다.AtScript라고 합니다.

나는 AtScript의 역사를 다시 기술하러 온 것이 아니다.Anders Hejlsberg(Typescript의 창립자)는 그의 짧은 버전을 제시했다.앤더스가 그의 강연에서 언급한 바와 같이 당시의 Typescript는 AtScript가 해결해야 할 두 가지 관건적인 특성이 부족했다.장식사와 반성.그것들은 IOC가 타자 원고에서 가능하게 하는 비밀 소스다.

장식사


이전에 Typescript 컨테이너를 사용한 경우 다음을 볼 수 있습니다.
@Injectable()
class SomeService {
    constructor(private anotherService: AnotherService) {}
}
꼭대기에 우리는 주사할 수 있는 장식기가 하나 있다.decorator는 이 종류가 자동으로 의존항을 주입할 수 있다고 말한다.
decorator는 포장류, 함수 또는 방법으로 행위에 추가하는 함수이다.이것은 대상과 관련된 메타데이터를 정의하는 데 매우 유용하다.또한 Typescript에서 반사되는 작업 방식과 관련이 있습니다.

반사


어떤 내용을 연결할지 알기 위해서, 실행할 때 유형을 검사할 수 있어야 합니다.Typescript에 들어가기 전에 자바스크립트가 어떻게 작동하는지 봅시다.
const a = "hello there";
const b = 0b1;
console.log(typeof a); // "string";
console.log(typeof b); // "number";
Javascript는 완벽하지는 않지만, 어느 정도의 기본 실행 시 반사를 지원합니다.언어의 기본 형식(num,boolean,object,string,array 등)을 제외하고 클래스는 실행 시 정보를 가지고 있습니다.
class Alpha {}
const a = new Alpha();
a instanceof Alpha; // true
우리는 방법 목록을 얻기 위해 클래스 prototype 를 검사할 수 있다.하지만 이것이 바로 우리가 한계에 도달하기 시작한 곳이다.클래스 속성이나 방법 매개 변수의 이름을 추출하는 것은 쉽지 않습니다.전통적인 순수한 자바스크립트 용기는 casting the function or class to a string and manually parsing that string to get the names of each parameter/property 같은 해커 공격을 사용할 것이다.그리고 용기는 이 이름을 사용하여 정확한 의존 항목을 찾습니다.물론 코드에서 축소기를 실행하면 모든 매개 변수의 이름이 바뀌기 때문에 실패합니다.이것은 Angular 1의 흔한 문제로 해결 방법은 alot of redundancy와 관련된다.
따라서 바닐라 자바스크립트는 반사 부문에서 우리에게 큰 도움이 되지 않는다.이 문제를 해결하기 위해 Typescript는 reflect-metadata라는 라이브러리를 사용하여 추가 유형의 정보를 저장합니다.예를 들어, 매개변수와 속성에 할당된 Typescript 유형은 런타임 시 사용할 수 있습니다."emitDecoratorMetadata"컴파일러 옵션을 통해 활성화됩니다.
@SomeDecorator()
function someFunc(a: number, b: string){}
Reflect.getMetadata('design:types', someFunc); // Number, String
하지만 두 가지 문제가 있다.
  • 류/함수는 메타데이터를 저장하기 위해 수식자가 있어야 합니다.
  • 클래스/매거/기원 유형만 기록할 수 있습니다.인터페이스와 결합 유형은 객체 형식으로 나타납니다.이 유형들은 컴파일된 후에 완전히 사라지고 클래스가 계속 존재하기 때문이다.
  • 어쨌든 지금의 배경은 충분하다.Typescript decorators/reflect 메타데이터가 여전히 혼란스러우면 official tutorial 을 보십시오.

    코드


    우리의 용기는 두 가지 주요 개념을 사용할 것이다.영패와 제공자.영패는 용기에서 어떻게 만드는지 알아야 하는 표지부호이며, 공급자는 그것을 어떻게 만드는지 설명합니다.이를 감안하여 용기류의 최소 공공 인터페이스는 다음과 같다.
    export class Container {
        addProvider<T>(provider: Provider<T>) {} // TODO
        inject<T>(type: Token<T>): T {} // TODO
    }
    
    이제 정의해 봅시다Token.영패는 하나의 종류를 인용할 수 있거나, 매개 변수 형식이 충분한 상하문을 제공하지 않아서 무엇을 주입해야 하는지 설명하지 않은 경우, 수식자로 매개 변수에 부가된 상수를 인용할 수 있다.
    const API_URL_TOKEN = new InjectionToken('some-identifier');
    const TWITTER_TOKEN = new InjectionToken('another-identifier');
    class SomeClass {
        // Both AService, API_URL_TOKEN, and TWITTER_URL_TOKEN are all tokens.
        // We will define the Inject decorator later.    
        constructor(b: AService, @Inject(API_URL_TOKEN) apiURL: string, @Inject(TWITTER_URL_TOKEN) twitterUrl: string) {}
    }
    
    우리는 토큰에 대한 정의가 다음과 같다.
    // We use this to refer to classes.
    export interface Type<T> extends Function {
        // Has a constructor which takes any number of arguments. 
        // Can be an implicit constructor.   
        new (...args: any[]): T; 
    }
    
    export class InjectionToken {
        constructor(public injectionIdentifier: string) {}
    }
    
    // Our combined Token type
    Token<T> = Type<T> | InjectionToken;
    
    다음은 공급자를 정의합시다.우리는 세 가지 다른 공급자 유형을 실현할 것이다.하나는 하나의 예로 기존 값을 제공하는 데 사용되고, 하나는 공장 함수를 통해 제공되며, 다른 하나는 사용할 클래스 이름만 제공하는 데 사용된다.
    // Every provider maps to a token.
    export interface BaseProvider<T> {
        provide: Token<T>;
    }
    
    export interface ClassProvider<T> extends BaseProvider<T> {
        useClass: Type<T>;
    }
    
    export interface ValueProvider<T> extends BaseProvider<T> {
        useValue: T;
    }
    
    // To keep things simple, a factory is just a function which creates the type.
    export type Factory<T> = () => T;
    
    export interface FactoryProvider<T> extends BaseProvider<T> {
        useFactory: Factory<T>;
    }
    
    export type Provider<T> = ClassProvider<T> | ValueProvider<T> | FactoryProvider<T>;
    
    편의를 위해서 우리도 몇 가지 유형의 방호를 가입한다.
    export function isClassProvider<T>(provider: BaseProvider<T>): provider is ClassProvider<T> {
        return (provider as any).useClass !== undefined;
    }
    export function isValueProvider<T>(provider: BaseProvider<T>): provider is ValueProvider<T> {
        return (provider as any).useValue !== undefined;
    }
    export function isFactoryProvider<T>(provider: BaseProvider<T>): provider is FactoryProvider<T> {
        return (provider as any).useFactory !== undefined;
    }
    
    이것은 우리의 기초 API에 있어서 매우 좋다.용기를 실현하기 전에 우리는 두 개의 장식기만 정의할 수 있다.
    // This class decorator adds a boolean property to the class
    // metadata, marking it as 'injectable'. 
    // It uses the reflect-metadata API.
    const INJECTABLE_METADATA_KEY = Symbol('INJECTABLE_KEY');
    export function Injectable() {
        return function(target: any) {
            // target in this case is the class being decorated.    
            Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
            return target;
        };
    }
    // We also provide an easy way to query whether a class is
    // injectable. Our container will reject classes which aren't
    // marked as injectable.
    export function isInjectable<T>(target: Type<T>) {
        return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true;
    }
    
    우리는 하나의 매개 변수를 다른 Inject에 비추는 장식기를 정의했다.
    const INJECT_METADATA_KEY = Symbol('INJECT_KEY');
    // This is a parameter decorator, it takes a token to map the parameter to.
    export function Inject(token: Token<any>) {
        return function(target: any, _: string | symbol, index: number) {
            Reflect.defineMetadata(INJECT_METADATA_KEY, token, target, `index-${index}`);
            return target;
        };
    }
    export function getInjectionToken(target: any, index: number) {
        return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined;
    }
    

    용기


    공급자 추가의 실현은 상당히 간단하다.간단한 키 값 저장소일 뿐이라는 것을 알 수 있습니다.공급자 맵은 모든 유형을 사용하지만, 우리는 Token 공급자와 항상 일치할 것을 알고 있습니다. 왜냐하면 이 맵을 삽입하는 유일한 방법은 Token 방법을 사용하는 것이기 때문입니다.
    class Container {
        private providers = new Map<Token<any>, Provider<any>>();
    
        addProvider<T>(provider: Provider<T>) {
            this.assertInjectableIfClassProvider(provider);
            this.providers.set(provider.provide, provider);
        }
        // ...
    }
    
    우리는 addProvider 방법을 사용하여 용기에 제공된 모든 종류가 assertInjectableIfClassProvider 로 표시되어 메타데이터가 있는지 확인한다.이것은 결코 절대적으로 필요한 것은 아니지만, 이것은 우리가 설정할 때 문제를 발견하는 데 도움을 줄 것이다.
    class Container {
        // ...
        private assertInjectableIfClassProvider<T>(provider: Provider<T>) {
            if (isClassProvider(provider) && !isInjectable(provider.useClass)) {
                throw new Error(
                `Cannot provide ${this.getTokenName(provider.provide)} using class ${this.getTokenName(
                    provider.useClass
                )}, ${this.getTokenName(provider.useClass)} isn't injectable`
                );
            }
        }
    
        // Returns a printable name for the token.
        private getTokenName<T>(token: Token<T>) {
            return token instanceof InjectionToken ? token.injectionIdentifier : token.name;
        }
        // ...
    }
    
    다음은 주입 함수입니다.첫 번째 방법은 공급자를 찾고, 두 번째 방법은 그것이 어떤 유형의 공급자인지 확인한 다음에 각각의 상황을 처리한다.
    class Container {
        // ...
        inject<T>(type: Token<T>): T {
            let provider = this.providers.get(type);
            return this.injectWithProvider(type, provider);
        }
    
        private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {
            if (provider === undefined) {
                throw new Error(`No provider for type ${this.getTokenName(type)}`);
            }
            if (isClassProvider(provider)) {
                return this.injectClass(provider as ClassProvider<T>);
            } else if (isValueProvider(provider)) {
                return this.injectValue(provider as ValueProvider<T>);
            } else {
                // Factory provider by process of elimination
                return this.injectFactory(provider as FactoryProvider<T>);
            }
        }
        // ...
    }
    
    가치와 공장 공급업체는 상당히 직접적이다.하나는 방법 호출이고, 하나는 값만 되돌려줍니다.클래스 제공 프로그램은 약간 복잡합니다. 구조 함수에 대한 매개 변수 목록의 항목을 구성한 다음 클래스 인용을 사용하여 구조 함수를 호출해야 합니다.
    class Container {
        // ...
        private injectValue<T>(valueProvider: ValueProvider<T>): T {
            return valueProvider.useValue;
        }
    
        private injectFactory<T>(valueProvider: FactoryProvider<T>): T {
            return valueProvider.useFactory();
        }
    
        private injectClass<T>(classProvider: ClassProvider<T>): T {
            const target = classProvider.useClass;
            const params = this.getInjectedParams(target);
            return Reflect.construct(target, params);
        }
        // ...
    }
    
    매개 변수 목록을 구축하는 실현은 기교가 필요한 곳이다.구조 함수 매개 변수의 유형 목록을 얻기 위해 Injectable API를 호출합니다.이 매개 변수 중의 하나하나에 대해 우리는 관련 영패를 찾은 후에 구조로 돌아간다.
    public class Container {
        // ...
        private getInjectedParams<T>(target: Type<T>) {
            const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (InjectableParam | undefined)[];
            if (argTypes === undefined) {
                return [];
            }
            return argTypes.map((argType, index) => {
                // The reflect-metadata API fails on circular dependencies,
                // and will return undefined for the argument instead.
                // We could handle this better, but for now let's just throw an error.
                if (argType === undefined) {
                    throw new Error(
                        `Injection error. Recursive dependency detected in constructor for type ${
                        target.name
                        } with parameter at index ${index}`
                    );
                }
                // Check if a 'Inject(INJECTION_TOKEN)' was added to the parameter.
                // This always takes priority over the parameter type.
                const overrideToken = getInjectionToken(target, index);
                const actualToken = overrideToken === undefined ? argType : overrideToken;
                let provider = this.providers.get(actualToken);
                return this.injectWithProvider(actualToken, provider);
            });
        }
    }
    

    그것을 사용하다


    이것이 바로 실시다.다음은 우리의 새 용기를 사용하는 효과입니다.
    
    const API_TOKEN = new InjectionToken('api-token');
    
    @Injectable()
    class SomeService {
        constructor(@Inject(API_TOKEN)) {}
    }
    
    @Injectable()
    class InjectableClass {
        constructor(public someService: SomeService) {}
    }
    
    const container = new Container();
    
    container.addProvider({ provide: API_TOKEN, useValue: 'https://some-url.com' });
    container.addProvider({ provide: SomeService, useClass: SomeService });
    container.addProvider({ provide: InjectableClass, useClass: InjectableClass });
    
    const instance = container.inject(InjectableClass);
    
    

    결론


    우리가 이곳에서 만든 장난감 용기는 상당히 간단하지만, 그것도 매우 강하다.다른 더 높은 용기가 어떻게 구축되었는지 보실 수 있습니다.테스트와 문서가 포함된 작업 프레젠테이션 저장소here를 찾을 수 있습니다.도전에 직면할 준비가 되어 있다면 다음 기능을 통해 확장할 수 있는지 확인하십시오.
  • 순환 인용의 초기 검사 (공급자를 추가할 때)
  • 컨테이너를 중첩하고 하위 용기에서 제공하는 유형의 능력을 추가합니다(Angular/NestJs 모듈과 유사).
  • 주입 파라미터가 있는 공장.
  • 프로그램에서 인스턴스 라이프 사이클의 특정 범위(예: 단일 예)를 제공합니다.
  • 좋은 웹페이지 즐겨찾기