Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlanie

Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlanie

Zawsze interesowało mnie, jak zbudowany jest Habr od wewnątrz, jak zorganizowany jest przepływ pracy, jak zorganizowana jest komunikacja, jakie standardy są stosowane i jak ogólnie pisze się tutaj kod. Na szczęście dostałem taką szansę, bo niedawno stałem się częścią zespołu Habra. Na przykładzie małej refaktoryzacji wersji mobilnej spróbuję odpowiedzieć na pytanie: jak wygląda praca tutaj, na froncie. W programie: Node, Vue, Vuex i SSR z sosem z notatek o osobistych przeżyciach w Habr.

Pierwszą rzeczą, którą musisz wiedzieć o zespole programistów, jest to, że jest nas niewielu. Mało – to trzy fronty, dwa tyły i przewaga techniczna całego Habra – Baxley. Jest też oczywiście tester, projektant, trzech Vadimów, cudowna miotła, specjalista ds. marketingu i inni Bumburumowie. Ale jest tylko sześciu bezpośrednich współautorów źródeł Habra. To dość rzadkie zjawisko – projekt z wielomilionową publicznością, który z zewnątrz wygląda jak gigantyczne przedsiębiorstwo, w rzeczywistości bardziej przypomina kameralny startup o możliwie płaskiej strukturze organizacyjnej.

Podobnie jak wiele innych firm IT, Habr wyznaje idee Agile, praktyki CI i to wszystko. Jednak według moich odczuć Habr jako produkt rozwija się bardziej falowo niż stale. Tak więc przez kilka sprintów z rzędu pilnie coś kodujemy, projektujemy i przeprojektowujemy, psujemy coś i naprawiamy, rozwiązujemy zgłoszenia i tworzymy nowe, wchodzimy na prowizję i strzelamy sobie w stopy, aby w końcu udostępnić tę funkcję produkcja. A potem następuje pewna cisza, okres przebudowy, czas na zrobienie tego, co jest w kwadrancie „ważne – niepilne”.

To właśnie ten sprint „poza sezonem” zostanie omówiony poniżej. Tym razem obejmował on refaktoryzację mobilnej wersji Habr. Generalnie firma wiąże z nim duże nadzieje i w przyszłości powinna zastąpić całe zoo wcieleń Habra i stać się uniwersalnym rozwiązaniem wieloplatformowym. Któregoś dnia pojawi się układ adaptacyjny, PWA, tryb offline, personalizacja użytkownika i wiele innych ciekawych rzeczy.

Ustalmy zadanie

Któregoś razu na zwykłym stand-upie jeden z frontmanów wypowiadał się na temat problemów w architekturze komponentu komentarzy w wersji mobilnej. Mając to na uwadze, zorganizowaliśmy mikrospotkanie w formie psychoterapii grupowej. Wszyscy po kolei mówili, gdzie boli, wszystko zapisywali na papierze, współczuli, rozumieli, tyle że nikt nie klaskał. W rezultacie powstała lista 20 problemów, która jasno pokazała, że ​​mobilny Habr ma przed sobą jeszcze długą i ciernistą drogę do sukcesu.

Chodziło mi przede wszystkim o efektywność wykorzystania zasobów i tzw. płynny interfejs. Codziennie, jadąc z domu do pracy, do domu, widziałem, jak mój stary telefon desperacko próbował wyświetlić w kanale 20 nagłówków. Wyglądało to mniej więcej tak:

Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlanieMobilny interfejs Habr przed refaktoryzacją

Co tu się dzieje? Krótko mówiąc, serwer udostępniał stronę HTML każdemu w ten sam sposób, niezależnie od tego, czy użytkownik był zalogowany, czy nie. Następnie ładowany jest klient JS i ponownie żąda niezbędnych danych, ale dostosowanych do autoryzacji. Oznacza to, że faktycznie wykonaliśmy tę samą pracę dwa razy. Interfejs zamigotał, a użytkownik pobrał dobre sto dodatkowych kilobajtów. W szczegółach wszystko wyglądało jeszcze bardziej przerażająco.

Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlanieStary schemat SSR-CSR. Autoryzacja jest możliwa tylko na etapach C3 i C4, kiedy Node JS nie jest zajęty generowaniem kodu HTML i może przekazywać żądania do API.

Naszą ówczesną architekturę bardzo trafnie opisał jeden z użytkowników Habr:

Wersja mobilna to porażka. Mówię jak jest. Straszne połączenie SSR i CSR.

Musieliśmy to przyznać, bez względu na to, jak smutne to było.

