// 反例
class InventoryTracker {
constructor(items) {
this.items = items;
// 依賴了一個具體實現
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
let inventoryTracker = new InventoryTracker(
['apples', 'bananas'],
);
inventoryTracker.requestItems();
// 正例
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ['WS'];
}
requestItem(item) {
// ...
}
}
// 依賴外置,經過參數注入依賴
// 咱們能夠輕鬆替換依賴項
let inventoryTracker = new InventoryTracker(
['apples', 'bananas'],
new InventoryRequesterV2(),
);
inventoryTracker.requestItems();
複製代碼
InversifyJS 一個特性是能夠利用 lazyInject 來實現無需把依賴放入 constructor 參數中,直接注入爲類屬性。node
@injectable()
export class Events {
@lazy_inject(BINDING.Blink) protected Blink:DI.Blink;
@lazy_inject(BINDING.touch_manager) protected touch_manager:DI.TouchManager;
@lazy_inject(BINDING.WorkspaceSvg) protected WorkspaceSvg:DI.WorkspaceSvgClass;
}
複製代碼
下面將從 Decorator、Reflect-Metadata 解釋 InversifyJS 的實現原理。git
Decorator 類型的定義:es6
當咱們使用註解時,TypeScript 編譯器就會產生一個 __decorator
函數。github
var __decorate = this.__decorate || function (decorators, target, key, desc) {
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
return Reflect.decorate(decorators, target, key, desc);
}
switch (arguments.length) {
case 2:
return decorators.reduceRight(function (o, d) {
return (d && d(o)) || o;
}, target);
case 3:
return decorators.reduceRight(function (o, d) {
return (d && d(target, key)), void 0;
}, void 0);
case 4:
return decorators.reduceRight(function (o, d) {
return (d && d(target, key, o)) || o;
}, desc);
}
};
複製代碼
@logClass
class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
}
// 編譯結果:
// 使用了__decorate 的返回值來覆蓋原始構造函數
// 這就是類裝飾器必須返回構造函數的緣由
// 類裝飾器接受一個參數 target
var Person = (function () {
function Person(name, surname) {
this.name = name;
this.surname = surname;
}
Person = __decorate(
[logClass], // decorators
Person, // target
);
return Person;
})();
複製代碼
function logClass(target: any) {
// 保存對原始構造函數的引用
const original = target;
// 生成類實例的工具函數
function construct(constructor, args) {
var c: any = function () {
return constructor.apply(this, args);
}
c.prototype = constructor.prototype;
return new c();
}
// 新的構造器行爲
var f: any = function (...args) {
console.log("New: " + original.name);
return construct(original, args);
}
// 複製原型,所以 instanceof 仍然有效
f.prototype = original.prototype;
// 返回新構造函數(將覆蓋原始構造函數)
return f;
}
複製代碼
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name: string, surname: string) {
this.name = name;
this.surname = surname;
}
}
// 編譯結果:
// 屬性裝飾器只須要 2 個參數(原型和鍵)
// 屬性裝飾器沒有返回值
var Person = (function () {
function Person(name, surname) {
this.name = name;
this.surname = surname;
}
__decorate(
[logProperty], // decorators
Person.prototype, // target
'name', // key
);
return Person;
})();
複製代碼
//
function logProperty(target: any, key: string) {
// 屬性值
var _val = this[key];
// 屬性 getter
var getter = function () {
console.log(`Get: ${key} => ${_val}`);
return _val;
};
// 屬性 setter
var setter = function (newVal) {
console.log(`Set: ${key} => ${newVal}`);
_val = newVal;
};
// 從類原型中刪除原始屬性
// 若是屬性被成功刪除
// Object.defineProperty() 方法用於使用原始屬性的名稱建立一個新的屬性
if (delete this[key]) {
// 使用 getter 和 setter 建立新屬性
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
複製代碼
class C {
@logMethod
foo(n:number) {
return n * 2;
}
}
// 編譯結果:
// 方法裝飾器須要 3 個參數(原型,鍵和屬性描述符)
var C = (function () {
function C() {
}
C.prototype.foo = function (n) {
return n * 2;
};
Object.defineProperty(
__decorate(
[log], // decorators
C.prototype, // target
"foo", // key
Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc
);
return C;
})();
複製代碼
/** * * @param target 被註解的方法 * @param key 被註解方法的名字 * @param value * 屬性的描述,若是這個屬性不存在於對象上則爲 undefined * 能夠經過 Object.getOwnPropertyDescriptor() 函數得到 */
function logMethod(target: Function, key: string, value: any) {
// target === C.prototype
// key === "foo"
// value === Object.getOwnPropertyDescriptor(C.prototype, "foo")
return {
value(...args: any[]) {
// 將 foo 參數列表轉換爲字符串
const a = args.map(a => JSON.stringify(a)).join();
// 調用 foo() 並獲取其返回值
const result = value.value.apply(this, args);
// 將結果轉換爲字符串
const r = JSON.stringify(result);
// 在控制檯中顯示函數調用詳細信息
console.log(`Call: ${key}(${a}) => ${r}`);
// 返回調用 foo 的結果
return result;
}
};
}
複製代碼
foo 註解以後也是正常執行,只是還會額外運行 log 的功能,這些功能是由 log 註解添加的。chrome
const c = new C();
const r = c.foo(23); // "Call: foo(23) => 46"
console.log(r); // 46
複製代碼
class Person {
public name: string;
public surname: string;
constructor(name: string, surname: string) {
this.name = name;
this.surname = surname;
}
public saySomething(@logParameter something: string): string {
return this.name + " " + this.surname + " says: " + something;
}
}
// 編譯後代碼
Object.defineProperty(
Person.prototype,
"saySomething",
__decorate(
// 當 __decorate 調用 __param 時 ,它的返回將不會用於覆蓋該 saySomething 方法
// 參數裝飾器不返回
[__param(0, logParameter)],
Person.prototype, // 正在裝飾的類的原型
"saySomething", // 包含要裝飾的參數的方法的名稱
Object.getOwnPropertyDescriptor(Person.prototype, "saySomething") // 要裝飾的參數的索引
),
);
return Person;
複製代碼
// __param 函數由 TypeScript 編譯器生成
// 裝飾器包裝器 __param 用於在閉包中存儲參數的索引
// 索引只是參數列表中的位置
var __param = this.__param || function (index, decorator) {
// 返回裝飾器函數(包裝器)
return function (target, key) {
//應用裝飾器(忽略返回)
decorator(target, key, index);
}
};
複製代碼
function logParameter(target: any, key: string, index: number) {
var metadataKey = `log_${key}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
}
else {
target[metadataKey] = [index];
}
}
複製代碼
上面的參數裝飾器 metadataKey 向類原型添加了一個新的屬性(metadataKey
)。新屬性是一個數組,包含要裝飾的參數的索引。咱們能夠將這個新屬性視爲元數據。express
參數裝飾器不該該修改構造函數,方法或屬性的行爲。參數裝飾器只應用於生成某種元數據。json
裝飾器工廠是一個能夠接受任意數量參數的函數,而且必須返回一種類型的裝飾器。api
@logClass
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name: string, surname: string) {
this.name = name;
this.surname = surname;
}
@logMethod
public saySomething(@logParameter something: string): string {
return this.name + " " + this.surname + " says: " + something;
}
}
複製代碼
上面一段代碼應用了全部可用的裝飾器(類、方法、屬性和參數),若是咱們能夠在任何地方使用裝飾器而不用管類型更好。數組
@log
class Person {
@log
public name: string;
public surname: string;
constructor(name: string, surname: string) {
this.name = name;
this.surname = surname;
}
@log
public saySomething(@log something: string): string {
return this.name + " " + this.surname + " says: " + something;
}
}
複製代碼
function log(...args: any[]) {
switch (args.length) {
case 1:
return logClass.apply(this, args);
case 2:
return logProperty.apply(this, args);
case 3:
if (typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}
複製代碼
@logClassWithArgs({ when : { name : "remo"} })
class Person {
public name: string;
// ...
}
複製代碼
function logClassWithArgs(filter: Object) {
return (target: Object) => {
// 在這裏實現類裝飾器
// 類裝飾師將有權訪問裝飾器參數(篩選器)
// 由於它們存儲在閉包中
}
}
複製代碼
咱們的 JS 應用程序愈來愈大,咱們開始須要一些工具(IoC)和(運行時類型斷言)這樣的功能來管理這種日益增長的複雜性。瀏覽器
強大的反射 API 應該容許咱們在運行時檢查未知對象並找出有關它的全部內容。咱們應該可以找到像這樣的東西:
在 JS 中,咱們可使用 Object.getOwnPropertyDescriptor()
或 Object.keys()
等函數來查找有關實例的一些信息,但咱們須要反射來實現更強大的開發工具。
Reflect Metadata
@Reflect.metadata('inClass', 'A')
class Test {
@Reflect.metadata('inMethod', 'B')
public hello(): string {
return 'hello world';
}
}
console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'
複製代碼
Metadata Key {Any}
: 元數據的 Key,簡寫 k。對於一個對象來講,他能夠有不少元數據,每個元數據都對應有一個 Key
name
的 Key 用來設置他的名字,用 created time
的 Key 來表示他建立的時間Metadata Value {Any}
: 簡寫 v。元數據的類型,任意類型都行Target {Object}
: 簡寫 o。表示要在這個對象上面添加元數據Property {String|Symbol}
: 簡寫 p。用於設置在那個屬性上面添加元數據。不只僅能夠在對象上面添加元數據,甚至還能夠在對象的屬性上面添加元數據。當給一個對象定義元數據的時候,默認指定了 undefined 做爲 Property安裝庫 reflect-metadata
:
import 'reflect-metadata';
// 定義對象或屬性的元數據
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
// 檢查對象或屬性的原型鏈上是否存在元數據鍵
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);
// 檢查對象或屬性是否存在本身的元數據鍵
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);
// 獲取對象或屬性原型鏈上元數據鍵的元數據值
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);
// 獲取對象或屬性的本身的元數據鍵的元數據值
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);
// 獲取對象或屬性原型鏈上的全部元數據鍵
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);
// 獲取對象或屬性的全部本身的元數據鍵
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);
// 從對象或屬性中刪除元數據
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);
// 經過裝飾器將元數據應用於構造函數
@Reflect.metadata(metadataKey, metadataValue)
class C {
// 經過裝飾器將元數據應用於方法(屬性)
@Reflect.metadata(metadataKey, metadataValue)
method() {
}
}
複製代碼
在 tsconfig.json 配置:
{
"compilerOptions": {
"target": "es5",
"lib": ["es6", "dom"],
"types": ["reflect-metadata"],
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
複製代碼
function logType(target: any, key: string) {
var t = Reflect.getMetadata("design:type", target, key);
console.log(`${key} type: ${t.name}`);
}
複製代碼
class Demo {
// 應用屬性裝飾器
// 會打印出 attr1 type: String
@logType
public attr1 : string;
}
複製代碼
function logParamTypes(target: any, key: string) {
var types = Reflect.getMetadata("design:paramtypes", target, key);
var s = types.map(a => a.name).join();
console.log(`${key} param types: ${s}`);
}
複製代碼
class Foo {}
interface IFoo {}
class Demo {
// 應用參數裝飾器
// 打印出:doSomething param types: String, Number, Foo, Object, Object, Function
@logParameters
doSomething(
param1: string,
param2: number,
param3: Foo,
param4: { test: string },
param5: IFoo,
param6: (a: number) => void
): number {
return 1;
}
}
複製代碼
咱們還可使用 "design:returntype"
元數據鍵獲取有關方法返回類型的信息:
Reflect.getMetadata("design:returntype", target, key);
複製代碼
全部的元數據都是存在於對象下面的 [[Metadata]]
屬性下面,可是不是經過 Symbol
實現的:
@Reflect.metadata('name', 'A')
class A {}
Object.getOwnPropertySymbols(A) // []
複製代碼
內部的數據結構:
WeakMap<any, Map<any, Map<any, any>>>
// 調用的角度
// 先根據對象獲取,而後在根據屬性,最後根據元數據的 Key 獲取最終要的數據
weakMap.get(o).get(p).get(k)
複製代碼
// app.js
@Controller('/test')
class SomeClass {
@Get('/a')
someGetMethod() {
return 'hello world';
}
@Post('/b')
somePostMethod() {}
}
複製代碼
// Controller.js
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');
複製代碼
// mapRoute.js
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
}
})
};
複製代碼
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' * }] * */
複製代碼
InversifyJS 在解析依賴關係以前執行:
註釋:一種將元數據添加到類聲明以供依賴注入或編譯指令使用的方法。
var obj = container.get<SomeType>("SomeType");
@injectable()
class FooBar implements FooBarInterface {
public foo : FooInterface;
public bar : BarInterface;
public log() {
console.log("foobar");
}
constructor(
@inject("FooInterface") foo : FooInterface,
@inject("BarInterface") bar : BarInterface
) {
this.foo = foo;
this.bar = bar;
}
}
var foobar = container.get<FooBarInterface>("FooBarInterface");
複製代碼
前面的代碼片斷將生成新的上下文和新的 Plan。該 Plan 將包含一個具備 null target 和兩個子 requests 的RootRequest:
若是配置了一些中間件,它將在分解階段開始以前執行。中間件能夠用來開發一些瀏覽器擴展,這將容許咱們使用一些數據可視化工具來顯示解決方案,好比 D3.js。這種工具將幫助開發人員在開發過程當中發現問題。
該計劃被傳遞給解析器類的實例。解析器將繼續解析請求樹中的每一個依賴項,從葉開始,以根請求結束。解析過程能夠同步或異步執行,這有助於提升性能。
激活在依賴關係解決後進行。就在它被添加到緩存(若是是單例)並被注入以前。能夠添加一個事件處理程序,在激活完成以前調用它。這個特性容許開發人員作一些事情,好比注入代理來攔截對該對象的屬性或方法的全部調用。
有許多可用的綁定:
interface BindingToSyntax<T> {
to(constructor: { new(...args: any[]): T; }): BindingInWhenOnSyntax<T>;
toSelf(): BindingInWhenOnSyntax<T>;
toConstantValue(value: T): BindingWhenOnSyntax<T>;
toDynamicValue(func: (context: Context) => T): BindingWhenOnSyntax<T>;
toConstructor<T2>(constructor: Newable<T2>): BindingWhenOnSyntax<T>;
toFactory<T2>(factory: FactoryCreator<T2>): BindingWhenOnSyntax<T>;
toFunction(func: T): BindingWhenOnSyntax<T>;
toAutoFactory<T2>(serviceIdentifier: ServiceIdentifier<T2>): BindingWhenOnSyntax<T>;
toProvider<T2>(provider: ProviderCreator<T2>): BindingWhenOnSyntax<T>;
}
複製代碼
就做用域的行爲方式而言,咱們能夠將這些類型的綁定分爲兩個主要組:
object
function
interface BindingToSyntax<T> {
to(constructor: { new(...args: any[]): T; }): BindingInWhenOnSyntax<T>;
toSelf(): BindingInWhenOnSyntax<T>;
toConstantValue(value: T): BindingWhenOnSyntax<T>;
toDynamicValue(func: (context: Context) => T): BindingInWhenOnSyntax<T>;
}
複製代碼
默認狀況下使用 inTransientScope
,咱們能夠選擇這種類型綁定的範圍,但 toConstantValue
除外,它將始終使用 inSingletonScope
。
當咱們第一次調用 container.get
時,咱們使用 to
、toSelf
或 toDynamicValue
,InversifyJS 容器將嘗試使用構造函數或動態值工廠生成對象實例或值。若是範圍設置爲 InSingleTonScope,則緩存該值。第二次咱們調用container.get 來獲取相同的資源 ID,若是選擇了 inSingletonScope,InversifyJS 將嘗試從緩存中獲取值。
interface BindingToSyntax<T> {
toConstructor<T2>(constructor: Newable<T2>): BindingWhenOnSyntax<T>;
toFactory<T2>(factory: FactoryCreator<T2>): BindingWhenOnSyntax<T>;
toFunction(func: T): BindingWhenOnSyntax<T>;
toAutoFactory<T2>(serviceIdentifier: ServiceIdentifier<T2>): BindingWhenOnSyntax<T>;
toProvider<T2>(provider: ProviderCreator<T2>): BindingWhenOnSyntax<T>;
}
複製代碼
咱們不能選擇這種綁定的範圍,由於要注入的值(工廠函數)老是單例的。然而,工廠內部實現可能返回也可能不返回單例。如下綁定將注入一個工廠,該工廠將始終是單例。
container.bind<interfaces.Factory<Katana>>("Factory<Katana>").toAutoFactory<Katana>("Katana");
複製代碼
工廠返回的值多是單一值,也可能不是單一值:
container.bind<Katana>("Katana").to(Katana).inTransientScope();
// or
container.bind<Katana>("Katana").to(Katana).inSingletonScope();
複製代碼
聲明容器快照是一項幫助您輕鬆編寫單元測試的功能:
import { expect } from "chai";
import * as sinon from "sinon";
// application container is shared by all unit tests
import container from "../../src/ioc/container";
describe("Ninja", () => {
beforeEach(() => {
// create a snapshot so each unit test can modify
// it without breaking other unit tests
container.snapshot();
});
afterEach(() => {
// Restore to last snapshot so each unit test
// takes a clean copy of the application container
container.restore();
});
// each test is executed with a snapshot of the container
it("Ninja can fight", () => {
let katanaMock = {
hit: () => { return "hit with mock"; }
};
container.unbind("Katana");
container.bind<Something>("Katana").toConstantValue(katanaMock);
let ninja = container.get<Ninja>("Ninja");
expect(ninja.fight()).eql("hit with mock");
});
it("Ninja can sneak", () => {
let shurikenMock = {
throw: () => { return "hit with mock"; }
};
container.unbind("Shuriken");
container.bind<Something>("Shuriken").toConstantValue(shurikenMock);
let ninja = container.get<Ninja>("Shuriken");
expect(ninja.sneak()).eql("hit with mock");
});
});
複製代碼
默認範圍是 transient
,能夠在聲明綁定時更改類型的範圍:
container.bind<Warrior>(TYPES.Warrior).to(Ninja).inTransientScope();
container.bind<Warrior>(TYPES.Warrior).to(Ninja).inSingletonScope();
複製代碼
使用容器選項更改應用程序級別使用的默認範圍:
let container = new Container({ defaultScope: "Singleton" });
複製代碼
你能夠用這個激活 @injectable()
裝飾類的自動綁定:
let container = new Container({ autoBindInjectable: true });
container.isBound(Ninja); // returns false
container.get(Ninja); // returns a Ninja
container.isBound(Ninja); // returns true
複製代碼
手動定義的綁定優先:
let container = new Container({ autoBindInjectable: true });
container.bind(Ninja).to(Samurai);
container.get(Ninja); // returns a Samurai
複製代碼
將兩個容器合併爲一個。
命名綁定。
標記綁定。
獲取給定標識符的全部可用綁定。
可使用 rebind
方法替換給定 serviceIdentifier
的全部現有綁定。該函數返回 BindingTexture
的實例,該實例容許建立替換綁定。
let TYPES = {
someType: "someType"
};
let container = new Container();
container.bind<number>(TYPES.someType).toConstantValue(1);
container.bind<number>(TYPES.someType).toConstantValue(2);
let values1 = container.getAll(TYPES.someType);
expect(values1[0]).to.eq(1);
expect(values1[1]).to.eq(2);
container.rebind<number>(TYPES.someType).toConstantValue(3);
let values2 = container.getAll(TYPES.someType);
expect(values2[0]).to.eq(3);
expect(values2[1]).to.eq(undefined);
複製代碼
Resolve 就像 container.get<T>(serviceIdentifier: ServiceIdentifier<T>)
,可是它容許用戶建立一個實例,即便沒有聲明綁定。
請注意,它只容許跳過聲明依賴關係圖中根元素的綁定(組合根)。全部的子依賴項都須要聲明綁定。
import "reflect-metadata";
import { Container, injectable, inject } from "inversify";
import getDecorators from "inversify-inject-decorators";
const container = new Container();
const { lazyInject } = getDecorators(container);
const TYPE = {
Dom: Symbol.for("Dom"),
DomUi: Symbol.for("DomUi")
};
@injectable()
class DomUi {
public dom: Dom;
public name: string;
constructor(
@inject(TYPE.Dom) dom: Dom
) {
this.dom = dom;
this.name = "DomUi";
}
}
@injectable()
class Dom {
public name: string;
@lazyInject(TYPE.DomUi) public domUi: DomUi;
public constructor() {
this.name = "Dom";
}
}
@injectable()
class Test {
public dom: Dom;
constructor(
@inject(TYPE.Dom) dom: Dom
) {
this.dom = dom;
}
}
container.bind<Dom>(TYPE.Dom).to(Dom).inSingletonScope();
container.bind<DomUi>(TYPE.DomUi).to(DomUi).inSingletonScope();
const test = container.resolve(Test); // Works!
複製代碼
工具:
中間件:
開發工具
文章參考: