Szukam najlepszych praktyk tworzenia powiązań z usługą w AngularJS.
Omówiłem wiele przykładów, aby zrozumieć, jak powiązać właściwości w usłudze utworzonej za pomocą AngularJS.
Poniżej mam dwa przykłady powiązania z właściwościami w usłudze; oboje pracują. W pierwszym przykładzie zastosowano podstawowe powiązania, a w drugim użyto $ scope. $ Watch do powiązania z właściwościami usługi
Czy któryś z tych przykładów jest preferowany podczas tworzenia powiązania z właściwościami w usłudze, czy też jest zalecana inna opcja, o której nie wiem?
Założeniem tych przykładów jest to, że usługa powinna aktualizować swoje właściwości „lastUpdated” i „call” co 5 sekund. Po zaktualizowaniu właściwości usługi widok powinien odzwierciedlać te zmiany. Oba te przykłady działają pomyślnie; Zastanawiam się, czy jest na to lepszy sposób.
Podstawowe wiązanie
Poniższy kod można wyświetlić i uruchomić tutaj: 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>
Innym sposobem, w jaki rozwiązałem powiązanie z właściwościami usługi, jest użycie $ scope. $ Watch w kontrolerze.
$ scope. $ watch
Poniższy kod można wyświetlić i uruchomić tutaj: 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>
Zdaję sobie sprawę, że mogę użyć $ rootscope. $ Broadcast w usłudze i $ root. $ On w kontrolerze, ale w innych przykładach, które utworzyłem, używających $ broadcast / $ w pierwszej transmisji nie jest przechwytywane przez kontroler, ale dodatkowe wywołania, które są transmitowane, są wyzwalane w kontrolerze. Jeśli znasz sposób rozwiązania problemu z emisją $ rootscope. $, Podaj odpowiedź.
Ale aby powtórzyć to, o czym wspomniałem wcześniej, chciałbym poznać najlepsze praktyki tworzenia powiązań z właściwościami usługi.
Aktualizacja
To pytanie zostało pierwotnie zadane i udzielono na nie odpowiedzi w kwietniu 2013 r. W maju 2014 r. Gil Birman udzielił nowej odpowiedzi, którą zmieniłem jako poprawną. Ponieważ odpowiedź Gila Birmana ma bardzo mało głosów pozytywnych, obawiam się, że ludzie czytający to pytanie zignorują jego odpowiedź na rzecz innych odpowiedzi, które otrzymają znacznie więcej głosów. Zanim podejmiesz decyzję o najlepszej odpowiedzi, gorąco polecam odpowiedź Gila Birmana.
źródło
Odpowiedzi:
Rozważ kilka zalet i wad drugiego podejścia :
0
{{lastUpdated}}
zamiast{{timerData.lastUpdated}}
, co mogłoby równie łatwo być{{timer.lastUpdated}}
, co może być bardziej czytelne (ale nie kłóćmy się ... daję temu punktowi ocenę neutralną, więc sam decydujesz)+1 Może być dogodne, aby kontroler działał jako rodzaj API dla znaczników, tak że jeśli w jakiś sposób zmieni się struktura modelu danych, można (teoretycznie) zaktualizować mapowania API kontrolera bez dotykania częściowej kodu HTML.
-1 jednak teoria nie zawsze ćwiczyć i ja zwykle znaleźć sobie konieczności modyfikowania znaczników i Logic Controller, gdy zmiany są nazywane dla, tak czy owak . Zatem dodatkowy wysiłek związany z napisaniem API neguje jego zalety.
-1 Co więcej, to podejście nie jest bardzo SUCHE.
-1 Jeśli chcesz powiązać dane z
ng-model
kodem, stań się jeszcze mniej SUCHY, ponieważ musisz ponownie spakować$scope.scalar_values
w kontrolerze, aby wykonać nowe wywołanie REST.-0,1 Jest mały hit wydajnościowy tworzący dodatkowych obserwatorów. Ponadto, jeśli właściwości danych są dołączone do modelu, które nie muszą być obserwowane w określonym kontrolerze, spowodują dodatkowe obciążenie dla głębokich obserwatorów.
-1 A jeśli wielu administratorów potrzebuje tych samych modeli danych? Oznacza to, że masz wiele interfejsów API do aktualizacji przy każdej zmianie modelu.
$scope.timerData = Timer.data;
zaczyna teraz brzmieć bardzo kusząco… Zanurzmy się trochę głębiej w ten ostatni punkt… O jakich zmianach modelu mówiliśmy? Model na zapleczu (serwerze)? A może model, który jest tworzony i żyje tylko na froncie? W obu przypadkach to, co zasadniczo jest interfejsem API mapowania danych, należy do warstwy usług front-end (fabryka lub usługa kątowa). (Zwróć uwagę, że Twój pierwszy przykład - moje preferencje - nie ma takiego interfejsu API w warstwie usług , co jest w porządku, ponieważ jest wystarczająco prosty, że go nie potrzebuje).Podsumowując , wszystko nie musi być odsprzęgane. Jeśli chodzi o całkowite oddzielenie znaczników od modelu danych, wady przeważają nad zaletami.
Ogólnie rzecz biorąc, kontrolery nie powinny być zaśmiecane
$scope = injectable.data.scalar
. Raczej powinny być posypane$scope = injectable.data
's,promise.then(..)
' i$scope.complexClickAction = function() {..}
„s”Jako alternatywne podejście do oddzielenia danych, a tym samym hermetyzacji widoku, jedynym miejscem, w którym naprawdę ma sens oddzielenie widoku od modelu, jest dyrektywa . Ale nawet tam nie należy
$watch
używać wartości skalarnych w funkcjachcontroller
lublink
. Nie zaoszczędzi to czasu ani nie sprawi, że kod będzie łatwiejszy w utrzymaniu ani czytelny. Nie ułatwi to nawet testowania, ponieważ solidne testy kątowe zwykle i tak testują wynikowy DOM . Zamiast tego w dyrektywie żądaj interfejsu API danych w postaci obiektowej i preferuj używanie tylko$watch
ers utworzonych przezng-bind
.Przykład http://plnkr.co/edit/MVeU1GKRTN4bqA3h9Yio
AKTUALIZACJA : W końcu wróciłem do tego pytania, aby dodać, że nie sądzę, aby którekolwiek podejście było „złe”. Pierwotnie napisałem, że odpowiedź Josha Davida Millera była niepoprawna, ale z perspektywy czasu jego uwagi są całkowicie słuszne, zwłaszcza jego uwaga dotycząca oddzielenia obaw.
Odkładając na bok obawy (ale są one stycznie powiązane), istnieje jeszcze jeden powód kopiowania obronnego , którego nie uwzględniłem. To pytanie dotyczy głównie odczytu danych bezpośrednio z usługi. Ale co, jeśli programista w Twoim zespole zdecyduje, że kontroler musi przekształcić dane w jakiś trywialny sposób, zanim widok je wyświetli? (To, czy kontrolery powinny w ogóle transformować dane, to inna dyskusja.) Jeśli najpierw nie utworzy kopii obiektu, może nieświadomie spowodować regresje w innym komponencie widoku, który zużywa te same dane.
To, co naprawdę podkreśla to pytanie, to architektoniczne wady typowej aplikacji kątowej (a tak naprawdę każdej aplikacji JavaScript): ścisłe powiązanie problemów i zmienność obiektów. Niedawno zakochałem się w projektowaniu aplikacji z Reactem i niezmiennymi strukturami danych. W ten sposób cudownie rozwiązuje to dwa następujące problemy:
Rozdzielenie problemów : komponent zużywa wszystkie swoje dane za pośrednictwem rekwizytów i w niewielkim stopniu polega na globalnych singletonach (takich jak usługi Angular) i nie wie nic o tym, co wydarzyło się nad nim w hierarchii widoku.
Zmienność : wszystkie rekwizyty są niezmienne, co eliminuje ryzyko niezamierzonej mutacji danych.
Angular 2.0 jest teraz na dobrej drodze do zapożyczenia od Reacta, aby osiągnąć dwa powyższe punkty.
źródło
Z mojej perspektywy
$watch
byłaby to najlepsza praktyka.Możesz nieco uprościć swój przykład:
To wszystko, czego potrzebujesz.
Ponieważ właściwości są aktualizowane jednocześnie, potrzebujesz tylko jednego zegarka. Ponadto, ponieważ pochodzą z jednego, raczej małego obiektu, zmieniłem je, aby po prostu obserwować
Timer.data
nieruchomość. Ostatni parametr przekazany do$watch
nakazuje mu sprawdzenie głębokiej równości, a nie tylko upewnienie się, że odwołanie jest takie samo.Aby podać trochę kontekstu, powodem, dla którego wolałbym tę metodę niż umieszczanie wartości usługi bezpośrednio w zakresie, jest zapewnienie właściwego rozdzielenia obaw. Twój pogląd nie powinien wymagać nic o twoich usługach, aby działać. Zadaniem kontrolera jest sklejenie wszystkiego razem; jego zadaniem jest pobieranie danych z Twoich usług i przetwarzanie ich w dowolny niezbędny sposób, a następnie dostarczanie Twojemu widokowi wszelkich potrzebnych mu szczegółów. Ale nie sądzę, że jego zadaniem jest po prostu przekazanie usługi bezpośrednio do widoku. W przeciwnym razie co w ogóle robi kontroler? Programiści AngularJS kierowali się tym samym rozumowaniem, gdy zdecydowali się nie włączać żadnej „logiki” do szablonów (np.
if
Instrukcji).Szczerze mówiąc, jest tu prawdopodobnie wiele perspektyw i nie mogę się doczekać innych odpowiedzi.
źródło
{{lastUpdated}}
vs.{{timerData.lastUpdated}}
Timer.data
w $ watch,Timer
musi być zdefiniowany w zakresie $ scope, ponieważ wyrażenie ciągu przekazywane do $ watch jest oceniane względem zakresu. Oto plunker, który pokazuje, jak to działa. Parametr objectEquality jest tutaj udokumentowany - trzeci parametr - ale niezbyt dobrze wyjaśniony.$watch
jest dość nieefektywny. Zobacz odpowiedzi na stackoverflow.com/a/17558885/932632 i stackoverflow.com/questions/12576798/ ...Spóźniony na przyjęcie, ale dla przyszłych pracowników Google - nie używaj podanej odpowiedzi.
JavaScript ma mechanizm przekazywania obiektów przez odniesienie, podczas gdy przekazuje tylko płytką kopię wartości „liczby, ciągi znaków itp.”.
W powyższym przykładzie, zamiast wiązać atrybuty usługi, dlaczego nie ujawnimy usługi w zakresie?
To proste podejście sprawi, że Angular będzie w stanie wykonać dwukierunkowe wiązanie i wszystkie magiczne rzeczy, których potrzebujesz. Nie hakuj kontrolera za pomocą obserwatorów lub niepotrzebnych znaczników.
A jeśli obawiasz się, że widok przypadkowo nadpisuje atrybuty usługi, użyj,
defineProperty
aby uczynić go czytelnym, wyliczalnym, konfigurowalnym lub zdefiniować metody pobierające i ustawiające. Możesz uzyskać dużą kontrolę, czyniąc swoją usługę solidniejszą.Ostatnia wskazówka: jeśli poświęcasz więcej czasu na pracę nad kontrolerem niż na usługach, robisz to źle :(.
W tym konkretnym kodzie demonstracyjnym, który dostarczyłeś, radziłbym:
Edytować:
Jak wspomniałem powyżej, możesz kontrolować zachowanie atrybutów usługi za pomocą
defineProperty
Przykład:
Teraz w naszym kontrolerze, jeśli to zrobimy
nasza usługa
propertyWithSetter
jakoś zmieni wartość, a także umieści nową wartość w bazie danych!Lub możemy przyjąć dowolne podejście.
Zapoznaj się z dokumentacją MDN dotyczącą
defineProperty
.źródło
$scope.model = {timerData: Timer.data};
prawie pewien, że to właśnie opisałem powyżej , po prostu dołączając go do modelu zamiast bezpośrednio na Scope.Myślę, że to pytanie ma element kontekstowy.
Jeśli po prostu pobierasz dane z usługi i emitujesz te informacje do jej widoku, myślę, że powiązanie bezpośrednio z właściwością usługi jest w porządku. Nie chcę pisać dużo standardowego kodu aby po prostu zmapować właściwości usługi na właściwości modelu do wykorzystania w moim widoku.
Co więcej, wydajność w kątach opiera się na dwóch rzeczach. Pierwsza to liczba powiązań na stronie. Drugi to kosztowne funkcje pobierające. Misko mówi o tym tutaj
Jeśli potrzebujesz wykonać logikę specyficzną dla instancji na danych usługi (w przeciwieństwie do masowania danych stosowanego w samej usłudze), a wynik tego wpływa na model danych wyświetlany w widoku, to powiedziałbym, że $ watcher jest odpowiedni, ponieważ pod warunkiem, że funkcja nie jest strasznie droga. W przypadku drogiej funkcji sugerowałbym buforowanie wyników w zmiennej lokalnej (do kontrolera), wykonywanie złożonych operacji poza funkcją $ watcher, a następnie wiązanie zakresu z wynikiem tego.
Uwaga: nie powinieneś zawieszać żadnych właściwości bezpośrednio na swoim $ scope.
$scope
Zmienna nie jest dany model. Zawiera odniesienia do Twojego modelu.Moim zdaniem „najlepsza praktyka” polegająca na prostym przekazywaniu informacji z serwisu do widoku:
A wtedy twój widok zawierałby
{{model.timerData.lastupdated}}
.źródło
Opierając się na powyższych przykładach, pomyślałem, że wrzucę sposób przezroczystego powiązania zmiennej kontrolera ze zmienną usługi.
W poniższym przykładzie zmiany w
$scope.count
zmiennej Controller zostaną automatycznie odzwierciedlone wcount
zmiennej Service .W środowisku produkcyjnym faktycznie używamy tego powiązania do aktualizowania identyfikatora usługi, która następnie asynchronicznie pobiera dane i aktualizuje zmienne usługi. Dalsze powiązanie oznacza, że kontrolery są automatycznie aktualizowane, gdy usługa aktualizuje się sama.
Poniższy kod można zobaczyć na stronie http://jsfiddle.net/xuUHS/163/
Widok:
Serwis / kontroler:
źródło
Myślę, że to lepszy sposób na powiązanie samej usługi zamiast atrybutów w niej.
Dlatego:
Możesz to zagrać na tym plunkrze .
źródło
Wolałbym, aby moi obserwatorzy byli mniej, jak to możliwe. Mój rozum oparty jest na moich doświadczeniach i można by to spierać teoretycznie.
Problem z używaniem obserwatorów polega na tym, że możesz użyć dowolnej właściwości w zakresie do wywołania dowolnej metody w dowolnym komponencie lub usłudze.
W prawdziwym projekcie wkrótce skończysz z niemożliwym do śledzenia (lepiej mówiąc trudnym do wyśledzenia) łańcuchem wywoływanych metod i zmienianych wartości, co szczególnie sprawia, że proces wdrażania jest tragiczny.
źródło
Powiązanie jakichkolwiek danych, które wysyła usługę nie jest dobrym pomysłem (architektura), ale jeśli już tego potrzebujesz, sugeruję 2 sposoby, aby to zrobić
1) możesz uzyskać dane, które nie są w Twoim serwisie. Możesz uzyskać dane wewnątrz swojego kontrolera / dyrektywy i nie będziesz miał problemu z ich powiązaniem
2) możesz używać zdarzeń angularjs, kiedy tylko chcesz, możesz wysłać sygnał (z $ rootScope) i złapać go w dowolnym miejscu, a nawet wysłać dane dotyczące tego eventName.
Może to ci pomoże. Jeśli potrzebujesz więcej przykładów, oto link
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html
źródło
Co powiesz na
Gdzie ParentScope jest usługą wstrzykniętą?
źródło
Najbardziej eleganckie rozwiązania ...
Piszę też EDA (architektury sterowane zdarzeniami), więc zwykle robię coś podobnego do następującego [uproszczona wersja]:
Następnie umieszczam odbiornik w moim kontrolerze na wybranym kanale i po prostu aktualizuję mój lokalny zakres w ten sposób.
Podsumowując, nie ma wiele z „Dobrych Praktyk” - a raczej jego głównie preferencji - tak długo, jak jesteś utrzymanie rzeczy stałych i zatrudniania słabego sprzężenia. Powodem, dla którego zalecałbym ten drugi kodeks, jest to, że EDA mają najniższe możliwe połączenie z natury. A jeśli nie przejmujesz się tym zbytnio, unikajmy wspólnej pracy nad tym samym projektem.
Mam nadzieję że to pomoże...
źródło