Je recherche les meilleures pratiques sur la façon de se lier à une propriété de service dans AngularJS.
J'ai travaillé sur plusieurs exemples pour comprendre comment se lier à des propriétés dans un service créé à l'aide d'AngularJS.
Ci-dessous, j'ai deux exemples de la manière de se lier à des propriétés dans un service; ils fonctionnent tous les deux. Le premier exemple utilise des liaisons de base et le deuxième exemple utilise $ scope. $ Watch pour se lier aux propriétés du service
L'un de ces exemples est-il préféré lors de la liaison à des propriétés dans un service ou existe-t-il une autre option dont je ne suis pas au courant qui serait recommandée?
Le principe de ces exemples est que le service devrait mettre à jour ses propriétés «lastUpdated» et «calls» toutes les 5 secondes. Une fois les propriétés du service mises à jour, la vue doit refléter ces changements. Ces deux exemples fonctionnent avec succès; Je me demande s'il existe une meilleure façon de procéder.
Reliure de base
Le code suivant peut être affiché et exécuté ici: 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>
L'autre façon dont j'ai résolu la liaison aux propriétés du service est d'utiliser $ scope. $ Watch dans le contrôleur.
$ scope. $ watch
Le code suivant peut être affiché et exécuté ici: 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>
Je sais que je peux utiliser $ rootscope. $ Broadcast dans le service et $ root. $ On dans le contrôleur, mais dans d'autres exemples que j'ai créés, l'utilisation de $ broadcast / $ lors de la première diffusion n'est pas capturée par le contrôleur, mais les appels supplémentaires qui sont diffusés sont déclenchés dans le contrôleur. Si vous connaissez un moyen de résoudre le problème de diffusion de $ rootscope. $, Veuillez fournir une réponse.
Mais pour reformuler ce que j'ai mentionné plus tôt, j'aimerais connaître la meilleure pratique de liaison à des propriétés de service.
Mettre à jour
Cette question a été initialement posée et répondue en avril 2013. En mai 2014, Gil Birman a fourni une nouvelle réponse, que j'ai changée en réponse correcte. Étant donné que la réponse de Gil Birman a très peu de votes positifs, je crains que les personnes lisant cette question ne tiendront pas compte de sa réponse en faveur d'autres réponses avec beaucoup plus de votes. Avant de prendre une décision sur la meilleure réponse, je recommande vivement la réponse de Gil Birman.
la source
Réponses:
Considérez quelques avantages et inconvénients de la deuxième approche :
0
{{lastUpdated}}
au lieu de{{timerData.lastUpdated}}
, ce qui pourrait tout aussi bien être{{timer.lastUpdated}}
, ce que je pourrais dire est plus lisible (mais ne discutons pas ... je donne à ce point une note neutre pour que vous décidiez par vous-même)+1 Il peut être pratique que le contrôleur agisse comme une sorte d'API pour le balisage, de sorte que si la structure du modèle de données change, vous pouvez (en théorie) mettre à jour les mappages d'API du contrôleur sans toucher au partiel html.
-1 Cependant, la théorie n'est pas toujours pratique et je me retrouve généralement à devoir modifier le balisage et la logique du contrôleur lorsque des changements sont nécessaires, de toute façon . Ainsi, l'effort supplémentaire d'écrire l'API annule son avantage.
-1 De plus, cette approche n'est pas très sèche.
-1 Si vous souhaitez lier les données à
ng-model
votre code, devenez encore moins DRY car vous devez reconditionner le$scope.scalar_values
dans le contrôleur pour effectuer un nouvel appel REST.-0.1 Il y a un petit coup de performance créant des observateurs supplémentaires. De plus, si des propriétés de données sont attachées au modèle qui n'ont pas besoin d'être surveillées dans un contrôleur particulier, elles créeront une surcharge supplémentaire pour les observateurs profonds.
-1 Et si plusieurs contrôleurs ont besoin des mêmes modèles de données? Cela signifie que vous avez plusieurs API à mettre à jour à chaque changement de modèle.
$scope.timerData = Timer.data;
cela commence à sembler très tentant en ce moment ... Plongeons-nous un peu plus dans ce dernier point ... De quel genre de changements de modèle parlions-nous? Un modèle sur le back-end (serveur)? Ou un modèle qui est créé et ne vit que dans le front-end? Dans les deux cas, ce qui est essentiellement l' API de mappage de données appartient à la couche de service frontale (une usine ou un service angulaire). (Notez que votre premier exemple - ma préférence - n'a pas une telle API dans la couche de service , ce qui est bien car c'est assez simple, il n'en a pas besoin.)En conclusion , tout n'a pas à être découplé. Et en ce qui concerne le découplage complet du balisage du modèle de données, les inconvénients l'emportent sur les avantages.
Les contrôleurs, en général , ne devraient pas être jonchés de
$scope = injectable.data.scalar
's. Ils devraient plutôt être saupoudrés de$scope = injectable.data
's,promise.then(..)
' s et$scope.complexClickAction = function() {..}
'sEn tant qu'approche alternative pour réaliser le découplage des données et donc l'encapsulation de la vue, le seul endroit où il est vraiment logique de découpler la vue du modèle est avec une directive . Mais même là, n'utilisez pas de
$watch
valeurs scalaires dans les fonctionscontroller
oulink
. Cela ne fera pas gagner du temps et ne rendra pas le code plus maintenable ni lisible. Cela ne facilitera même pas les tests car des tests robustes en angulaire testent généralement le DOM résultant de toute façon . Au lieu de cela, dans une directive, demandez votre API de données sous forme d'objet et privilégiez l'utilisation uniquement des$watch
ers créés parng-bind
.Exemple http://plnkr.co/edit/MVeU1GKRTN4bqA3h9Yio
MISE À JOUR : Je suis enfin revenu sur cette question pour ajouter que je ne pense pas que l'une ou l'autre approche soit «mauvaise». À l'origine, j'avais écrit que la réponse de Josh David Miller était incorrecte, mais rétrospectivement, ses arguments sont tout à fait valables, en particulier son point sur la séparation des préoccupations.
Séparation des préoccupations mise à part (mais liée de manière tangentielle), il y a une autre raison pour la copie défensive que j'ai omis de considérer. Cette question concerne principalement la lecture de données directement à partir d'un service. Mais que se passe-t-il si un développeur de votre équipe décide que le contrôleur doit transformer les données de manière triviale avant que la vue ne les affiche? (La question de savoir si les contrôleurs doivent transformer les données est une autre discussion.) Si elle ne fait pas d'abord une copie de l'objet, elle peut involontairement provoquer des régressions dans un autre composant de vue qui consomme les mêmes données.
Ce que cette question met vraiment en évidence, ce sont les lacunes architecturales de l'application angulaire typique (et vraiment de toute application JavaScript): couplage étroit des préoccupations et mutabilité des objets. Je suis récemment devenu amoureux des applications d'architecture avec React et des structures de données immuables. Cela résout à merveille les deux problèmes suivants:
Séparation des préoccupations : un composant consomme toutes ses données via des accessoires et dépend peu ou pas des singletons globaux (tels que les services angulaires), et ne sait rien de ce qui s'est passé au-dessus de lui dans la hiérarchie des vues.
Mutabilité : tous les accessoires sont immuables, ce qui élimine le risque de mutation involontaire des données.
Angular 2.0 est maintenant sur la bonne voie pour emprunter fortement à React pour atteindre les deux points ci-dessus.
la source
De mon point de vue, ce
$watch
serait la meilleure pratique.Vous pouvez en fait simplifier un peu votre exemple:
C'est tout ce dont vous avez besoin.
Étant donné que les propriétés sont mises à jour simultanément, vous n'avez besoin que d'une seule montre. De plus, comme ils proviennent d'un seul objet plutôt petit, je l'ai changé pour simplement regarder la
Timer.data
propriété. Le dernier paramètre passé à lui$watch
dit de vérifier l'égalité profonde plutôt que de simplement s'assurer que la référence est la même.Pour fournir un peu de contexte, la raison pour laquelle je préférerais cette méthode à la valeur du service directement sur la portée est d'assurer une séparation adéquate des préoccupations. Votre vue ne devrait pas avoir besoin de savoir quoi que ce soit sur vos services pour fonctionner. Le travail du contrôleur est de tout coller ensemble; son travail est d'obtenir les données de vos services et de les traiter de la manière nécessaire, puis de fournir à votre vue les détails dont elle a besoin. Mais je ne pense pas que son travail consiste simplement à transmettre le service directement à la vue. Sinon, que fait même le contrôleur là-bas? Les développeurs AngularJS ont suivi le même raisonnement lorsqu'ils ont choisi de ne pas inclure de «logique» dans les modèles (par exemple les
if
déclarations).Pour être juste, il y a probablement plusieurs perspectives ici et j'attends avec impatience d'autres réponses.
la source
{{lastUpdated}}
vs.{{timerData.lastUpdated}}
Timer.data
dans un $ watch,Timer
doit être défini sur la portée $, car l'expression de chaîne que vous passez à $ watch est évaluée par rapport à la portée. Voici un plunker qui montre comment faire fonctionner cela. Le paramètre objectEquality est documenté ici - 3ème paramètre - mais pas vraiment trop bien expliqué.$watch
est assez inefficace. Voir les réponses sur stackoverflow.com/a/17558885/932632 et stackoverflow.com/questions/12576798/…En retard à la fête, mais pour les futurs Googleurs - n'utilisez pas la réponse fournie.
JavaScript a un mécanisme de passage d'objets par référence, alors qu'il ne transmet qu'une copie superficielle pour les valeurs "nombres, chaînes, etc.".
Dans l'exemple ci-dessus, au lieu de lier les attributs d'un service, pourquoi n'exposons-nous pas le service à l'étendue?
Cette approche simple rendra angulaire capable de faire une liaison bidirectionnelle et toutes les choses magiques dont vous avez besoin. Ne piratez pas votre contrôleur avec des observateurs ou des balises inutiles.
Et si vous craignez que votre vue écrase accidentellement vos attributs de service, utilisez
defineProperty
pour la rendre lisible, énumérable, configurable ou pour définir des getters et des setters. Vous pouvez gagner beaucoup de contrôle en rendant votre service plus solide.Dernier conseil: si vous passez votre temps à travailler sur votre contrôleur plus que sur vos services, vous le faites mal :(.
Dans ce code de démonstration particulier que vous avez fourni, je vous recommande de faire:
Éditer:
Comme je l'ai mentionné ci-dessus, vous pouvez contrôler le comportement de vos attributs de service en utilisant
defineProperty
Exemple:
Maintenant dans notre contrôleur si nous le faisons
notre service changera la valeur de
propertyWithSetter
et publiera également la nouvelle valeur dans la base de données en quelque sorte!Ou nous pouvons adopter n'importe quelle approche que nous voulons.
Reportez-vous à la documentation MDN pour
defineProperty
.la source
$scope.model = {timerData: Timer.data};
, simplement en l'attachant au modèle au lieu de directement sur Scope.Je pense que cette question a une composante contextuelle.
Si vous extrayez simplement des données d'un service et que vous diffusez ces informations vers sa vue, je pense que la liaison directe à la propriété du service est très bien. Je ne veux pas écrire beaucoup de code standard pour simplement mapper les propriétés du service aux propriétés du modèle à consommer à mon avis.
De plus, les performances angulaires sont basées sur deux choses. Le premier est le nombre de liaisons sur une page. Le second est le coût des fonctions getter. Misko en parle ici
Si vous avez besoin d'exécuter une logique spécifique à une instance sur les données du service (par opposition au massage des données appliqué dans le service lui-même), et que le résultat de cela a un impact sur le modèle de données exposé à la vue, alors je dirais qu'un $ watcher est approprié, car tant que la fonction n'est pas terriblement chère. Dans le cas d'une fonction coûteuse, je suggérerais de mettre en cache les résultats dans une variable locale (au contrôleur), d'effectuer vos opérations complexes en dehors de la fonction $ watcher, puis de lier votre portée au résultat de cela.
En guise de mise en garde, vous ne devriez pas suspendre de propriétés directement à partir de votre $ scope. La
$scope
variable n'est PAS votre modèle. Il a des références à votre modèle.Dans mon esprit, les "meilleures pratiques" pour simplement diffuser des informations d'un service à l'autre:
Et puis votre point de vue contiendrait
{{model.timerData.lastupdated}}
.la source
En me basant sur les exemples ci-dessus, j'ai pensé lancer une manière de lier de manière transparente une variable de contrôleur à une variable de service.
Dans l'exemple ci-dessous, les modifications apportées à la
$scope.count
variable Controller seront automatiquement reflétées dans lacount
variable Service .En production, nous utilisons en fait la liaison this pour mettre à jour un identifiant sur un service qui récupère ensuite les données de manière asynchrone et met à jour ses variables de service. Une liaison supplémentaire signifie que les contrôleurs sont automatiquement mis à jour lorsque le service se met à jour.
Le code ci-dessous peut être vu fonctionner à http://jsfiddle.net/xuUHS/163/
Vue:
Service / contrôleur:
la source
Je pense que c'est une meilleure façon de se lier au service lui-même au lieu des attributs qu'il contient.
Voici pourquoi:
Vous pouvez le jouer sur ce plunker .
la source
Je préfère garder mes observateurs le moins possible. Ma raison est basée sur mes expériences et on pourrait la soutenir théoriquement.
Le problème avec l'utilisation des observateurs est que vous pouvez utiliser n'importe quelle propriété sur l'étendue pour appeler l'une des méthodes dans n'importe quel composant ou service que vous aimez.
Dans un projet du monde réel, vous vous retrouverez bientôt avec une chaîne non traçable (mieux dit difficile à tracer) de méthodes appelées et de valeurs modifiées, ce qui rend particulièrement tragique le processus d'intégration.
la source
Lier des données, qui envoie un service n'est pas une bonne idée (architecture), mais si vous en avez plus besoin, je vous suggère 2 façons de le faire
1) vous pouvez obtenir les données qui ne sont pas à l'intérieur de votre service.Vous pouvez obtenir des données à l'intérieur de votre contrôleur / directive et vous n'aurez aucun problème pour les lier n'importe où
2) vous pouvez utiliser les événements angularjs. Quand vous le souhaitez, vous pouvez envoyer un signal (à partir de $ rootScope) et l'attraper où vous le souhaitez. Vous pouvez même envoyer des données sur cet eventName.
Peut-être que cela peut vous aider. Si vous avez besoin de plus d'exemples, voici le lien
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html
la source
Qu'en est-il de
Où ParentScope est un service injecté?
la source
Les solutions les plus élégantes ...
De plus, j'écris des EDA (Event-Driven Architectures), donc j'ai tendance à faire quelque chose comme ce qui suit [version simplifiée à l'extrême]:
Ensuite, je mets un auditeur dans mon contrôleur sur le canal souhaité et maintiens simplement ma portée locale à jour de cette façon.
En conclusion, il n'y a pas vraiment de «meilleure pratique» - plutôt sa préférence principale - tant que vous gardez les choses SOLIDES et que vous utilisez un couplage faible. La raison pour laquelle je recommanderais ce dernier code est que les EDA ont le couplage le plus bas possible par nature. Et si vous n'êtes pas trop préoccupé par ce fait, évitons de travailler ensemble sur le même projet.
J'espère que cela t'aides...
la source