【问题标题】:Where to put model data and behaviour? [tl; dr; Use Services]将模型数据和行为放在哪里? [tl;博士;使用服务]
【发布时间】:2018-12-10 02:42:27
【问题描述】:

我正在为我的最新项目使用 AngularJS。在文档和教程中,所有模型数据都放入控制器范围内。我知道它必须存在于控制器中,因此在相应的视图中。

但是我认为该模型实际上不应该在那里实施。例如,它可能很复杂并且具有私有属性。此外,人们可能希望在另一个上下文/应用程序中重用它。将所有内容都放入控制器中完全破坏了 MVC 模式。

这同样适用于任何模型的行为。如果我使用DCI architecture 并将行为与数据模型分开,我将不得不引入额外的对象来保存行为。这将通过引入角色和上下文来完成。

DCI == Data C协作I交互

当然,模型数据和行为可以用普通的 javascript 对象或任何“类”模式来实现。但是 AngularJS 的方法是什么?使用服务?

所以归结为这个问题:

如何按照 AngularJS 最佳实践实现与控制器分离的模型?

【问题讨论】:

  • 如果您可以定义 DCI 或至少提供拼写形式,我会赞成这个问题。我从未在任何软件文献中看到过这个首字母缩写词。谢谢。
  • 我刚刚添加了一个 DCI 链接作为参考。
  • @JimRaden DCI 是 Dataq,Context,interaction,是 MVC 之父(Trygve Reenskauge)首先提出的范式。现在已经有相当多的关于这个主题的文献了。一本好书是 Coplien 和 Bjørnvig “精益架构”
  • 谢谢。不管是好是坏,大多数人现在甚至都不知道原始文学。据谷歌称,有 5500 万篇关于 MVC 的文章,但只有 250,000 篇提到了 MCI 和 MVC。在 Microsoft.com 上呢? 7. AngularJS.org 甚至没有提到 DCI 的首字母缩写词:“Your search - site:angularjs.org dci - did not match any documents”。
  • 资源对象基本上是 Angular.js 中的模型。我正在扩展它们。

标签: javascript model-view-controller angularjs dci


【解决方案1】:

我已经尝试在this blog post 中解决这个确切的问题。

基本上,数据建模的最佳场所是服务和工厂。但是,根据您检索数据的方式和所需行为的复杂性,有很多不同的方法可以实现。 Angular 目前没有标准方式或最佳实践。

这篇文章介绍了三种方法,分别使用 $http$resourceRestangular

以下是每个示例代码,在 Job 模型上使用自定义 getResult() 方法:

Restangular(简单易懂):

angular.module('job.models', [])
  .service('Job', ['Restangular', function(Restangular) {
    var Job = Restangular.service('jobs');

    Restangular.extendModel('jobs', function(model) {
      model.getResult = function() {
        if (this.status == 'complete') {
          if (this.passed === null) return "Finished";
          else if (this.passed === true) return "Pass";
          else if (this.passed === false) return "Fail";
        }
        else return "Running";
      };

      return model;
    });

    return Job;
  }]);

$resource(稍微复杂一些):

angular.module('job.models', [])
    .factory('Job', ['$resource', function($resource) {
        var Job = $resource('/api/jobs/:jobId', { full: 'true', jobId: '@id' }, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: function(data, header) {
                    var wrapped = angular.fromJson(data);
                    angular.forEach(wrapped.items, function(item, idx) {
                        wrapped.items[idx] = new Job(item);
                    });
                    return wrapped;
                }
            }
        });

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    }]);

$http(硬核):

angular.module('job.models', [])
    .service('JobManager', ['$http', 'Job', function($http, Job) {
        return {
            getAll: function(limit) {
                var params = {"limit": limit, "full": 'true'};
                return $http.get('/api/jobs', {params: params})
                  .then(function(response) {
                    var data = response.data;
                    var jobs = [];
                    for (var i = 0; i < data.objects.length; i ++) {
                        jobs.push(new Job(data.objects[i]));
                    }
                    return jobs;
                });
            }
        };
    }])
    .factory('Job', function() {
        function Job(data) {
            for (attr in data) {
                if (data.hasOwnProperty(attr))
                    this[attr] = data[attr];
            }
        }

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    });

博客文章本身更详细地介绍了您可能使用每种方法的原因,以及如何在控制器中使用模型的代码示例:

AngularJS Data Models: $http VS $resource VS Restangular

Angular 2.0 有可能为数据建模提供更强大的解决方案,让每个人都在同一个页面上。

