AngularJS: A maneira correta de vincular-se a propriedades de serviço

Eu estou procurando a melhor prática de como vincular-se a uma propriedade de serviço no AngularJS.

Eu trabalhei através de vários exemplos para entender como se ligar a propriedades em um serviço que é criado usando AngularJS.

Abaixo eu tenho dois exemplos de como ligar as propriedades em um serviço; ambos trabalham. O primeiro exemplo usa ligações básicas e o segundo exemplo usado $ scope. $ Watch para vincular-se às propriedades do serviço

Esses exemplos são preferidos ao ligar a propriedades em um serviço ou há outra opção que eu não conheço que seria recomendada?

A premissa desses exemplos é que o serviço deve atualizar suas propriedades “lastUpdated” e “calls” a cada 5 segundos. Depois que as propriedades do serviço forem atualizadas, a exibição deverá refletir essas alterações. Ambos os exemplos funcionam com sucesso; Eu me pergunto se existe uma maneira melhor de fazer isso.

Encadernação Básica

O código a seguir pode ser visto e executado aqui: 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 }; });

A outra maneira que resolvi a vinculação às propriedades de serviço é usar $ scope. $ Watch no controlador.

$ scope. $ watch

O código a seguir pode ser visto e executado aqui: 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 }; });

Eu estou ciente que eu posso usar $ rootscope. $ Broadcast no serviço e $ root. $ On no controller, mas em outros exemplos que eu criei que usam $ broadcast / $ na primeira transmissão não é capturado pelo controlador, mas chamadas adicionais que são transmitidas são acionadas no controlador. Se você está ciente de uma maneira de resolver o problema $ broadcast, por favor, forneça uma resposta.

Mas para reafirmar o que mencionei anteriormente, gostaria de saber a melhor prática de como vincular-se a propriedades de serviço.


Atualizar

Esta pergunta foi originalmente feita e respondida em abril de 2013. Em maio de 2014, Gil Birman forneceu uma nova resposta, que eu mudei como a resposta correta. Uma vez que a resposta de Gil Birman tem poucos votos positivos, minha preocupação é que as pessoas que lerem esta pergunta desconsiderem sua resposta em favor de outras respostas com muito mais votos. Antes de tomar uma decisão sobre qual é a melhor resposta, recomendo a resposta de Gil Birman.

Considere alguns prós e contras da segunda abordagem :

  • 0 {{lastUpdated}} vez de {{timerData.lastUpdated}} , que poderia facilmente ser {{timer.lastUpdated}} , o que eu {{timer.lastUpdated}} que é mais legível (mas não vamos discutir … eu estou dando isso apontar uma sorting neutra para você decidir por si mesmo)

  • +1 Pode ser conveniente que o controlador atue como uma espécie de API para a marcação, de modo que, se de alguma forma a estrutura do modelo de dados for alterada, você pode (em teoria) atualizar os mapeamentos da API do controlador sem tocar na parte parcial do html.

  • -1 No entanto, a teoria nem sempre é prática e eu geralmente me encontro tendo que modificar a marcação e a lógica do controlador quando as mudanças são necessárias, de qualquer forma . Portanto, o esforço extra de escrever a API nega sua vantagem.

  • -1 Além disso, esta abordagem não é muito seca.

  • -1 Se você quiser vincular os dados ao ng-model seu código se tornará ainda menos DRY, já que você terá que $scope.scalar_values o $scope.scalar_values no controller para fazer uma nova chamada REST.

  • -0.1 Há um pequeno desempenho atingindo a criação de observadores extras. Além disso, se as propriedades de dados estiverem anexadas ao modelo que não precisam ser observadas em um determinado controlador, elas criarão uma sobrecarga adicional para os observadores profundos.

  • -1 E se vários controladores precisarem dos mesmos modelos de dados? Isso significa que você tem várias APIs para atualizar a cada mudança de modelo.

$scope.timerData = Timer.data; está começando a parecer muito tentador agora … Vamos nos aprofundar um pouco mais nesse último ponto … De que tipo de mudança de modelo estamos falando? Um modelo no back-end (servidor)? Ou um modelo que é criado e vive apenas no front-end? Em ambos os casos, o que é essencialmente a API de mapeamento de dados pertence à camada de serviço front-end (uma fábrica ou serviço angular). (Note que o seu primeiro exemplo – minha preferência – não tem essa API na camada de serviço , o que é bom porque é simples o suficiente, não precisa disso.)

Em conclusão , tudo não precisa ser dissociado. E quanto a dissociar totalmente a marcação do modelo de dados, as desvantagens superam as vantagens.


Os controladores, em geral , não devem estar cheios de $scope = injectable.data.scalar . Em vez disso, eles devem ser polvilhados com $scope = injectable.data ‘s, promise.then(..) ‘ s, e $scope.complexClickAction = function() {..} ‘s

Como uma abordagem alternativa para alcançar o desacoplamento de dados e, portanto, o encapsulamento de visualizações, o único lugar que realmente faz sentido desacoplar a visão do modelo é com uma diretiva . Mas mesmo lá, não $watch valores escalares nas funções de controller ou link . Isso não poupará tempo nem tornará o código mais fácil de manter nem legível. Ele nem vai tornar o teste mais fácil, já que testes robustos em angular geralmente testam o DOM resultante de qualquer maneira . Em vez disso, em uma diretriz, exija sua API de dados em forma de object e prefira usar apenas os $watch watchers criados por ng-bind .


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

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

ATUALIZAÇÃO : Eu finalmente voltei a essa pergunta para acrescentar que não acho que essa abordagem esteja “errada”. Originalmente, eu havia escrito que a resposta de Josh David Miller estava incorreta, mas, em retrospecto, seus pontos são completamente válidos, especialmente seu ponto sobre separação de preocupações.

Separação de preocupações à parte (mas relacionadas tangencialmente), há outra razão para a cópia defensiva que não considerei. Esta questão lida principalmente com a leitura de dados diretamente de um serviço. Mas e se um desenvolvedor em sua equipe decidir que o controlador precisa transformar os dados de alguma maneira trivial antes que a exibição seja exibida? (Se os controladores devem transformar os dados é outra discussão.) Se ela não fizer uma cópia do object primeiro, ela poderá, inadvertidamente, causar regressões em outro componente de visualização que consuma os mesmos dados.

O que essa questão realmente destaca são as deficiências arquitetônicas da aplicação angular típica (e realmente qualquer aplicativo JavaScript): acoplamento rígido de interesses e mutabilidade de object. Recentemente me tornei apaixonado por arquitetar aplicativos com React e estruturas de dados imutáveis. Isso resolve os dois problemas a seguir maravilhosamente:

  1. Separação de interesses : um componente consome todos os seus dados via adereços e tem pouca ou nenhuma dependência de singletons globais (como serviços angulares) e não sabe nada sobre o que aconteceu acima dele na hierarquia de visões.

  2. Mutabilidade : Todos os adereços são imutáveis, o que elimina o risco de mutação involuntária de dados.

O Angular 2.0 está agora no caminho certo para emprestar fortemente do React para alcançar os dois pontos acima.

Do meu ponto de vista, $watch seria a melhor maneira de praticar.

Você pode realmente simplificar seu exemplo um pouco:

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

Isso é tudo que você precisa.

Como as propriedades são atualizadas simultaneamente, você precisa apenas de um relógio. Além disso, como eles vêm de um único object pequeno, mudei para apenas observar a propriedade Timer.data . O último parâmetro passado para $watch diz para verificar a igualdade profunda em vez de apenas garantir que a referência seja a mesma.


Para fornecer um pouco de contexto, a razão pela qual eu preferiria que esse método colocasse o valor do serviço diretamente no escopo é garantir a separação adequada das preocupações. Sua visão não precisa saber nada sobre seus serviços para operar. O trabalho do controlador é colar tudo junto; Seu trabalho é obter os dados de seus serviços e processá-los de qualquer maneira necessária e, em seguida, fornecer sua visão com quaisquer especificidades de que necessita. Mas eu não acho que seu trabalho é apenas passar o serviço para a exibição. Caso contrário, o que o controlador está fazendo lá? Os desenvolvedores do AngularJS seguiram o mesmo raciocínio quando optaram por não include nenhuma “lógica” nos modelos (por exemplo, if instruções).

Para ser justo, provavelmente há várias outlook aqui e estou ansioso para outras respostas.

Tarde para a festa, mas para futuros googlers Não use a resposta fornecida.

js tem um mecanismo único de passar objects por referência, enquanto apenas passa uma cópia superficial para valores “números, strings etc”.

No exemplo acima, em vez de vincular atributos de um serviço. Por que não expomos o serviço ao escopo?

 $scope.hello = HelloService; 

Essa abordagem simples fará com que o angular seja capaz de fazer ligações de duas vias e todas as coisas mágicas de que você precisa. Não hackear seu controlador com observadores ou marcações desnecessárias.

E caso esteja preocupado com a sua visualização, substitua acidentalmente os atributos do serviço. Uso fino DefineAttribute () para torná-lo, readable, enummable, configurável, set getters e setters. U nome dele. você pode ganhar muito controle, tornando seu serviço mais sólido.

