我真正喜歡成熟框架的一點就是它們都實現了某種依賴注入轧膘。最近我一直在鉆研 TypeScript 這門技術灯节,以便更好地從表面上理解它是如何工作的猾警。
什么是依賴注入 (DI)妨蛹?
如果你還不了解什么是 依賴注入拆讯,我強烈推薦你了解一下脂男。既然這篇文章不是來介紹什么是 DI 的,更多地是講 DI 是如何實現的种呐,我們盡量用簡單點的話來描述它:
依賴注入是一種通過外部注入來表明對象的依賴關系的技術宰翅。
什么意思呢?原本我們通過各種參數來構造對象爽室,現在只需接收一個已實例化的對象汁讼。
想象以下代碼:
class Foo {
}
class Bar {
foo: Foo;
constructor() {
this.foo = new Foo();
}
}
class Foobar {
foo: Foo;
bar: Bar;
constructor() {
this.foo = new Foo();
this.bar = new Bar();
}
}
Bad.
這段代碼帶來很多弊端,由于類之間存在了直接和不可替換的依賴關系,會使代碼難以測試嘿架,可用性和復用性極差瓶珊。而通過依賴注入的方式,將依賴項注入到你的構造函數中耸彪,可以很好地解決這些弊端伞芹。
class Foo {
}
class Bar {
constructor(foo: Foo) {
}
}
class Foobar {
constructor(foo: Foo, bar: Bar) {
}
}
Better.
初始化 Foobar
實例你需要通過以下方式:
const foobar = new Foobar(new Foo(), new Bar(new Foo()));
Not cool.
通過 Injector
來創(chuàng)建對象,你可以簡單地這樣寫:
const foobar = Injector.resolve<Foobar>(Foobar); // returns an instance of Foobar, with all injected dependencies
Better.
為什么要使用依賴注入的原因有很多搜囱,包括可測試行丑瞧,可維護性,可讀性等等蜀肘。再說一遍绊汹,如果你還不了解它,那么很有必要去學習一下了扮宠。
TypeScript 中的依賴注入
這篇文章將介紹我們自己實現的(非澄鞴裕基礎的)Injector
。如果你只是想找一些現有 DI
解決方案來運用到你的項目中的話坛增,建議你看看 InversifyJS获雕,它是一個很好的控制反轉(IoC) 的 TypeScript 庫。
在本文中收捣,我們將會實現自己的 Injector
類届案,該類能夠通過注入所有必需的依賴項來解析實例。為此我們將實現一個 @Service
的裝飾器(如果你用過 Angular
的話可能知道 @Injectable
)來定義我們的服務以及解析實際實例的 Injector
罢艾。
在具體實現之前楣颠,你看你需要先了解有關 TypeScript
和 DI
的一些知識:
反射和裝飾
我們將使用 reflect-metadata
包來獲取運行時的反射功能 。通過這個包可以了解一個類是如何實現的 - 例如:
const Service = () : ClassDecorator => {
return target => {
console.log(Reflect.getMetadata('design:paramtypes', target));
};
};
class Bar {}
@Service()
class Foo {
constructor(bar: Bar, baz: string) {}
}
控制臺將輸出以下日志:
[ [Function: Bar], [Function: String] ]
因此我們確實知道需要注入的依賴項是什么咐蚯。如果你對這里的 Bar
是一個 Function
感到困惑:我將在下一個章節(jié)中介紹童漩。
重要提示:必須注意的是,沒有裝飾器的類是沒有任何 metadata 的春锋,這似乎是 reflect-metadata
的設計選擇矫膨,盡管我不確定其背后的原因。
target 的類型
有個讓我非常困惑的地方是 Service
中的 target
的類型期奔。它很明顯是個 object
而非 Function
呀侧馅,但這是因為在 Javascript
中,類實際上就是一個特殊的函數:
class Foo {
constructor() {
// the constructor
}
bar() {
// a method
}
}
轉變:
var Foo = /** @class */ (function () {
function Foo() {
// the constructor
}
Foo.prototype.bar = function () {
// a method
};
return Foo;
}());
編譯后代碼
但 Function
顯然不是我們想要的目標類型呐萌,因為它太泛了馁痴。由于我們現在還沒有真正使用一個實例,我們需要得到通過 new
創(chuàng)建的實例的確切類型:
interface Type<T> {
new(...args: any[]): T;
}
Type<T>
能夠讓我們知道對象的原型是什么搁胆?或者換句話說:當我們使用 new
后拿到的是什么弥搞。回頭看看我們的 @Service
裝飾器渠旁,真正的目標類型應該是:
const Service = () : ClassDecorator => {
return target => {
// `target` in this case is `Type<Foo>`, not `Foo`
};
};
讓我困擾的一個點是 ClassDecorator
攀例,它看起來像這樣:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
不幸的是,因為我們現在的確知道對象的類型顾腊,要為類裝飾器獲得一個更靈活更通用的類型粤铭。
export type GenericClassDecorator<T> = (target: T) => void;
編譯后 interface
不見了
由于接口不是 Javascript
的一部分,所以 TypeScript
編譯后它們就消失了杂靶。這并不是什么新鮮事梆惯,但這意味著我們不能使用接口進行依賴注入。舉個例子:
interface LoggerInterface {
write(message: string);
}
class Server {
constructor(logger: LoggerInterface) {
this.logger.write('Service called');
}
}
由于接口在運行時不可用吗垮,所以我們的 Injector
將無法知道要注入什么垛吗。
這實在是個遺憾,因為這意味著我們總是必須通過類型提示來替代接口烁登。特別是在我們進行測試的時候非常不好怯屉。
還有一些解決方法,比如通過類而不是接口(感覺很奇怪饵沧,喪失了接口的意義)或者像這樣:
interface LoggerInterface {
kind: 'logger';
}
class FileLogger implements LoggerInterface {
kind: 'logger';
}
然而我真的不喜歡這種方法锨络,因為它又冗長又難看。
循環(huán)依賴帶來的麻煩
如果你試圖這樣做:
@Service()
class Bar {
constructor(foo: Foo) {}
}
@Service()
class Foo {
constructor(bar: Bar) {}
}
你將會得到一個 ReferenceError
狼牺,告訴你:
ReferenceError: Foo is not defined
原因很明顯:TypeScript
在解釋 Bar
的時候羡儿, Foo
還不存在。
我不想在這里細究是钥,但有一個可能的解決方法是實現類似 Angular
的 forwardRef 方法掠归。
實現我們自己的 Injector
好了,理論講得差不多了咏瑟。讓我們實現一個非撤鞯剑基礎的 Injector
類吧。
我們將使用以上學到的所有知識码泞,來開始我們的 @Service
裝飾器兄旬。
@Service
裝飾器
我們將會對所有服務進行裝飾處理,否則他們將無法返回元數據(就無法注入依賴項了)余寥。
// ServiceDecorator.ts
const Service = () : GenericClassDecorator<Type<object>> => {
return (target: Type<object>) => {
// do something with `target`, e.g. some kind of validation or passing it to the Injector and store them
};
};
Injector
Injector
主要用于解析實例领铐。它可能也具備其他能力如存儲解析后的實例(我比較喜歡將它們稱為共享實例),但為了簡單起見宋舷,我們現在將盡可能簡單的實現绪撵。
// Injector.ts
export const Injector = new class {
// Injector implementation
};
導出 const 而非 class(比如 export class Injector
) 的原因在于我們的 Injector
是單例的。否則祝蝠,我們將永遠無法得到相同的 Injector
實例音诈,這意味著任何時候你引入 Injector
所得到的實例都是未注冊過服務的幻碱。(像所有單例一樣也存在一些缺點,尤其在測試方面)
下一步我們需要實現一個方法來解析實例:
// Injector.ts
export const Injector = new class {
// resolving instances
resolve<T>(target: Type<any>): T {
// tokens are required dependencies, while injections are resolved tokens from the Injector
let tokens = Reflect.getMetadata('design:paramtypes', target) || [],
injections = tokens.map(token => Injector.resolve<any>(token));
return new target(...injections);
}
};
到此為止细溅,我們的 Injector
已經可以解析請求的實例了褥傍。讓我們回到開頭的例子(現在稍微擴展一下),通過 Injector
來解析它:
@Service()
class Foo {
doFooStuff() {
console.log('foo');
}
}
@Service()
class Bar {
constructor(public foo: Foo) {
}
doBarStuff() {
console.log('bar');
}
}
@Service()
class Foobar {
constructor(public foo: Foo, public bar: Bar) {
}
}
const foobar = Injector.resolve<Foobar>(Foobar);
foobar.bar.doBarStuff();
foobar.foo.doFooStuff();
foobar.bar.foo.doFooStuff();
控制臺輸出:
bar
foo
foo
這意味著我們的 Injector
成功注入了所有依賴項喇聊。哇嗚恍风!
總結
依賴注入絕對是你應該使用的強大工具。這篇文章是關于依賴注入是如何工作的誓篱,給你一些實現自己的 Injector
的靈感朋贬。
還有不少事情需要去做,大概舉幾個例子:
- 錯誤處理
- 解決循環(huán)依賴
- 存儲已解析的實例
- 注入比構造函數token更多的能力
- 等等
但基本上窜骄,這就是Injector的工作原理锦募。
正如開頭所說的,我最近剛開始研究 DI 的實現邻遏。如果你對這篇文章或者Injector的實現感到任何困惑御滩,歡迎給我留言或者通過郵件聯(lián)系我。
跟往常一樣党远,所有的代碼(包含示例和測試)都可以在 Github上找到削解。