AngularJS: la forma correcta de vincularse a las propiedades de un servicio

Estoy buscando la mejor práctica de cómo enlazar a una propiedad de servicio en AngularJS.

He trabajado a través de múltiples ejemplos para comprender cómo enlazar propiedades en un servicio que se crea usando AngularJS.

A continuación tengo dos ejemplos de cómo enlazar propiedades en un servicio; ambos trabajan. El primer ejemplo usa enlaces básicos y el segundo ejemplo usó $ scope. $ Watch para vincularse a las propiedades del servicio

¿Se prefiere alguno de estos ejemplos cuando se vinculan a propiedades en un servicio o existe otra opción que no conozco que sería recomendable?

La premisa de estos ejemplos es que el servicio debería actualizar sus propiedades “lastUpdated” y “calls” cada 5 segundos. Una vez que se actualicen las propiedades del servicio, la vista debe reflejar estos cambios. Ambos ejemplos funcionan con éxito; Me pregunto si hay una mejor manera de hacerlo.

Enlace básico

El siguiente código se puede ver y ejecutar aquí: http://plnkr.co/edit/d3c16z

  
TimerCtrl1
Last Updated: {{timerData.lastUpdated}}
Last Updated: {{timerData.calls}}
var app = angular.module("ServiceNotification", []); function TimerCtrl1($scope, Timer) { $scope.timerData = Timer.data; }; app.factory("Timer", function ($timeout) { var data = { lastUpdated: new Date(), calls: 0 }; var updateTimer = function () { data.lastUpdated = new Date(); data.calls += 1; console.log("updateTimer: " + data.lastUpdated); $timeout(updateTimer, 5000); }; updateTimer(); return { data: data }; });

La otra forma en que resolví el enlace a las propiedades del servicio es usar $ scope. $ Watch en el controlador.

$ scope. $ watch

El siguiente código se puede ver y ejecutar aquí: http://plnkr.co/edit/dSBlC9

   
TimerCtrl1
Last Updated: {{lastUpdated}}
Last Updated: {{calls}}
var app = angular.module("ServiceNotification", []); function TimerCtrl1($scope, Timer) { $scope.$watch(function () { return Timer.data.lastUpdated; }, function (value) { console.log("In $watch - lastUpdated:" + value); $scope.lastUpdated = value; } ); $scope.$watch(function () { return Timer.data.calls; }, function (value) { console.log("In $watch - calls:" + value); $scope.calls = value; } ); }; app.factory("Timer", function ($timeout) { var data = { lastUpdated: new Date(), calls: 0 }; var updateTimer = function () { data.lastUpdated = new Date(); data.calls += 1; console.log("updateTimer: " + data.lastUpdated); $timeout(updateTimer, 5000); }; updateTimer(); return { data: data }; });

Soy consciente de que puedo usar $ rootscope. $ Broadcast en el servicio y $ root. $ On en el controlador, pero en otros ejemplos que he creado que usan $ broadcast / $ en la primera transmisión no son capturados por el controlador, pero las llamadas adicionales que se emiten se activan en el controlador. Si conoce una forma de resolver el problema $ rootscope. $ Broadcast, por favor proporcione una respuesta.

Pero para reafirmar lo que mencioné anteriormente, me gustaría saber cuál es la mejor práctica de cómo vincular las propiedades de un servicio.


Actualizar

Esta pregunta fue formulada y respondida originalmente en abril de 2013. En mayo de 2014, Gil Birman proporcionó una nueva respuesta, que cambié como la respuesta correcta. Dado que la respuesta de Gil Birman tiene muy pocos votos, mi preocupación es que las personas que lean esta pregunta descarten su respuesta a favor de otras respuestas con muchos más votos. Antes de tomar una decisión sobre cuál es la mejor respuesta, recomiendo encarecidamente la respuesta de Gil Birman.

Considere algunos pros y contras del segundo enfoque :

  • 0 {{lastUpdated}} lugar de {{timerData.lastUpdated}} , que podría ser igual de fácil {{timer.lastUpdated}} , lo que podría decirse que es más legible (pero no discutamos … Le doy esto apunte una calificación neutral para que decida por usted mismo)

  • +1 Puede ser conveniente que el controlador actúe como un tipo de API para el marcado, de modo que si de algún modo la estructura del modelo de datos cambia, puede (en teoría) actualizar las asignaciones de API del controlador sin tocar el html parcial.

  • -1 Sin embargo, la teoría no siempre es práctica y generalmente me veo obligado a modificar el marcado y la lógica del controlador cuando se requieren cambios, de todos modos . Entonces, el esfuerzo extra de escribir la API niega su ventaja.

  • -1 Además, este enfoque no es muy SECO.

  • -1 Si desea vincular los datos a ng-model su código $scope.scalar_values aún menos DRY ya que tendrá que volver a empaquetar $scope.scalar_values en el controlador para realizar una nueva llamada REST.

  • -0.1 Hay un pequeño golpe de rendimiento que crea un (a) espectador (es) adicional (es). Además, si las propiedades de los datos se adjuntan al modelo que no necesitan ser vistos en un controlador en particular, crearán una sobrecarga adicional para los observadores profundos.

  • -1 ¿Qué pasa si múltiples controladores necesitan los mismos modelos de datos? Eso significa que tiene múltiples API para actualizar con cada cambio de modelo.

$scope.timerData = Timer.data; comienza a sonar muy tentador justo ahora … profundicemos un poco más en ese último punto … ¿De qué tipo de cambios de modelo estamos hablando? Un modelo en el back-end (servidor)? ¿O un modelo que se crea y vive solo en el front-end? En cualquier caso, en esencia, la API de mapeo de datos pertenece a la capa de servicio de front-end (una fábrica o servicio angular). (Tenga en cuenta que su primer ejemplo, mi preferencia, no tiene dicha API en la capa de servicio , lo cual está bien porque es lo suficientemente simple como para no necesitarlo).

En conclusión , no todo tiene que estar desacoplado. Y en cuanto a desacoplar el marcado por completo del modelo de datos, los inconvenientes superan las ventajas.


Los controladores, en general , no deberían estar llenos de $scope = injectable.data.scalar ‘s. Más bien, deberían estar salpicados con $scope = injectable.data , promise.then(..) ‘s, y $scope.complexClickAction = function() {..} ‘ s

Como un enfoque alternativo para lograr el desacoplamiento de datos y, por lo tanto, la encapsulación de vistas, el único lugar donde realmente tiene sentido desacoplar la vista del modelo es con una directiva . Pero incluso allí, no $watch valores escalares en el controller o funciones de link . Eso no ahorrará tiempo ni hará que el código sea más fácil de mantener ni legible. Ni siquiera hará las pruebas más fáciles ya que las pruebas robustas en angularjs usualmente prueban el DOM resultante de todos modos . Por el contrario, en una directiva, exija su API de datos en forma de objeto y favorezca el uso de solo los $watch creados por ng-bind .


Ejemplo http://plnkr.co/edit/MVeU1GKRTN4bqA3h9Yio

  
TimerCtrl1
Bad:
Last Updated: {{lastUpdated}}
Last Updated: {{calls}}
Good:
Last Updated: {{data.lastUpdated}}
Last Updated: {{data.calls}}

ACTUALIZACIÓN : finalmente volví a esta pregunta para agregar que no creo que ninguno de los enfoques sea “incorrecto”. Originalmente había escrito que la respuesta de Josh David Miller era incorrecta, pero en retrospectiva sus puntos son completamente válidos, especialmente su punto sobre la separación de preocupaciones.

Aparte de las preocupaciones (pero tangencialmente relacionadas), hay otra razón para la copia defensiva que no consideré. Esta pregunta se refiere principalmente a la lectura de datos directamente desde un servicio. Pero, ¿qué sucede si un desarrollador en su equipo decide que el controlador necesita transformar los datos de alguna manera trivial antes de que la vista lo muestre? (Si los controladores deben transformar los datos en absoluto es otra discusión.) Si no hace una copia del objeto primero, es posible que involuntariamente provoque regresiones en otro componente de vista que consume los mismos datos.

Lo que realmente resalta esta pregunta son las deficiencias arquitectónicas de la aplicación angular típica (y en realidad cualquier aplicación de JavaScript): estrecha relación de inquietudes y mutabilidad de objetos. Recientemente me he enamorado de la aplicación de architecture con Reaccionar y estructuras de datos inmutables. Al hacerlo, resuelve maravillosamente los siguientes dos problemas:

  1. Separación de inquietudes : un componente consume todos sus datos a través de accesorios y tiene poca o ninguna dependencia en singletons globales (como servicios angulares), y no sabe nada sobre lo que sucedió arriba en la jerarquía de vistas.

  2. Mutabilidad : todos los accesorios son inmutables, lo que elimina el riesgo de mutación involuntaria de datos.

Angular 2.0 está ahora en camino de tomar mucho de React para alcanzar los dos puntos anteriores.

Desde mi punto de vista, $watch sería la mejor forma de practicar.

En realidad puedes simplificar tu ejemplo un poco:

 function TimerCtrl1($scope, Timer) { $scope.$watch( function () { return Timer.data; }, function (data) { $scope.lastUpdated = data.lastUpdated; $scope.calls = data.calls; }, true); } 

Eso es todo lo que necesitas.

Como las propiedades se actualizan simultáneamente, solo necesita un reloj. Además, dado que provienen de un único objeto bastante pequeño, lo cambié para simplemente mirar la propiedad Timer.data . El último parámetro pasado a $watch le dice que verifique la igualdad profunda en lugar de simplemente asegurarse de que la referencia sea la misma.


Para proporcionar un pequeño contexto, la razón por la que preferiría este método para colocar el valor del servicio directamente en el scope es garantizar la separación adecuada de las preocupaciones. Su punto de vista no debería necesitar saber nada sobre sus servicios para poder operar. El trabajo del controlador es pegar todo junto; su trabajo es obtener los datos de sus servicios y procesarlos de la forma que sea necesaria y luego proporcionarle a su vista los detalles que necesita. Pero no creo que su trabajo sea simplemente pasar el servicio directamente a la vista. De lo contrario, ¿qué hace allí el controlador? Los desarrolladores de AngularJS siguieron el mismo razonamiento cuando eligieron no incluir ninguna “lógica” en las plantillas (por ejemplo, declaraciones if ).

Para ser justos, probablemente haya múltiples perspectivas aquí y espero otras respuestas.

Tarde para festejar pero para futuros googlers. No use la respuesta proporcionada.

js tiene un mecanismo único de pasar objetos por referencia, mientras que solo pasa una copia superficial para los valores “números, cadenas, etc.”.

En el ejemplo anterior, en lugar de atributos vinculantes de un servicio. ¿Por qué no exponemos el servicio al scope?

 $scope.hello = HelloService; 

Este sencillo enfoque hará que angular sea capaz de hacer uniones de 2 vías y todas las cosas mágicas que necesites. No hack su controlador con vigilantes o marcados no deseados.

Y en caso de que le preocupe que su vista sobrescriba accidentalmente sus atributos de servicio. Uso fino DefineAttribute () para hacerlo, legible, enumerable, configurable, set getters y setters. U nombrelo. puede obtener un gran control haciendo que su servicio sea más sólido.

Consejo final: si pasa su tiempo trabajando en su controlador más que sus servicios. entonces lo estás haciendo mal :(.

en ese código de demostración en particular que proporcionaste, te recomendaría que lo hicieras

  function TimerCtrl1($scope, Timer) { $scope.timer = Timer; } ///Inside view {{ timer.time_updated }} {{ timer.other_property }} etc... 

Demasiado para una publicación de 3 años, pero si alguien necesita más información, házmelo saber.

Editar:

como mencioné anteriormente, puedes controlar el comportamiento de tus atributos de servicio usando defineProperty

ejemplo:

 // Lets expose a property named "propertyWithSetter" on our service // and hook a setter function that automaticly save new value to db ! Object.defineProperty(self, 'propertyWithSetter', { get: function() { return self.data.variable; }, set: function(newValue) { self.data.variable = newValue; //lets update the database too to reflect changes in data-model ! self.updateDatabaseWithNewData(data); }, enumerable: true, configurable: true }); 

ahora en out contorller si lo hacemos

 $scope.hello = HelloService; $scope.hello.propertyWithSetter = 'NEW VALUE'; 

¡nuestro servicio cambiará el valor de propertyWithSetter y también publicará un nuevo valor a la base de datos de alguna manera!

o podemos tomar cualquier enfoque que queramos. por favor, consulte https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Gracias por los votos favorables 🙂

Creo que esta pregunta tiene un componente contextual.

Si simplemente está extrayendo datos de un servicio e irradiando esa información a su vista, creo que enlazar directamente a la propiedad del servicio está bien. No quiero escribir una gran cantidad de código estándar para simplemente mapear las propiedades del servicio a las propiedades del modelo para consumir en mi opinión.

Además, el rendimiento en angular se basa en dos cosas. El primero es cuántas uniones hay en una página. El segundo es qué tan costosas son las funciones getter. Misko habla de esto aquí

Si necesita realizar una lógica específica de la instancia en los datos del servicio (a diferencia del masaje de datos aplicado dentro del mismo servicio), y el resultado de esto impacta el modelo de datos expuesto a la vista, entonces diría que un $ watcher es apropiado, como siempre que la función no sea terriblemente costosa. En el caso de una función costosa, sugeriría almacenar en caché los resultados en una variable local (a la controladora), realizar sus operaciones complejas fuera de la función $ watcher y luego vincular su scope al resultado de eso.

Como advertencia, no debe colgar ninguna propiedad directamente de su $ scope. La variable $scope NO es tu modelo. Tiene referencias a su modelo.

En mi opinión, la “mejor práctica” para simplemente irradiar información desde el servicio hacia abajo para ver:

 function TimerCtrl1($scope, Timer) { $scope.model = {timerData: Timer.data}; }; 

Y luego su vista contendría {{model.timerData.lastupdated}} .

Sobre la base de los ejemplos anteriores, pensé que incluiría una forma de vincular de forma transparente una variable de controlador a una variable de servicio.

En el ejemplo siguiente, los cambios en la variable Controller $scope.count se reflejarán automáticamente en la variable de count servicios.

En producción, estamos usando este enlace para actualizar una identificación en un servicio que luego obtiene de forma asíncrona datos y actualiza sus valores de servicio. Un enlace adicional significa que los controladores automágicamente se actualizan cuando el servicio se actualiza.

El siguiente código se puede ver trabajando en http://jsfiddle.net/xuUHS/163/

Ver:

 

This is my countService variable : {{count}}

This is my updated after click variable : {{countS}}

Servicio / Controlador:

 var app = angular.module('myApp', []); app.service('testService', function(){ var count = 10; function incrementCount() { count++; return count; }; function getCount() { return count; } return { get count() { return count }, set count(val) { count = val; }, getCount: getCount, incrementCount: incrementCount } }); function ServiceCtrl($scope, testService) { Object.defineProperty($scope, 'count', { get: function() { return testService.count; }, set: function(val) { testService.count = val; }, }); $scope.clickC = function () { $scope.count++; }; $scope.chkC = function () { alert($scope.count); }; $scope.clickS = function () { ++testService.count; }; $scope.chkS = function () { alert(testService.count); }; } 

Creo que es una mejor manera de vincular el servicio en sí mismo en lugar de los atributos que contiene.

Este es el por qué:

   
ArrService.arrOne: {{v}}
ArrService.arrTwo: {{v}}

arrOne: {{v}}
arrTwo: {{v}}

Puedes jugarlo en este plunker .

Prefiero mantener a mis observadores lo menos posible. Mi razón se basa en mis experiencias y se podría argumentar teóricamente.
El problema con el uso de vigilantes es que puede usar cualquier propiedad en el scope para llamar a cualquiera de los métodos en cualquier componente o servicio que desee.
En un proyecto del mundo real, muy pronto terminará con una cadena de métodos no trazables (mejor dicho, difíciles de rastrear) que se invocan y se modifican los valores, lo que hace especialmente trágico el proceso de incorporación.

Para enlazar cualquier dato, que envía el servicio no es una buena idea (architecture), pero si lo necesita más, le sugiero 2 maneras de hacerlo

1) puede obtener los datos que no están dentro de su servicio. Puede obtener datos dentro de su controlador / directiva y no tendrá problemas para vincularlos en cualquier lugar.

2) puede usar eventos angularjs. Siempre que lo desee, puede enviar una señal (desde $ rootScope) y atraparlo donde desee. Incluso puede enviar datos sobre ese eventName.

Tal vez esto pueda ayudarle. Si necesita más con ejemplos, aquí está el enlace

http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html

Qué pasa

 scope = _.extend(scope, ParentScope); 

¿Donde ParentScope es un servicio inyectado?

Las soluciones más elegantes …

 app.service('svc', function(){ this.attr = []; return this; }); app.controller('ctrl', function($scope, svc){ $scope.attr = svc.attr || []; $scope.$watch('attr', function(neo, old){ /* if necessary */ }); }); app.run(function($rootScope, svc){ $rootScope.svc = svc; $rootScope.$watch('svc', function(neo, old){ /* change the world */ }); }); 

Además, escribo EDA (architectures controladas por eventos) así que tiendo a hacer algo como la siguiente [versión simplificada]:

 var Service = function Service($rootScope) { var $scope = $rootScope.$new(this); $scope.that = []; $scope.$watch('that', thatObserver, true); function thatObserver(what) { $scope.$broadcast('that:changed', what); } }; 

Luego, pongo un oyente en mi controlador en el canal deseado y mantengo mi scope local actualizado de esta manera.

En conclusión, no hay muchas “Mejores Prácticas”, sino preferencia, siempre y cuando mantenga las cosas SÓLIDAS y emplee un acoplamiento débil. La razón por la que abogaría por este último código es porque las EDA tienen el acoplamiento más bajo posible por naturaleza. Y si no está demasiado preocupado por este hecho, evitemos trabajar juntos en el mismo proyecto.

Espero que esto ayude…