关于javascript:在哪里放置模型数据和行为?

Where to put model data and behaviour? [tl; dr; Use Services]

我正在为我的最新项目与AngularJS合作。在文档和教程中,所有模型数据都放在控制器范围内。我理解,必须在那里,以供控制器使用,从而在相应的视图中使用。

然而,我认为模型不应该在那里实际实现。例如,它可能很复杂并且具有私有属性。此外,人们可能希望在另一个上下文/应用程序中重用它。把所有的东西都放到控制器中完全打破了MVC模式。

任何模型的行为都是如此。如果我使用DCI体系结构并将行为与数据模型分离,我将不得不引入其他对象来保持行为。这将通过引入角色和上下文来实现。

dci==数据协作交互

当然,模型数据和行为可以用普通的JavaScript对象或任何"类"模式实现。但是安古拉吉斯的方法是什么呢?使用服务?

因此,归根结底就是这个问题:

遵循AngularJS的最佳实践,如何实现与控制器分离的模型?


如果您希望多个控制器能够使用某些服务,则应该使用这些服务。下面是一个简单的人为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}


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

注意,我只在模型对象需要处理其自身属性的方法时使用这个模式,我可能会在任何地方使用它(例如改进的getter/setter)。我并不主张系统地为每项服务都这样做。

编辑:我曾经认为这个模式会违背"角度模型是普通的老javascript对象"的咒语,但现在我觉得这个模式非常好。

编辑(2):更清楚地说,我只使用模型类来考虑简单的getter/setter(例如:在视图模板中使用)。对于大型业务逻辑,我建议使用单独的服务,这些服务"知道"模型,但与它们保持分离,并且只包括业务逻辑。如果您需要,可以称之为"业务专家"服务层

service/elementservices.js(注意声明中如何注入元素)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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工厂,用于创建对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
});


AngularJS文档清楚地说明:

Unlike many other frameworks Angular makes no restrictions or
requirements on the model. There are no classes to inherit from or
special accessor methods for accessing or changing the model. The
model can be primitive, object hash, or a full object Type. In short
the model is a plain JavaScript object.

— AngularJS Developer Guide - V1.5 Concepts - Model

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

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


DCI是一个范例,因此没有AngularJS的方法来实现它,或者语言支持DCI,或者它不支持DCI。如果您愿意使用源代码转换,JS支持DCI会更好,如果不愿意,也会有一些缺点。同样,DCI与依赖注入的关系也不比C类所具有的多,而且肯定也不是服务。所以用AngulusJS做DCI最好的方法就是用JS做DCI,这与DCI最初的公式非常接近。除非您进行源代码转换,否则您将无法完全完成转换,因为角色方法将是对象的一部分,即使在上下文之外,但这通常是基于方法注入的DCI的问题。如果你看一下fulloo.info这个DCI的权威站点,你可以看看Ruby的实现,它们也使用方法注入,或者你可以在这里了解更多关于DCI的信息。它主要使用Ruby示例,但是DCI的东西对此不可知。DCI的关键之一是,系统的功能与系统的功能是分离的。因此,数据对象相当愚蠢,但一旦绑定到上下文角色方法中的角色,就可以使用某些行为。角色只是一个标识符,不再是,当通过该标识符访问对象时,角色方法就可用了。没有角色对象/类。使用方法注入时,角色方法的范围并不完全如所述,而是很接近。JS中的一个上下文示例可以是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
   };
}


这篇关于AngularJS中模型的文章可能有助于:

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


一个老问题,但我认为这个主题比任何时候都更相关,因为新的角度2.0。我要说的是,最佳实践是尽可能少地依赖于特定框架编写代码。仅使用框架特定的部分,在这些部分添加直接价值。

目前看来,角度服务似乎是少数几个将使其成为下一代角度服务的概念之一,因此遵循将所有逻辑转移到服务的一般准则可能是明智的。但是,我认为即使不直接依赖于角度服务,您也可以创建解耦模型。创建仅具有必要依赖性和职责的自包含对象可能是一种方法。当进行自动化测试时,它也使生活变得容易得多。如今,单一的责任是一项热门的工作,但它的确很有意义!

下面是一个我认为有利于将对象模型与DOM分离的模式示例。

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

一个关键目标是以一种方式构造代码,使其在单元测试中和在视图中一样容易使用。如果您实现了这一点,那么您就可以很好地编写实际而有用的测试。


如其他海报所述,Angular不提供开箱即用的建模基础类,但可以有效地提供以下几种功能:

  • 与RESTful API交互并创建新对象的方法
  • 建立模型之间的关系
  • 在持久化到后端之前验证数据;对于显示实时错误也很有用
  • 缓存和延迟加载以避免产生浪费的HTTP请求
  • 状态机挂钩(保存、更新、创建、新建等之前/之后)
  • 一个能够很好地完成所有这些任务的库是ngactiveresource(https://github.com/facultycreative/ngactiveresource)。完全公开——我编写了这个库——并且我在构建几个企业级应用程序时成功地使用了它。它经过了很好的测试,并且提供了一个Rails开发人员应该熟悉的API。

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


    我在这篇博文中试图解决这个问题。

    基本上,数据建模的最佳归宿是服务和工厂。然而,根据您如何检索数据以及所需行为的复杂性,有许多不同的方法来实现。Angular目前没有标准方法或最佳实践。

    本文介绍了三种方法,使用$http、$resource和restangular。

    下面是每个示例的代码,在作业模型上有一个自定义的getResult()方法:

    重新调整角度(简单易行):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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(稍微复杂一点):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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(硬核):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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数据模型:$http vs$resource vs restangular

    有可能Angular2.0将为数据建模提供一个更强大的解决方案,使每个人都能在同一个页面上工作。