前端 IoC 理念入門
背景
近幾年,前端應用(WebApp)正朝著大規模方向發展,在這個過程中我們會對項目拆解成多個模塊/組件來組合使用,以此提高我們代碼的復用性,最終提高研發效率。
在編寫一個復雜組件的時候,總會依賴其他組件來協同完成某個邏輯功能。組件越復雜,依賴越多,可復用性就越差,我們可以借助軟件工程中優秀的編程理念來提高復雜組件的可復用性,以下將詳述其中之一的依賴倒置理念。
什么是 IoC
IoC 全稱 Inversion of Control,中文術語為依賴倒置(反轉),包含兩個準則:
-
高層次的模塊不應該依賴于低層次的模塊,他們都應該依賴于抽象。
-
抽象不應該依賴于具體實現,具體實現應該依賴于抽象
其背后的核心思想還是:面向接口編程。
我們用一個例子來說明:我們要實現一個列表 A,能夠加載一系列的信息并展示,
于是很自然的我們遵守職責單一功能,將展示和加載兩個邏輯拆分成2個類:
// Loader.js
export default class Loader {
constructor(url) {
this.url = url;
}
async load() {
let result = await fetch(this.url);
return result.text();
}
}
// List.js
import Loader from './Loader';
export default class List {
constructor(container) {
this.container = container;
this.loader = new Loader('list.json');
}
async render() {
let items = await this.loader.load();
this.container.textContent = items;
}
}
// main.js
import List from './List';
let list = new List(document.getElementById('a'));
List.render();
</code></pre>
列表 A 很快開發完畢,于是你要繼續開發下一個列表 B,B 的功能和 A 類似,也是加載數據展示數據,區別在于 B 的數據來源是一個第三方的服務,他們提供一個 js sdk 給你調用能夠返回數據信息。很自然的我們想到 A 的展示邏輯是可以復用的,對于數據加載這個邏輯我們重新實現一個 ThirdLoader 來專門加載第三方服務就是了,但回到 List 模塊,我們發現在其構造函數中寫死了對 Loader 的依賴:this.loader = new Loader(‘/list’); 導致無法對 List 設置第三方數據加載邏輯。這個問題就在于 List 依賴了具體的實現而不是依賴一個 Loader 接口。
IoC 正是解決這一類問題的最佳良藥,我們再回顧 IoC 的兩條準則,看看如何利用 IoC 理念解決這類問題:
1. 高層次的模塊不應該依賴于低層次的模塊,他們都應該依賴于抽象
上述代碼中,列表模塊是高層次的模塊,Loader 是低層次模塊, 高層次的 List 依賴了低層次的 Loader,違背了該準則。好在準則也提供了解決方案: 應該依賴于抽象 。那什么是抽象? 放在我們編程語言中正是廣為周知的接口,放在 JS 語言中,接口則是隱式的。
我們正好實踐下該準則:
-
我們定義一個隱式的接口 ILoader,ILoader 聲明了一個 load 方法,該方法簽名是返回一個包含請求結果的 Promise。
-
將 List 模塊對 Loader 模塊的依賴調整為對 ILoader 接口的依賴:我們在 List 模塊中移除對 Loader 模塊的依賴(即移除 import 語句),同時構造函數中增加一個參數,該參數是一個實現了 ILoader 接口的實例。
// List.js
export default class List {
constructor(container, loader) {
this.container = container;
this.loader = loader;
}
async render() {
this.container.textContent = await this.loader.load();
}
}
</code></pre> </li>
-
為了完成列表 A 的功能,我們還要改造 main.js,將實現了 ILoader 的 Loader 模塊實例化傳遞給 List 模塊:
// main.js
import List from './List';
import Loader from './Loader';
let list = new List(document.getElementById('a'), new Loader('list.json'));
list.render();
</ol>
至此,我們完成了對 List 模塊的一次改造,List 從對具體實現 Loader 的依賴變成了對抽象接 口 ILoader 的依賴,而 List 模塊中對 Loader 模塊的導入和實例化過程轉移到了 main.js, 這一過程就是我們的依賴倒置,依賴創建的控制權交給了外部(main.js),而在 main.js 中查找創建依賴并將依賴傳遞給 List 模塊的這一過程我們稱之為依賴注入(Denpendency Injection)。
我們再來看看 IoC 的第二個準則: 抽象不應該依賴于具體實現,具體實現應該依賴于抽象 ,我們的 ILoader 接口顯然不會依賴于任何具體實現,而 Loader 這個具體實現了依賴于 ILoader 接口,完全符合了 IoC 的第二個準則。
原有系統的依賴關系圖轉變結果如下:

原有系統的依賴關系圖
基于新的依賴架構,List 模塊具備了設置不同數據加載邏輯的能力,現在我們可以復用 List 模塊再實現列表 B 的 數據加載邏輯并在 main 中組裝即可完成列表 B 的功能:
// ThirdLoader.js
import {request} from '../third/sdk';
export default class ThirdServiceLoader {
async load() {
return request();
}
}
// main.js
import List from './List';
import Loader from './Loader';
import ThirdLoader from './ThirdLoader';
let listA = new List(document.getElementById('a'), new Loader('list.json'));
listA.render();
let listB = new List(document.getElementById('b'), new ThirdLoader());
listB.render();
</code></pre>
最終的一個依賴關系圖如下:

