依賴注入是一個很重要的設計模式。 它使用得非常廣泛,以至於幾乎每個人都把它簡稱爲 DI 。
Angular 有自己的依賴注入框架,離開它,你幾乎沒辦法構建出 Angular 應用。
下面介紹一種簡單的(只有100行左右代碼)實現類似Angular依賴注入的方式,先看一個例子
// a.service.ts
@Injectable()
export class AService {
constructor() {
}
public doSomething() {
console.log('this is AService::doSomething');
}
}
// b.service.ts
@Injectable()
export class BService {
constructor(private readonly a: AService) {
}
public doSomething() {
this.a.doSomething();
console.log('this is BService::doSomething');
}
}
// some.module.ts
@Module({
providers: [
AService,
BService,
],
})
export class SomeModule {
}
在上面的例子中,我們創建了兩個Service
,其中BService
依賴於AService
,那麼BService
可以在其構造函數中聲明其依賴,我們需要一種方法去自動將AService
的實例注入到BService
的私有隻讀變量a
中,接下來介紹實現的步驟。
首先我們應該對Typescript
進行配置,使其支持Javascript
的裝飾器(或者說是註解),下面是我的配置文件:
{
"compilerOptions": {
"lib": [
"dom",
"es2015"
],
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
其中,experimentalDecorators
表示爲ES裝飾器啓用實驗支持,emitDecoratorMetadata
表示在源代碼中爲裝飾聲明產生類型的元數據,配置好了這兩項之後,我們才能使用Typescript
的裝飾器,接下來我們需要安裝依賴reflect-metadata
,用於讀取和設置元數據。
@Injectable的實現
@Injectable
是一個裝飾器,它標識被裝飾的類是一個Provider
,它的聲明方式如下
export function Injectable(): ClassDecorator {
return (target) => {
};
}
我們在此裝飾器中什麼都不做,他只起到一個標識的作用。
@Module的實現
@Module
是一個裝飾器,它標識被裝飾的類是一個Module
,它的聲明方式如下
const DI_IMPORTS_SYMBOL = Symbol('di:imports')
const DI_PROVIDERS_SYMBOL = Symbol('di:providers')
export function Module(options: { imports?: Array<any>, providers?: Array<any> }): ClassDecorator {
return (target) => {
Reflect.defineMetadata(DI_IMPORTS_SYMBOL, new Set(options.imports || []), target);
Reflect.defineMetadata(DI_PROVIDERS_SYMBOL, new Set(options.providers || []), target);
}
}
我們使用Set
來存儲一個Module
作用域中它所聲明的Providers
和它所引入的其他模塊。
Factory的實現
我們希望達到的目的是,在使用時可以通過Factory.create(SomeModule)
來獲取一個Module
的實例,然後通過Module
實例來獲取一個Provider
,例如Factory.create(SomeModule).get(BService).doSomething()
,此時應該輸出
Factory.create(SomeModule).get(BService).doSomething();
// this is AService::doSomething
// this is BService::doSomething
Talk is cheap. Show me the code:
export namespace Factory {
export function create(module: Type) {
const imports: Set<Type> = Reflect.getMetadata(DI_IMPORTS_SYMBOL, module);
const providers: Set<any> = Reflect.getMetadata(DI_PROVIDERS_SYMBOL, module);
const providersMap = new Map();
const importModules = Array.from(imports).map((importModule) => {
let moduleInstance: ModuleInstance = moduleInstances.get(importModule);
if(!moduleInstance) {
moduleInstance = create(importModule);
moduleInstances.set(importModule, moduleInstance);
}
return moduleInstance;
});
const moduleInstance = new ModuleInstance(importModules, providersMap);
providers.forEach(provider => {
createProvider(provider, providers, moduleInstance);
});
return moduleInstance;
}
function createProvider(provider: any, providers: Set<any>, moduleInstance: ModuleInstance) {
let providerInstance = moduleInstance.providers.get(provider);
if(providerInstance) {
return providerInstance;
}
const deps: Array<any> = Reflect.getMetadata('design:paramtypes', provider);
if(!deps) {
throw new Error(`No provider named ${ provider.name }, do yout add @Injectable() to this provider?`);
}
const args = deps.map(dep => {
let depInstance = moduleInstance.providers.get(dep);
if(!depInstance) {
if(providers.has(dep)) {
depInstance = createProvider(dep, providers, moduleInstance);
} else {
moduleInstance.imports.some(imp => {
depInstance = createProvider(dep, new Set(), imp);
return !!depInstance;
});
}
}
if(!depInstance) {
throw new Error(`can not found provider ${ dep.name }`);
}
return depInstance;
});
providerInstance = new provider(...args);
moduleInstance.providers.set(provider, providerInstance);
return providerInstance;
}
export class ModuleInstance {
constructor(
public imports: Array<ModuleInstance>,
public providers: Map<any, any>) {
}
get<T>(provider: Type<T>) {
let instance: T = this.providers.get(provider);
if(!instance) {
this.imports.some(imp => {
instance = imp.get(provider);
return !!instance;
});
}
if(!instance) {
throw new Error(`No provider named: ${ provider.name }`);
}
return instance;
}
}
}
以上就是整個依賴注入的實現了,感興趣的朋友可以到我的Github上面查看源代碼,核心文件就是lib/di.ts
,地址是
https://github.com/hungtcs/li...