AngularJS中的依賴注入實際應用場景?

依賴注入聽起來挺玄乎,是不是就是指通過給函數添加形參,使得caller傳入的參數和callee接受的參數邏輯分離,使得函數通過依賴管理系統僅僅需要聲明需要的協作對象,而不需要知道從哪裡來,如何創建等問題?


謝邀。

確實是你說的這樣。

所謂依賴注入,通俗地舉例,有個人養了一隻寵物,他可以喂寵物吃東西,寵物會自己吃:

function PetKeeper(pet) {
this.pet = pet;
}

PetKeeper.prototype.feed = function(food) {
this.pet.eat(food);
};

function Pet(type) {
this.type = type;
}

Pet.prototype.eat = function(food) {
alert("I am a " + this.type + ", I"m eating " + food);
};

var tom = new Pet("cat");
var jerry = new Pet("mouse");

var keeper = new PetKeeper(tom);
keeper.feed("fish");

keeper.pet = jerry;
keeper.feed("rice");

這個例子里,pet是外部注入的,在feed函數定義里,並不知道pet到底是什麼(在帶介面的語言里,至少還是知道是個什麼,在動態語言里就是兩眼一抹黑了……),只有當它被調用的時候,才知道pet是什麼。

這個過程的好處是什麼呢?如果我們在PetKeeper內部去創建tom或jerry,就表示PetKeeper要對Pet產生依賴。一個對別人有依賴的東西,它想要單獨測試,就需要在依賴項齊備的情況下進行。如果我們在運行時注入,就可以減少這種依賴,比如在單元測試的時候使用模擬類就行。

比如你有一個a,依賴於b,實際業務中,b的實現很複雜:

function A(b) {
this.b = b;
}

A.prototype.a1 = function() {
alert(100 + this.b.b1());
};

function B() {}

B.prototype.b1 = function() {
//這裡可能很複雜而且不好模擬,比如依賴於生產環境的一些調用
}

那麼,我如何用單元測試來驗證A自身的邏輯是正確的呢?如果有強依賴,這裡就不好辦了,必須實例化真正的B,但是B的調用要依賴於生產環境。換個方式考慮,我們用一個介面與B相同的類來做模擬,只要改變它的返回值,實現各種邊界條件,把它的實例注入到A的構造函數中,就可以讓A自身的邏輯得到測試了。

function MockB() {}

MockB.prototype.b1 = function() {
return 99;
};

在AngularJS里,依賴注入的目的是為了減少組件間的耦合,它的實現是這個過程:

function Art(Bar, Car) {}

我怎麼知道這個Art在實例化的時候要傳入Bar和Car的實例呢?形參名稱是沒法取到的,所以只有狠一點,用toString()來取到剛才這一行字元串,然後用正則表達式取到Bar和Car這兩個字元串,然後到模塊映射中取到對應的模塊,實例化之後傳入。

但是這樣也有問題,如果這個js被壓縮了,很可能命名都變了,壓縮成了這樣:

function a1(b1, b2) {}

這時候再這樣就不知道原先是什麼類型了。在這裡,有類型聲明的語言就不會有問題,比如:

function art(bar:Bar, car:Car) : Art {}

就算你把art, bar, car都改名了,也還是能知道類型,但js里不行。所以,怎麼辦呢?

aaa.controller("Art", [function(Bar, Car) {}, "Bar", "Car"]);

注意在AngularJS裡面,他很可能建議你這麼寫,但也可以這麼寫:

Art.$inject = ["Bar", "Car"];

這麼一來,我只要拿到Art,就能取到依賴項的名稱了,就可以實例化再注入,也不怕壓縮了。


@徐飛 回答了 IoC 的基本概念,並舉例說明了 IoC 有助於提高可測性的好處,同時也詳細解答了angular 的 IoC 解決方案,我這裡補充些實際的場景案例來說明下 di 對模塊化復用的巨大優勢,以及脫離 angualr 的 IoC 解決方案。

實際場景

我們的業務系統是基於 MVC 的架構,假設我們現在有一個項目A,要開發一個列表的頁面,於是骨架代碼會這樣:

// A/ListModel.js
define(
function (require) {
function ListModel() {
this.store = {};
}

ListModel.prototype.load = function () {
this.set("items", [1, 2, 3, 4, 5]);
};

ListModel.prototype.set = function (key, value) {
this.store[key] = value;
};

ListModel.prototype.get = function (key) {
return this.store[key];
};

return ListModel;
}
);

// A/ListView.js
define(
function (require) {

function ListView() {}

ListView.prototype.render = function () {
document.body.innerHTML = this.model.get("items");
};

return ListView;
}
);

// A/ListController.js
define(
function (require) {
var Model = require("./ListModel");
var View = require("./ListView");

function ListController() {
this.model = new Model();
this.view = new View();
this.view.model = this.model;
}

ListController.prototype.enter = function () {
this.model.load();
this.view.render();
};

return ListController;
}
);

// A/main.js
define(
function (require) {
var List = require("ListController");
var list = new List();
list.enter();
}
);

運行結果就是在頁面中展示列表數據。

過了一段時間,另一個新項目B來了,B項目也要開發一個列表頁,但交互展示和A項目的列表頁是一致的,不同之處在於數據源要來自 B 項目的後端。 我們來看看 A項目的代碼,發現ListController.js, ListView.js好像都不用變,僅僅需要覆寫 ListModel.js的load方法,使得其載入的數據來自 B 項目就解決了數據源變化的需求。

好的,面向對象的多態解決方案來了,我們繼承 A 項目的 ListModel就好了:

// B/ListModel.js
define(
function (require) {

var AListModel = require("A/ListModel");

function ListModel() {
AListModel.apply(this, arguments);
}

// 數據源設置為了 B 的數據
ListModel.prototype.load = function () {
this.set("items", [5, 4, 3, 2, 1]);
};

return ListModel;
}
);

我們在 B/ListModel.js中重寫了 load 方法,接下來就是怎麼去重用A/ListController, A/ListView, 繼續看下 A 的代碼,看到 A/ListController中的:

var Model = require("./ListModel");
var View = require("./ListView");

我去,這直接依賴了當前包的ListModel和 ListView 喂~~,ListView 還好咱不用變,ListModel 咋辦? 咋替換成B/ListModel啊?

上述問題的本質在於,A 項目的代碼針對了具體實現編程,而非介面。 A/ListController直接依賴了具體實現(A/ListModel, A/ListView),這使得其復用性大大降低(好像你只能去複製粘貼代碼,改其中幾行來複用了- -!) 。

於是我們重構下 A項目的代碼,將依賴外置,由外部傳入依賴實例,也就是具體實現,不同的項目有不同的實現,但都遵守同一個介面(js 中則是隱式介面):

// A/ListController.js
define(
function (require) {

function ListController(model, view) {
this.model = model;
this.view = view;
this.view.model = this.model;
}

ListController.prototype.enter = function () {
this.model.load();
this.view.render();
};

return ListController;
}
);

// A/main.js
define(
function (require) {
var List = require("ListController");
var Model = require("ListModel");
var View = require("ListView");

var model = new Model();
var view = new View();
var list = new List(model, view);

list.enter();
}
);

上面的代碼中將A/ListController對具體 model 和 view 的依賴都外置了,由外部(這裡是 A/main.js)創建好傳入構造函數,A/ListController對model 和 view 如何構造,是怎麼實現的都不需要關心,只要知道 model 實現了 load 介面,view 實現了 render 介面就行了。 好了,到這一步基本解決了對具體依賴的解耦,接下來我們看看 B 項目的代碼怎麼寫:

// B/ListModel.js
define(
function (require) {

var AListModel = require("A/ListModel");

function ListModel() {
AListModel.apply(this, arguments);
}

// 繼承 A/ListModel
ListModel.prototype = new AListModel();

// 數據源設置為了 B 的數據
ListModel.prototype.load = function () {
this.set("items", [5, 4, 3, 2, 1]);
};

return ListModel;
}
);

// B/main.js
define(
function (require) {
// 重用 A項目的Controller 和 View
var List = require("A/ListController");
var View = require("A/ListView");

// 引入自己的定製 Model
var Model = require("ListModel");

var model = new Model();
var view = new View();
// 由構造函數將 model 和 view 兩個依賴注入給控制器
var list = new List(model, view);

list.enter();
}
);

我們看到,通過依賴注入,B 項目的列表開發工作量僅僅是簡單的重寫 A.ListMdel#load,以及在入口文件處創建好依賴即可。控制和視圖的開發工作量都節約下來了,這無疑是巨大的收益。

簡單小結

真正的項目複雜性遠不止上面代碼中的這麼簡單,還包括了數據源對象,模板,對 dom 封裝的控制項等其他模塊。

我們系統原來的 mvc 架構,開發一個業務模塊將 m,v,c等各個依賴緊緊的耦合在了一起,隨著業務的發展,項目也越來越多,我們發現這些項目具有很多共同點,僅僅是局部不同,於是我們通過控制反轉將業務模塊中各個容易變化的部件抽象解耦,不同的項目去實現自己的定製需求,而通用代碼不要重複開發,大概的架構演變如下圖(Action 對應代碼中的 Controller):

基本思路都躲不過:封裝變化的,固化不變的。難點在於區分哪些是變化的,哪些又是不變的。

IoC的解決方案

上面的代碼中,我們將模塊的依賴在 main.js 中手動創建好,然後調用模塊的構造函數傳入,這個過程就是依賴反轉,依賴的創建轉移給了外部 main.js,模塊僅僅做獲取依賴的工作。這一過程我們發現也是冗餘重複的,當需要創建的依賴多了後,main.js的代碼也要隨之冗餘膨脹,於是有了 IoC 容器來做這一過程:項目聲明依賴配置,IoC 容器根據配置做好相關的依賴創建工作即可。

在 angular 中,依賴聲明是在構造函數中或者$inject中做的,在構造函數中angualr根據命名參數去查找依賴聲明,並做好依賴的創建工作,原理自然是利用 Function#toString方法了,所以說怕壓縮,不過這點也不是什麼大不了的問題,一個方案是上面答案提到的寫死$inject或者在controller 工廠參數中寫好依賴,另一個是通過構件工具去做:比如:https://www.npmjs.com/package/ng-annotate,我推薦後者。

但 angular 的依賴注入存在以下問題:

1. 和 angular 緊密整合,移植成本較大。

2. 依賴注入方式單一,僅有構造函數注入。要是一個模塊依賴很多的話,構造函數中的依賴聲明得寫脫。

既然我們可以通過構造函數傳入依賴,那完全也可以提供另一個函數給 IoC 容器,讓 IoC 容器調用這個函數傳入依賴,這個注入方式稱之為介面注入;如果函數命名風格為setter(setXXX),又可以稱之為 setter注入;再加上 js 語言的動態性,可以動態的給對象賦值新屬性,於是我們又可以這樣注入:instance.dependency = xxxx, 這個我們暫時稱之為屬性注入。

3. 未能和模塊載入器結合。 在瀏覽器環境中,很多場景都是非同步的過程,我們需要的依賴模塊並不是一開始就載入好的,或許我們在創建的時候才會去載入依賴模塊,再進行依賴創建,而 angualr 的 IoC 容器沒法做到這點。

以下是軟文

針對 angular 的這些問題,我們自己開發了一個 IoC 容器:ecomfe/uioc · GitHub,主要特點:

1. 獨立的庫,不和任何框架整合,隨便用。

2. 配置上支持 AMD/CMD 規範的非同步 Loader (nodejs自不必說,同步 loader更簡單了)。

3. 豐富的 IoC 注入方式:setter 注入,構造函數注入,屬性注入。

4. 簡化配置的方案:根據setter自動注入,配置導入。

5. 依賴作用域的管理:單例,多例,靜態。

6. 支持construtor和factory兩種依賴構造方式。

拿上面的B 項目用我們的 IoC容器改造後如下:

// B/config.js
define(
{
// key 為提供給 ioc 的組件id,值為相關配置
list: {
// 組件所在的模塊id,這裡復用了 A的 ListController
module: "A/ListController",
// 構造函數注入,$ref聲明依賴,兩個依賴id分別為 model 和 view
args: [
{$ref: "model"}, {$ref: "view"}
]
},

// 這裡使用了B定製的 ListModel
model: {module: "B/ListModel"},

view: {module: "A/ListView"}
}
);

// B/ListModel.js
define(
function (require) {

var AListModel = require("A/ListModel");

function ListModel() {
AListModel.apply(this, arguments);
}

// 繼承 A/ListModel
ListModel.prototype = new AListModel();

// 數據源設置為了 B 的數據
ListModel.prototype.load = function () {
this.set("items", [5, 4, 3, 2, 1]);
};

return ListModel;
}
);

// B/main.js
define(
function (require) {
var IoC = require("uioc");
var config = require("config");

// 實例化ioc容器,傳入配置註冊各個組件
var ioc = IoC(config);

// 獲取 list 組件後,調用對應的 enter 方法
ioc.getComponent("list", function (list) {
list.enter();
});
}
);

嗯,改造後,以後要是有個 C 項目,在view 層上有定製需求,那 C 就自己繼承或者重寫一個 ListView,在配置中將 view 定製為 C/ListView即可完成新一輪的開發。變的只是配置和定製部分。

好了- -,uioc 這貨的配置 api 和 spring ioc 是不是很像?嗯,配置語法確實是參考了 spring,在此基礎上,整合了前端的 loader, 順便利用了js 的動態性做了一些擴充。


推薦閱讀:

C#有哪些有名的依賴注入的框架?
設計模式有何不妥,所謂的荼毒體現在哪?

TAG:前端開發 | 設計模式 | AngularJS |