最終依賴關系圖
至此我們上面演示了應用 IoC 理念對高層模塊的一個依賴架構改造,提高了高層模塊的可復用性。
IoC 小結
總結我們最開始遇到的問題:類 A 直接依賴類 B,假如要將類 A 改為依賴類 C,則必須通過修改類 A 的代碼來達成。這種場景下,類 A 一般是高層模塊,負責復雜的業務邏輯;類 B 和類 C 是低層模塊,負責基本的原子操作;假如修改類 A,會給程序帶來不必要的風險。
IoC 解決方案:將類 A 修改為依賴接口 I,類 B 和類 C 各自實現接口 I,類 A 通過接口 I 間接與類 B 或者類 C 發生聯系,則會大大降低修改類 A 的幾率。
利用 uioc 框架簡化依賴注入過程
在上一節中我們抽象了 List 中的數據加載邏輯,而依賴注入這一過程轉移到了應用的入口文件 main.js 中,這也導致了我們需要在 main.js 中手動創建并組裝各個依賴,隨著項目規模的增加,依賴數量必然也是成規模上升,手動組裝模塊顯然是一件繁瑣的事;再加上模塊對依賴注入的方式,依賴的創建方式,依賴的實例數量等都有多方面的需求,于是就有了 IoC 框架來幫助我們解決這些問題,簡化依賴注入這個過程,最終讓業務開發者精力集中在業務邏輯層。
接下來我要安利一下如何利用我們開發的 uioc 框架來實現依賴注入,在這之前先介紹一下 uioc 中的一些術語:
-
組件:是完成一項或一系列功能的集合,對外提供相關功能的接口。
-
注冊組件:即聲明一個組件如何創建(類,工廠方法),創建時需要什么樣的依賴,創建完畢后又需要哪些依賴,組件是單例的還是多例的。
-
獲取組件:IoC 容器根據組件的注冊信息創建并返回組件的過程。
整個應用在 IoC 中就被看成是一系列組件的組裝和獲取調用:注冊組件 -> 獲取組件 -> 調用組件方法完成業務邏輯,基于以上概念我們來著手改造前面的例子:
-
安裝 uioc:
install uioc --save```
2. 新建一個 config.js 文件用來存放組件的注冊信息
```javascript
// config.js
import List from './List';
import Loader from './Loader';
import ThirdLoader from './ThirdLoader'
export default {
listA: {
creator: List,
args: [document.getElementById('a'), {$ref: 'loader'}]
},
listB: {
creator: List,
args: [document.getElementById('b'), {$ref: 'thirdLoader'}]
},
loader: {
creator: Loader,
args: ['list.json']
},
thirdLoader: {
creator: ThirdLoader
}
};
</code></pre>
上面的代碼我們聲明了4個組件配置:listA, listB, loader, thirdLoader。 其中組件配置中的 creator 表示創建組件的類,組件 listA, listB 對應的 creator 都是 List,那如何給 listA 和 listB 分別注入不同的 loader 實現呢?這個工作由 args 配置來完成,args 是一個數組,表示要傳遞給組件構造函數的參數配置。
args 中的第一個元素是一個 dom 節點,作為 List 的容器;第二元素中使用了 $ref 關鍵字,其作用是聲明該參數是一個組件, $ref 對應的值則是組件名稱。 listA 第二個參數為 loader 組件,listB 則是 thirdLoader。
在獲取組件時,uioc 會先創建該關鍵字聲明的組件,接著調用 creator 將 dom 節點和 $ref 對應的組件按其在 args 聲明的順序作為參數傳入。
</li>
-
最后我們在 main.js 中實例化一個 IoC 容器并傳入組件注冊信息,最后通過容器獲取組件并渲染頁面:
// main.js
import {IoC} from 'uioc';
import config from './config';
// 實例化 IoC 容器
let ioc = new IoC(config);
// 獲取應用初始化需要的組件
ioc.getComponent(['listA', 'listB']).then(([listA, listB]) => {
listA.render();
listB.render();
});
</code></pre> </li>
</ol>
至此我們借助了 uioc 對原應用進行了依賴注入的改造,通過配置化的方式完成了整個應用的組裝。
上述示例中完整的代碼可以查看: https://github.com/ecomfe/uioc/tree/develop/examples/simple-es6-module
事實上 uioc 還提供了更多強大的功能:
- 結合前端 AMD Loader 的異步組件模塊加載
- 多種注入方式
- 實例生命周期管理
- aop 支持
- 多種依賴類型支持
- 不同的組件創建方式
- 插件機制
細節參考: https://github.com/ecomfe/uioc/wiki/Index
總結
本文首先介紹了如何利用 IoC 理念改造模塊,從而提高模塊的復用性;最后通過 uioc 框架簡化了依賴的創建和注入,來幫助我們更方便的實施 IoC 理念。
關于 uioc 框架,非常歡迎任何人一起與我(d_xinxin@163.com)討論和吐槽,也可以通過 issue 和 pr 的方式來完善 uioc。 要是覺得不錯的話,就順手賞個 star 唄 :)
來自:http://efe.baidu.com/blog/introduction-about-ioc-in-frontend/