AngularJS:了解设计模式

Art*_*nov 147 javascript architecture design-patterns client-side angularjs

在AngularJS的负责人Igor Minar 的这篇文章的背景下:

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

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

能够对框架进行分类并将其放入MV*桶之一具有一些优势.它可以帮助开发人员更轻松地使用它的apis,从而更容易创建一个表示使用框架构建的应用程序的心理模型.它还可以帮助建立开发人员使用的术语.

话虽如此,我宁愿看到开发人员构建精心设计的kick-ass应用程序并遵循关注点分离,而不是看到他们浪费时间争论MV*废话.因此,我在此声明 AngularJSMVW框架 - 模型 - 视图 - 随便.无论什么代表" 对你有用的东西 ".

Angular为您提供了很大的灵活性,可以很好地将表示逻辑与业务逻辑和表示状态分开.请使用它来提高您的生产力和应用程序的可维护性,而不是热烈讨论那些在一天结束时无关紧要的事情.

在客户端应用程序中是否有任何关于实现AngularJS MVW(Model-View-Whatever)设计模式的建议或指南?

Art*_*nov 223

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


调节器

  • 控制器应该只是模型和视图之间的中间层.尽量让它变薄.

  • 强烈建议避免控制器中的业务逻辑.它应该转移到模型.

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

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

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

  • 控制器中的范围应该用于绑定模型和视图,并
    封装视图模型表示模型设计模式.


范围

将范围视为模板中的只读,并将只读控制在控制器中.范围的目的是引用模型,而不是模型.

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


模型

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

Model提供了一种分离数据和显示的绝佳方法.

模型是单元测试的主要候选者,因为它们通常只有一个依赖项(某种形式的事件发射器,通常是$ rootScope),并包含高度可测试的域逻辑.

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

  • Model应该封装您的应用程序的数据,并提供 访问和操作该数据的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
  };
});
Run Code Online (Sandbox Code Playgroud)

创建新实例

尽量避免让工厂返回一个新的功能,因为这会开始打破依赖注入,并且库会表现得很笨拙,特别是对于第三方.

完成同样事情的更好方法是使用工厂作为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
    // ...
  };
});
Run Code Online (Sandbox Code Playgroud)

全球模型

一般情况下,尽量避免这种情况并正确设计模型,因此可以将其注入控制器并在视图中使用.

在特定情况下,某些方法需要在应用程序内进行全局访 为了实现这一点,您可以在$ rootScope中定义' common '属性,并在应用程序引导期间将其绑定到commonModel:

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

您的所有全球方法都将存在于" 共同 "属性中.这是某种命名空间.

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


资源

资源允许您与不同的数据源进行交互.

应该使用单一责任原则来实施.

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

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

资源实施

一个工厂,它创建一个资源对象,使您可以与REST服务器端数据源进行交互.

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


服务

模型和资源都是服务.

服务是无关联的,松散耦合的功能单元,是独立的.

服务是Angular从服务器端为客户端Web应用程序带来的一项功能,其中服务已经很长时间使用.

Angular应用程序中的服务是使用依赖项注入连接在一起的可替换对象.

Angular提供不同类型的服务.每个人都有自己的用例.有关详细信息,请阅读了解服务类型

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

一般根据Web Services Glossary:

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


客户端结构

通常,应用程序的客户端分为模块.每个模块应作为一个单元进行测试.

尝试根据功能/功能视图定义模块,而不是按类型定义.有关详细信息,请参阅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
Run Code Online (Sandbox Code Playgroud)

角度应用程序结构的良好示例由angular-app实现 - https://github.com/angular-app/angular-app/tree/master/client/src

