AngularJS:了解设计模式


147

在AngularJS负责人Igor Minar 的这篇文章中:

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

几年来,AngularJS更加接近于MVC(或其客户端变体之一),但是随着时间的流逝,由于许多重构和api的改进,它现在更加接近MVVM$ scope对象可以被认为是正在被开发的ViewModel。由我们称为Controller的函数修饰。

能够对框架进行分类并将其放入MV *存储桶之一具有一些优势。通过使创建代表使用框架构建的应用程序的思维模型更容易,它可以帮助开发人员更熟悉其api。它还可以帮助建立开发人员使用的术语。

话虽如此,我宁愿看到开发人员构建精心设计并遵循关注点分离的踢屁股应用程序,而不希望他们浪费时间争论MV *废话。出于这个原因,我特此声明 AngularJSMVW框架-Model-View-Whatever。凡代表什么“ 对您有用 ”。

Angular为您提供了很大的灵活性,可以很好地将表示逻辑与业务逻辑和表示状态分开。请使用它来提高您的生产力和应用程序可维护性,而不必热烈讨论最后没什么大不了的事情。

在客户端应用程序中实现AngularJS MVW(模型-视图-任何)设计模式是否有任何建议或准则?


赞成...而不是看到他们浪费时间争论MV *废话。
Shirgill Farhan

1
您不需要Angular遵循单词类设计模式。
有用蜜蜂

Answers:


223

感谢大量宝贵的资源,我为在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


5
我担心一个问题:“强烈建议避免在控制器中使用业务逻辑。应将其移至模型中。” 但是,从官方文档中,您可以阅读:“通常,Controller不应尝试做太多事情。它应该只包含单个视图所需的业务逻辑。” 我们在谈论同一件事吗?
op1ekun 2014年

3
我会说-将控制器视为视图模型。
Artem Platonov 2014年

1
+1。这里有一些很棒的建议!2.不幸的是,该示例searchModel未遵循可重用性建议。最好通过constant服务导入常量。3.这里有什么解释吗?Try to avoid having a factory that returns a new able function
Dmitri Zaitsev 2014年

1
同样,覆盖对象的prototype属性也破坏了继承,而是可以使用Car.prototype.save = ...
Dmitri Zaitsev 2014年

2
@ChristianAichinger,这与JavaScript原型链的本质有关,它迫使您要么object在双向绑定表达式中使用,以确保您写入确切的属性或setter函数。如果使用范围的直接属性(不带点),则在写入原型时,可能会在原型链中最接近的上部范围中隐藏新创建的目标属性。在Misko的演示文稿中
Artem Platonov

46

我相信Igor对此的看法,正如您提供的报价所示,只是一个更大问题的冰山一角。

MVC及其派生产品(MVP,PM,MVVM)在单个代理程序中都是不错的选择,但是服务器-客户端体系结构出于所有目的都是两个代理程序的系统,人们常常被这些模式所困扰,以至于他们忘记了当前的问题要复杂得多。通过尝试遵守这些原则,他们实际上最终会出现一个有缺陷的体系结构。

让我们一点一点地做。

指引

观看次数

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

做:

  • 当前作用域变量(只读)。
  • 致电控制器以采取行动。

别:

  • 提出任何逻辑。

诱人,简短而无害的外观如下:

ng-click="collapsed = !collapsed"

这几乎意味着任何开发人员现在都必须了解检查Javascript文件和HTML文件所需的系统工作方式。

控制器

做:

  • 通过将数据放在合并范围内,将视图绑定到“模型”。
  • 响应用户操作。
  • 处理表示逻辑。

别:

  • 处理任何业务逻辑。

最后一条准则的原因是,控制者是视图的姐妹,而不是实体。它们也不是可重用的。

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

当然,有时视图表示实体,但这是一个相当具体的情况。

换句话说,控制器应专注于呈现-如果您引入业务逻辑,不仅最终可能会导致膨胀的,难以管理的控制器,而且还违反了关注点分离原则。

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

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

什么是模特?

您的客户模型通常是不完整的

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

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

真实模型必须坚持

在传统的MCV,该模型是被唯一坚持。每当我们谈论模型时,都必须在某些时候坚持下去。您的客户端可以随意操作模型,但是直到成功完成与服务器的往返操作,该工作才得以完成。

后果

以上两点应作为警告-客户拥有的模型仅涉及部分,主要是简单的业务逻辑。

因此,在客户端上下文中使用小写字母可能是明智的选择,M因此实际上是mVCmVPmVVm。最大的M是服务器。

商业逻辑

