Jak budujemy komponenty interfejsu użytkownika w Railsach

Opublikowany: 2024-06-28

Utrzymanie spójności wizualnej w dużej aplikacji internetowej jest wspólnym problemem wielu organizacji. Główna aplikacja internetowa naszego produktu Flywheel jest zbudowana w Ruby on Rails, a każdego dnia około wielu programistów Rails i trzech programistów front-end pracuje nad nią. Zajmujemy się także projektowaniem (jest to jedna z podstawowych wartości naszej firmy) i mamy trzech projektantów, którzy blisko współpracują z programistami w naszych zespołach Scrumowych.

dwie osoby współpracują przy projektowaniu strony internetowej

Naszym głównym celem jest zapewnienie każdemu programiście możliwości zbudowania responsywnej strony bez żadnych przeszkód. Do przeszkód na ogół zalicza się brak wiedzy, których istniejących komponentów użyć do zbudowania makiety (co prowadzi do nadmuchania bazy kodu bardzo podobnymi, zbędnymi komponentami) oraz brak wiedzy, kiedy omówić możliwość ponownego użycia z projektantami. Przyczynia się to do niespójnych doświadczeń klientów, frustracji programistów i odmiennego języka projektowania między programistami i projektantami.

Przeszliśmy przez kilka iteracji przewodników po stylach oraz metod budowania/utrzymywania wzorców i komponentów interfejsu użytkownika, a każda iteracja pomogła rozwiązać problemy, z którymi się wówczas borykaliśmy. Jesteśmy pewni, że nasze nowe podejście zapewni nam dobrą pozycję na długi czas. Jeśli napotykasz podobne problemy w aplikacji Railsowej i chciałbyś podejść do komponentów od strony serwera, mam nadzieję, że ten artykuł podsunie Ci kilka pomysłów.

brodaty mężczyzna uśmiecha się do kamery, siedząc przed monitorem komputera wyświetlającym linie kodu

W tym artykule zajmę się:

  • Po co rozwiązujemy
  • Wiązanie komponentów
  • Renderowanie komponentów po stronie serwera
  • Gdzie nie możemy używać komponentów po stronie serwera

Co rozwiązujemy

Chcieliśmy całkowicie ograniczyć nasze komponenty interfejsu użytkownika i wyeliminować możliwość utworzenia tego samego interfejsu użytkownika na więcej niż jeden sposób. Chociaż klient może nie być w stanie tego stwierdzić (na początku), brak ograniczeń dotyczących komponentów prowadzi do dezorientacji programistów, bardzo utrudnia konserwację i wprowadzanie globalnych zmian w projekcie.

Tradycyjny sposób podejścia do komponentów opierał się na naszym przewodniku po stylach, który zawierał listę wszystkich znaczników wymaganych do zbudowania danego komponentu. Oto jak na przykład wyglądała strona przewodnika po stylach dla naszego komponentu listew:

strona przewodnika po stylu elementu listwy

Działało to dobrze przez kilka lat, ale problemy zaczęły się pojawiać, gdy dodaliśmy warianty, stany lub alternatywne sposoby użycia komponentu. W przypadku złożonego interfejsu użytkownika korzystanie z przewodnika po stylach, aby wiedzieć, jakich klas należy używać, a których unikać, oraz w jakiej kolejności musiały być stosowane znaczniki, aby uzyskać żądaną odmianę, stało się kłopotliwe.

Często projektanci wprowadzali niewielkie dodatki lub poprawki do danego komponentu. Ponieważ przewodnik po stylach nie do końca to wspierał, alternatywne hacki umożliwiające poprawne wyświetlanie tego ulepszenia (np. niewłaściwe kanibalizm części innego komponentu) stały się irytująco powszechne.

Przykład komponentu nieograniczonego

Aby zilustrować, jak niespójności ujawniają się z biegiem czasu, użyję prostego (i wymyślonego), ale bardzo częstego przykładu jednego z naszych komponentów w aplikacji Flywheel: nagłówków kart.

Zaczynając od makiety projektu, tak wyglądał nagłówek karty. To było całkiem proste z tytułem, przyciskiem i dolną ramką.

 nagłówek .karty__
  .card__header-left
    %h2 Kopie zapasowe
  .card__header-right
    = link_do "#" zrób
      = ikona("plus_mały")

Po zakodowaniu wyobraź sobie projektanta chcącego dodać ikonę po lewej stronie tytułu. Po wyjęciu z pudełka nie będzie żadnego marginesu między ikoną a tytułem.

 ...
  .card__header-left
    = icon("arrow_backup", kolor: "szary25")
    %h2 Kopie zapasowe
...

Idealnie rozwiązalibyśmy ten problem w CSS dla nagłówków kart, ale w tym przykładzie załóżmy, że inny programista pomyślał: „Och, wiem! Mamy kilku pomocników na marginesie. Po prostu wkleję w tytule klasę pomocniczą.

 ...
  .card__header-left
    = icon("arrow_backup", kolor: "szary25")
    %h2.--ml-10 Kopie zapasowe
...

Cóż, technicznie rzecz biorąc, wygląda to tak, jak na makiecie, prawda?! Jasne, ale powiedzmy, że miesiąc później inny programista potrzebuje nagłówka karty, ale bez ikony. Znajdują ostatni przykład, kopiują/wklejają go i po prostu usuwają ikonę.

Znowu wygląda poprawnie, prawda? Wyrwane z kontekstu, dla kogoś, kto nie interesuje się projektowaniem, jasne! Ale spójrz na to obok oryginału. Lewy margines w tytule nadal tam jest, ponieważ nie zdawali sobie sprawy, że lewy pomocnik marginesu musi zostać usunięty!

Idąc o krok dalej w tym przykładzie, powiedzmy, że inna makieta wymagała nagłówka karty bez dolnej krawędzi. Można znaleźć stan, który mamy w przewodniku po stylach, nazwany „bez obramowania” i zastosować go. Doskonały!

Inny programista mógłby następnie spróbować ponownie wykorzystać ten kod, ale w tym przypadku faktycznie potrzebuje obramowania. Załóżmy hipotetycznie, że ignorują prawidłowe użycie udokumentowane w przewodniku po stylach i nie zdają sobie sprawy, że usunięcie klasy borderless da im granicę. Zamiast tego dodają linię poziomą. Kończy się na dodatkowym dopełnieniu między tytułem a krawędzią, więc stosują klasę pomocniczą do hr i voila!

Po wszystkich tych modyfikacjach oryginalnego nagłówka karty mamy teraz bałagan w kodzie.

 .card__header.--bez obramowania
  .card__header-left
    %h2.--ml-10 Kopie zapasowe
  .card__header-right
    = link_do "#" zrób
      = ikona("plus_mały")
  %hr.--mt-0.--mb-0

Należy pamiętać, że powyższy przykład ma jedynie na celu zilustrowanie faktu, że niezwiązane komponenty mogą z czasem stać się nieuporządkowane. Jeśli ktokolwiek z naszego zespołu próbował wysłać odmianę nagłówka karty, powinien zostać wykryty podczas przeglądu projektu lub kodu. Ale takie rzeczy czasami prześlizgują się przez szczeliny, stąd nasza potrzeba kuloodporności!


Wiązanie komponentów

Być może myślisz, że problemy wymienione powyżej zostały już rozwiązane za pomocą komponentów. To słuszne założenie! Frameworki front-endowe, takie jak React i Vue, są bardzo popularne właśnie w tym celu; to niesamowite narzędzia do enkapsulacji interfejsu użytkownika. Jest jednak z nimi jeden problem, który nie zawsze nam się podoba — wymagają, aby interfejs użytkownika był renderowany przez JavaScript.

Nasza aplikacja Flywheel ma bardzo rozbudowane zaplecze i zawiera głównie kod HTML renderowany na serwerze — ale na szczęście dla nas komponenty mogą mieć wiele postaci. Ostatecznie komponent interfejsu użytkownika to hermetyzacja stylów i zasad projektowania, która wysyła znaczniki do przeglądarki. Dzięki tej realizacji możemy przyjąć to samo podejście do komponentów, ale bez narzutu związanego z frameworkiem JavaScript.

Poniżej omówimy sposób budowania ograniczonych komponentów, ale oto kilka korzyści, jakie odkryliśmy dzięki ich zastosowaniu:

  • Nigdy nie ma złego sposobu na złożenie komponentu w całość.
  • Komponent wykonuje całe myślenie projektowe za Ciebie. (Po prostu przekazujesz opcje!)
  • Składnia tworzenia komponentu jest bardzo spójna i łatwa do uzasadnienia.
  • Jeśli konieczna jest zmiana projektu w komponencie, możemy ją zmienić raz w komponencie i mieć pewność, że zostanie zaktualizowana wszędzie.

Renderowanie komponentów po stronie serwera

O czym więc mówimy, ograniczając komponenty? Zagłębmy się!

