W trakcie opracowywania aplikacji jednostronicowej w czasie rzeczywistym stopniowo wprowadzałem websockets, aby umożliwić moim użytkownikom dostęp do aktualnych danych. Podczas tej fazy ze smutkiem zauważyłem, że niszczę zbyt wiele struktury mojej aplikacji i nie udało mi się znaleźć rozwiązania tego problemu.
Zanim przejdę do szczegółów, wystarczy trochę kontekstu:
- Aplikacja internetowa to SPA w czasie rzeczywistym;
- Backend jest w Ruby on Rails. Zdarzenia w czasie rzeczywistym są wypychane przez Ruby do klucza Redis, a następnie serwer mikro-węzłów wyciąga to i wypycha do Socket.Io;
- Frontend jest w AngularJS i łączy się bezpośrednio z serwerem socket.io w Node.
Po stronie serwera, w czasie rzeczywistym miałem wyraźny rozdział zasobów oparty na kontrolerze / modelu, z przetwarzaniem dołączonym do każdego z nich. Ten klasyczny projekt MVC został całkowicie zniszczony, a przynajmniej ominięty, w momencie, gdy zacząłem przesyłać rzeczy za pośrednictwem gniazd sieciowych do moich użytkowników. Mam teraz pojedynczy potok, w którym cała moja aplikacja płynie mniej lub bardziej ustrukturyzowanymi danymi . I uważam to za stresujące.
W interfejsie głównym problemem jest powielanie logiki biznesowej. Gdy użytkownik ładuje stronę, muszę ładować moje modele przez klasyczne wywołania AJAX. Ale muszę też radzić sobie z zalewaniem danych w czasie rzeczywistym i duplikuję większość logiki biznesowej po stronie klienta, aby zachować spójność modeli po stronie klienta.
Po kilku badaniach nie mogę znaleźć żadnych dobrych postów, artykułów, książek ani czegokolwiek, co dałoby porady na temat tego, jak można i należy zaprojektować architekturę nowoczesnej aplikacji internetowej z uwzględnieniem kilku konkretnych tematów:
- Jak uporządkować dane wysyłane z serwera do użytkownika?
- Czy powinienem wysyłać tylko zdarzenia typu „ten zasób został zaktualizowany i należy go ponownie załadować za pośrednictwem wywołania AJAX”, czy też przesłać zaktualizowane dane i zastąpić poprzednie dane załadowane za pomocą początkowych wywołań AJAX?
- Jak zdefiniować spójny i skalowalny szkielet wysyłanych danych? jest to komunikat o aktualizacji modelu lub komunikat „wystąpił błąd związany z blahblahblah”
- Jak nie wysyłać danych o wszystkim z dowolnego miejsca w wewnętrznej bazie danych?
- Jak ograniczyć powielanie logiki biznesowej zarówno po stronie serwera, jak i klienta?
źródło
Odpowiedzi:
Użyj wzorca wiadomości . Cóż, już używasz protokołu przesyłania wiadomości, ale mam na myśli strukturę zmian jako wiadomości ... konkretnie zdarzenia. Zmiana po stronie serwera powoduje zdarzenia biznesowe. W twoim scenariuszu opinie klientów są zainteresowane tymi wydarzeniami. Zdarzenia powinny zawierać wszystkie dane istotne dla tej zmiany (niekoniecznie wszystkie dane przeglądania). Strona klienta powinna następnie zaktualizować części widoku, które obsługuje, o dane zdarzenia.
Na przykład, jeśli aktualizowałeś giełdowy giełdowy i zmieniono AAPL, nie chciałbyś zepchnąć wszystkich cen akcji ani nawet wszystkich danych o AAPL (nazwa, opis itp.). Naciskasz tylko AAPL, deltę i nową cenę. Na kliencie zaktualizowałbyś wtedy tylko tę cenę akcji w widoku.
Nie powiedziałbym ani jednego. Jeśli wysyłasz zdarzenie, śmiało i wyślij wraz z nim odpowiednie dane (nie dane całego obiektu). Nadaj mu nazwę tego rodzaju wydarzenia. (Nazewnictwo i dane istotne dla tego zdarzenia wykraczają poza mechaniczną pracę systemu. Ma to więcej wspólnego z tym, jak modelowana jest logika biznesowa.) Osoby aktualizujące widok muszą wiedzieć, jak przełożyć każde konkretne zdarzenie na precyzyjna zmiana widoku (tj. aktualizuj tylko to, co się zmieniło).
Powiedziałbym, że jest to duże, otwarte pytanie, które należy podzielić na kilka innych pytań i opublikować osobno.
Ogólnie jednak system zaplecza powinien tworzyć i wysyłać zdarzenia dotyczące ważnych wydarzeń w firmie. Mogą one pochodzić z zewnętrznych źródeł lub z aktywności w samym zapleczu.
Użyj wzorca publikowania / subskrypcji . Gdy SPA wczyta nową stronę, która jest zainteresowana otrzymywaniem aktualizacji w czasie rzeczywistym, strona powinna subskrybować tylko te zdarzenia, z których może korzystać, i wywoływać logikę aktualizacji widoku w miarę pojawiania się tych zdarzeń. Prawdopodobnie będziesz potrzebować logiki pub / sub na serwer w celu zmniejszenia obciążenia sieci. Istnieją biblioteki dla publikacji / subskrybentów Websocket, ale nie jestem pewien, jakie są w ekosystemie Railsów.
Wygląda na to, że musisz zaktualizować dane widoku zarówno na kliencie, jak i na serwerze. Domyślam się, że potrzebujesz danych widoku po stronie serwera, aby mieć migawkę, aby uruchomić klienta w czasie rzeczywistym. Ponieważ w grę wchodzą dwa języki / platformy (Ruby i Javascript), logika aktualizacji widoku będzie musiała zostać napisana w obu. Oprócz transpilacji (która ma swoje własne problemy), nie widzę rozwiązania tego problemu.
Punkt techniczny: manipulacja danymi (aktualizacja widoku) nie jest logiką biznesową. Jeśli chodzi o sprawdzanie poprawności przypadków użycia, wydaje się to nieuniknione, ponieważ sprawdzanie poprawności klienta jest konieczne dla dobrego doświadczenia użytkownika, ale ostatecznie nie może być zaufane przez serwer.
Oto, w jaki sposób widzę dobrze taką strukturę.
Widoki klienta:
Polecenia klienta:
Po stronie serwera można faktycznie podzielić na kilka komponentów o ograniczonej odpowiedzialności. Taki, który po prostu przetwarza przychodzące żądania i tworzy zdarzenia. Inny może zarządzać subskrypcjami klientów, nasłuchiwać zdarzeń (powiedzmy w toku) i przekazywać odpowiednie zdarzenia subskrybentom. Możesz mieć jedną trzecią, która nasłuchuje zdarzeń i aktualizuje widoki po stronie serwera - być może dzieje się to nawet zanim subskrybenci otrzymają zdarzenia.
To, co opisałem, to forma CQRS + Messaging i typowa strategia rozwiązywania problemów, z którymi się borykasz .
Nie włączyłem w ten opis Sourcing zdarzeń, ponieważ nie jestem pewien, czy jest to coś, co chcesz podjąć, czy też potrzebujesz go koniecznie. Ale jest to powiązany wzór.
źródło
Po kilku miesiącach pracy głównie nad backendem, mogłem skorzystać z niektórych porad tutaj, aby rozwiązać problemy, z którymi borykała się platforma.
Głównym celem przy ponownym przemyśleniu backendu było jak najtrudniejsze trzymanie się CRUD. Wszystkie działania, wiadomości i żądania rozproszone po wielu trasach zostały zgrupowane w zasoby, które są tworzone, aktualizowane, czytane lub usuwane . Brzmi to teraz oczywisto, ale to bardzo trudny sposób, aby ostrożnie je zastosować.
Po tym, jak wszystko zostało zorganizowane w zasoby, byłem w stanie dołączyć wiadomości w czasie rzeczywistym do modeli.
W interfejsie API Reszta wszystkie metody tworzenia, aktualizowania i usuwania generują odpowiedź „tylko głowa”, kod HTTP informujący o sukcesie lub niepowodzeniu oraz faktyczne dane przesyłane przez gniazda sieciowe.
W interfejsie każde zasoby są obsługiwane przez określony komponent, który ładuje je przez HTTP podczas inicjalizacji, a następnie subskrybuje aktualizacje i utrzymuje ich stan w czasie. Widoki następnie łączą się z tymi komponentami, aby wyświetlić zasoby i wykonać działania na tych zasobach przez te same komponenty.
Uważam, że CQRS + Messaging i Event Sourcing czyta bardzo interesujące, ale czułem, że było to trochę skomplikowane dla mojego problemu i może być bardziej dostosowane do intensywnych aplikacji, w których przekazywanie danych do scentralizowanej bazy danych jest drogim luksusem. Ale na pewno będę pamiętać o tym podejściu.
W takim przypadku aplikacja będzie miała kilku współbieżnych klientów, a ja postanowiłem polegać na bazie danych. Najbardziej zmieniające się modele są przechowywane w Redis, które, jak ufam, obsługuje kilkaset aktualizacji na sekundę.
źródło