关于业务模型的最重要的概念之一可能是,您可以将它们细分为两种类型(我省略了第三种视图业务,因为这是另一回事了):

  • 域逻辑 -aka 企业业务规则,即与应用程序无关的逻辑。例如,给一个具有firstNamesirName属性的模型,getFullName()可以将getter之类的应用视为独立于应用程序的。
  • 应用程序逻辑 -aka 应用程序业务规则,它是特定于应用程序的。例如,错误检查和处理。

需要强调的是,在客户端上下文中,这两者都不是“真实的”业务逻辑 -它们仅处理对客户端重要的部分。应用逻辑(不是域逻辑)应负责促进与服务器的通信以及大多数用户的交互;而领域逻辑在很大程度上是小规模的,特定于实体的并且是表示驱动的。

问题仍然存在-您如何将它们扔到有角度的应用程序中?

3 vs 4层架构

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

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

但是,对于客户而言,存在两个基本问题:

  • 该模型是局部的,过时的并且不会持久。
  • 没有放置应用程序逻辑的地方。

此策略的替代方法是4层策略

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

真正的问题是应用程序业务规则层(用例),通常对客户而言是不对的。

该层由交互者(鲍勃叔叔)实现,这几乎就是马丁·福勒(Martin Fowler)所称的操作脚本服务层

具体例子

考虑以下Web应用程序:

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

现在应该发生一些事情:

  • 该表格应经过客户验证。
  • 请求应发送到服务器。
  • 如果有错误,则应进行处理。
  • 用户列表可能(也可能不需要)(由于分页)需要更新。

我们将所有这些丢到哪里?

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

拟议的解决方案

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

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

因此,我们在$ resource的控制器之间添加了一层,这一层(我们称其为interactor):

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

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

  • 用户单击“添加用户”。
  • 控制器向交互器询问空白的用户模型,并用业务逻辑方法修饰,例如 validate()
  • 提交后,控制器将调用模型validate()方法。
  • 如果失败,则控制器处理错误。
  • 如果成功,则控制器使用以下命令调用交互器 createUser()
  • 交互器调用$ resource
  • 响应后,交互器将所有错误委托给控制器,由控制器进行处理。
  • 成功响应后,交互器将确保在需要时更新用户列表。

因此,AngularJS被定义为MVW(W代表任何东西),因为我可以选择使用BL中的Controller(包含所有业务逻辑)或View Model / Presenter(不包含业务逻辑,只需要一些代码来填充视图)。一项单独的服务?我对吗?
BAD_SEED 2015年

最佳答案。您在GitHub上有一个4层角度应用程序的真实示例吗?
RPallas

1
@RPallas,不,我不(希望我有时间这样做)。我们目前正在尝试一种架构,其中“应用程序逻辑”只是一个边界交互器。它和控制器之间的解析器以及具有某些视图逻辑的视图模型。我们仍在试验中,所以不是100%的利弊。但是一旦完成,我希望在某个地方写博客。
Izhaki

1
@heringer基本上,我们引入了模型-表示域实体的OOP构造。这些模型与资源而不是控制器进行通信。它们封装域逻辑。控制器调用模型,后者又调用资源。
伊扎基

1
@ alex440否。尽管已经过去两个月了,但是关于这个主题的一篇严肃的博客文章却触手可及。圣诞即将到来-可能然后。
伊扎基

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, ...) {
    ...
    }
  }
});

如果return对象看起来“太拥挤”,则表明该服务做得太多。


0

AngularJS并非以传统方式实现MVC,而是实现了更接近MVVM(Model-View-ViewModel)的东西,ViewModel也可以称为活页夹(在角度情况下,它可以是$ scope)。Model->我们知道,角度模型可以只是普通的旧JS对象或我们应用程序中的数据

View-> angularJS中的视图是由angularJS通过应用指令,指令或绑定来解析和编译的HTML,此处的要点是有角,输入不仅是纯HTML字符串(innerHTML),而是是由浏览器创建的DOM。

ViewModel-> ViewModel实际上是angularJS情况下视图和模型之间的绑定器/桥梁,它是$ scope,用于初始化和扩展我们使用Controller的$ scope。

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


-1

为了清楚地回答这个问题,Angular使用了我们在常规编程中已经遇到的不同设计模式。1)当我们注册有关模块的控制器或指令,工厂,服务等时。这里它正在隐藏全局空间中的数据。这是模块模式。2)当angular使用其脏检查来比较范围变量时,此处使用Observer Pattern。3)我们控制器中的所有父子范围都使用原型模式。 4)如果注入服务,则使用Factory Pattern

总体而言,它使用不同的已知设计模式来解决问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.