Dica final: se você gastar seu tempo trabalhando em seu controlador mais que seus serviços. então você está fazendo errado :(.

nesse código de demonstração particular que você forneceu eu recomendo que você faça

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

Demasiado para post 3 anos de idade, mas se alguém precisar de mais informações, deixe-me saber

Editar:

Como mencionei acima, você pode controlar o comportamento de seus atributos de serviço usando defineProperty

exemplo:

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

agora em fora contorller se o fizermos

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

nosso serviço irá alterar o valor de propertyWithSetter e também postar novo valor para o database de alguma forma!

ou podemos tomar qualquer abordagem que quisermos. por favor, consulte https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Obrigado por upvotes 🙂

Eu acho que essa questão tem um componente contextual.

Se você simplesmente extrai dados de um serviço e irradia essas informações para a visualização, acho que vincular diretamente à propriedade de serviço é bom. Eu não quero escrever um monte de código clichê para simplesmente mapear propriedades de serviço para modelar propriedades para consumir na minha opinião.

Além disso, o desempenho em angular é baseado em duas coisas. A primeira é quantas ligações estão em uma página. O segundo é o quão caras são as funções getter. Misko fala sobre isso aqui

Se você precisa executar uma lógica específica da instância nos dados do serviço (em oposição à massificação de dados aplicada no próprio serviço), e o resultado disso impacta o modelo de dados exposto à visão, então eu diria que um $ watcher é apropriado, desde que a function não seja muito cara. No caso de uma function cara, eu sugeriria o armazenamento em cache dos resultados em uma variável local (para controlador), realizando suas operações complexas fora da function $ watcher e vinculando seu escopo ao resultado disso.

Como uma ressalva, você não deveria estar pendurando quaisquer propriedades diretamente fora do seu $ escopo. A variável $scope não é seu modelo. Tem referências ao seu modelo.

Na minha opinião, “melhor prática” para simplesmente irradiar informações do serviço para baixo para ver:

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

E, em seguida, sua visualização conteria {{model.timerData.lastupdated}} .

Com base nos exemplos acima, pensei em criar uma maneira de vincular de forma transparente uma variável do controlador a uma variável de serviço.

No exemplo abaixo, as alterações na variável Controller $scope.count serão automaticamente refletidas na variável de count serviços.

Na produção, estamos realmente usando a binding this para atualizar um id em um serviço que, de forma assíncrona, busca dados e atualiza seus serviços vars. Ligação adicional significa que os controladores são atualizados automaticamente quando o serviço se atualiza.

O código abaixo pode ser visto trabalhando em http://jsfiddle.net/xuUHS/163/

Visão:

 

This is my countService variable : {{count}}

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

Serviço / 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); }; } 

Eu acho que é uma maneira melhor de ligar o serviço em si, em vez dos atributos nele.

Aqui está o porquê:

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

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

Você pode jogar neste plunker .

Eu preferiria manter meus observadores menos possíveis. Minha razão é baseada em minhas experiências e pode-se argumentar teoricamente.
O problema com o uso de observadores é que você pode usar qualquer propriedade no escopo para chamar qualquer um dos methods em qualquer componente ou serviço que desejar.
Em um projeto do mundo real, muito em breve você terminará com uma cadeia de methods não rastreável (melhor dizendo que é difícil rastrear) e os valores sendo alterados, o que torna o processo de integração especialmente trágico.

Para vincular quaisquer dados, o que envia serviço não é uma boa ideia (arquitetura), mas se você precisar mais eu sugiro que você 2 maneiras de fazer isso

1) você pode obter os dados que não estão dentro do seu serviço. Você pode obter dados dentro do seu controlador / diretiva e você não terá problemas para vinculá-los em qualquer lugar.

2) você pode usar events angularjs. Sempre que você quiser, você pode enviar um sinal (de $ rootScope) e pegá-lo onde quiser. Você pode até mesmo enviar um dado sobre esse eventName.

Talvez isso possa te ajudar. Se você precisar de mais exemplos, aqui está o link

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

Sobre o quê

 scope = _.extend(scope, ParentScope); 

Onde o ParentScope é um serviço injetado?

As soluções mais 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 */ }); }); 

Além disso, eu escrevo EDAs (Event-Driven Architectures), então tenho a tendência de fazer algo como o seguinte [versão 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); } }; 

Em seguida, coloco um ouvinte no meu controlador no canal desejado e apenas mantenho meu escopo local atualizado dessa maneira.

Em conclusão, não há muita “Melhor Prática” – em vez disso, é principalmente preferência – contanto que você esteja mantendo as coisas SÓLIDAS e empregando um acoplamento fraco. A razão pela qual eu defenderia o último código é porque as EDAs têm o menor acoplamento viável por natureza. E se você não estiver muito preocupado com esse fato, vamos evitar trabalhar juntos no mesmo projeto.

Espero que isto ajude…