Strona główna / Artykuły / Jak małe decyzje kumulują się w długowiecznych kodbasach React

Jak małe decyzje kumulują się w długowiecznych kodbasach React

Piętnaście nawyków konserwacyjnych dla aplikacji React, które funkcjonują przez lata: czytelny kod, skoncentrowane komponenty, stan ograniczony do określonego zakresu, minimalne zależności, testy oraz monitorowanie.

2206 słów

Rozpoczęcie projektu React to najłatwiejsza część. Prawdziwy test następuje po kilku miesiącach lub latach, gdy aplikacja ma większą liczbę użytkowników, więcej funkcji, więcej współpracowników oraz większą liczbę zależności, a kilka „tymczasowych” rozwiązań po cichu staje się niezbędnych do funkcjonowania. W tym momencie sam React rzadko jest problemem; problemem jest utrzymanie zrozumiałości bazy kodu, podczas gdy wszystko wokół niej ciągle się zmienia. Ten przewodnik przedstawia piętnaście nawyków, ugrupowanych według miejsc, w których faktycznie gromadzą się koszty konserwacji, abyś mógł zidentyfikować decyzje, które się kumulują, zanim przekształcą bazę kodu w coś, czego nikt nie chce dotykać.

Kod napisany z myślą o przyszłym czytelniku

Niech kod będzie oczywisty, a nie sprytny

Gęste, zwięzłe linijki kodu, zaawansowane abstrakcje oraz funkcje pomocnicze stworzone dla dziesiątek hipotetycznych przypadków sprawiają wrażenie produktywnych podczas ich pisania. Sześć miesięcy później stają się zagadką, często dla samego osoby, która je napisała, który już nie pamięta dlaczego.

Aльтernatywą jest celowo prosty kod. Rozważmy wyświetlanie ekranu, który może być w trakcie ładowania, nieudany lub gotowy. Wczesne zwracanie wartości sprawia, że każdy stan jest wyraźny – po jednej warunku na raz:

if (isLoading) {
  return <LoadingState />;
}

Gdy dochodzi do błędu, struktura pozostaje taka sama, a prawidłowy scenariusz znajduje się na końcu:

if (error) {
  return <ErrorState />;
}
return <Dashboard />;

Nic tutaj nie jest imponujące, i to właśnie chodzi: każdy może w ciągu sekund zobaczyć, który komponent się wyświetla i kiedy. W dużej aplikacji to, jak szybko następny programista zrozumie twój kod, ma znacznie większe znaczenie niż zaimponowanie temu obecnemu.

Nazwy to dokumentacja, która nigdy nie staje się przestarzała

Gdy piszesz kod, nadawanie nazw wydaje się błahą kwestią, ale staje się niezwykle ważne podczas debugowania po pół roku. Porównaj poszukiwanie czegoś w bazie kodu w tych przypadkach:

handleData()

z poszukiwaniem tego:

calculateMonthlyRevenue()

Druga nazwa informuje cię, co oblicza funkcja, zanim otworzysz plik. Duże bazy kodu są w dużej mierze kanałem komunikacji między programistami; precyzyjna nazwa wyraża intencję, a niejasna zamienia każdego czytelnika w detektywa.

Uwaga na ogólne nazwy, które pojawiają się pod presją terminów:

data
item
temp
helper
value
thing

Tak, thing rzeczywiście występuje w prawdziwych bazach kodu. Praktyczna zasada: jeśli nazwa pasowałaby równie dobrze do dowolnego pliku w projekcie, prawdopodobnie nie mówi wystarczająco wiele o tym konkretnym pliku.

Grenice komponentów i stanu

Zbyt duże komponenty są kosztowne

Prawie każdy długo istniejący projekt React z czasem posiada komponent składający się z czterech cyfr linii kodu. Rzadko tak się zaczyna – zwykle ma około 150 linii, potem pojawia się modala, następnie filtry, funkcje pobierania danych, sprawdzanie uprawnień i druga modala. W końcu otwierasz plik i znajdujesz coś w tym rodzaju:

Dashboard.tsx
1,247 lines

Komponenty o takiej wielkości są trudne do zrozumienia, testowania, ponownego użycia i debugowania, a ich modyfikacja jest ryzykowna, ponieważ każda zmiana może wpłynąć na stan, od którego zależą inne części pliku. Lepiej dzielić je wcześniej, niż to konieczne. Zamiast jednego pliku:

Dashboard.tsx

podziel ekran na części, z których każda pełni jedną funkcję:

DashboardHeader.tsx
DashboardStats.tsx
DashboardFilters.tsx
RecentActivity.tsx
DashboardTable.tsx

