AngularJS:了解设计模式
AngularJS负责人Igor Minar在本文的背景中提到:
MVC vs MVVM vs MVP 。 许多开发人员可能会花费数小时甚至数小时进行辩论和争论,这是多么有争议的话题。
多年来,AngularJS更接近于MVC(或者更确切地说是它的一个客户端变体),但随着时间的推移以及许多重构和api改进,现在更接近于MVVM - $ scope对象可以被视为ViewModel由我们称之为控制器的功能装饰。
能够将框架分类并将其放入其中一个MV *桶中具有一些优势。 它可以帮助开发人员更轻松地创建一个代表正在使用框架构建的应用程序的智能模型,从而更好地使用它的API。 它也可以帮助建立开发人员使用的术语。
话虽如此,我宁愿看开发人员建立设计良好并遵循问题分离的kick-ass应用程序,而不是看他们浪费时间争论MV *废话。 为此,我在此声明AngularJS是MVW框架 - Model-View-Whatever 。 无论什么地方代表“ 为你工作 ”。
Angular为您提供了很多灵活性,可以很好地将表示逻辑与业务逻辑和表示状态分开。 请使用它来提高您的生产力和应用程序的可维护性,而不是热烈讨论关于一天结束的事情并不重要。
在客户端应用程序中实现AngularJS MVW(Model-View-Whatever)设计模式有什么建议或指导原则吗?
感谢大量有价值的资源,我已经为在AngularJS应用中实现组件提供了一些一般性建议:
调节器
控制器应该只是模型和视图之间的中间层 。 尽量让它尽可能薄 。
强烈建议避免控制器中的业务逻辑 。 它应该转移到模型。
控制器可以使用方法调用与其他控制器进行通信(当孩子想要与父母通信时可能)或$ emit,$ broadcast和$ on方法。 发出的和广播的消息应该保持在最低限度。
控制器不应该关心表示或DOM操作。
尽量避免嵌套控制器 。 在这种情况下,父控制器被解释为模型。 相反,将模型注入共享服务。
控制器中的范围应该用于绑定模型与视图和
为演示文稿模型设计模式封装视图模型 。
范围
在范本中将范围视为只读,并在控制器 中将范围视为只 写 。 范围的目的是指模型,而不是模型。
在进行双向绑定(ng-model)时,确保不直接绑定到范围属性。
模型
AngularJS中的模型是由服务定义的单例 。
模型提供了分离数据和显示的绝佳方式。
模型是单元测试的主要候选对象,因为它们通常只有一个依赖项(某种形式的事件发生器,通常情况下为$ rootScope),并包含高度可测试的域逻辑 。
模型应被视为特定单元的实现。 它基于单一责任原则。 Unit是一个实例,负责它自己的相关逻辑范围,它可能代表真实世界中的单个实体,并在数据和状态方面在编程世界中描述它。
模型应该封装你的应用程序的数据并提供一个API来访问和操作这些数据。
模型应该是便携式的,因此它可以很容易地运输到类似的应用。
通过隔离模型中的单元逻辑,您可以更轻松地定位,更新和维护。
模型可以使用更一般的全局模型的方法,这对于整个应用程序来说是很常见的。
尽量避免使用依赖注入将其他模型组合到您的模型中,如果它不依赖于减少组件耦合并增加单元可测试性和可用性 。
尽量避免在模型中使用事件侦听器。 这使得它们更难以测试,并且通常以单一责任原则来杀死模型。
模型实施
由于模型应该在数据和状态方面封装一些逻辑,因此它应该在架构上限制对其成员的访问,因此我们可以保证松耦合。
在AngularJS应用程序中实现它的方法是使用工厂服务类型来定义它。 这将允许我们很容易地定义私有属性和方法,并且还可以在单个地方返回可公开访问的地方,这将使开发人员可以读取它们。
一个例子 :
angular.module('search')
.factory( 'searchModel', ['searchResource', function (searchResource) {
var itemsPerPage = 10,
currentPage = 1,
totalPages = 0,
allLoaded = false,
searchQuery;
function init(params) {
itemsPerPage = params.itemsPerPage || itemsPerPage;
searchQuery = params.substring || searchQuery;
}
function findItems(page, queryParams) {
searchQuery = queryParams.substring || searchQuery;
return searchResource.fetch(searchQuery, page, itemsPerPage).then( function (results) {
totalPages = results.totalPages;
currentPage = results.currentPage;
allLoaded = totalPages <= currentPage;
return results.list
});
}
function findNext() {
return findItems(currentPage + 1);
}
function isAllLoaded() {
return allLoaded;
}
// return public model API
return {
/**
* @param {Object} params
*/
init: init,
/**
* @param {Number} page
* @param {Object} queryParams
* @return {Object} promise
*/
find: findItems,
/**
* @return {Boolean}
*/
allLoaded: isAllLoaded,
/**
* @return {Object} promise
*/
findNext: findNext
};
});
创建新的实例
尽量避免让工厂返回一个新的可用函数,因为这会开始打乱依赖注入,并且库会表现不好,特别是对于第三方。
完成同样事情的一种更好的方法是使用工厂作为API来返回对象集合,并在其中附加getter和setter方法。
angular.module('car')
.factory( 'carModel', ['carResource', function (carResource) {
function Car(data) {
angular.extend(this, data);
}
Car.prototype = {
save: function () {
// TODO: strip irrelevant fields
var carData = //...
return carResource.save(carData);
}
};
function getCarById ( id ) {
return carResource.getById(id).then(function (data) {
return new Car(data);
});
}
// the public API
return {
// ...
findById: getCarById
// ...
};
});
全球模式
一般情况下,尽量避免这种情况并正确设计模型,因此可以将其注入到控制器中并用于您的视图中。
特别是某些方法需要应用程序内的全局可访问性。 为了使它成为可能,您可以在$ rootScope中定义'common'属性,并在应用程序引导过程中将其绑定到commonModel:
angular.module('app', ['app.common'])
.config(...)
.run(['$rootScope', 'commonModel', function ($rootScope, commonModel) {
$rootScope.common = 'commonModel';
}]);
你所有的全球方法都将生活在'共同'属性内。 这是某种名称空间 。
但不要直接在$ rootScope中定义任何方法。 这可能会导致在视图范围内与ngModel指令一起使用时出现意外行为,通常会抛弃您的范围并导致范围方法覆盖问题。
资源
资源可让您与不同的数据源进行交互。
应该采用单一责任原则来实施。
特别是它是HTTP / JSON端点的可重用代理。
资源被注入模型并提供发送/检索数据的可能性。
资源实施
一个创建资源对象的工厂,可让您与RESTful服务器端数据源进行交互。
返回的资源对象具有提供高级行为的操作方法,而无需与低级$ http服务进行交互。
服务
模型和资源都是服务 。
服务是无关联的, 松散耦合的功能单元是自包含的。
服务是Angular从服务器端向客户端Web应用程序提供的一项功能,服务已经在很长一段时间内普遍使用。
Angular应用中的服务是使用依赖注入连接在一起的可替换对象。
Angular带有不同类型的服务。 每个人都有自己的用例。 详情请参阅了解服务类型。
尝试在应用程序中考虑服务架构的主要原则。
一般来说,根据Web服务术语表:
服务是一种抽象资源,表示从提供者实体和请求者实体的角度来执行形成一致功能的任务的能力。 要使用,服务必须由具体的提供商代理实现。
客户端结构
通常,应用程序的客户端被分割成模块 。 每个模块应作为一个单元进行测试 。
尝试根据功能/功能或视图来定义模块,而不是按类型。 详情请看Misko的介绍。
模块组件可按传统方式按照控制器,模型,视图,过滤器,指令等类型进行分组。
但模块本身保持可重用 , 可 转移和可测试 。
开发人员也更容易找到代码的一些部分及其所有依赖关系。
有关详细信息,请参阅大型AngularJS和JavaScript应用程序中的代码组织。
文件夹结构的一个例子 :
|-- src/
| |-- app/
| | |-- app.js
| | |-- home/
| | | |-- home.js
| | | |-- homeCtrl.js
| | | |-- home.spec.js
| | | |-- home.tpl.html
| | | |-- home.less
| | |-- user/
| | | |-- user.js
| | | |-- userCtrl.js
| | | |-- userModel.js
| | | |-- userResource.js
| | | |-- user.spec.js
| | | |-- user.tpl.html
| | | |-- user.less
| | | |-- create/
| | | | |-- create.js
| | | | |-- createCtrl.js
| | | | |-- create.tpl.html
| |-- common/
| | |-- authentication/
| | | |-- authentication.js
| | | |-- authenticationModel.js
| | | |-- authenticationService.js
| |-- assets/
| | |-- images/
| | | |-- logo.png
| | | |-- user/
| | | | |-- user-icon.png
| | | | |-- user-default-avatar.png
| |-- index.html
角度应用程序结构的好例子由angular-app实现 - https://github.com/angular-app/angular-app/tree/master/client/src
这也被现代应用程序生成器所考虑 - https://github.com/yeoman/generator-angular/issues/109
我相信伊戈尔承担这一点,正如你所提供的报价所看到的,只是一个更大问题的冰山一角。
MVC及其衍生产品(MVP,PM,MVVM)在一个代理中都很好,并且很好用,但服务器 - 客户端架构适用于所有目的的双代理系统,而人们往往对这些模式非常着迷,以至于他们忘记了手头的问题要复杂得多。 通过试图坚持这些原则,他们实际上最终会出现一个有缺陷的架构。
让我们一点一点地做这件事。
准则
查看
在Angular上下文中,视图是DOM。 准则是:
做:
别:
作为诱人,简短和无害,这看起来:
ng-click="collapsed = !collapsed"
它几乎意味着任何开发人员现在都了解系统如何检查他们需要检查的Javascript文件和HTML文件。
控制器
做:
别:
最后一条准则的原因是管制员是姐妹,而不是实体; 也不可重复使用。
你可以争辩说,指令是可重用的,但指令也是姐妹观看(DOM) - 它们从来没有打算与实体相对应。
当然,有时候视图代表实体,但这是一个相当特殊的情况。
换句话说,控制者应该把注意力集中在表现上 - 如果你投入商业逻辑,你不仅可能最终得到一个膨胀的,可控制的小控制器,而且你也违反了关注分离原则。
因此,Angular中的控制器实际上更多是Presentation Model或MVVM 。
所以,如果管制员不应该处理业务逻辑,谁应该?
什么是模型?
你的客户模型往往是局部的和陈旧的
除非您正在编写离线Web应用程序或非常简单的应用程序(很少实体),否则您的客户端模型很可能是:
真正的模型必须坚持下去
在传统的MCV中,模型是唯一被坚持的东西。 每当我们谈论模型时,都必须坚持这一点。 您的客户可能会随意操作模型,但在成功完成服务器往返之前,该作业尚未完成。
后果
以上两点应该谨慎 - 您客户所持有的模式只能涉及部分,大部分简单的业务逻辑。
因此,在客户端上下文中使用小写的M
也许是明智的 - 因此它确实是mVC , mVP和mVVm 。 大M
代表服务器。
商业逻辑
也许关于商业模式的一个最重要的概念是,你可以将它们细分为两种类型(我省略了第三种观点 - 商业模式,因为这是另一天的故事):
firstName
和sirName
属性的模型,像getFullName()
这样的getter可以被认为是应用程序无关的。 需要强调的是,这些在客户环境中都不是“真正的”业务逻辑 - 它们只处理对客户很重要的部分。 应用程序逻辑(不是域逻辑)应该有促进与服务器通信和大多数用户交互的责任; 而领域逻辑主要是小规模,特定实体和演示驱动。
问题仍然存在 - 你在哪里把它们放在角度应用程序中?
3 vs 4层架构
所有这些MVW框架使用3层:
但对于客户来说,这有两个基本问题:
这一战略的替代方案是4层策略:
这里真正的交易是应用程序业务规则层(用例),这经常在客户端出现问题。
该层由交互者(Bob叔叔)实现,这几乎与Martin Fowler所称的操作脚本服务层相同。
具体的例子
考虑下面的Web应用程序:
现在应该发生一些事情:
我们在哪里扔掉所有这些?
如果您的架构涉及调用$resource
的控制器,则所有这些都将在控制器内发生。 但是有一个更好的策略。
建议的解决方案
下图显示了如何通过在Angular客户端中添加另一个应用程序逻辑层来解决上述问题:
所以我们在controller到$ resource之间添加一个图层,这个图层(让我们称之为交互图层):
UserInteractor
。 因此,根据以上具体示例的要求:
validate()
validate()
方法。 createUser()
调用交互器 与Artem的答案中的伟大建议相比,这是一个小问题,但在代码可读性方面,我发现最好在return
对象内部完全定义API,以尽量减少代码中来回查看是否定义了变量。
angular.module('myModule', [])
// or .constant instead of .value
.value('myConfig', {
var1: value1,
var2: value2
...
})
.factory('myFactory', function(myConfig) {
...preliminary work with myConfig...
return {
// comments
myAPIproperty1: ...,
...
myAPImethod1: function(arg1, ...) {
...
}
}
});
如果return
对象看起来“太拥挤”,那表明该服务正在做的太多了。