AngularJS中的依赖注入实际应用场景?
4 个回答
谢邀。
确实是你说的这样。
所谓依赖注入,通俗地举例,有个人养了一只宠物,他可以喂宠物吃东西,宠物会自己吃:
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 的动态性做了一些扩充。