Estou procurando a melhor prática de como vincular a uma propriedade de serviço no AngularJS.
Eu trabalhei com vários exemplos para entender como vincular às propriedades em um serviço criado usando o AngularJS.
Abaixo, tenho dois exemplos de como vincular às propriedades em um serviço; ambos trabalham. O primeiro exemplo usa ligações básicas e o segundo exemplo usa $ scope. $ Watch para vincular às propriedades do serviço
Um destes exemplos é preferido ao vincular propriedades em um serviço ou há outra opção que eu não saiba 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 são atualizadas, a exibição deve refletir essas alterações. Ambos os exemplos funcionam com sucesso; Gostaria de saber se existe uma maneira melhor de fazê-lo.
Ligação básica
O código a seguir pode ser visualizado e executado aqui: http://plnkr.co/edit/d3c16z
<html>
<body ng-app="ServiceNotification" >
<div ng-controller="TimerCtrl1" style="border-style:dotted">
TimerCtrl1 <br/>
Last Updated: {{timerData.lastUpdated}}<br/>
Last Updated: {{timerData.calls}}<br/>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
<script type="text/javascript">
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
};
});
</script>
</body>
</html>
A outra maneira de resolver a ligação às propriedades de serviço é usar $ scope. $ Watch no controlador.
$ scope. $ watch
O código a seguir pode ser visualizado e executado aqui: http://plnkr.co/edit/dSBlC9
<html>
<body ng-app="ServiceNotification">
<div style="border-style:dotted" ng-controller="TimerCtrl1">
TimerCtrl1<br/>
Last Updated: {{lastUpdated}}<br/>
Last Updated: {{calls}}<br/>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
<script type="text/javascript">
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
};
});
</script>
</body>
</html>
Estou ciente de que posso usar $ rootscope. $ Broadcast no serviço e $ root. $ On no controlador, mas em outros exemplos que criei que usam $ broadcast / $ na primeira transmissão não são capturados pelo controlador, mas as chamadas adicionais transmitidas são acionadas no controlador. Se você conhece uma maneira de resolver o problema de transmissão $ rootscope. $, Forneça uma resposta.
Mas, para reafirmar o que mencionei anteriormente, gostaria de conhecer as melhores práticas de como vincular a propriedades de um 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 mudei como a resposta correta. Como a resposta de Gil Birman tem muito poucos votos positivos, minha preocupação é que as pessoas que leem essa 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 vivamente a resposta de Gil Birman.
fonte
Respostas:
Considere alguns prós e contras da segunda abordagem :
0 em
{{lastUpdated}}
vez de{{timerData.lastUpdated}}
, o que poderia ser tão facilmente{{timer.lastUpdated}}
, o que eu poderia argumentar que é mais legível (mas não vamos discutir ... eu estou dando a este ponto uma classificação neutra para que você decida por si mesmo)+1 Pode ser conveniente que o controlador atue como um tipo de API para a marcação, de modo que, se de alguma forma a estrutura do modelo de dados for alterada, você possa (em teoria) atualizar os mapeamentos de API do controlador sem tocar no parcial html.
-1 No entanto, a teoria nem sempre é prática e eu costumo encontrar-me ter que modificar marcação e lógica do controlador quando as mudanças são chamados para, de qualquer maneira . Portanto, o esforço extra de escrever a API nega sua vantagem.
-1 Além disso, essa abordagem não é muito SECA.
-1 Se você deseja vincular os dados ao
ng-model
seu código, fica ainda menos SECO, pois é necessário empacotá-lo novamente$scope.scalar_values
no controlador para fazer uma nova chamada REST.-0.1 Há um pequeno impacto no desempenho, criando observadores extras. Além disso, se as propriedades dos dados forem anexadas ao modelo que não precisam ser observadas em um controlador específico, 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 alteração de modelo.
$scope.timerData = Timer.data;
está começando a parecer muito tentador agora ... Vamos mergulhar um pouco mais nesse último ponto ... De que tipo de mudanças de modelo estávamos falando? Um modelo no back-end (servidor)? Ou um modelo que é criado e vive apenas no front-end? Nos dois casos, o que é essencialmente a API de mapeamento de dados pertence à camada de serviço front-end (uma fábrica ou serviço angular). (Observe que seu primeiro exemplo - minha preferência - não possui essa API na camada de serviço , o que é bom porque é simples o suficiente para não ser necessário).Em conclusão , tudo não precisa ser dissociado. E, quanto à dissociação total da marcação do modelo de dados, as desvantagens superam as vantagens.
Controladores, em geral , não devem estar cheios de
$scope = injectable.data.scalar
's. Em vez disso, eles devem ser polvilhados com$scope = injectable.data
's,promise.then(..)
' s e$scope.complexClickAction = function() {..}
'sComo uma abordagem alternativa para obter o desacoplamento de dados e, portanto, o encapsulamento de visão, o único lugar em que realmente faz sentido desacoplar a visão do modelo é com uma diretiva . Mas mesmo lá, não
$watch
dimensione valores nas funçõescontroller
oulink
. Isso não economizará tempo nem tornará o código mais sustentável ou legível. Isso nem tornará os testes mais fáceis, pois testes robustos em angular geralmente testam o DOM resultante de qualquer maneira . Em vez disso, em uma diretiva exija sua API de dados em forma de objeto e use apenas os$watch
campos criados porng-bind
.Exemplo http://plnkr.co/edit/MVeU1GKRTN4bqA3h9Yio
ATUALIZAR : Finalmente voltei a esta pergunta para acrescentar que não acho que nenhuma das abordagens esteja "errada". Originalmente, eu havia escrito que a resposta de Josh David Miller estava incorreta, mas, retrospectivamente, seus pontos são completamente válidos, especialmente seu ponto sobre a separação de preocupações.
Separando as preocupações de lado (mas tangencialmente relacionadas), 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 da sua equipe decidir que o controlador precisa transformar os dados de alguma maneira trivial antes que a exibição os exiba? (Se os controladores devem transformar os dados de maneira alguma é outra discussão.) Se ela não fizer uma cópia do objeto primeiro, poderá inconscientemente causar regressões em outro componente de exibição que consome os mesmos dados.
O que essa pergunta realmente destaca são as deficiências arquitetônicas do aplicativo angular típico (e realmente de qualquer aplicativo JavaScript): forte acoplamento de preocupações e mutabilidade de objetos. Recentemente, fiquei encantado com o aplicativo de arquitetura com o React e estruturas de dados imutáveis. Fazer isso resolve maravilhosamente os dois problemas a seguir:
Separação de preocupações : um componente consome todos os seus dados por meio de adereços e depende muito pouco de singletons globais (como serviços Angular) e não sabe nada sobre o que aconteceu acima na hierarquia de visualizações.
Mutabilidade : todos os objetos 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 muito do React para alcançar os dois pontos acima.
fonte
Na minha perspectiva,
$watch
seria a melhor maneira de praticar.Você pode realmente simplificar um pouco o seu exemplo:
É tudo o 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 objeto bastante pequeno, mudei para apenas observar a
Timer.data
propriedade. 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 esse método a colocar o valor do serviço diretamente no escopo é garantir uma 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; seu trabalho é obter os dados de seus serviços e processá-los da maneira que for necessária e, em seguida, fornecer à sua visão as especificidades necessárias. Mas não acho que seu trabalho seja apenas repassar o serviço para a visualizaçã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 incluir nenhuma "lógica" nos modelos (por exemplo,
if
instruções).Para ser justo, provavelmente há várias perspectivas aqui e estou ansioso por outras respostas.
fonte
{{lastUpdated}}
{{timerData.lastUpdated}}
Timer.data
em um $ watch,Timer
deve ser definido no $ scope, porque a expressão da string que você passa para $ watch é avaliada em relação ao escopo. Aqui está um desentupidor que mostra como fazer isso funcionar. O parâmetro objectEquality está documentado aqui - terceiro parâmetro - mas não é realmente muito bem explicado.$watch
é bastante ineficiente. Veja as respostas na stackoverflow.com/a/17558885/932632 e stackoverflow.com/questions/12576798/...Tarde para a festa, mas para futuros googlers - não use a resposta fornecida.
O JavaScript possui um mecanismo de passagem de objetos por referência, enquanto apenas passa uma cópia superficial para os valores "números, seqüências de caracteres etc."
No exemplo acima, em vez de vincular atributos de um serviço, por que não expomos o serviço ao escopo?
Essa abordagem simples tornará o angular capaz de fazer ligações bidirecionais e todas as coisas mágicas que você precisa. Não corte seu controlador com observadores ou marcação desnecessária.
E se você estiver preocupado com a exibição acidental de seus atributos de serviço, use-o
defineProperty
para torná-lo legível, enumerável, configurável ou definir getters e setters. Você pode obter muito controle, tornando seu serviço mais sólido.Dica final: se você gasta seu tempo trabalhando mais em seu controlador do que em seus serviços, está fazendo errado :(.
Nesse código de demonstração específico que você forneceu, recomendo que você faça:
Editar:
Como mencionei acima, você pode controlar o comportamento de seus atributos de serviço usando
defineProperty
Exemplo:
Agora em nosso controlador, se fizermos
nosso serviço alterará o valor de
propertyWithSetter
e também lançará o novo valor no banco de dados de alguma forma!Ou podemos adotar qualquer abordagem que quisermos.
Consulte a documentação do MDN para
defineProperty
.fonte
$scope.model = {timerData: Timer.data};
, apenas anexando-o ao modelo em vez de diretamente no Scope.Eu acho que essa pergunta tem um componente contextual.
Se você está simplesmente puxando dados de um serviço e irradiando essas informações para a visualização, acho que a ligação direta à propriedade do serviço é ótima. Eu não quero escrever muito código clichê para simplesmente mapear propriedades de serviço para modelar propriedades para consumir em minha opinião.
Além disso, o desempenho em angular é baseado em duas coisas. A primeira é quantas ligações existem em uma página. O segundo é o quão caras são as funções getter. Misko fala sobre isso aqui
Se você precisar executar lógica específica da instância nos dados do serviço (em oposição à massagem de dados aplicada no próprio serviço), e o resultado disso impactar o modelo de dados exposto à exibição, eu diria que um $ watcher é apropriado, como desde que a função não seja terrivelmente cara. No caso de uma função cara, eu sugeriria que os resultados fossem armazenados em cache em uma variável local (ao controlador), executando suas operações complexas fora da função $ watcher e vinculando seu escopo ao resultado disso.
Como uma ressalva, você não deve suspender nenhuma propriedade diretamente do seu escopo $. A
$scope
variável NÃO é o seu modelo. Tem referências ao seu modelo.Na minha opinião, a "melhor prática" para simplesmente irradiar informações do serviço para baixo para exibição:
E então sua visão conteria
{{model.timerData.lastupdated}}
.fonte
Com base nos exemplos acima, pensei em lançar uma maneira de vincular de forma transparente uma variável de controlador a uma variável de serviço.
No exemplo abaixo, as alterações na
$scope.count
variável Controller serão refletidas automaticamente nacount
variável Service .Na produção, estamos usando a ligação this para atualizar um ID em um serviço que, em seguida, busca assincronamente dados e atualiza seus vars de serviço. Vinculação adicional que significa que os controladores são atualizados automaticamente quando o serviço é atualizado.
O código abaixo pode ser visto trabalhando em http://jsfiddle.net/xuUHS/163/
Visão:
Serviço / Controlador:
fonte
Eu acho que é uma maneira melhor de vincular o serviço em si, em vez dos atributos nele.
Aqui está o porquê:
Você pode jogar neste plunker .
fonte
Prefiro manter meus observadores o menos possível. 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 métodos em qualquer componente ou serviço que desejar.
Em um projeto do mundo real, em breve você terminará com uma cadeia de métodos não rastreáveis (melhor dizendo, difícil de rastrear) sendo chamada e valores alterados, o que torna especialmente trágico o processo de integração.
fonte
Vincular qualquer dado que envie serviço não é uma boa ideia (arquitetura), mas se você precisar mais, sugiro duas 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 não terá problemas para vinculá-los a qualquer lugar
2) você pode usar os eventos angularjs. Sempre que quiser, você pode enviar um sinal (de $ rootScope) e capturá-lo onde quiser. Você pode até enviar dados 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
fonte
A respeito
Onde ParentScope é um serviço injetado?
fonte
As soluções mais elegantes ...
Além disso, eu escrevo EDAs (arquiteturas orientadas a eventos), de modo que costumo fazer algo como a seguinte [versão simplificada]:
Em seguida, coloquei um ouvinte no meu controlador no canal desejado e apenas mantive meu escopo local atualizado dessa maneira.
Em conclusão, não há muito de um "Best Practice" - em vez disso, a sua principalmente preferência - contanto que você está mantendo coisas sólidas e empregando acoplamento fraco. A razão pela qual eu defenderia esse último código é porque as EDAs têm o menor acoplamento possível por natureza. E se você não estiver muito preocupado com esse fato, evitemos trabalhar juntos no mesmo projeto.
Espero que isto ajude...
fonte