Każdy plik ma teraz jedną responsybilność i znacznie mniejszy zasięg wpływu. Praktyczny kryterium: jeśli nie możesz opisać komponentu w jednym zdaniu bez użycia kilku „i”, podziel go.

Wykorzystuj wzorce, które już widziałeś, a nie te, które sobie wyobrażasz

Komponenty wielokrotnego użycia to dobry instynkt, który łatwo przesadzić. Częstym błędem jest używanie jednego przycisku, który ma pełnić rolę wszystkich przycisków w aplikacji:

<UniversalButton
  type="primary"
  variant="rounded"
  size="medium"
  iconPosition="left"
  loadingStyle="spinner"
/>

Każda nowa właściwość wydaje się nieszkodliwa, ale razem tworzą one przestrzeń kombinacyjną wariantów, których nikt w pełni nie testuje, a „wielokrotnie używalny” komponent okazuje się trudniejszy w użyciu niż trzy małe, specyficzne przyciski. Ponowne wykorzystanie jest cenne; przedwczesna uogólnianie – nie.

Wyciągaj wspólną abstrakcję dopiero wtedy, gdy zobaczysz, że jakiś wzorzec rzeczywiście się powtarza, a nie dlatego, że przyszła strona może jej potrzebować. Jeśli taka potrzeba się pojawi, zaprojektujesz abstrakcję na podstawie rzeczywistych przykładów, a nie domysłów.

Uporządkuj stan według tego, do czego należy

Zarządzanie stanem ma tendencję do postępującego pogorszenia się w sposób niewidoczny. Mała aplikacja zaczyna się od stanu komponentów:

useState()

Następnie dodaje wspólny stan poprzez kontekst:

useContext()

Ostatecznie projekt zawiera jednocześnie Redux, kilka kontekstów, stan lokalny, stan URL oraz stan serwera, bez jasnej odpowiedzi na to, który z nich kontroluje pasek boczny. Każde narzędzie wydawało się rozsądne w momencie dodania; brakuje jednak zasady określającej, co ma znajdować się gdzie. Prostym sposobem na przywrócenie porządku jest sklasyfikowanie stanu według jego charakteru. Stan lokalny interfejsu użytkownika obejmuje takie elementy jak:

modal open
dropdown selected
input value

Powinien pozostać w komponencie, który go używa. Stan serwera to dane znajdujące się na stronie backendowej, które są jedynie zapisywane w pamięci podręcznej przeglądarki:

users
products
analytics

Dla tej kategorii należy użyć biblioteki przeznaczonej do pobierania, przechowywania w pamięci podręcznej i ponownego walidowania danych zdalnych, zamiast ręcznie kopiować odpowiedzi do globalnego magazynu. Jeśli wasz zespół rozważa taką zmianę, korzyści i wady są omówione w naszym porównaniu React Query i Redux dla stanu serwera. Wreszcie, prawdziwy stan globalny składa się z niewielkiej liczby elementów:

theme
authenticated user
app-wide preferences

Zachowaj tę ostatnią grupę w minimalnym wymiarze: każdy komponent może czytać lub zmieniać stan globalny, więc im mniej go ma, tym mniej błędów pojawia się bez wyraźnej przyczyny. Filtry, karty i stronifikacja często powinny znajdować się w URL, gdzie przetrwają ponowne załadowanie i mogą być udostępniane.

Struktura i zależności

Zrób tak, by układ folderów był przewidywalny

Wyobraź sobie dołączenie do projektu i znalezienie tego w jego korzeniu:

src/
components/
shared/
common/
helpers/
utils/
services/
core/
misc/
new/
new2/

Gdzie powinien znaleźć się nowy komponent profilu użytkownika? Nikt nie potrafi tego powiedzieć, więc każdy programista decyduje inaczej, co sprawia, że struktura stale się zmienia. Nakładające się kategorie takie jak shared, common, helpers i utils pokazują, że nikt nie ustalił, co oznacza każda z nich.

Rozwiązanie oparte na funkcjach eliminuje większość tych niejasności, grupując kod według części produktu, do której służy:

features/
  auth/
  dashboard/
  users/
  billing/

W ramach każdej funkcji mała, powtarzająca się liczba podfolderek pomaga utrzymać spójność struktury:

components/
hooks/
services/
types/

Teraz każdy, kto pracuje nad rozliczeniami, wie, gdzie znajduje się kod odpowiedzialny za nie, a przerabianie nowej funkcjonalności dotyczy tylko jednego katalogu zamiast dziesięciu. Mogą sprawdzić się też inne struktury (zobacz nasze porównanie struktur folderów w React); ważne jest, aby zasada była przewidywalna i spisana.

Traktuj każdą zależność jako element przyszłej konserwacji

Dodanie pakietu wymaga jednego polecenia:

npm install something-cool

