AngularJS:了解devise模式

在AngularJS的领导者Igor Minar的这篇文章中 ,

MVC vs MVVM vs MVP 。 这是一个多么有争议的话题,许多开发人员可以花费数小时来讨论和争论。

多年来,AngularJS更接近于MVC(或者更确切地说是它的一个客户端变体),但是随着时间的推移以及许多重构和API改进,现在更接近于MVVM$ scope对象可以被视为ViewModel由一个我们称之为Controller的函数来装饰。

能够将框架分类并将其放入MV *桶中的一个具有一些优点。 它可以帮助开发人员更轻松地创build一个代表正在使用框架构build的应用程序的心智模型。 它也可以帮助build立开发人员使用的术语。

话虽如此,我宁愿看开发人员build立devise良好,并关注分离的踢屁股的应用程序,而不是看他们浪费时间争论MV *废话。 为此,我在此声明AngularJSMVW框架 – Model-View-Whatever 。 无论什么地方代表着“ 无论你为什么工作 ”。

Angular为您提供了很大的灵活性,可以很好地将表示逻辑从业务逻辑和表示状态中分离出来。 请使用它来提高您的工作效率和应用程序的可维护性,而不是对一天结束时的事情进行激烈的讨论。

有没有在客户端应用程序中实现AngularJS MVW(Model-View-Whatever)devise模式的build议或指导原则?

感谢大量有价值的资源,我已经在AngularJS应用程序中实现了一些一般的build议:


调节器

  • 控制器应该只是模型和视图之间的中间层 。 尽量使其尽可能

  • 强烈build议避免在控制器中的业务逻辑 。 应该转移到模型。

  • 控制器可以使用方法调用与其他控制器通信(当孩子想要与父母通信时可能)或$ emit$ broadcast$ on方法。 发射和广播的信息应该保持在最低限度。

  • 控制器不应该关心表示或DOM操作。

  • 尽量避免嵌套的控制器 。 在这种情况下,父级控制器被解释为模型。 将模型注入为共享服务。

  • 控制器中的范围应该用于绑定模型与视图和
    封装视图模型至于介绍模型devise模式。


范围

在范本中将范围视为只读,在控制器 中将范围视为 。 范围的目的是指模型,而不是模型。

在进行双向绑定(ng-model)时,请确保您不直接绑定到范围属性。


模型

AngularJS中的模型是由服务定义的单例

模型提供了分离数据和显示的极好方法。

模型是unit testing的主要候选者,因为它们通常只有一个依赖项(某种forms的事件发生器,通常情况下是$ rootScope ),并且包含高度可testing的域逻辑

  • 模型应被视为特定单元的实现。 它是基于单一责任原则。 单元是一个负责自己的相关逻辑范围的实例,它可以代表真实世界中的单个实体,并在数据和状态方面在编程世界中进行描述。

  • 模型应该封装你的应用程序的数据,并提供一个API来访问和操作这些数据。

  • 模型应该是便携式的,因此可以很容易地运输到类似的应用程序

  • 通过隔离模型中的单元逻辑,您可以更轻松地定位,更新和维护。

  • 模型可以使用整个应用程序中常见的更一般的全局模型的方法。

  • 尽量避免使用dependency injection将其他模型组合到您的模型中,如果它不依赖于减less组件耦合并增加单元可testing性可用性

  • 尽量避免在模型中使用事件侦听器。 这使得它们更难以testing,并且通常以单一责任原则来杀死模型。

模型实现

因为模型应该在数据和状态方面封装一些逻辑,所以它应该在架构上限制对它的成员的访问,这样我们可以保证松耦合。

在AngularJS应用程序中使用的方法是使用工厂服务types来定义它。 这将使我们能够非常容易地定义私有属性和方法,并且还可以在单​​个地方返回可公开访问的地方,从而使开发人员可以读取它们。

一个例子

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 }; }); 

创build新的实例

尽量避免有一个工厂返回一个新的function,因为这开始打乱dependency injection和图书馆将performance不好,尤其是对第三方。

一个更好的方法来完成同样的事情是使用工厂作为一个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 // ... }; }); 

全球模式

一般来说,尽量避免这种情况,并正确devise模型,这样可以将其注入到控制器中,并在您的视图中使用。

在特定情况下,某些方法需要应用程序内的全局可访 为了使它成为可能,你可以在$ rootScope中定义' common '属性,并在应用程序引导过程中将其绑定到commonModel

 angular.module('app', ['app.common']) .config(...) .run(['$rootScope', 'commonModel', function ($rootScope, commonModel) { $rootScope.common = 'commonModel'; }]); 

你所有的全球方法都将生活在“ 共同 ”的财产之内。 这是某种名字空间

但是不要直接在$ rootScope中定义任何方法。 当在视图范围内与ngModel指令一起使用时,这可能会导致意想不到的行为 ,通常会抛弃您的范围并导致范围方法覆盖问题。


资源

资源可让您与不同的数据源进行交互。

应该采用单一责任原则来实施。

在特殊情况下,它是HTTP / JSON端点的可重用代理。

资源被注入到模型中,并提供发送/检索数据的可能性。

资源实施

一个创build资源对象的工厂,使您可以与RESTful服务器端数据源进行交互。

返回的资源对象具有提供高级行为的操作方法,而不需要与低级$ http服务进行交互。


服务

模型和资源都是服务

服务是无关联的, 松散耦合的function单元是自包含的。

服务是Angular从服务器端向客户端Web应用程序提供的一项function,服务已经在很长时间内被广泛使用。

Angular应用程序中的服务是使用dependency injection连接在一起的可replace对象。

Angular带有不同types的服务。 每个人都有自己的用例。 详情请参阅了解服务types 。

尝试在应用程序中考虑服务体系结构的主要原则 。

一般来说根据Web服务术语表 :

服务是一种抽象资源,表示从提供者实体和请求者实体的angular度来执行形成一致function的任务的能力。 要使用,服务必须由具体的提供者代理来实现。


客户端结构

通常,应用程序的客户端被分解成模块 。 每个模块应该作为一个单元进行testing

尝试根据function/function视图来定义模块,而不是按types。 详情请看Misko的介绍 。

模块组件可按传统方式按照控制器,模型,视图,filter,指令等types进行分组。

但是模块本身仍然是可重用的 转移的可testing的

开发人员也更容易find代码的一部分及其所有的依赖关系。

有关详细信息,请参阅大型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度应用程序结构的好例子是通过angular-app实现的 – https://github.com/angular-app/angular-app/tree/master/client/src

这也被现代的应用程序生成器所考虑 – https://github.com/yeoman/generator-angular/issues/109

我相信伊戈尔承担这一点,正如你所提供的报价所看到的,只是一个更大的问题的冰山一angular。

MVC及其衍生产品(MVP,PM,MVVM)在一个代理中都是很好的,但是一个服务器 – 客户端架构适用于所有目的的双代理系统,人们往往对这些模式非常着迷,手头的问题要复杂得多。 通过试图坚持这些原则,他们实际上最终有一个有缺陷的架构。

我们一点一点地做。

准则

查看

在Angular上下文中,视图是DOM。 准则是:

做:

  • 当前范围variables(只读)。
  • 调用控制器来执行操作。

别:

  • 放任何逻辑。

作为诱人的,简短的,无害的,这看起来像:

 ng-click="collapsed = !collapsed" 

它几乎意味着任何开发人员现在了解系统如何工作,他们需要检查Javascript文件和HTML文件。

控制器

做:

  • 通过在范围上放置数据将视图绑定到“模型”。
  • 回应用户操作。
  • 处理演示逻辑。

别:

  • 处理任何业务逻辑。

最后一条准则的原因是pipe制员是姐妹,而不是实体。 也不可重复使用。

你可以争辩说,指令是可重用的,但指令也是姐妹来观看(DOM) – 他们从来没有打算与实体相对应。

当然,有时意见代表实体,但这是一个相当具体的情况。

换句话说,控制者应该把注意力集中在performance上 – 如果你把业务逻辑放进去,不但你可能最终得到一个膨胀的,pipe理得很less的控制器,而且你也违反了关注分离原则。

因此,Angular中的控制器实际上更多是Presentation ModelMVVM

所以,如果pipe制员不应该处理业务逻辑,谁应该?

什么是模型?

你的客户模式往往是局部的和陈旧的

除非你正在编写一个离线的web应用程序,或者一个非常简单的应用程序(很less的实体),否则你的客户端模型很可能是:

  • 局部
    • 要么它没有所有的实体(如在分页的情况下)
    • 或者它没有所有的数据(比如在分页的情况下)
  • 陈旧 – 如果系统有多个用户,那么在任何时候您都不能确定客户端拥有的模型与服务器所持有的模型是否一致。

真正的模式必须坚持

在传统的MCV中,模型是唯一被坚持的东西。 每当我们谈论模型时,都必须坚持到某一点。 你的客户可以随意操作模型,但是直到服务器往返成功完成,工作就没有完成。

后果

以上两点应该谨慎 – 您的客户所持有的模式只能涉及部分,大部分简单的业务逻辑。

因此,在客户端环境下,使用小写的M也许是明智的 – 所以它就是mVCmVPmVVm 。 大M是服务器。

商业逻辑

也许关于商业模式的最重要的概念之一是,你可以将它们细分为两种types(我省略了第三种观点 – 商业模式,因为这是另外一天的故事):

  • 域逻辑 – 又名企业业务规则 ,是独立于应用程序的逻辑。 例如,给一个带有firstNamesirName属性的模型,像getFullName()这样的getter可以被认为是应用程序无关的。
  • 应用程序逻辑 – 又名应用程序业务规则 ,这是应用程序特定的。 例如,错误检查和处理。

需要强调的是,在客户环境中这两种情况都不是“真正的”业务逻辑 – 它们只处理对客户很重要的部分。 应用程序逻辑(不是域逻辑)应该有促进与服务器通信和大部分用户交互的责任; 而领域逻辑主要是小规模的,特定于实体的和呈现驱动的。

问题仍然存在 – 你把它们扔在一个angular度应用程序中?

3 vs 4层架构

所有这些MVW框架使用3层:

三个圈子。内部模型,中间控制器,外部视图

但是对于客户来说,这有两个基本的问题:

  • 模型是部分的,陈旧的,不会持续下去。
  • 没有地方放置应用程序逻辑。

这一策略的另一种select是4层策略 :

从内到外4个圈子 - 企业业务规则,应用业务规则,接口适配器,框架和驱动程序

这里真正的交易是应用程序业务规则层(Use Cases),这在客户端经常会出现问题。

这个层是由交互者(Bob叔叔)实现的,这几乎是Martin Fowler所说的操作脚本服务层

具体的例子

考虑下面的Web应用程序:

  • 该应用程序显示用户的分页列表。
  • 用户点击“添加用户”。
  • 模型将打开一个表单来填充用户的详细信息。
  • 用户填写表单并点击提交。

现在应该发生一些事情:

  • 表单应该是客户端validation的。
  • 一个请求应该被发送到服务器。
  • 如果有错误,则应处理错误。
  • 用户列表可能会(也可能不会)(由于分页)需要更新。

我们在哪里扔这一切?

如果你的架构涉及一个调用$resource的控制器,所有这些都将在控制器内部发生。 但是有一个更好的策略。

build议的解决scheme

下图显示了如何通过在Angular客户端中添加另一个应用程序逻辑层来解决上述问题:

4个盒子 -  DOM指向Controller,指向应用程序逻辑,指向$ resource

所以我们在控制器到$ resource之间添加一个图层,这个图层(让我们称之为交互图层):

  • 是一项服务 。 在用户的情况下,它可能被称为UserInteractor
  • 它提供了与用例相对应的方法, 封装了应用程序逻辑
  • 控制对服务器的请求。 而不是控制器使用自由forms参数调用$ resource,这个层确保向服务器发出的请求返回域逻辑可以执行的数据。
  • 它用域逻辑原型来装饰返回的数据结构。

所以,根据以上具体例子的要求:

  • 用户点击“添加用户”。
  • 控制器询问交互器的空白用户模型,用业务逻辑方法来装饰,比如validate()
  • 提交后,控制器调用模型validate()方法。
  • 如果失败,控制器将处理该错误。
  • 如果成功,控制器使用createUser()调用交互器
  • 交互者调用$资源
  • 响应后,交互者将任何错误委托给处理它们的控制器。
  • 在成功响应后,交互者确保在需要时更新用户列表。

与Artem的回答中的伟大build议相比,这是一个小问题,但就代码可读性而言,我发现最好在return对象内部完全定义API,以最小化在代码中来回查看是否定义了variables。

 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也可以被称为binder(在angular度情况下它可以是$ scope)。 模型 – >正如我们所知道的angular度模型可以只是普通的旧JS对象或我们的应用程序中的数据

View – > angularJS中的视图是由angularJS通过应用指令或指令或绑定进行parsing和编译的HTML,这里主要指的是input不仅仅是简单的HTMLstring(innerHTML),而是它是由浏览器创build的DOM。

ViewModel – > ViewModel实际上是您的视图和模型在angularJS之间的绑定器/桥梁,它是$ scope,初始化和增加我们使用Controller的$ scope。

如果我想总结一下答案:在angularJS应用程序中,$ scope引用了数据,Controller控制行为,而View通过与控制器交互来处理布局以相应地执行。

为了使问题更加清晰,Angular使用了我们在常规编程中遇到的不同的devise模式。 1)当我们注册我们的控制器或指令,工厂,服务等相对于我们的模块。 这里是隐藏全球空间的数据。 这是模块模式 。 2)当angular度使用它的脏检查比较范围variables,这里它使用观察者模式 。 3)我们的控制器中的所有父亲子范围使用原型模式。 4)在注入服务的情况下,它使用工厂模式

总的来说,它使用不同的已知devise模式来解决问题。