【讨论】:

    【解决方案2】:

    Angularjs 文档明确指出:

    与许多其他框架不同,Angular 没有任何限制或 对模型的要求。没有可继承的类或 用于访问或更改模型的特殊访问器方法。这 模型可以是原始的、对象散列或完整的对象类型。简而言之 该模型是一个普通的 JavaScript 对象。

    ——AngularJS Developer Guide - V1.5 Concepts - Model

    所以这意味着如何声明模型取决于您。 这是一个简单的 Javascript 对象。

    我个人不会使用 Angular 服务,因为它们的行为类似于你可以使用的单例对象,例如,在你的应用程序中保持全局状态。

    【讨论】:

    【解决方案3】:

    一个较老的问题,但考虑到 Angular 2.0 的新方向,我认为这个主题比以往任何时候都更相关。我想说一个最佳实践是编写对特定框架的依赖尽可能少的代码。仅使用可以增加直接价值的框架特定部分。

    目前看来,Angular 服务是少数几个将成为下一代 Angular 的概念之一,因此遵循将所有逻辑移至服务的一般准则可能是明智之举。但是,我认为即使不直接依赖 Angular 服务,您也可以制作解耦模型。创建仅具有必要依赖项和职责的自包含对象可能是要走的路。在进行自动化测试时,它也让生活变得更加轻松。如今,单一职责是一项热门工作,但它确实很有意义!

    这是我认为有利于将对象模型与 dom 解耦的模式示例。

    http://www.syntaxsuccess.com/viewarticle/548ebac8ecdac75c8a09d58e

    一个关键目标是构建代码,使其在单元测试中与在视图中一样易于使用。如果你做到了,你就可以很好地编写现实和有用的测试。

    【讨论】:

      【解决方案4】:

      如果您想要多个控制器可以使用的东西,您应该使用服务。这是一个简单的人为示例:

      myApp.factory('ListService', function() {
        var ListService = {};
        var list = [];
        ListService.getItem = function(index) { return list[index]; }
        ListService.addItem = function(item) { list.push(item); }
        ListService.removeItem = function(item) { list.splice(list.indexOf(item), 1) }
        ListService.size = function() { return list.length; }
      
        return ListService;
      });
      
      function Ctrl1($scope, ListService) {
        //Can add/remove/get items from shared list
      }
      
      function Ctrl2($scope, ListService) {
        //Can add/remove/get items from shared list
      }
      

      【讨论】:

      • 与仅仅创建一个普通的 Javascript 对象作为模型并将其分配给控制器范围相比,使用服务有什么好处?
      • 如果您需要在多个控制器之间共享相同的逻辑。此外,这种方式更容易独立测试。
      • 最后一个例子有点糟糕,这个更有意义。我编辑了它。
      • 是的,使用普通的旧 Javascript 对象,您将无法将任何 Angular 注入您的 ListService。就像在这个例子中一样,如果你需要 $http.get 在开始时检索 List 数据,或者如果你需要注入 $rootScope 以便你可以 $broadcast 事件。
      • 为了让这个例子更像 DCI,数据不应该在 ListService 之外吗?
      【解决方案5】:

      我目前正在尝试这种模式,虽然不是 DCI,但它提供了经典的服务/模型解耦(用于与 Web 服务对话的服务(也称为模型 CRUD),以及定义对象属性和方法的模型)。

      请注意,我只在模型对象需要在其自身属性上工作的方法时才使用这种模式,我可能会在任何地方使用这种模式(例如改进的 getter/setter)。我提倡系统地为每项服务执行此操作。

      编辑: 我曾经认为这种模式会违背“Angular 模型是普通的旧 javascript 对象”的口头禅,但现在在我看来,这种模式非常好。

      编辑(2): 更清楚地说,我使用 Model 类仅用于分解简单的 getter/setter(例如:用于视图模板)。对于大业务逻辑,我建议使用“了解”模型的单独服务,但要与它们分开,并且只包含业务逻辑。如果您愿意,可以将其称为“业务专家”服务层

      service/ElementServices.js(注意元素是如何注入到声明中的)

      MyApp.service('ElementServices', function($http, $q, Element)
      {
          this.getById = function(id)
          {
              return $http.get('/element/' + id).then(
                  function(response)
                  {
                      //this is where the Element model is used
                      return new Element(response.data);
                  },
                  function(response)
                  {
                      return $q.reject(response.data.error);
                  }
              );
          };
          ... other CRUD methods
      }
      

      model/Element.js(使用 angularjs 工厂,用于创建对象)

      MyApp.factory('Element', function()
      {
          var Element = function(data) {
              //set defaults properties and functions
              angular.extend(this, {
                  id:null,
                  collection1:[],
                  collection2:[],
                  status:'NEW',
                  //... other properties
      
                  //dummy isNew function that would work on two properties to harden code
                  isNew:function(){
                      return (this.status=='NEW' || this.id == null);
                  }
              });
              angular.extend(this, data);
          };
          return Element;
      });
      

      【讨论】:

      • 我刚接触 Angular,但我很想知道退伍军人是否/为什么会认为这是异端邪说。这可能也是我最初处理它的方式。有人可以提供一些反馈吗?
      • @Aaronius 只是为了明确一点:我从来没有在任何 angularjs 文档或博客上真正读过“你永远不应该那样做”,但我总是读到诸如“angularjs 不需要模型”之类的东西,它只是使用普通的旧javascript”,我不得不自己发现这种模式。由于这是我在 AngularJS 上的第一个真正项目,因此我提出了这些强烈的警告,这样人们就不会在没有先考虑的情况下复制/粘贴。
      • 我已经确定了一个大致相似的模式。遗憾的是,Angular 没有任何“经典”意义上的模型真正支持(或看似希望支持)。
      • 在我看来这不是异端邪说,您正在使用工厂来实现它们的创建目的:构建对象。我相信“angularjs 不需要模型”这句话的意思是“你不需要从一个特殊的类继承,或者使用特殊的方法(如 ko.observable,在淘汰赛中)来使用 angular 中的模型,a纯 js 对象就足够了”。
      • 每个集合都没有适当命名的 ElementService 会导致一堆几乎相同的文件吗?
      【解决方案6】:

      正如其他海报所说,Angular 没有提供开箱即用的建模基类,但可以提供几个有用的功能:

      1. 与 RESTful API 交互和创建新对象的方法
      2. 建立模型之间的关系
      3. 在持久化到后端之前验证数据;也可用于显示实时错误
      4. 缓存和延迟加载以避免产生浪费的 HTTP 请求
      5. 状态机挂钩(保存、更新、创建、新建等之前/之后)

      ngActiveResource (https://github.com/FacultyCreative/ngActiveResource) 是一个很好地完成所有这些事情的库。完全公开——我编写了这个库——我已经成功地使用它来构建几个企业级应用程序。它经过了很好的测试,并提供了 Rails 开发人员应该熟悉的 API。

      我和我的团队继续积极开发这个库,我希望看到更多 Angular 开发人员为它做出贡献并对其进行实战测试。

      【讨论】:

      • 嘿!这真是太棒了!我现在将其插入我的应用程序。战斗测试刚刚开始。
      • 我只是在看你的帖子,想知道你的 ngActiveResource 和 Angular 的 $resource 服务有什么区别。我对 Angular 有点陌生,并且很快浏览了两组文档,但它们似乎提供了很多重叠。 ngActiveResource 是在 $resource 服务可用之前开发的吗?
      【解决方案7】:

      这篇关于 AngularJS 模型的文章可能会有所帮助:

      http://joelhooks.com/blog/2013/04/24/modeling-data-and-state-in-your-angularjs-application/

      【讨论】:

      • 请注意 link-only answers 是不鼓励的,所以答案应该是寻找解决方案的终点(而不是另一个参考中途停留,随着时间的推移往往会变得陈旧)。请考虑在此处添加独立的概要,并保留链接作为参考。
      • 在对问题的评论中添加这样的链接会很好。
      • 此链接实际上是一篇非常好的文章,但同样需要将其制作成适合 SO 的答案
      【解决方案8】:

      DCI 是一种范式,因此没有 angularJS 方法可以做到这一点,无论该语言支持还是不支持 DCI。如果您愿意使用源转换,那么 JS 对 DCI 的支持相当好,如果您不愿意,则有一些缺点。同样,DCI 与依赖注入没有更多关系,就像说 C# 类具有并且绝对不是服务一样。因此,使用 angulusJS 进行 DCI 的最佳方式是使用 JS 方式进行 DCI,这非常接近于最初制定 DCI 的方式。除非您进行源转换,否则您将无法完全做到这一点,因为即使在上下文之外,角色方法也将成为对象的一部分,但这通常是基于方法注入的 DCI 的问题。如果您查看fullOO.info DCI 的权威站点,您可以查看他们也使用方法注入的 ruby​​ 实现,或者您可以查看here 以获取有关 DCI 的更多信息。它主要是 RUby 示例,但 DCI 的东西对此是不可知的。 DCI 的关键之一是系统所做的与系统是什么是分开的。因此,数据对象非常愚蠢,但一旦绑定到上下文中的角色,角色方法就会使某些行为可用。角色只是一个标识符,仅此而已,当通过该标识符访问对象时,角色方法可用。没有角色对象/类。使用方法注入,角色方法的作用域并不完全像描述的那样,而是接近的。 JS 中的上下文示例可以是

      function transfer(source,destination){
         source.transfer = function(amount){
              source.withdraw(amount);
              source.log("withdrew " + amount);
              destination.receive(amount);
         };
         destination.receive = function(amount){
            destination.deposit(amount);
            destination.log("deposited " + amount);
         };
         this.transfer = function(amount){
          source.transfer(amount);
         };
      }
      

      【讨论】:

      • 感谢您详细说明 DCI 内容。这是一本很棒的书。但我的问题实际上是针对“将模型对象放在 angularjs 中的什么位置”。 DCI 只是在那里供参考,我可能不仅有一个模型,而且还以 DCI 的方式对其进行拆分。将编辑问题以使其更清楚。
      猜你喜欢
      • 1970-01-01
      • 2010-09-08
      • 2013-07-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-29
      • 1970-01-01
      相关资源
      最近更新 更多