Koszty pojawiają się później: pakiety są porzucane, wprowadzają zmiany dewastujące funkcjonalność, ujawniają problemy bezpieczeństwa i powiększają rozmiar pliku z kodem, a wasz zespół musi ciągle aktualizować kod, którego nikt z jego członków nie czytał.

Zanim zainstalujesz coś, zastanów się, czy naprawdę tego potrzebujesz. Pakiet, który oszczędza sporo wysiłku lub rozwiązuje naprawdę trudny problem, np. obsługę dat z uwzględnieniem stref czasowych, zasługuje na swoje miejsce. Taki, który jedynie formatuje ciąg znaków, nie.

Sieci bezpieczeństwa rozwijające się wraz z aplikacją

Przetestuj procesy, które najbardziej ucierpią w przypadku awarii

W małej aplikacji klikanie po ekranach przed publikacją wydaje się wystarczające. W dużej natomiast edycja pojedynczej funkcji może spowodować błędy w rozliczeniach z niezrozumiałych powodów, i właśnie wtedy przydają się testy automatyczne: pozwalają one z pewnością siebie zmieniać kod, którego nie napisałeś.

Nie musisz objąć każdym szczegółem implementacji. Skup się na procesach użytkownika, w których awaria ma duże konsekwencje:

  • logowanie
  • proces zakupu
  • wysyłanie ważnych formularzy
  • weryfikacja uprawnień
  • obliczenia, od których zależy działanie aplikacji

Testy nie udowodnią, że aplikacja jest bezbłędna; wykrywają oczywiste regresje zanim to zrobią użytkownicy. Testy zachowania, a nie elementów wewnętrznych, radzą sobie również znacznie lepiej podczas refaktoryzacji.

Uważaj na powolne, kumulatywne pogorszenie wydajności

Duże aplikacje React rzadko stają się wolne już po pierwszej wersji. Wydajność pogarsza się stopniowo, na skutek pojedynczych, rozsądnych decyzji: ciężkich zależności, ogromnych obrazów, uniknionych ponownych renderowań, dziesięciu żądań na jednej stronie. Wszystko to może sprawić, że ładowanie dashboardu potrwa pięć sekund, dlatego należy stale obserwować:

  • ponowne renderowanie komponentów, mimo że nic z tego, co pokazują, się nie zmieniło
  • stopniowy wzrost rozmiaru pliku bundle z każdą nową wersją
  • wysyłanie tego samego żądania więcej niż raz na jedną stronę
  • poszczególne komponenty, które wolno renderują
  • długie listy renderowane bez wykorzystania wirtualizacji
  • zbyt duże obrazy

Niewielkie sukcesy się kumulują, a wykrycie regresji w momencie jej wystąpienia jest o wiele tańsze niż próba jej znalezienia później.

Celowo projektuj drogę do niepowodzenia

Najwięcej wysiłku wkłada się w „szczęśliwą ścieżkę”, gdzie każda prośba jest realizowana pomyślnie. W środowisku produkcyjnym połączenia się rozrywają, API zawodzą, uprawnienia ulegają zmianie, a backend zwraca dane, których nikt się nie spodziewał. Użytkownik powinien zobaczyć jasną, możliwą do naprawy wiadomość w stylu „Nie udało się załadować twoich danych. Spróbuj ponownie.” zamiast surowego błędu w czasie wykonywania, np.:

TypeError: Cannot read properties of undefined

W kontekście React oznacza to wyraźne stany błędu podczas pobierania danych, granice błędów, które zapobiegają wyświetleniu pustej strony przy awarii pojedynczego komponentu, oraz operacje ponownego próbowania tam, gdzie to ma sens.

Traktuj monitorowanie w środowisku produkcyjnym jako część rozwoju

Rozpakowanie aplikacji to nie koniec pracy. Środowisko produkcyjne ujawnia problemy, których lokalny rozwój nigdy nie pokazałby, więc potrzebna jest możliwość obserwacji:

  • błędy w czasie wykonywania i miejsca ich występowania
  • żądania, które są wolniejsze niż oczekiwano
  • wołania API, które zawodzą
  • ogólna wydajność strony i interakcji
  • sposób, w jaki ludzie faktycznie używają produktu

Bez tego raport o błędzie to po prostu „użytkownik powiedział, że coś się wczoraj zepsuło”. Śledzenie błędów i logi kontekstowe przekształcają to w ślad wywołań i datę czasową, na podstawie których można podjąć działania.

Habity, które utrzymują zespół w harmonii

Pisz dokumentację dla osób już pracujących w zespole

