¿Dónde poner los datos del modelo y el comportamiento? [tl; dr; Usar Servicios]


Estoy trabajando con AngularJS para mi último proyecto. En la documentación y tutoriales todos los datos del modelo se colocan en el ámbito del controlador. Entiendo que tiene que estar allí para estar disponible para el controlador y, por lo tanto, dentro de las vistas correspondientes.

Sin embargo, no creo que el modelo deba implementarse allí. Podría ser complejo y tener atributos privados, por ejemplo. Además, uno podría querer reutilizarlo en otro contexto / aplicación. Poniendo todo en el el controlador rompe totalmente el patrón MVC.

Lo mismo se aplica al comportamiento de cualquier modelo. Si usara la arquitectura DCI y separara el comportamiento del modelo de datos, tendría que introducir objetos adicionales para mantener el comportamiento. Esto se haría introduciendo roles y contextos.

Por supuesto, los datos y el comportamiento del modelo podrían implementarse con objetos javascript simples o cualquier patrón de "clase". Pero, ¿cuál sería la forma AngularJS de hacerlo? Utilizar los servicios?

Así que todo se reduce a esta pregunta:

¿Cómo implementar modelos desacoplados del controlador, siguiendo las mejores prácticas de AngularJS?

Author: georgeawg, 2012-06-20

8 answers

Debe usar servicios si desea algo utilizable por varios controladores. He aquí un simple ejemplo artificial:

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
}
 152
Author: Andrew Joslin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-29 17:40:39

Actualmente estoy probando este patrón, que, aunque no DCI, proporciona un desacoplamiento clásico de servicio / modelo (con servicios para hablar con servicios web (también conocido como model CRUD), y un modelo que define las propiedades y métodos del objeto).

Tenga en cuenta que solo uso este patrón cuando el objeto modelo necesita métodos que funcionen en sus propias propiedades, que probablemente usaré en todas partes (como getter/setters mejorados). Estoyno abogando por hacer esto para cada servicio sistemática.

EDITAR: Solía pensar que este patrón iría en contra del mantra" El modelo Angular es un objeto javascript simple y viejo", pero ahora me parece que este patrón está perfectamente bien.

EDITAR (2): Para ser aún más claro, utilizo una clase Modelo solo para factorizar getters / setters simples (por ejemplo : para ser utilizado en plantillas de vista). Para la lógica de las grandes empresas, recomiendo usar servicios separados que "conozcan" el modelo, pero se mantengan separados de ellos y solo incluyan lógica de negocios. Llámelo una capa de servicio" experto en negocios " si desea

Servicio / ElementServices.js (observe cómo se inyecta el elemento en la declaración)

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
}

Modelo/Elemento.js (usando angularjs Factory, hecho para la creación de objetos)

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;
});
 80
Author: Ben G,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-12 17:57:08

La documentación de Angularjs establece claramente:

A diferencia de muchos otros frameworks Angular no hace restricciones o requisitos del modelo. No hay clases de las que heredar o métodos de acceso especiales para acceder o cambiar el modelo. El el modelo puede ser primitivo, hash de objeto o un tipo de objeto completo. En breve el modelo es un objeto JavaScript simple.

Significa que depende de usted cómo declarar un modelo. Es un simple objeto Javascript.

I personalmente, no usará Servicios de Angular, ya que estaban destinados a comportarse como objetos singleton que puede usar, por ejemplo, para mantener estados globales en su aplicación.

 30
Author: S.C.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-02 21:07:08

DCI es un paradigma y, como tal, no hay una forma AngularJS de hacerlo, ya sea que el lenguaje soporte DCI o no. JS soporta DCI bastante bien si está dispuesto a usar la transformación de código fuente y con algunos inconvenientes si no lo está. De nuevo DCI no tiene más que ver con la inyección de dependencias que decir que una clase C# tiene y definitivamente no es un servicio tampoco. Entonces, la mejor manera de hacer DCI con angulusJS es hacer DCI de la manera JS, que es bastante similar a cómo se formula DCI en primer lugar. A menos que haga la transformación de origen, no podrá hacerlo completamente ya que los métodos de rol serán parte del objeto incluso fuera del contexto, pero ese es generalmente el problema con el DCI basado en inyección de métodos. Si nos fijamos en fullOO.info el sitio autorizado para DCI puede echar un vistazo a las implementaciones de ruby que también usan inyección de métodos o puede echar un vistazo a aquí para obtener más información sobre DCI. Es principalmente con ejemplos de RUby, pero el DCI es agnóstico a eso. Una una de las claves de DCI es que lo que hace el sistema está separado de lo que es el sistema. Por lo tanto, el objeto de datos es bastante tonto, pero una vez vinculado a un rol en un contexto los métodos de rol hacen que cierto comportamiento esté disponible. Un rol es simplemente un identificador, nada más, y cuando se accede a un objeto a través de ese identificador, entonces los métodos de rol están disponibles. No hay ningún objeto/clase de rol. Con la inyección de método, el alcance de los métodos de rol no es exactamente como se describe, sino cercano. Un ejemplo de contexto en JS podría 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);
   };
}
 8
