在AngularJS负责人Igor Minar的这篇文章中:
MVC vs MVVM vs MVP。许多开发人员可能花费数小时来辩论和争论,这是一个有争议的话题。
几年来,AngularJS更接近于MVC(或者更确切地说,它的客户端的一个变体),但随着时间的推移和感谢许多重构和API改进,它现在已接近对MVVM -在$范围对象可以被认为是视图模型正被由我们称为Controller的函数修饰。
能够对框架进行分类并将其放入MV *存储桶之一具有一些优势。通过使创建代表使用框架构建的应用程序的思维模型更容易,它可以帮助开发人员更熟悉其api。它还可以帮助建立开发人员使用的术语。
话虽如此,我宁愿看到开发人员构建经过精心设计并遵循关注点分离的踢屁股应用程序,也不愿看到他们浪费时间争论MV *废话。出于这个原因,我特此声明
AngularJS为MVW框架-Model-View-Whatever。凡代表什么“对您有用”。Angular为您提供了很大的灵活性,可以很好地将表示逻辑与业务逻辑和表示状态分开。请使用它来提高您的生产力和应用程序可维护性,而不是热烈讨论那些最终无关紧要的事情。
在客户端应用程序中实现AngularJS MVW(模型-视图-任何)设计模式是否有任何建议或准则?
感谢大量有价值的资源,我为在AngularJS应用程序中实现组件提供了一些一般性建议:
控制者
-
控制器应该只是模型和视图之间的中间层。尝试使其尽可能薄。
-
强烈建议避免在控制器中使用业务逻辑。应该将其移至模型。
-
控制器可以使用方法调用(在孩子想与父母进行通信时)或$ emit,$ broadcast和$ on方法与其他控制器进行通信。发出和广播的消息应保持最少。
-
控制器不应该关心表示或DOM操作。
-
尽量避免嵌套控制器。在这种情况下,父控制器被解释为模型。而是将模型作为共享服务注入。
-
控制器的作用域应用于与视图绑定模型,并像表示模型设计模式那样
封装视图模型。
范围
如治疗范围只读模板和只写在控制器。范围的目的是引用模型,而不是模型。
在进行双向绑定(ng-model)时,请确保您不直接绑定到范围属性。
模型
AngularJS中的模型是service定义的单例。
模型提供了分离数据和显示的绝佳方法。
模型是单元测试的主要候选者,因为它们通常仅具有一种依赖关系(某种形式的事件发射器,在通常情况下为$ rootScope)并且包含高度可测试的域逻辑。
-
模型应被视为特定单元的实现。它基于单一职责原则。单元是一个实例,负责其自身的相关逻辑范围,该逻辑可以表示现实世界中的一个实体,并在编程世界中根据数据和状态对其进行描述。
-
模型应封装您的应用程序数据,并提供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';
}]);
您的所有全局方法都将位于“ common ”属性内。这是某种名称空间。
但是不要直接在$ rootScope中定义任何方法。在视图范围内与ngModel指令一起使用时,这可能导致意外的行为,通常使您的范围乱七八糟,并导致范围方法覆盖问题。
资源资源
资源使您可以与不同的数据源进行交互。
应该使用single-responsibility-principle实现。
在特定情况下,它是HTTP / JSON端点的可重用代理。
资源被注入模型中,并提供了发送/检索数据的可能性。
资源实施
工厂创建一个资源对象,使您可以与RESTful服务器端数据源进行交互。
返回的资源对象具有操作方法,这些方法可提供高级行为,而无需与低级$ http服务进行交互。
服务
模型和资源都是服务。
服务是独立的,独立的,松散耦合的功能单元。
服务是Angular从服务器端带到客户端Web应用程序的一项功能,在该服务中,服务已被长期使用。
Angular应用程序中的服务是可替换对象,这些对象使用依赖项注入连接在一起。
Angular带有不同类型的服务。每个人都有自己的用例。有关详细信息,请阅读了解服务类型。
尝试考虑应用程序中服务体系结构的主要原理。
通常根据Web服务词汇表:
服务是一种抽象资源,代表从提供者实体和请求者实体的角度来看,执行形成一致功能的任务的能力。要使用,服务必须由具体的提供者代理来实现。
客户端结构
通常,应用程序的客户端分为模块。每个模块都应作为一个单元进行测试。
尝试根据功能/特性或视图(而不是类型)定义模块。有关详细信息,请参见Misko的演示。
按照惯例,模块组件可以按类型分组,例如控制器,模型,视图,过滤器,指令等。
但是模块本身仍然是可重用,可转让和可测试的。
对于开发人员来说,查找代码的某些部分及其所有依赖性也要容易得多。
有关详细信息,请参阅Large 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
从您提供的报价中可以看出,我相信Igor对此的看法只是一个更大问题的冰山一角。
MVC及其派生产品(MVP,PM,MVVM)在单个代理程序中都是好事,但服务器-客户端体系结构出于所有目的都是两个代理程序的系统,人们常常被这些模式所困扰,以至于忘记了当前的问题要复杂得多。通过努力遵守这些原则,他们实际上最终会出现一个有缺陷的体系结构。
让我们一点一点地做。
指引
观看次数
在Angular上下文中,视图是DOM。准则是:
做:
- 当前作用域变量(只读)。
- 调用控制器进行操作。
别:
- 提出任何逻辑。
诱人,简短而无害的外观如下:
ng-click="collapsed = !collapsed"
这几乎意味着任何开发人员现在都需要了解检查Javascript文件和HTML文件所需的系统工作方式。
控制器
做:
- 通过将数据放在合并范围内将视图绑定到“模型”。
- 响应用户操作。
- 处理表示逻辑。
别:
- 处理任何业务逻辑。
最后一条准则的原因是,控制者是视图的姐妹,而不是实体。它们也不是可重用的。
您可能会争辩说指令是可重用的,但指令也是视图(DOM)的姐妹-它们从未打算与实体相对应。
当然,有时视图表示实体,但这是一个相当具体的情况。
换句话说,控制器应专注于呈现-如果您加入业务逻辑,不仅最终可能会导致膨胀的,难以管理的控制器,而且还违反了关注点分离原则。
因此,Angular中的控制器实际上更多是Presentation Model或MVVM。
因此,如果控制器不应该处理业务逻辑,那么谁应该呢?
什么是模特?
您的客户模型通常是不完整的
除非编写脱机Web应用程序或非常简单的应用程序(很少实体),否则客户端模型很可能是:
- 部分的
- 要么没有所有实体(例如在分页的情况下)
- 或它没有所有数据(例如分页)
- 陈旧-如果系统有多个用户,则在任何时候都无法确保客户端拥有的模型与服务器拥有的模型相同。
真实模型必须坚持
在传统的MCV,该模型是被唯一坚持。每当我们谈论模型时,都必须在某些时候坚持下来。您的客户端可以随意操作模型,但是直到成功完成与服务器的往返操作,该工作才得以完成。
后果
以上两点应作为警告-客户拥有的模型仅涉及部分,主要是简单的业务逻辑。
因此,在客户端上下文中使用小写字母可能是明智的选择,M
因此实际上是mVC,mVP和mVVm。最大的M
是服务器。
商业逻辑
关于业务模型的最重要的概念之一可能是可以将它们细分为两种类型(我省略了第三种视图业务,因为这是另一回事了):
- 域逻辑-aka企业业务规则,即与应用程序无关的逻辑。例如,给一个具有
firstName
和sirName
属性的模型,getFullName()
可以将getter之类的应用视为独立于应用程序的。 - 应用程序逻辑-aka应用程序业务规则,它是特定于应用程序的。例如,错误检查和处理。
需要强调的是,在客户端上下文中,这两者都不是“真实的”业务逻辑-它们仅处理对客户端重要的部分。应用程序逻辑(不是域逻辑)应负责促进与服务器的通信以及大多数用户的交互;而领域逻辑在很大程度上是小规模的,特定于实体的并且是表示驱动的。
问题仍然存在-您将它们扔在角度应用程序中的什么位置?
3 vs 4层架构
所有这些MVW框架都使用3层:
但是,涉及到客户时有两个基本问题:
- 该模型是局部的,过时的并且不会持久。
- 没有放置应用程序逻辑的地方。
此策略的替代方法是4层策略:
真正的问题是应用程序业务规则层(用例),该层通常对客户不满意。
该层由交互者(鲍勃叔叔)实现,这几乎就是马丁·福勒(Martin Fowler)所称的操作脚本服务层。
具体例子
考虑以下Web应用程序:
- 该应用程序显示用户的分页列表。
- 用户单击“添加用户”。
- 将打开一个带有表单的模型,以填充用户详细信息。
- 用户填写表格并点击提交。
现在应该发生一些事情:
- 该表格应经过客户验证。
- 请求应发送到服务器。
- 如果有错误,则应进行处理。
- 用户列表可能(也可能不需要)(由于分页)需要更新。
我们将所有这些丢到哪里?
如果您的体系结构涉及调用的控制器$resource
,则所有这些都将在控制器内发生。但是有更好的策略。
拟议的解决方案
下图显示了如何通过在Angular客户端中添加另一个应用程序逻辑层来解决上述问题:
因此,我们在$ resource控制器之间添加了一层,这一层(我们称其为interactor):
- 是一种服务。对于用户,它可以称为
UserInteractor
。 - 它提供了与用例相对应的方法,封装了应用程序逻辑。
- 它控制对服务器的请求。该层可以确保对服务器的请求返回的数据可以由域逻辑执行,而不是由控制器使用自由格式的参数调用$ resource。
- 它使用域逻辑原型装饰返回的数据结构。
因此,根据上述具体示例的要求:
- 用户单击“添加用户”。
- 控制器向交互器询问空白的用户模型,并用业务逻辑方法修饰,例如
validate()
- 提交后,控制器将调用模型
validate()
方法。 - 如果失败,则控制器处理错误。
- 如果成功,则控制器使用以下命令调用交互器
createUser()
- 交互者调用$ resource
- 响应后,交互器会将所有错误委托给控制器,由控制器进行处理。
- 成功响应后,交互器将确保在需要时更新用户列表。
与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
对象看起来“太拥挤”,则表明该服务做得太多。
AngularJS并非以传统方式实现MVC,而是实现了更接近MVVM(Model-View-ViewModel)的东西,ViewModel也可以称为活页夹(在角度情况下,它可以是$ scope)。模型->我们知道,角度模型可以只是普通的旧JS对象或应用程序中的数据
View-> angularJS中的视图是angularJS通过应用指令,指令或绑定来解析和编译的HTML,此处的要点是有角,输入不仅是纯HTML字符串(innerHTML),而是是由浏览器创建的DOM。
ViewModel-> ViewModel实际上是在您的视图和模型之间的绑定器/桥梁,在angularJS情况下,它是$ scope,用于初始化和扩展我们使用Controller的$ scope。
如果我想总结答案:在angularJS应用程序中,$ scope引用了数据,Controller控制了行为,View通过与控制器交互以相应地执行操作来处理布局。
为了清楚地回答这个问题,Angular使用了我们在常规编程中已经遇到的不同设计模式。1)当我们注册有关模块的控制器或指令,工厂,服务等时。这里它正在隐藏全局空间中的数据。这是模块模式。2)当angular使用其脏检查来比较范围变量时,此处使用Observer Pattern。3)我们控制器中的所有父子范围都使用原型模式。
4)在注入服务的情况下,它使用Factory Pattern。
总体而言,它使用不同的已知设计模式来解决问题。
文章标签:angularjs , architecture , client-side , design-patterns , javascript
版权声明:本文为原创文章,版权归 javascript 所有,欢迎分享本文,转载请保留出处!
评论已关闭!