Oceniłem możliwości, stworzyłem w Jirze zgłoszenie z opisem na poziomie „teraz jest źle, zrób to dobrze” i ogólnie rozłożyłem zadanie:

  • ponowne wykorzystanie danych,
  • zminimalizować liczbę przerysowań,
  • wyeliminować duplikaty żądań,
  • sprawić, że proces ładowania będzie bardziej oczywisty.

Wykorzystajmy dane ponownie

Teoretycznie renderowanie po stronie serwera ma na celu rozwiązanie dwóch problemów: uniknięcie ograniczeń wyszukiwarek w zakresie Indeksowanie SPA i poprawić metrykę FMP (nieuchronnie się pogarsza TTI). W klasycznym scenariuszu to w końcu sformułowany w Airbnb w 2013 r roku (wciąż na Backbone.js), SSR to ta sama izomorficzna aplikacja JS działająca w środowisku Node. Serwer po prostu wysyła wygenerowany układ w odpowiedzi na żądanie. Następnie po stronie klienta następuje ponowne nawodnienie i wtedy wszystko działa bez przeładowywania strony. Dla Habr, podobnie jak dla wielu innych zasobów zawierających treść tekstową, renderowanie serwerowe jest krytycznym elementem w budowaniu przyjaznych relacji z wyszukiwarkami.

Pomimo tego, że od pojawienia się technologii minęło już ponad sześć lat i w tym czasie naprawdę dużo wody przepłynęło pod mostem w świecie front-endu, dla wielu programistów ten pomysł nadal jest owiany tajemnicą. Nie zatrzymaliśmy się i wdrożyliśmy na produkcję aplikację Vue ze wsparciem SSR, pomijając jeden mały szczegół: nie wysłaliśmy klientowi stanu początkowego.

Dlaczego? Nie ma dokładnej odpowiedzi na to pytanie. Albo nie chcieli zwiększać rozmiaru odpowiedzi z serwera, albo z powodu szeregu innych problemów architektonicznych, albo po prostu nie wyszło. Tak czy inaczej, wyrzucenie stanu i ponowne wykorzystanie wszystkiego, co zrobił serwer, wydaje się całkiem odpowiednie i przydatne. Zadanie jest właściwie banalne - stan jest po prostu wstrzykiwany do kontekstu wykonania, a Vue automatycznie dodaje go do wygenerowanego układu jako zmienną globalną: window.__INITIAL_STATE__.

Jednym z problemów, który się pojawił, jest brak możliwości konwersji struktur cyklicznych na JSON (odniesienie cykliczne); został rozwiązany poprzez proste zastąpienie takich konstrukcji ich płaskimi odpowiednikami.

Ponadto, mając do czynienia z treściami UGC, należy pamiętać, że dane należy przekonwertować na encje HTML, aby nie złamać kodu HTML. Do tych celów używamy he.

Minimalizowanie przerysowań

Jak widać na powyższym diagramie, w naszym przypadku jedna instancja Node JS pełni dwie funkcje: SSR oraz „proxy” w API, gdzie następuje autoryzacja użytkownika. Ta okoliczność uniemożliwia autoryzację podczas działania kodu JS na serwerze, ponieważ węzeł jest jednowątkowy, a funkcja SSR jest synchroniczna. Oznacza to, że serwer po prostu nie może wysyłać żądań do siebie, gdy stos wywołań jest czymś zajęty. Okazało się, że zaktualizowaliśmy stan, ale interfejs nie przestał drgać, ponieważ dane na kliencie musiały zostać zaktualizowane z uwzględnieniem sesji użytkownika. Musieliśmy nauczyć naszą aplikację, aby w stanie początkowym umieszczała prawidłowe dane, biorąc pod uwagę login użytkownika.

Były tylko dwa rozwiązania tego problemu:

  • dołączać dane autoryzacyjne do żądań między serwerami;
  • podziel warstwy Node JS na dwie oddzielne instancje.

Pierwsze rozwiązanie wymagało użycia zmiennych globalnych na serwerze, natomiast drugie wydłużyło termin wykonania zadania o co najmniej miesiąc.

Jak dokonać wyboru? Habr często porusza się po ścieżce najmniejszego oporu. Nieformalnie istnieje ogólna chęć skrócenia cyklu od pomysłu do prototypu do minimum. Model podejścia do produktu przypomina nieco postulaty booking.com, z tą różnicą, że Habr znacznie poważniej podchodzi do opinii użytkowników i ufa Tobie, jako programiście, w podejmowaniu takich decyzji.

Kierując się tą logiką i własną chęcią szybkiego rozwiązania problemu, wybrałem zmienne globalne. I jak to często bywa, prędzej czy później trzeba za nie zapłacić. Zapłaciliśmy niemal natychmiast: pracowaliśmy w weekend, posprzątaliśmy konsekwencje, napisaliśmy post mortem i zaczął dzielić serwer na dwie części. Błąd był bardzo głupi, a związany z nim błąd nie był łatwy do odtworzenia. I tak, szkoda tego, ale tak czy inaczej, potykając się i jęcząc, mój PoC ze zmiennymi globalnymi mimo wszystko wszedł do produkcji i działa całkiem pomyślnie, czekając na przejście na nową architekturę „dwuwęzłową”. To był ważny krok, bo formalnie cel został osiągnięty - SSR nauczyło się dostarczać w pełni gotową do użycia stronę, a interfejs użytkownika stał się znacznie spokojniejszy.

Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlanieMobilny interfejs Habr po pierwszym etapie refaktoryzacji

Ostatecznie architektura SSR-CSR wersji mobilnej prowadzi do tego obrazu:

Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlanie„Dwuwęzłowy” obwód SSR-CSR. API Node JS jest zawsze gotowe na asynchroniczne wejścia/wyjścia i nie jest blokowane przez funkcję SSR, ponieważ ta ostatnia znajduje się w osobnej instancji. Łańcuch zapytań nr 3 nie jest potrzebny.

Eliminacja duplikatów żądań

Po wykonaniu manipulacji początkowe renderowanie strony nie wywoływało już epilepsji. Jednak dalsze używanie Habr w trybie SPA nadal powodowało zamieszanie.

Podstawą przepływu użytkowników są bowiem przejścia formularza lista artykułów → artykuł → komentarze i odwrotnie, ważna była przede wszystkim optymalizacja zużycia zasobów tego łańcucha.

Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlaniePowrót do kanału postów wywołuje nowe żądanie danych

Nie trzeba było kopać głęboko. Na powyższym zrzucie ekranu widać, że aplikacja ponownie żąda listy artykułów podczas przesuwania wstecz, a podczas żądania nie widzimy artykułów, co oznacza, że ​​poprzednie dane gdzieś znikają. Wygląda na to, że komponent listy artykułów używa stanu lokalnego i traci go po zniszczeniu. W rzeczywistości aplikacja korzystała ze stanu globalnego, ale architektura Vuex została zbudowana od podstaw: moduły są powiązane ze stronami, które z kolei są powiązane z trasami. Co więcej, wszystkie moduły są „jednorazowe” – każda kolejna wizyta na stronie powoduje przepisanie całego modułu:

ArticlesList: [
  { Article1 },
  ...
],
PageArticle: { ArticleFull1 },

W sumie mieliśmy moduł Lista artykułów, który zawiera obiekty typu Artykuł i moduł StronaArtykuł, który był rozszerzoną wersją obiektu Artykuł, rodzaj ArtykułPełny. W zasadzie ta implementacja nie niesie w sobie niczego strasznego - jest bardzo prosta, można by nawet powiedzieć naiwna, ale niezwykle zrozumiała. Jeśli zresetujesz moduł za każdym razem, gdy zmienisz trasę, możesz nawet z tym żyć. Jednak na przykład poruszanie się między kanałami artykułów /feed → /all, gwarantuje wyrzucenie wszystkiego, co jest związane z osobistym kanałem, ponieważ mamy tylko jeden Lista artykułów, do którego należy wprowadzić nowe dane. To znowu prowadzi nas do powielania żądań.

Zebrawszy wszystko, co udało mi się wygrzebać na ten temat, sformułowałem nową strukturę państwa i przedstawiłem ją moim kolegom. Dyskusje były długie, ale ostatecznie argumenty za przeważyły ​​nad wątpliwościami i przystąpiłem do realizacji.

Logikę rozwiązania najlepiej przedstawić w dwóch krokach. Najpierw próbujemy oddzielić moduł Vuex od stron i powiązać bezpośrednio z trasami. Tak, w sklepie będzie trochę więcej danych, gettery staną się nieco bardziej skomplikowane, ale artykułów nie będziemy ładować dwukrotnie. W przypadku wersji mobilnej jest to chyba najmocniejszy argument. Będzie to wyglądać mniej więcej tak:

ArticlesList: {
  ROUTE_FEED: [ 
    { Article1 },
    ...
  ],
  ROUTE_ALL: [ 
    { Article2 },
    ...
  ],
}

Ale co, jeśli listy artykułów mogą nakładać się na wiele tras i co jeśli chcemy ponownie wykorzystać dane obiektowe Artykuł aby wyrenderować stronę postu, zamieniając ją w ArtykułPełny? W takim przypadku bardziej logiczne byłoby użycie takiej struktury:

ArticlesIds: {
  ROUTE_FEED: [ '1', ... ],
  ROUTE_ALL: [ '1', '2', ... ],
},
ArticlesList: {
  '1': { Article1 }, 
  '2': { Article2 },
  ...
}

