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"];
@徐飛 回答了 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;
}
);
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 的動態性做了一些擴充。
推薦閱讀: