承接笔记:NestJS原理探究之——依赖注入
首先看一下温习一下Demo中的 IoC容器
展开查看源码
import { Provider, isClassProvider, ClassProvider, ValueProvider, FactoryProvider, isValueProvider, Token, InjectionToken } from "./provider"; import { Type } from "./type"; import { isInjectable } from "./injectable"; import "reflect-metadata"; import { getInjectionToken } from "./inject"; type InjectableParam = Type<any>; const REFLECT_PARAMS = "design:paramtypes"; export class Container { private providers = new Map<Token<any>, Provider<any>>(); addProvider<T>(provider: Provider<T>) { this.assertInjectableIfClassProvider(provider); this.providers.set(provider.provide, provider); } inject<T>(type: Token<T>): T { let provider = this.providers.get(type); if (provider === undefined && !(type instanceof InjectionToken)) { provider = { provide: type, useClass: type }; this.assertInjectableIfClassProvider(provider); } 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 c194a9eg<!-- begin-inline-katex{this.getTokenName(type)}`); if (isClassProvider(provider)) return this.injectClass(provider as ClassProvider<T>); else if (isValueProvider(provider)) return this.injectValue(provider as ValueProvider<T>); return this.injectFactory(provider as FactoryProvider<T>); } private assertInjectableIfClassProvider<T>(provider: Provider<T>) { if (isClassProvider(provider) && !isInjectable(provider.useClass)) throw new Error(`Cannot provide end-inline-katex-->{this.getTokenName(provider.provide)} using class c194a9eg<!-- begin-inline-katex{this.getTokenName(provider.useClass)}, end-inline-katex-->{this.getTokenName(provider.useClass)} isn't injectable`); } private injectClass<T>(classProvider: ClassProvider<T>): T { const target = classProvider.useClass; const params = this.getInjectedParams(target); return Reflect.construct(target, params); } private injectValue<T>(valueProvider: ValueProvider<T>): T { return valueProvider.useValue; } private injectFactory<T>(factoryProvider: FactoryProvider<T>): T { return factoryProvider.useFactory(); } 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) => { // 在遇到循环依赖时,reflect-metadata API会失效,返回undefined if (argType === undefined) throw new Error(`Injection error. Recursive dependency detected in constructor for type c194a9eg<!-- begin-inline-katex{target.name} with parameter at index end-inline-katex-->{index}`); const overrideToken = getInjectionToken(target, index); const actualToken = overrideToken === undefined ? argType : overrideToken; let provider = this.providers.get(actualToken); return this.injectWithProvider(actualToken, provider); }); } private getTokenName<T>(token: Token<T>) { return token instanceof InjectionToken ? token.injectionIdentifier : token.name; } }
还有使用方法:
展开查看源码
import { Container } from "./container"; import { Inject } from "./inject"; import { Injectable } from "./injectable"; import { InjectionToken } from "./provider"; const API_URL = new InjectionToken("apiUrl"); @Injectable() class HttpClient {} @Injectable() class HttpService { constructor( private httpClient: HttpClient, @Inject(API_URL) private apiUrl: string ) {} } const container = new Container(); container.addProvider({ provide: API_URL, useValue: "https://www.baidu.com/", }); container.addProvider({ provide: HttpClient, useClass: HttpClient }); container.addProvider({ provide: HttpService, useClass: HttpService }); const httpService = container.inject(HttpService); console.dir(httpService);
NestJS
中的 IoC容器
源码在 packages/core/injector/container.ts
文件中实现。
展开查看源码
import { DynamicModule, Provider } from '@nestjs/common'; import { GLOBAL_MODULE_METADATA } from '@nestjs/common/constants'; import { Injectable } from '@nestjs/common/interfaces/injectable.interface'; import { Type } from '@nestjs/common/interfaces/type.interface'; import { ApplicationConfig } from '../application-config'; import { CircularDependencyException } from '../errors/exceptions/circular-dependency.exception'; import { UndefinedForwardRefException } from '../errors/exceptions/undefined-forwardref.exception'; import { UnknownModuleException } from '../errors/exceptions/unknown-module.exception'; import { ExternalContextCreator } from '../helpers/external-context-creator'; import { HttpAdapterHost } from '../helpers/http-adapter-host'; import { REQUEST } from '../router/request/request-constants'; import { ModuleCompiler } from './compiler'; import { ContextId } from './instance-wrapper'; import { InternalCoreModule } from './internal-core-module'; import { InternalProvidersStorage } from './internal-providers-storage'; import { Module } from './module'; import { ModuleTokenFactory } from './module-token-factory'; import { ModulesContainer } from './modules-container'; export class NestContainer { private readonly globalModules = new Set<Module>(); private readonly moduleTokenFactory = new ModuleTokenFactory(); private readonly moduleCompiler = new ModuleCompiler(this.moduleTokenFactory); private readonly modules = new ModulesContainer(); private readonly dynamicModulesMetadata = new Map< string, Partial<DynamicModule> >(); private readonly internalProvidersStorage = new InternalProvidersStorage(); private internalCoreModule: Module; constructor( private readonly _applicationConfig: ApplicationConfig = undefined, ) {} get applicationConfig(): ApplicationConfig | undefined { return this._applicationConfig; } public setHttpAdapter(httpAdapter: any) { this.internalProvidersStorage.httpAdapter = httpAdapter; if (!this.internalProvidersStorage.httpAdapterHost) { return; } const host = this.internalProvidersStorage.httpAdapterHost; host.httpAdapter = httpAdapter; } public getHttpAdapterRef() { return this.internalProvidersStorage.httpAdapter; } public async addModule( metatype: Type<any> | DynamicModule | Promise<DynamicModule>, scope: Type<any>[], ): Promise<Module> { // In DependenciesScanner#scanForModules we already check for undefined or invalid modules // We sill need to catch the edge-case of `forwardRef(() => undefined)` if (!metatype) { throw new UndefinedForwardRefException(scope); } const { type, dynamicMetadata, token } = await this.moduleCompiler.compile( metatype, ); if (this.modules.has(token)) { return; } const moduleRef = new Module(type, this); this.modules.set(token, moduleRef); await this.addDynamicMetadata( token, dynamicMetadata, [].concat(scope, type), ); if (this.isGlobalModule(type, dynamicMetadata)) { this.addGlobalModule(moduleRef); } return moduleRef; } public async addDynamicMetadata( token: string, dynamicModuleMetadata: Partial<DynamicModule>, scope: Type<any>[], ) { if (!dynamicModuleMetadata) { return; } this.dynamicModulesMetadata.set(token, dynamicModuleMetadata); const { imports } = dynamicModuleMetadata; await this.addDynamicModules(imports, scope); } public async addDynamicModules(modules: any[], scope: Type<any>[]) { if (!modules) { return; } await Promise.all(modules.map(module => this.addModule(module, scope))); } public isGlobalModule( metatype: Type<any>, dynamicMetadata?: Partial<DynamicModule>, ): boolean { if (dynamicMetadata && dynamicMetadata.global) { return true; } return !!Reflect.getMetadata(GLOBAL_MODULE_METADATA, metatype); } public addGlobalModule(module: Module) { this.globalModules.add(module); } public getModules(): ModulesContainer { return this.modules; } public getModuleByKey(moduleKey: string): Module { return this.modules.get(moduleKey); } public getInternalCoreModuleRef(): Module | undefined { return this.internalCoreModule; } public async addImport( relatedModule: Type<any> | DynamicModule, token: string, ) { if (!this.modules.has(token)) { return; } const moduleRef = this.modules.get(token); const { token: relatedModuleToken } = await this.moduleCompiler.compile( relatedModule, ); const related = this.modules.get(relatedModuleToken); moduleRef.addRelatedModule(related); } public addProvider(provider: Provider, token: string): string { if (!provider) { throw new CircularDependencyException(); } if (!this.modules.has(token)) { throw new UnknownModuleException(); } const moduleRef = this.modules.get(token); return moduleRef.addProvider(provider); } public addInjectable( injectable: Provider, token: string, host?: Type<Injectable>, ) { if (!this.modules.has(token)) { throw new UnknownModuleException(); } const moduleRef = this.modules.get(token); moduleRef.addInjectable(injectable, host); } public addExportedProvider(provider: Type<any>, token: string) { if (!this.modules.has(token)) { throw new UnknownModuleException(); } const moduleRef = this.modules.get(token); moduleRef.addExportedProvider(provider); } public addController(controller: Type<any>, token: string) { if (!this.modules.has(token)) { throw new UnknownModuleException(); } const moduleRef = this.modules.get(token); moduleRef.addController(controller); } public clear() { this.modules.clear(); } public replace(toReplace: any, options: any & { scope: any[] | null }) { this.modules.forEach(moduleRef => moduleRef.replace(toReplace, options)); } public bindGlobalScope() { this.modules.forEach(moduleRef => this.bindGlobalsToImports(moduleRef)); } public bindGlobalsToImports(moduleRef: Module) { this.globalModules.forEach(globalModule => this.bindGlobalModuleToModule(moduleRef, globalModule), ); } public bindGlobalModuleToModule(target: Module, globalModule: Module) { if (target === globalModule || target === this.internalCoreModule) { return; } target.addRelatedModule(globalModule); } public getDynamicMetadataByToken( token: string, metadataKey: keyof DynamicModule, ) { const metadata = this.dynamicModulesMetadata.get(token); if (metadata && metadata[metadataKey]) { return metadata[metadataKey] as any[]; } return []; } public createCoreModule(): DynamicModule { return InternalCoreModule.register([ { provide: ExternalContextCreator, useValue: ExternalContextCreator.fromContainer(this), }, { provide: ModulesContainer, useValue: this.modules, }, { provide: HttpAdapterHost, useValue: this.internalProvidersStorage.httpAdapterHost, }, ]); } public registerCoreModuleRef(moduleRef: Module) { this.internalCoreModule = moduleRef; this.modules[InternalCoreModule.name] = moduleRef; } public getModuleTokenFactory(): ModuleTokenFactory { return this.moduleTokenFactory; } public registerRequestProvider<T = any>(request: T, contextId: ContextId) { const wrapper = this.internalCoreModule.getProviderByKey(REQUEST); wrapper.setInstanceByContextId(contextId, { instance: request, isResolved: true, }); } }
这个文件不仅仅是依赖注入,他还有其他的功能。这个文件并没有具体落实资源注入逻辑,具体的逻辑在 packages/core/injector/module.ts
文件中实现。可以看到 packages/core/injector/container.ts
文件中声明的 NestContainer
类中有一个私有属性,为 modules
,其类型为: Map<string, Module>
。
这样做的原因是因为 NestJS
的依赖注入是有作用域的,不同 module
中的资源如无特殊声明,是不允许被其他模块调用的。所以它要维护一个 Map
,每个 module
有自己的一套 IoC容器
。
接下来看一下 packages/core/injector/module.ts
中的代码:
展开查看源码
import { Abstract, ClassProvider, Controller, DynamicModule, ExistingProvider, FactoryProvider, Injectable, NestModule, Provider, ValueProvider, } from '@nestjs/common/interfaces'; import { Type } from '@nestjs/common/interfaces/type.interface'; import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; import { isFunction, isNil, isString, isSymbol, isUndefined, } from '@nestjs/common/utils/shared.utils'; import { iterate } from 'iterare'; import { ApplicationConfig } from '../application-config'; import { InvalidClassException } from '../errors/exceptions/invalid-class.exception'; import { RuntimeException } from '../errors/exceptions/runtime.exception'; import { UnknownExportException } from '../errors/exceptions/unknown-export.exception'; import { createContextId } from '../helpers'; import { getClassScope } from '../helpers/get-class-scope'; import { CONTROLLER_ID_KEY } from './constants'; import { NestContainer } from './container'; import { InstanceWrapper } from './instance-wrapper'; import { ModuleRef } from './module-ref'; interface ProviderName { name?: string | symbol; } export class Module { private readonly _id: string; private readonly _imports = new Set<Module>(); private readonly _providers = new Map<any, InstanceWrapper<Injectable>>(); private readonly _injectables = new Map<any, InstanceWrapper<Injectable>>(); private readonly _middlewares = new Map<any, InstanceWrapper<Injectable>>(); private readonly _controllers = new Map< string, InstanceWrapper<Controller> >(); private readonly _exports = new Set<string | symbol>(); private _distance = 0; constructor( private readonly _metatype: Type<any>, private readonly container: NestContainer, ) { this.addCoreProviders(); this._id = randomStringGenerator(); } get id(): string { return this._id; } get providers(): Map<any, InstanceWrapper<Injectable>> { return this._providers; } get middlewares(): Map<any, InstanceWrapper<Injectable>> { return this._middlewares; } get imports(): Set<Module> { return this._imports; } /** * Left for backward-compatibility reasons */ get relatedModules(): Set<Module> { return this._imports; } /** * Left for backward-compatibility reasons */ get components(): Map<string, InstanceWrapper<Injectable>> { return this._providers; } /** * Left for backward-compatibility reasons */ get routes(): Map<string, InstanceWrapper<Controller>> { return this._controllers; } get injectables(): Map<string, InstanceWrapper<Injectable>> { return this._injectables; } get controllers(): Map<string, InstanceWrapper<Controller>> { return this._controllers; } get exports(): Set<string | symbol> { return this._exports; } get instance(): NestModule { if (!this._providers.has(this._metatype.name)) { throw new RuntimeException(); } const module = this._providers.get(this._metatype.name); return module.instance as NestModule; } get metatype(): Type<any> { return this._metatype; } get distance(): number { return this._distance; } set distance(value: number) { this._distance = value; } public addCoreProviders() { this.addModuleAsProvider(); this.addModuleRef(); this.addApplicationConfig(); } public addModuleRef() { const moduleRef = this.createModuleReferenceType(); this._providers.set( ModuleRef.name, new InstanceWrapper({ name: ModuleRef.name, metatype: ModuleRef as any, isResolved: true, instance: new moduleRef(), host: this, }), ); } public addModuleAsProvider() { this._providers.set( this._metatype.name, new InstanceWrapper({ name: this._metatype.name, metatype: this._metatype, isResolved: false, instance: null, host: this, }), ); } public addApplicationConfig() { this._providers.set( ApplicationConfig.name, new InstanceWrapper({ name: ApplicationConfig.name, isResolved: true, instance: this.container.applicationConfig, host: this, }), ); } public addInjectable<T extends Injectable>( injectable: Provider, host?: Type<T>, ) { if (this.isCustomProvider(injectable)) { return this.addCustomProvider(injectable, this._injectables); } let instanceWrapper = this.injectables.get(injectable.name); if (!instanceWrapper) { instanceWrapper = new InstanceWrapper({ name: injectable.name, metatype: injectable, instance: null, isResolved: false, scope: getClassScope(injectable), host: this, }); this._injectables.set(injectable.name, instanceWrapper); } if (host) { const token = host && host.name; const hostWrapper = this._controllers.get(host && host.name) || this._providers.get(token); hostWrapper && hostWrapper.addEnhancerMetadata(instanceWrapper); } } public addProvider(provider: Provider): string { if (this.isCustomProvider(provider)) { return this.addCustomProvider(provider, this._providers); } this._providers.set( (provider as Type<Injectable>).name, new InstanceWrapper({ name: (provider as Type<Injectable>).name, metatype: provider as Type<Injectable>, instance: null, isResolved: false, scope: getClassScope(provider), host: this, }), ); return (provider as Type<Injectable>).name; } public isCustomProvider( provider: Provider, ): provider is | ClassProvider | FactoryProvider | ValueProvider | ExistingProvider { return !isNil( (provider as | ClassProvider | FactoryProvider | ValueProvider | ExistingProvider).provide, ); } public addCustomProvider( provider: ( | ClassProvider | FactoryProvider | ValueProvider | ExistingProvider ) & ProviderName, collection: Map<string, any>, ): string { const name = this.getProviderStaticToken(provider.provide) as string; provider = { ...provider, name, }; if (this.isCustomClass(provider)) { this.addCustomClass(provider, collection); } else if (this.isCustomValue(provider)) { this.addCustomValue(provider, collection); } else if (this.isCustomFactory(provider)) { this.addCustomFactory(provider, collection); } else if (this.isCustomUseExisting(provider)) { this.addCustomUseExisting(provider, collection); } return name; } public isCustomClass(provider: any): provider is ClassProvider { return !isUndefined((provider as ClassProvider).useClass); } public isCustomValue(provider: any): provider is ValueProvider { return !isUndefined((provider as ValueProvider).useValue); } public isCustomFactory(provider: any): provider is FactoryProvider { return !isUndefined((provider as FactoryProvider).useFactory); } public isCustomUseExisting(provider: any): provider is ExistingProvider { return !isUndefined((provider as ExistingProvider).useExisting); } public isDynamicModule(exported: any): exported is DynamicModule { return exported && exported.module; } public addCustomClass( provider: ClassProvider & ProviderName, collection: Map<string, InstanceWrapper>, ) { const { name, useClass } = provider; let { scope } = provider; if (isUndefined(scope)) { scope = getClassScope(useClass); } collection.set( name as string, new InstanceWrapper({ name, metatype: useClass, instance: null, isResolved: false, scope, host: this, }), ); } public addCustomValue( provider: ValueProvider & ProviderName, collection: Map<string, InstanceWrapper>, ) { const { name, useValue: value } = provider; collection.set( name as string, new InstanceWrapper({ name, metatype: null, instance: value, isResolved: true, async: value instanceof Promise, host: this, }), ); } public addCustomFactory( provider: FactoryProvider & ProviderName, collection: Map<string, InstanceWrapper>, ) { const { name, useFactory: factory, inject, scope } = provider; collection.set( name as string, new InstanceWrapper({ name, metatype: factory as any, instance: null, isResolved: false, inject: inject || [], scope, host: this, }), ); } public addCustomUseExisting( provider: ExistingProvider & ProviderName, collection: Map<string, InstanceWrapper>, ) { const { name, useExisting } = provider; collection.set( name as string, new InstanceWrapper({ name, metatype: (instance => instance) as any, instance: null, isResolved: false, inject: [useExisting], host: this, isAlias: true, }), ); } public addExportedProvider( provider: (Provider & ProviderName) | string | symbol | DynamicModule, ) { const addExportedUnit = (token: string | symbol) => this._exports.add(this.validateExportedProvider(token)); if (this.isCustomProvider(provider as any)) { return this.addCustomExportedProvider(provider as any); } else if (isString(provider) || isSymbol(provider)) { return addExportedUnit(provider); } else if (this.isDynamicModule(provider)) { const { module } = provider; return addExportedUnit(module.name); } addExportedUnit(provider.name); } public addCustomExportedProvider( provider: | FactoryProvider | ValueProvider | ClassProvider | ExistingProvider, ) { const provide = provider.provide; if (isString(provide) || isSymbol(provide)) { return this._exports.add(this.validateExportedProvider(provide)); } this._exports.add(this.validateExportedProvider(provide.name)); } public validateExportedProvider(token: string | symbol) { if (this._providers.has(token)) { return token; } const importsArray = [...this._imports.values()]; const importsNames = iterate(importsArray) .filter(item => !!item) .map(({ metatype }) => metatype) .filter(metatype => !!metatype) .map(({ name }) => name) .toArray(); if (!importsNames.includes(token as string)) { const { name } = this.metatype; throw new UnknownExportException(token, name); } return token; } public addController(controller: Type<Controller>) { this._controllers.set( controller.name, new InstanceWrapper({ name: controller.name, metatype: controller, instance: null, isResolved: false, scope: getClassScope(controller), host: this, }), ); this.assignControllerUniqueId(controller); } public assignControllerUniqueId(controller: Type<Controller>) { Object.defineProperty(controller, CONTROLLER_ID_KEY, { enumerable: false, writable: false, configurable: true, value: randomStringGenerator(), }); } public addRelatedModule(module: Module) { this._imports.add(module); } public replace(toReplace: string | symbol | Type<any>, options: any) { if (options.isProvider && this.hasProvider(toReplace)) { const name = this.getProviderStaticToken(toReplace); const originalProvider = this._providers.get(name); return originalProvider.mergeWith({ provide: toReplace, ...options }); } else if (!options.isProvider && this.hasInjectable(toReplace)) { const name = this.getProviderStaticToken(toReplace); const originalInjectable = this._injectables.get(name); return originalInjectable.mergeWith({ provide: toReplace, ...options, }); } } public hasProvider(token: string | symbol | Type<any>): boolean { const name = this.getProviderStaticToken(token); return this._providers.has(name); } public hasInjectable(token: string | symbol | Type<any>): boolean { const name = this.getProviderStaticToken(token); return this._injectables.has(name); } public getProviderStaticToken( provider: string | symbol | Type<any> | Abstract<any>, ): string | symbol { return isFunction(provider) ? (provider as Function).name : (provider as string | symbol); } public getProviderByKey<T = any>(name: string | symbol): InstanceWrapper<T> { return this._providers.get(name) as InstanceWrapper<T>; } public getNonAliasProviders(): Array<[string, InstanceWrapper<Injectable>]> { return [...this._providers].filter(([_, wrapper]) => !wrapper.isAlias); } public createModuleReferenceType(): Type<ModuleRef> { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; return class extends ModuleRef { constructor() { super(self.container); } public get<TInput = any, TResult = TInput>( typeOrToken: Type<TInput> | string | symbol, options: { strict: boolean } = { strict: true }, ): TResult { return !(options && options.strict) ? this.find<TInput, TResult>(typeOrToken) : this.find<TInput, TResult>(typeOrToken, self); } public resolve<TInput = any, TResult = TInput>( typeOrToken: Type<TInput> | string | symbol, contextId = createContextId(), options: { strict: boolean } = { strict: true }, ): Promise<TResult> { return this.resolvePerContext(typeOrToken, self, contextId, options); } public async create<T = any>(type: Type<T>): Promise<T> { if (!(type && isFunction(type) && type.prototype)) { throw new InvalidClassException(type); } return this.instantiateClass<T>(type, self); } }; } }
这面的代码更多,我们先专注于 _providers
这个私有属性,可以发现它的类型是: Map<any, InstanceWrapper<Injectable>>
,这就是Demo中的私有属性 providers
!当然, nestjs
中的功能更加的完善,类型也更加的复杂。
下面看一下核心方法 addProvider()
public addProvider(provider: Provider): string { if (this.isCustomProvider(provider)) { return this.addCustomProvider(provider, this._providers); } this._providers.set( (provider as Type<Injectable>).name, new InstanceWrapper({ name: (provider as Type<Injectable>).name, metatype: provider as Type<Injectable>, instance: null, isResolved: false, scope: getClassScope(provider), host: this, }), ); return (provider as Type<Injectable>).name; }点击复制复制失败已复制
首先,它判断了这个 Provider
是否是自定义的,通过阅读源码,发现判断依据为 provider
中的 provide
属性是否为 null
或 undefined
。这个写过自定义 provider
的都明白。先看一下自定义的 Provider
处理逻辑:
public addCustomProvider( provider: ( | ClassProvider | FactoryProvider | ValueProvider | ExistingProvider ) & ProviderName, collection: Map<string, any>, ): string { const name = this.getProviderStaticToken(provider.provide) as string; provider = { ...provider, name, }; if (this.isCustomClass(provider)) { this.addCustomClass(provider, collection); } else if (this.isCustomValue(provider)) { this.addCustomValue(provider, collection); } else if (this.isCustomFactory(provider)) { this.addCustomFactory(provider, collection); } else if (this.isCustomUseExisting(provider)) { this.addCustomUseExisting(provider, collection); } return name; }点击复制复制失败已复制
在这里,它先获取了 name
,这个 name
就是Demo中的 token
,作为标识使用。下面的代码就是按照不同的 Provider类型
将其插入到 _providers
私有属性中,具体插入方法很简单,篇幅原因,不再赘述。
接下来分析一下注入方法