Jak wspomnieliśmy wcześniej, chcemy, aby każdy programista pracujący w aplikacji mógł rzucić okiem na makietę projektową strony i móc od razu bez przeszkód zbudować taką stronę. Oznacza to, że metoda tworzenia interfejsu użytkownika musi być: A) bardzo dobrze udokumentowana i B) bardzo deklaratywna i wolna od domysłów.

Części na ratunek (a przynajmniej tak nam się wydawało)

Pierwszą próbą, którą próbowaliśmy w przeszłości, było użycie części składowych Railsów. Części są jedynym narzędziem, które Railsy umożliwiają ponownemu użyciu w szablonach. Naturalnie, są pierwszą rzeczą, po którą wszyscy sięgają. Poleganie na nich ma jednak istotne wady, ponieważ jeśli chcesz połączyć logikę z szablonem wielokrotnego użytku, masz dwie możliwości: powielić logikę na każdym kontrolerze, który używa częściowego lub osadzić logikę w samym częściowym.

Częściowe zapobiegają błędom kopiowania/wklejania i działają dobrze przez pierwsze kilka razy, gdy trzeba coś ponownie wykorzystać. Jednak z naszego doświadczenia wynika, że ​​części składowe szybko stają się zaśmiecone obsługą coraz większej funkcjonalności i logiki. Ale logika nie powinna żyć w szablonach!

Wprowadzenie do komórek

Na szczęście istnieje lepsza alternatywa dla części, która pozwala nam zarówno ponownie wykorzystać kod , jak i zachować logikę poza widokiem. Nazywa się Cells, klejnot Rubinu opracowany przez Trailblazer. Komórki istniały na długo przed wzrostem popularności w frameworkach front-end, takich jak React i Vue, i umożliwiały pisanie hermetyzowanych modeli widoków, które obsługują zarówno logikę , jak i szablonowanie. Zapewniają abstrakcję modelu widoku, której Railsy tak naprawdę nie mają od razu po wyjęciu z pudełka. Właściwie używamy komórek w aplikacji Flywheel już od jakiegoś czasu, ale nie na globalną skalę wielokrotnego użytku.

Na najprostszym poziomie Cells pozwalają nam wyodrębnić fragment znaczników w następujący sposób (w naszym języku szablonów używamy Hamla):

 %dział
  %h1 Witaj, świecie!

Do modelu widoku wielokrotnego użytku (w tym momencie bardzo podobnego do częściowych) i zamień go w ten:

 = komórka("witaj, świecie")

To ostatecznie pomaga nam ograniczyć komponent do miejsc, w których nie można dodać klas pomocniczych lub nieprawidłowych komponentów podrzędnych bez modyfikowania samej komórki.

Konstruowanie komórek

Umieściliśmy wszystkie nasze komórki interfejsu użytkownika w katalogu app/cells/ui. Każda komórka musi zawierać tylko jeden plik Ruby z przyrostkiem _cell.rb. Technicznie rzecz biorąc, możesz napisać szablony bezpośrednio w Ruby za pomocą pomocnika content_tag, ale większość naszych komórek zawiera również odpowiedni szablon Hamla, który znajduje się w folderze nazwanym przez komponent.

Super podstawowa komórka pozbawiona logiki wygląda mniej więcej tak:

 //cells/ui/slat_cell.rb
interfejs modułu
  klasa SlatCell < ViewModel
    zdecydowanie pokaż
    koniec
  koniec
koniec

Metoda show jest renderowana podczas tworzenia instancji komórki i automatycznie szuka odpowiedniego pliku show.haml w folderze o tej samej nazwie co komórka. W tym przypadku jest to app/cells/ui/slat (zakresujemy wszystkie nasze komórki interfejsu użytkownika do modułu interfejsu użytkownika).

W szablonie możesz uzyskać dostęp do opcji przekazanych do komórki. Na przykład, jeśli komórka jest utworzona w widoku = cell(„ui/slat”, tytuł: „Tytuł”, podtytuł: „Podtytuł”, etykieta: „Etykieta”), możemy uzyskać dostęp do tych opcji poprzez obiekt opcji.

 //cells/ui/slat/show.haml
.listwa
  .listwa__wewnętrzna
    .slat__treść
      %h4= opcje[:title]
      %p= opcje[:napisy]
      = ikona(opcje[:ikona], kolor: „niebieski”)

Często będziemy przenosić proste elementy i ich wartości do metody w komórce, aby zapobiec renderowaniu pustych elementów, jeśli nie ma takiej opcji.

 //cells/ui/slat_cell.rb
