IOC技術在前端項目中的應用

  • 2021 年 1 月 23 日
  • 筆記

背景

前端發展至今已經過去30餘年,前端應用領域在不斷壯大的過程中,也變得越來越複雜,隨著程式碼行數和項目需求的增加,內部模組間的依賴可能也會隨之越來越複雜,模組間的 低復用性 導致應用 難以維護,不過我們可以藉助電腦領域的一些優秀的編程理念來一定程度上解決這些問題,接下來要講述的 IoC 就是其中之一。

什麼是IOC

其實學過java的就一定會知道java中有一個非常著名的框架叫做springboot,它就是將AOP和IOC等概念運用到了極致的代表作,那麼具體IOC是做什麼的呢,我們可以看下下面一段描述。

IoC 的全稱叫做 Inversion of Control,可翻譯為為「控制反轉」或「依賴倒置」,它主要包含了三個準則:

  1. 高層次的模組不應該依賴於低層次的模組,它們都應該依賴於抽象
  2. 抽象不應該依賴於具體實現,具體實現應該依賴於抽象
  3. 面向介面編程 而不要面向實現編程

假設我們有一個類Human,要實例一個Human,我們需要實例一個類Clothes。而實例化衣服Clothes,我們又需要實例化布Cloth,實例化紐扣等等。

當需求達到一定複雜的程度時,我們不能為了一個人穿衣服去從布從紐扣開始從頭實現,最好能把所有的需求放到一個工廠或者是倉庫,我們需要什麼直接從工廠的倉庫裡面直接拿。

這個時候就需要依賴注入了,我們實現一個IOC容器(倉庫),然後需要衣服就從倉庫裡面直接拿實例好的衣服給人作為屬性穿上去。

這也就大大減少了我們編碼的成本。

如何實現一個IOC

其實實現IOC的思路很簡單,或者說這是一個很輕的東西,任何人只要知道原理都能去實現它。首先我們重複下剛剛所描述的ioc的概念,在正常情況下我們需要Human,Clothes類的時候都只能一個一個新建。

export class Human {}

export class Clothes {}

function test() {
    const human = new Human();
    const clothes = new Clothes();
}

我們不難看出少量的對象需要新建的時候這麼做確實沒啥問題,但是如果在一個龐大系統中存在上百上千個對象,我們在不同業務場景又需要load不同的對象,同時我們還需要控制對象銷毀避免GC。這樣來說我們想要處理好前端對象我們得做很多工作,這樣我們就引出了接下來我們需要做的工作如何去管理對象。

第一步:實現一個容器

容器其實是一個高大上的概念,其實簡單來說就是個Map對象之類的東西,用於存放現有的對象。下面是我具體實現的一個小demo,主要是存放的容器類。為了保證容器唯一,所以我將其設計成了單例模式。

export class SimpleContainer {
    private containerMap = new Map<string | symbol, any>();
    private static _instance: SimpleContainer;

    public set(id: string | symbol, value: any): void {
      this.containerMap.set(id, value);
    }
    
    public get<T extends any>(id: string | symbol): T {
      return this.containerMap.get(id) as T;
    }
  
    public has(id: string | symbol): Boolean{
      return this.containerMap.has(id);
    }

    public remove(id: string | symbol): void {
      if (this.containerMap.has(id)) {
          this.containerMap.delete(id);
      }
    }

    public static getInstance(): SimpleContainer {
        if(!this._instance) {
            this._instance = new SimpleContainer();
        }
        return this._instance;
    }

    public get container(): SimpleContainer {
      return SimpleContainer._instance;
    }
}

第二步:用好裝飾器

隨著TypeScript和ES6里引入了類,在一些場景下我們需要額外的特性來支援標註或修改類及其成員。 裝飾器(Decorators)為我們在類的聲明及成員上通過元編程語法添加標註提供了一種方式。 Javascript里的裝飾器目前處在 建議徵集的第二階段,但在TypeScript里已做為一項實驗性特性予以支援。

注意  裝飾器是一項實驗性特性,在未來的版本中可能會發生改變。

如果需要使用裝飾器,我們得在tsconfig.json中配置experimentalDecoratorstrue開啟支援。

首先我們先看下我們需要實現的最後效果

@Service('human')
export class Human {}

@Service('clothes')
export class Clothes {}

export class Test {

    @Inject()
    private human!: Human;

}

我們需要通過Service注入需要實例化的類,然後再通過Inject在外面需要的對象中注入進去,這就是裝飾器在IOC中所發揮的作用。

那麼Service是如何實現的呢?

export function Service(idOrSingleton?: string | boolean, singleton?: boolean): Function {
    return (target: ConstructableFunction) => {
        let id;
        let singleton;
        const container = SimpleContainer.getInstance();
      	// 程式碼邏輯複雜有所刪減
        container.set(id, singleInstance || new target());
    };
};

我們所有的實例初始化都在Service中實現也就是這麼一個句話,container.set(id, singleInstance || new target());。

export function Inject(value?: string): PropertyDecorator {
    return (target: any, propertyKey: string | symbol) => {  
      const id = value || propertyKey;
      const container = SimpleContainer.getInstance();
      const _dependency = container.get(id) ? container.get(id) : null;
      if (_dependency) {
        target[propertyKey] = _dependency;
      }
      return target;
    };
}

通過Inject來實現對象的實例話和返還,所利用的特性也是PropertyDecorator所支援的能夠對參數賦值的能力。識別到對應裝飾器的對象的時候,我們通過屬性裝飾器來進行賦值和初始化。

這裡需要補充一下裝飾器的相關知識。

1.裝飾器對類的行為改變是在編譯時,而非在運行時。

2.裝飾器運行順序,並非按照類,屬性,方法來進行的,我們在使用的時候需要注意,我這裡的順序是:屬性->類->方法

第三步:使用容器

我們又回到了第二步的最初,當我們實現了Inject和Service裝飾器之後我們就可以快樂的初始化了。

@Inject()
private human!: Human;

通過如上操作之後我們就可以使用該對象的內容了。

擴展和展望

回到我們實現IOC的初衷,我們希望通過某種技術來管理我們繁亂的對象和程式碼,所以我們才做了這麼一個容器,當然現在這個容器還十分簡陋,依然還有很多可以擴展的空間,比如說:關於對象的生命周期的控制,如何更加友好的使用容器中的對象。

最後

一個小廣告,歡迎使用基於上述程式碼所開發的ioc包,目前還能簡陋,不過筆者會迅速強化和迭代它。

easy-ts-di://github.com/guanjiangtao/easy-ts-di

歡迎大佬們可以提供意見,鋼筋走開~~~~