Author: Rune FS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-15 07:45:51
 7
Author: marianboda,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-12 16:03:53

Como se indica en otros posters, Angular no proporciona ninguna clase base lista para usar para modelar, pero uno puede proporcionar varias funciones útiles:

  1. Métodos para interactuar con una API RESTful y crear nuevos objetos
  2. Establecer relaciones entre modelos
  3. Validar datos antes de persistir en el backend; también es útil para mostrar errores en tiempo real
  4. Almacenamiento en caché y carga lenta para evitar hacer solicitudes HTTP derrochadoras
  5. Ganchos de máquina de estado (antes/después de guardar, actualizar, crear, nuevo, etc)

Una biblioteca que hace bien todas estas cosas es ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource). Divulgación completa wrote Escribí esta biblioteca.y la he utilizado con éxito en la construcción de varias aplicaciones a escala empresarial. Está bien probado, y proporciona una API que debería ser familiar para los desarrolladores de Rails.

Mi equipo y yo seguimos desarrollando activamente esta biblioteca, y me encantaría ver más Los desarrolladores de Angular contribuyen a él y lo prueban en batalla.

 5
Author: Brett Cassette,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-11 07:44:32

Una pregunta antigua, pero creo que el tema es más relevante que nunca dada la nueva dirección de Angular 2.0. Yo diría que una buena práctica es escribir código con el menor número posible de dependencias en un marco en particular. Solo use las partes específicas del marco donde agrega valor directo.

Actualmente parece que el servicio Angular es uno de los pocos conceptos que llegará a la próxima generación de Angular, por lo que probablemente sea inteligente seguir la pauta general de mover todo lógica a los servicios. Sin embargo, yo diría que puede hacer modelos desacoplados incluso sin una dependencia directa de los servicios de Angular. Crear objetos autónomos con solo las dependencias y responsabilidades necesarias es probablemente el camino a seguir. También hace la vida mucho más fácil al hacer pruebas automatizadas. La responsabilidad individual es un trabajo de zumbido en estos días, pero tiene mucho sentido!

Aquí hay un ejemplo de un patrón que considero bueno para desacoplar el modelo de objetos del dom.

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

Un objetivo clave es estructurar su código de una manera que lo haga tan fácil de usar desde una prueba unitaria como desde una vista. Si logras eso estás bien posicionado para escribir pruebas realistas y útiles.

 5
Author: TGH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-29 02:33:20

He tratado de abordar ese problema exacto en esta entrada de blog .

Básicamente, el mejor hogar para el modelado de datos está en servicios y fábricas. Sin embargo, dependiendo de cómo recupere sus datos y la complejidad de los comportamientos que necesita, hay muchas maneras diferentes de llevar a cabo la implementación. Angular actualmente no tiene una manera estándar o una mejor práctica.

El post cubre tres enfoques, usando http http, $resource , y Restangular.

Aquí hay un código de ejemplo para cada uno, con un método personalizado getResult() en el modelo de trabajo:

Restangular (fácil peasy):

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

$recurso (un poco más complicado):

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 (hardcore):

angular.module('job.models', [])
    .service('JobManager', ['$q', '$http', 'Job', function($q, $http, Job) {
        return {
            getAll: function(limit) {
                var deferred = $q.defer();

                $http.get('/api/jobs?limit=' + limit + '&full=true').success(function(data) {
                    var jobs = [];
                    for (var i = 0; i < data.objects.length; i ++) {
                        jobs.push(new Job(data.objects[i]));
                    }
                    deferred.resolve(jobs);
                });

                return deferred.promise;
            }
        };
    }])
    .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;
    });

La entrada del blog en sí entra en más detalles sobre el razonamiento detrás de por qué podría usar cada enfoque, así como ejemplos de código de cómo usar los modelos en su controladores:

Modelos de datos AngularJS: http http VS resource resource VS Restangular

Existe la posibilidad de que Angular 2.0 ofrezca una solución más robusta para el modelado de datos que ponga a todos en la misma página.

 4
Author: Alan Christopher Thomas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-17 19:34:33