Lista artykułów tutaj jest to po prostu rodzaj repozytorium artykułów. Wszystkie artykuły, które zostały pobrane podczas sesji użytkownika. Traktujemy je z najwyższą starannością, ponieważ jest to ruch, który mógł zostać pobrany przez ból gdzieś w metrze pomiędzy stacjami, a my na pewno nie chcemy ponownie sprawiać użytkownikowi tego bólu, zmuszając go do wczytania danych, które już posiadał pobrany. Obiekt Identyfikatory artykułów jest po prostu tablicą identyfikatorów (jak gdyby „linkami”) do obiektów Artykuł. Taka struktura pozwala uniknąć powielania danych wspólnych dla tras i ponownego wykorzystania obiektu Artykuł podczas renderowania strony z postem poprzez połączenie z nią rozszerzonych danych.

Dane wyjściowe listy artykułów również stały się bardziej przejrzyste: komponent iteratora iteruje po tablicy z identyfikatorami artykułów i rysuje komponent zwiastuna artykułu, przekazując identyfikator jako atrybut, a komponent podrzędny z kolei pobiera niezbędne dane z Lista artykułów. Po wejściu na stronę publikacji otrzymujemy już istniejącą datę Lista artykułów, zwracamy się z prośbą o pozyskanie brakujących danych i po prostu dodajemy je do istniejącego obiektu.

Dlaczego to podejście jest lepsze? Jak pisałem powyżej, takie podejście jest delikatniejsze w stosunku do pobranych danych i pozwala na ich ponowne wykorzystanie. Ale poza tym otwiera drogę do nowych możliwości, które idealnie pasują do takiej architektury. Na przykład odpytywanie i ładowanie artykułów do kanału w miarę ich pojawiania się. Możemy po prostu umieścić najnowsze posty w „magazynu” Lista artykułów, zapisz osobną listę nowych identyfikatorów w Identyfikatory artykułów i powiadomić o tym użytkownika. Kiedy klikniemy w przycisk „Pokaż nowe publikacje”, po prostu wstawimy nowe Id na początek tablicy aktualnej listy artykułów i wszystko będzie działać niemal magicznie.

Dzięki czemu pobieranie staje się przyjemniejsze

Wisienką na torcie refaktoryzacji jest koncepcja szkieletów, która sprawia, że ​​proces pobierania treści w wolnym Internecie jest nieco mniej obrzydliwy. Nie było dyskusji na ten temat, droga od pomysłu do prototypu trwała dosłownie dwie godziny. Projekt praktycznie sam się narysował, a my nauczyliśmy nasze komponenty renderowania prostych, ledwo migoczących bloków div w oczekiwaniu na dane. Subiektywnie takie podejście do ładowania faktycznie zmniejsza ilość hormonów stresu w organizmie użytkownika. Szkielet wygląda tak:

Dzienniki programistów front-end Habr: refaktoryzacja i odzwierciedlanie
Ładowanie Habra

Odbicie

Pracuję w Habré już pół roku i znajomi wciąż pytają: no i jak ci się tam podoba? Ok, wygodne - tak. Jest jednak coś, co sprawia, że ​​ta praca różni się od innych. Pracowałem w zespołach, które były zupełnie obojętne na swój produkt, nie wiedziały i nie rozumiały, kim są ich użytkownicy. Ale tutaj wszystko jest inne. Tutaj czujesz się odpowiedzialny za to, co robisz. W procesie tworzenia funkcjonalności częściowo stajesz się jej właścicielem, bierzesz udział we wszystkich spotkaniach produktowych związanych z Twoją funkcjonalnością, sam zgłaszasz sugestie i podejmujesz decyzje. Tworzenie produktu, którego sam używasz na co dzień, jest bardzo fajne, ale pisanie kodu dla osób, które prawdopodobnie są w tym lepsze od Ciebie, to po prostu niesamowite uczucie (bez sarkazmu).

Po wydaniu wszystkich tych zmian otrzymaliśmy pozytywne opinie i było to bardzo, bardzo miłe. To inspirujące. Dziękuję! Napisz więcej.

Przypomnę, że po zmiennych globalnych postanowiliśmy zmienić architekturę i wydzielić warstwę proxy do osobnej instancji. Architektura „dwuwęzłowa” została już udostępniona w formie publicznych testów beta. Teraz każdy może się na nią przełączyć i pomóc nam ulepszyć mobilny Habr. To wszystko na dzisiaj. Chętnie odpowiem na wszystkie Twoje pytania w komentarzach.

Źródło: www.habr.com

Dodaj komentarz