Dependency Injection (DI,依賴注入)是一種軟件設計模式,用於處理如何讓程序獲得其依賴(對象的)引用。
依賴注入簡述 | DI in a nutshell
對象或者函數只有以下3種獲取其依賴(的對象)引用的方式:
- 依賴可以被使用者自己創建,通過 new 操作符.
- 依賴可以通過全局變量(如 window)來查找並引用
- 依賴可以在需要的地方被傳入
前兩種創建或查找依賴的方式並不是最優的,因爲他們對依賴進行了硬編碼. 這就使得當依賴變得不可用時,要修改依賴相關的代碼變得非常困難和繁瑣。在測試中更是有問題,因爲通常需要通過模擬依賴來進行隔離測試。
第三種選擇可以說是最可行的方式,因爲它從組件中消除了查找依賴位置的責任。只需要簡單地將依賴傳遞給組件即可。
要管理依賴創建的責任,每個 Angular 應用程序都有一個 injector 。該injector是一個 service locator(定位器) ,負責創建並查找依賴。
下面是一個使用 injector服務的示例:
/ 在一個模塊中提供連接信息
angular.module('myModule', []).
// 告訴 injector 如何去構建一個 'greeter'
// 注意, greeter 自身是依賴於 '$window' 的
factory('greeter', function($window) {
// 這是一個 factory function,
// 職責是爲創建 'greet' 服務.
return {
greet: function(text) {
$window.alert(text);
}
};
});
// 新的 injector 從 module 創建.
// (這通常由 angular bootstrap 自動創建)
var injector = angular.injector(['myModule', 'ng']);
// 從 injector 獲取所有依賴
var greeter = injector.get('greeter');
要解決依賴關係硬編碼的問題,也就意味着 injector 需要貫穿整個應用程序生命週期。傳遞 injector 打破了 得墨忒耳定律(Law of Demeter, 最少知識原則)。爲了彌補這一點,在下面的例子中,我們通過依賴聲明的方式將查找依賴的職責交給了 injector:
HTML代碼:
[html] view plain copy 在CODE上查看代碼片派生到我的代碼片
<!-- Given this HTML -->
<div ng-controller="MyController">
<button ng-click="sayHello()">Hello</button>
</div>
Angular代碼
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
// 這是 controller 定義
function MyController($scope, greeter) {
$scope.sayHello = function() {
greeter.greet('Hello World');
};
}
// 由 'ng-controller' directive 在後臺執行
injector.instantiate(MyController);
注意通過 ng-controller實例化此類,它可以 在 controller 不知道有 injector 的情況下滿足MyController 所有的依賴。這是最好的結果。應用程序代碼簡單地要求所需的依賴項,無需和 injector 打交道。這個設置不違背 得墨忒耳定律。
依賴註解 | Dependency Annotation
injector 怎麼知道需要注入何種 service 呢?
爲了解決依賴關係,應用程序開發者需要提供 injector 需要的 annotation 信息。在 Angular 中,某些API函數通過使用 injector 來調用,請按照API文檔。injector 需要知道注入哪些服務給函數。下面是通過 service name 信息對代碼進行註解的三種等價方式。他們都是等價的,你可以在適當的地方互換使用.
推斷依賴關係 | Inferring Dependencies
最簡單的獲取依賴的方式,就是讓函數參數名和依賴的名字一致。
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
function MyController($scope, greeter) {
...
}
給定一個 function, injector 通過檢查函數聲明和提取參數名稱可以推斷出 service 的名稱 。在上面的例子中, $scope 和 greeter 是需要注入 function 的兩個 services。
雖然簡單直接, 但這種方法在 JavaScript 壓縮/混淆 時會失效,因爲會重命名方法的參數名。這使得這種註解方式只適用於 pretotyping, 或者 demo 程序中。
$inject 註解 | $inject Annotation
爲了可以在壓縮代碼後依然可以注入正確的 services, 函數需要通過
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
var MyController = function(renamed$scope, renamedGreeter) {
...
}
MyController['$inject'] = ['$scope', 'greeter'];
在這種情況下,$inject數組中的值的順序必須和要注入的參數的順序一致。使用上面的代碼片段作爲一個例子, ‘
內聯註解 | Inline Annotation
有時候並不方便使用 $inject 註解,比如在註解 directives的時候。
比如下面的示例:
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
someModule.factory('greeter', function($window) {
...
});
因爲需要使用臨時變量,導致了代碼膨脹爲:
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
var greeterFactory = function(renamed$window) {
...
};
greeterFactory.$inject = ['$window'];
someModule.factory('greeter', greeterFactory);
這也是提供第三種註解方式的原因.
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
someModule.factory('greeter', ['$window', function(renamed$window) {
...
}]);
記住,所有的 annotation 風格都是等價的,在 Angular 中,只有支持注入的地方都可以使用.
什麼地方應該使用DI | Where can I use DI?
DI在 Angular 中無處不在。它通常用於 controllers 和工廠方法。
控制器中使用DI | DI in controllers
Controllers 類負責應用程序的行爲。聲明 controllers 的推薦的方法是使用數組表示法:
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {
...
$scope.aMethod = function() {
...
}
...
}]);
這避免了爲 controllers 創建全局函數,並且在代碼壓縮時繼續可用.
工廠方法 | Factory methods
工廠方法在 Angular 中負責創建大多數的對象。例子是 directives, services, 以及 filters。工廠方法被註冊到模塊, 聲明工廠的推薦方法是:
[javascript] view plain copy 在CODE上查看代碼片派生到我的代碼片
angular.module('myModule', []).
config(['depProvider', function(depProvider){
...
}]).
factory('serviceId', ['depService', function(depService) {
...
}]).
directive('directiveName', ['depService', function(depService) {
...
}]).
filter('filterName', ['depService', function(depService) {
...
}]).
run(['depService', function(depService) {
...
}]);