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

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

这同样适用于任何模型的行为。如果我要使用DCI体系结构并将行为与数据模型分离,我将不得不引入额外的对象来保存行为。这可以通过引入角色和上下文来实现。

DCI ==数据协作交互

当然,模型数据和行为可以用简单的javascript对象或任何“类”模式来实现。但是AngularJS会怎么做呢?使用服务?

所以这就归结为一个问题:

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


当前回答

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

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

这篇文章涵盖了三种方法,使用$http、$resource和Restangular。

下面是它们的一些示例代码,在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数据模型:$http VS $resource VS Restangular

Angular 2.0可能会提供一个更健壮的数据建模解决方案,让所有人都能达成共识。

其他回答

DCI is a paradigm and as such there's no angularJS way of doing it, either the language support DCI or it doesn't. JS support DCI rather well if you are willing to use source transformation and with some drawbacks if you are not. Again DCI has no more to do with dependency injection than say a C# class has and is definitely not a service either. So the best way to do DCI with angulusJS is to do DCI the JS way, which is pretty close to how DCI is formulated in the first place. Unless you do source transformation, you will not be able to do it fully since the role methods will be part of the object even outside the context but that's generally the problem with method injection based DCI. If you look at fullOO.info the authoritative site for DCI you could have a look at the ruby implementations they also use method injection or you could have a look at here for more information on DCI. It's mostly with RUby examples but the DCI stuff is agnostic to that. One of the keys to DCI is that what the system does is separated from what the system is. So the data object are pretty dumb but once bound to a role in a context role methods make certain behaviour available. A role is simply an identifier, nothing more, an when accessing an object through that identifier then role methods are available. There's no role object/class. With method injection the scoping of role methods is not exactly as described but close. An example of a context in JS could be

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

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

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

这篇文章涵盖了三种方法,使用$http、$resource和Restangular。

下面是它们的一些示例代码,在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数据模型:$http VS $resource VS Restangular

Angular 2.0可能会提供一个更健壮的数据建模解决方案,让所有人都能达成共识。

如果您希望多个控制器都可以使用服务,则应该使用服务。这里有一个简单的例子:

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)。我并不提倡对每项服务都系统地这样做。

编辑: 我曾经认为这种模式违背了“Angular模型是普通的javascript对象”的信条,但现在对我来说,这种模式完全没问题。

编辑(2): 更清楚地说,我使用Model类只分解简单的getter / setter(例如:在视图模板中使用)。对于大的业务逻辑,我建议使用单独的服务,这些服务“知道”模型,但与模型保持分离,并且只包括业务逻辑。如果愿意,可以将其称为“业务专家”服务层

service/ElementServices.js(注意Element是如何在声明中被注入的)

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 Factory,用于对象创建)

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没有提供开箱即用的建模基类,但可以提供几个有用的函数:

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

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

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