现代应用程序生成器也考虑了这一点 - https://github.com/yeoman/generator-angular/issues/109

  • 我有一个问题:"强烈建议避免控制器中的业务逻辑.应该将其转移到模型中." 但是从官方文档中你可以读到:"通常,Controller不应该尝试做太多.它应该只包含单个视图所需的业务逻辑." 我们在谈论同样的事情吗? (5认同)
  • 我会说 - 将Controller视为视图模型. (3认同)
  • @ChristianAichinger,这是关于JavaScript原型链的本质,它强迫你在双向绑定表达式中使用`object`来确保你写入确切的属性或`setter`函数.如果使用示波器的直接属性(_不带dot_),则在写入时,您可能会在原型链中最近的较高范围内使用新创建的属性隐藏所需的目标属性.这在[Misko的演示]中有更好的解释(http://www.youtube.com/watch?v=ZhfUv0spHCY&feature=youtu.be&t=32m51s) (2认同)

Izh*_*aki 46

我相信伊戈尔对此的看法,就像你提供的报价中所看到的那样,只是一个更大问题的冰山一角.

MVC及其衍生产品(MVP,PM,MVVM)在单个代理中都是好的和花花公子,但服务器 - 客户端架构对于所有目的而言都是一个双代理系统,人们常常对这些模式如此着迷,以至于忘记了手头的问题要复杂得多.通过努力遵守这些原则,他们实际上最终会出现一个有缺陷的架构.

让我们一点一点地做这件事.

准则

查看

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

做:

  • 当前范围变量(只读).
  • 调用控制器进行操作.

别:

  • 放任何逻辑.

这看起来很诱人,简短而无害:

ng-click="collapsed = !collapsed"
Run Code Online (Sandbox Code Playgroud)

它几乎意味着任何开发人员现在都要了解系统如何工作以检查Javascript文件和HTML文件.

控制器

做:

  • 通过在范围上放置数据将视图绑定到"模型".
  • 响应用户操作.
  • 处理表示逻辑.

别:

  • 处理任何业务逻辑.

最后一条准则的原因是控制者是观点的姐妹,而不是实体; 也不是可以重复使用的.

您可以争辩说指令是可重用的,但指令也是视图的姐妹(DOM) - 它们从不打算与实体对应.

当然,有时视图代表实体,但这是一个相当具体的案例.

换句话说,控制器应该专注于演示 - 如果你抛出业务逻辑,不仅你可能最终得到一个膨胀的,易于管理的控制器,而且你也违反了关注分离原则.

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

因此,如果控制器不应该处理业务逻辑,谁应该?

什么是模特?

您的客户端模型通常是部分和陈旧的

除非您正在编写脱机Web应用程序或非常简单的应用程序(少数实体),否则您的客户端模型很可能是:

  • 局部
    • 要么它没有所有实体(如在分页的情况下)
    • 或者它没有所有数据(例如在分页的情况下)
  • 陈旧 - 如果系统有多个用户,则无论何时您都无法确定客户端所持有的模型与服务器所持有的模型相同.

真正的模型必须坚持下去

在传统的MCV中,模型是唯一被持久化的东西.每当我们谈论模型时,必须在某些时候坚持这些模型.您的客户端可以随意操作模型,但在成功完成到服务器的往返之前,该作业尚未完成.

后果

上述两点应该是一个警告 - 您的客户端所持有的模型只能涉及部分的,大多数是简单的业务逻辑.

因此,在客户端环境中使用小写也许是明智的M- 因此它实际上是mVC,mVPmVVm.最重要的M是服务器.

商业逻辑

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

  • 域逻辑 - 又名企业业务规则,与应用程序无关的逻辑.例如,给出一个带有firstNamesirName属性的模型,类似于getter getFullName()可以被认为是与应用程序无关的.
  • 应用程序逻辑 - 也称应用程序业务规则,它是特定于应用程 例如,错误检查和处理.

重要的是要强调,客户端上下文中的这两者都不是"真正的"业务逻辑 - 它们只处理对客户端重要的部分.应用程序逻辑(不是域逻辑)应该负责促进与服务器的通信和大多数用户交互; 而域逻辑主要是小规模的,特定于实体的,并且是表示驱动的.

问题仍然存在 - 你将它们放在一个有角度的应用程序中?

3对4层架构

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

三个圆圈. 内模型,中控制器,外视图

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

  • 该模型是部分的,陈旧的并且不会持久.
  • 没有地方放置应用程序逻辑.

该战略的另一种选择是4层战略:

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

这里的真正优势是应用程序业务规则层(用例),这通常会对客户造成不利影响.

这个层是由交互者(Uncle Bob)实现的,这正是Martin Fowler所谓的操作脚本服务层.

具体例子

考虑以下Web应用程序:

  • 该应用程序显示了一个分页的用户列表.
  • 用户点击"添加用户".
  • 将打开一个模型,其中包含用于填写用户详细信
  • 用户填写表单并点击提交.

现在应该发生一些事情:

  • 表单应该是客户端验证的.
  • 请求应发送到服务器.
  • 如果存在错误,则应处理错误.
  • 用户列表可能会或可能不会(由于分页)需要更新.

我们在哪里抛出所有这些?

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

建议的解决方案

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

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

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

  • 是一项服务.在用户的情况下,可以调用它UserInteractor.
  • 它提供了与用例相对应的方法,封装了应用程序逻辑.
  • 控制对服务器的请求.该层不是控制器使用自由格式参数调用$ resource,而是确保对服务器的请求返回关于哪个域逻辑可以执行的数据.
  • 它使用域逻辑原型装饰返回的数据结构.

因此,根据上述具体示例的要求:

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


Dmi*_*sev 5

与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, ...) {
    ...
    }
  }
});
Run Code Online (Sandbox Code Playgroud)

如果return对象看起来"太拥挤",那就表明该服务做得太多了.