zdecydowanie tytuł
  wróć, chyba że opcje[:title]
  content_tag :h4, opcje[:title]
koniec
zdecydowanie podtytuł
  wróć, chyba że opcje[:subtitle]
  content_tag :p, opcje[:subtitle]
koniec
 //cells/ui/slat/show.haml
.listwa
  .listwa__wewnętrzna
    .slat__treść
      = tytuł
      = podtytuł

Zawijanie komórek za pomocą narzędzia interfejsu użytkownika

Po udowodnieniu koncepcji, że może to działać na dużą skalę, chciałem zająć się zewnętrznymi znacznikami wymaganymi do wywołania komórki. Po prostu nie płynie całkiem dobrze i trudno go zapamiętać. Dlatego stworzyliśmy do tego małego pomocnika! Teraz możemy po prostu wywołać = ui „nazwa_komponentu” i przekazać opcje w linii.

 = ui "listwa", tytuł: "Tytuł", podtytuł: "Napisy", etykieta: "Etykieta"

Opcje przekazywania jako blok zamiast w linii

Idąc nieco dalej w narzędziu interfejsu użytkownika, szybko stało się jasne, że komórka z wieloma opcjami w jednej linii byłaby bardzo trudna do śledzenia i po prostu brzydka. Oto przykład komórki z wieloma wbudowanymi opcjami:

 = ui „listwa”, tytuł: „Tytuł”, podtytuł: „Napisy”, etykieta: „Etykieta”, link: „#”, tertiary_title: „Trzeciono”, wyłączone: true, lista kontrolna: [„Pozycja 1”, „Pozycja 2”, „Pozycja 3”]

Jest to bardzo kłopotliwe i dlatego stworzyliśmy klasę o nazwie OptionProxy, która przechwytuje metody ustawiające komórki i tłumaczy je na wartości skrótu, które następnie są łączone w opcje. Jeśli brzmi to skomplikowanie, nie martw się – dla mnie też jest to skomplikowane. Oto streszczenie klasy OptionProxy, którą napisał Adam, jeden z naszych starszych inżynierów oprogramowania.

Oto przykład użycia klasy OptionProxy wewnątrz naszej komórki:

 interfejs modułu
  klasa SlatCell < ViewModel
    zdecydowanie pokaż
      OptionProxy.new(self).yield!(opcje, &blokuj)
      Super()
    koniec
  koniec
koniec

Teraz, mając to wszystko na swoim miejscu, możemy zamienić nasze kłopotliwe opcje wbudowane w przyjemniejszy blok!

 = ui "listwa" do |listwa|
  - listwa.title = "Tytuł"
  - listwa.subtitle = "Napisy"
  - slat.label = "Etykieta"
  - listwa.link = "#"
  - slat.tertiary_title = "Trzeciorzędny"
  - listwa.wyłączona = prawda
  - listwa.checklist = ["Pozycja 1", "Pozycja 2", "Pozycja 3"]

Przedstawiamy logikę

Do tego momentu przykłady nie zawierały żadnej logiki dotyczącej tego, co wyświetla widok. To jedna z najlepszych rzeczy, jakie oferuje Cells, więc porozmawiajmy o tym!

Trzymając się naszego komponentu listwy, czasami musimy renderować całość jako łącze, a czasami jako element div, w zależności od tego, czy dostępna jest opcja łącza. Uważam, że to jedyny komponent, jaki mamy, który można wyrenderować jako element div lub łącze, ale jest to całkiem niezły przykład mocy Cells.

Poniższa metoda wywołuje pomocnika link_to lub content_tag w zależności od obecności opcji [:link] .

 def kontener(&blok)
  znacznik =
    jeśli opcje[:link]
      [:link_to, opcje[:link]]
    w przeciwnym razie
      [:content_tag, :div]
    koniec
  send(*tag, klasa: „slat__inner”, &block)
koniec

Dzięki temu możemy zastąpić element .slat__inner w szablonie blokiem kontenera:

 .listwa
  = kontener tak
  ...

Innym przykładem logiki w Cells, której często używamy, jest warunkowe wyświetlanie klas. Załóżmy, że dodajemy wyłączoną opcję do komórki. Nic innego w wywołaniu komórki się nie zmienia, poza tym, że możesz teraz przekazać opcję wyłączona: prawda i obserwować, jak całość zmienia się w stan wyłączony (wyszarzony i z nieklikalnymi linkami).

 = ui "listwa" do |listwa|
  ...
  - listwa.wyłączona = prawda