Dokumentacja jest często traktowana jako obowiązek przy przekazywaniu obowiązków, a jednak pomaga wszystkim członkom zespołu już dziś, szczególnie w kwestiach złożonych uprawnień, nietypowej architektury, integracji z podmiotami trzecimi oraz ważnych reguł biznesowych.

Nie jest potrzebny długi podręcznik. Krótkie notatki na temat sposobu działania autoryzacji, przepływu danych, lokalizacji głównych funkcji oraz powodów podjętych decyzji oszczędzają godziny pracy. Zadawanie pytań oryginalnemu programiście przestaje być skuteczne po jego odejściu, a w projektach długoterminowych to zawsze się dzieje.

Refaktoryzacja to rutynowa konserwacja, a nie przyznanie się do porażki

Refaktoryzacja nie jest dowodem na to, że oryginalny kod był zły. Wymagania, zespoły i produkty się zmieniają, a kod, który pasował rok temu, może już nie rozwiązywać swojego problemu.

Niebezpieczeństwo polega na twierdzeniu, że coś „już działa”. Tak samo jest z trójnogim krzesłem, dopóki ktoś na nie nie usiądzie. Małe, regularne refaktoryzacje wsparte testami utrzymują zadłużenie techniczne w ryzach.

Jednolitość jest ważniejsza niż osobisty gust

Jeden programista pisze identyfikatory w ten sposób:

camelCase

Inny woli taki styl:

snake_case

A trzeci z nich co kilka tygodni wymyśla nowy wzorzec komponentu. Duży zespół nie może efektywnie pracować, gdy każdy plik odpowiada upodobaniom jego autora, dlatego należy sprawić, by spójność była automatyczna poprzez:

  • linter z ustalonymi regułami
  • automatyczny formatator
  • dokumentowane konwencje nazewania
  • współdzielone, sprawdzone wzorce dla typowych zadań

Baza kodu powinna przypominać jeden projekt, a nie tuzin programistów kłócących się o strukturę plików.

Wybierz architekturę, którą twój zespół naprawdę może wdrożyć

Dyskusje na temat architektury to ulubiona rozrywka: mikrosłuzby, monorepo, Clean Architecture, projektowanie napędzane domeną – zawsze ktoś ma przygotowany diagram. Jednak najbardziej zaawansowana opcja nie jest automatycznie najlepsza. Dobry wybór musi spełniać trzy kryteria:

  • rozwiązuje problemy, które faktycznie masz
  • cały zespół potrafi go wyjaśnić
  • Zespół może utrzymywać system w działaniu i go rozwijać
  • Zawsze lepszy jest prosty projekt stosowany konsekwentnie niż genialny, którego rozumie tylko jego twórca.

    Dlaczego to wszystko nie jest ekscytujące i dlaczego to działa

    Długoterminowa konserwacja zmienia znaczenie pojęcia „dobrego rozwoju”: szybkość pisania ma mniejsze znaczenie niż czytelność, łatwość przyszłych zmian, potrzeby innych programistów oraz zachowanie w środowisku produkcyjnym. Oto lista kontrolna do stosowania:

    • Komponenty pozostają małe i służą jednemu celowi
    • Stan globalny to krótka, przemyślana lista
    • Nazwy opisują intencję
    • Każdy nowy pakiet ma uzasadnienie
    • Struktura folderów podąża za jedną udokumentowaną zasadą
    • Refaktoryzacja odbywa się ciągle
    • Kluczowe ścieżki użytkownika mają testy
    • Błędy w środowisku produkcyjnym są widoczne
    • Prostsza opcja wygrywa w przypadku remisu

    Te zasady nie są efektowne, co prawdopodobnie jest powodem, dla którego przetrwają. Aby zapoznać się z praktykami strukturalnymi na poziomie architektury, zobacz dziesięć nawyków architektonicznych, które utrzymują bazy kodu frontendu w stanie nadajączym się do utrzymania przez lata.

    Główne wnioski

    • Wielkie aplikacje React rzadko cierpią z powodu samego Reacta; cierpią, ponieważ gromadzą się małe oszczędności: jeden ogromny komponent, jedna tajemnicza funkcja pomocnicza, jeden niepotrzebny pakiet, jeden „hack”, który miał być tymczasowy.
    • Nadającość do utrzymania nie można dodać na końcu. Jest sumą wielu małych decyzji, podejmowanych po kolei.
    • Najtańszy moment na wdrożenie tych nawyków to przed pojawieniem się problemów, gdy podział komponentu lub odrzucenie zależności zajmuje jeszcze minuty, a nie tygodnie.
  • Gdy jesteś kuszony sprytnym rozwiązaniem, pamiętaj, że osoba, która będzie je utrzymywać za sześć miesięcy – być może ty – będzie musiała zrozumieć, dlaczego działa ono bez obecnego kontekstu.
  • Literatura pokrewna