Gdy opcja wyłączona ma wartość true, możemy ustawić klasy na elementach szablonu, które są wymagane do uzyskania pożądanego wyłączonego wyglądu.

 .slat{klasa: możliwe_klasy("--disabled": opcje[:disabled]) }
  .listwa__wewnętrzna
    .slat__treść
      %h4{klasa: możliwe_klasy("--alt": opcje[:wyłączone]) }= opcje[:tytuł]
      %p{klasa: możliwe_klasy("--alt": opcje[:wyłączone]) }=
      opcje[:napisy]
      = ikona(opcje[:ikona], kolor: „szary”)

Tradycyjnie musielibyśmy pamiętać (lub odwołać się do przewodnika po stylach), które poszczególne elementy wymagały dodatkowych klas, aby całość działała poprawnie w stanie wyłączonym. Komórki pozwalają nam zadeklarować jedną opcję, a następnie wykonać za nas ciężkie zadanie.

Uwaga: możliwe_klasy to metoda, którą stworzyliśmy, aby w przyjemny sposób umożliwić warunkowe stosowanie klas w Haml.


Gdzie nie możemy używać komponentów po stronie serwera

Chociaż podejście oparte na komórkach jest niezwykle pomocne w naszym konkretnym zastosowaniu i sposobie, w jaki pracujemy, nie chciałbym powiedzieć, że rozwiązuje ono w 100% nasze problemy. Nadal piszemy JavaScript (dużo) i tworzymy sporo doświadczeń w Vue w całej naszej aplikacji. W 75% przypadków nasz szablon Vue nadal znajduje się w Haml i wiążemy nasze instancje Vue z elementem zawierającym, co pozwala nam nadal korzystać z podejścia komórkowego.

Jednakże w miejscach, gdzie bardziej sensowne jest całkowite ograniczenie komponentu jako jednoplikowej instancji Vue, nie możemy używać komórek. Na przykład wszystkie nasze listy wyboru to Vue. Ale myślę, że to w porządku! Tak naprawdę nie napotkaliśmy potrzeby posiadania zduplikowanych wersji komponentów zarówno w Cells, jak i Vue, więc nie ma nic złego w tym, że niektóre komponenty są w 100% zbudowane w Vue, a inne w Cells.

Jeśli komponent jest zbudowany w Vue, oznacza to, że do zbudowania go w DOM wymagany jest JavaScript i korzystamy w tym celu ze frameworku Vue. Jednak większość naszych pozostałych komponentów nie wymaga JavaScriptu, a jeśli tak, to wymaga już zbudowania modelu DOM, a my po prostu podłączamy się i dodajemy detektory zdarzeń.

W miarę postępu w podejściu komórkowym zdecydowanie będziemy eksperymentować z kombinacją komponentów komórkowych i komponentów Vue, abyśmy mieli jeden i tylko jeden sposób tworzenia i używania komponentów. Nie wiem jeszcze, jak to wygląda, więc przejdziemy przez ten most, kiedy tam dotrzemy!


Nasze wnioski

Do tej pory przekonwertowaliśmy około trzydziestu naszych najczęściej używanych komponentów wizualnych na komórki. Zapewniło nam to ogromny wzrost produktywności i dało programistom poczucie potwierdzenia, że ​​tworzone przez nich doświadczenia są poprawne i nie są zhakowane.

Nasz zespół projektowy ma większą niż kiedykolwiek pewność, że komponenty i funkcje naszej aplikacji są 1:1 z tym, co zaprojektowali w programie Adobe XD. Zmiany lub uzupełnienia komponentów są teraz realizowane wyłącznie poprzez interakcję z projektantem i programistą front-end, dzięki czemu reszta zespołu jest skupiona i nie musi się martwić, jak dostosować komponent, aby pasował do makiety projektu.

Stale udoskonalamy nasze podejście do ograniczania komponentów interfejsu użytkownika, ale mam nadzieję, że techniki przedstawione w tym artykule dadzą Ci wgląd w to, co się u nas sprawdza!


Przyjdź i pracuj z nami!

Każdy dział pracujący nad naszymi produktami ma znaczący wpływ na naszych klientów i wyniki finansowe. Niezależnie od tego, czy chodzi o obsługę klienta, rozwój oprogramowania, marketing czy cokolwiek innego, wszyscy razem pracujemy nad naszą misją, jaką jest zbudowanie firmy hostingowej, w której ludzie mogą naprawdę się zakochać.

Chcesz dołączyć do naszego zespołu? Zatrudniamy! Złóż wniosek tutaj.