Jak budujemy komponenty UI w Rails w Flywheel

Opublikowany: 2019-11-16

Utrzymanie spójności wizualnej w dużej aplikacji internetowej to wspólny problem wielu organizacji. W Flywheel nie jesteśmy inni. Nasza główna aplikacja internetowa jest zbudowana przy użyciu Ruby on Rails i mamy około 15 programistów Rails i trzech programistów front-end, którzy wprowadzają do niej kod każdego dnia. Zajmujemy się również projektowaniem (to jedna z naszych podstawowych wartości jako firmy) i mamy trzech projektantów, którzy bardzo blisko współpracują z programistami w naszych zespołach Scrum.

Naszym głównym celem jest zapewnienie, że każdy programista Flywheel może zbudować responsywną stronę bez żadnych przeszkód. Przeszkody na wyłączność generalnie obejmowały brak wiedzy, których istniejących komponentów użyć do zbudowania makiety (co prowadzi do rozdęcia bazy kodu bardzo podobnymi, nadmiarowymi komponentami) i brak wiedzy, kiedy należy omówić z projektantami możliwość ponownego wykorzystania. Przyczynia się to do niespójnych doświadczeń klientów, frustracji programistów i odmiennego języka projektowania między programistami a projektantami.

Przeszliśmy przez kilka iteracji przewodników po stylach i metod budowania/utrzymywania wzorców i komponentów interfejsu użytkownika, a każda iteracja pomogła rozwiązać problemy, z którymi mieliśmy do czynienia w tamtym czasie. Jesteśmy teraz na nowym (dla nas) podejściu, które, jestem przekonany, ustawi nas na długi czas. Jeśli napotykasz podobne problemy w swojej aplikacji Railsowej i chciałbyś podejść do komponentów od strony serwera, mam nadzieję, że ten artykuł da ci kilka pomysłów.

W tym artykule omówię:

  • Co rozwiązujemy
  • Ograniczające komponenty
  • 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ść tworzenia tego samego interfejsu użytkownika na więcej niż jeden sposób. Chociaż klient może nie być w stanie powiedzieć (na początku), brak ograniczeń dotyczących komponentów prowadzi do mylącego doświadczenia programisty, bardzo utrudnia utrzymanie rzeczy i utrudnia wprowadzanie globalnych zmian projektowych.

Tradycyjnym sposobem, w jaki podeszliśmy do komponentów, był nasz przewodnik po stylu, w którym wymieniono całą masę znaczników wymaganych do zbudowania danego komponentu. Na przykład, oto jak wyglądała strona przewodnika stylu dla naszego komponentu listew:

Działało to dobrze i odpowiadało nam przez kilka lat, ale problemy zaczęły się pojawiać, gdy dodaliśmy warianty, stany lub alternatywne sposoby korzystania z komponentu. W przypadku złożonego interfejsu użytkownika kłopotliwe stało się odwoływanie się do przewodnika po stylu, aby wiedzieć, których klas użyć, a których unikać, oraz w jakiej kolejności muszą znajdować się znaczniki, aby uzyskać pożądaną odmianę. I często projektanci wprowadzali niewielkie dodatki lub poprawki do danego komponentu. Ponieważ przewodnik po stylu nie do końca to wspierał, alternatywne hacki mające na celu prawidłowe wyświetlanie tej poprawki (np. Niewłaściwe kanibalizowanie części innego komponentu) stały się irytująco powszechne.

Przykład komponentu niezwiązanego

Aby zilustrować, jak niespójności pojawiają się w czasie, użyję prostego (i wymyślonego), ale bardzo popularnego 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ą.

.card__header
  .card__header-left
    %h2 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

Po zakodowaniu wyobraź sobie projektanta, który chce 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", color: "gray25")
    %h2 Backups
...

Idealnie rozwiązalibyśmy to w CSS dla nagłówków kart, ale w tym przykładzie powiedzmy, że inny programista pomyślał „Och, wiem! Mamy kilku pomocników na marginesie. Po prostu przykleję klasę pomocników do tytułu.

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Backups
...

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

Znowu wygląda poprawnie, prawda? Wyrwane z kontekstu, dla kogoś, kto nie interesuje się projektowaniem, na pewno! Ale spójrz na to obok oryginału. Ten lewy margines na tytule nadal tam jest, ponieważ nie zdawali sobie sprawy, że należy usunąć lewy pomocnik!

Idąc 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 stylu zwanym „bez granic” i zastosować go. Idealny!

Inny programista może wtedy spróbować ponownie wykorzystać ten kod, ale w tym przypadku faktycznie potrzebuje ramki. Załóżmy hipotetycznie, że ignorują właściwe użycie udokumentowane w przewodniku stylu i nie zdają sobie sprawy, że usunięcie klasy bez obramowania da im obramowanie. Zamiast tego dodają linię poziomą. Kończy się to dodatkowym wypełnieniem między tytułem a obramowaniem, więc stosują klasę pomocniczą do hr i voila!

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

.card__header.--borderless
  .card__header-left
    %h2.--ml-10 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

  %hr.--mt-0.--mb-0

Należy pamiętać, że powyższy przykład ma na celu zilustrowanie tego, w jaki sposób niezwiązane komponenty mogą z czasem stać się bałaganem. Jeśli ktoś z naszego zespołu próbował wysłać odmianę nagłówka karty, powinien zostać wyłapany przez przegląd projektu lub przegląd kodu. Ale takie rzeczy czasami prześlizgują się przez pęknięcia, stąd nasza potrzeba kuloodporności!


Ograniczające komponenty

Być może myślisz, że problemy wymienione powyżej zostały już wyraźnie rozwiązane za pomocą komponentów. To jest prawidłowe założenie! Struktury front-end, takie jak React i Vue, są bardzo popularne właśnie w tym celu; są niesamowitymi narzędziami do hermetyzacji interfejsu użytkownika. Jednak jest z nimi jeden problem, który nie zawsze nam się podoba — wymagają renderowania interfejsu użytkownika przez JavaScript.

Aplikacja Flywheel jest bardzo obciążona back-endem, głównie z kodem HTML renderowanym przez serwer – ale na szczęście dla nas komponenty mogą przybierać różne formy. W ostatecznym rozrachunku składnik interfejsu użytkownika jest enkapsulacją stylów i reguł projektowych, które wyświetlają znaczniki w przeglądarce. Dzięki tej realizacji możemy przyjąć to samo podejście do komponentów, ale bez narzutu frameworka JavaScript.

Poniżej omówimy, w jaki sposób tworzymy ograniczone komponenty, ale oto kilka korzyści, które odkryliśmy, używając ich:

  • Tak naprawdę nigdy nie ma złego sposobu na połączenie komponentu.
  • Komponent wykonuje za Ciebie całe myślenie projektowe. (Po prostu przekazujesz opcje!)
  • Składnia tworzenia komponentu jest bardzo spójna i łatwa do zrozumienia.
  • Jeśli w komponencie potrzebna jest zmiana projektu, możemy ją zmienić raz w komponencie i mieć pewność, że jest wszędzie aktualizowana.

Renderowanie komponentów po stronie serwera

Więc o czym mówimy przez wiązanie komponentów? Zagłębmy się!

Jak wspomniano wcześniej, chcemy, aby każdy programista pracujący w aplikacji Flywheel mógł spojrzeć na projekt strony i móc natychmiast ją zbudować bez przeszkód. Oznacza to, że metoda tworzenia interfejsu użytkownika musi być A) bardzo dobrze udokumentowana i B) bardzo deklaratywna i wolna od zgadywania.

Częściowo na ratunek (a przynajmniej tak myśleliśmy)

Pierwszym krokiem w tym kierunku, którego próbowaliśmy w przeszłości, było użycie podszablonów Rails. Części to jedyne narzędzie, które Railsy dają do ponownego wykorzystania w szablonach. Oczywiście są to pierwsza rzecz, po którą wszyscy sięgają. Jednak poleganie na nich ma poważne wady, ponieważ jeśli chcesz połączyć logikę z szablonem wielokrotnego użytku, masz dwie możliwości: zduplikuj logikę w każdym kontrolerze, który używa podszablonu, lub osadź logikę w samym podszablonie.

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

Wprowadzenie do komórek

Na szczęście istnieje lepsza alternatywa dla podszablonów, która pozwala nam zarówno ponownie wykorzystać kod, jak i zachować logikę poza widokiem. Nazywa się Cells, klejnotem rubinowym opracowanym przez Trailblazer. Komórki istniały na długo przed wzrostem popularności frameworków front-end, takich jak React i Vue, i umożliwiają pisanie hermetyzowanych modeli widoków, które obsługują zarówno logikę, jak i szablony. Zapewniają abstrakcję modelu widoku, której Railsy po prostu nie mają po wyjęciu z pudełka. Właściwie od jakiegoś czasu używamy Cells w aplikacji Flywheel, ale nie na globalną, super wielokrotnego użytku.

Na najprostszym poziomie komórki pozwalają nam wyabstrahować taki fragment znaczników (używamy Hamla dla naszego języka szablonów):

%div
  %h1 Hello, world!

W model widoku wielokrotnego użytku (bardzo podobny do podszablonów w tym momencie) i zamień go w ten:

= cell("hello_world")

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

Konstruowanie komórek

Wszystkie nasze komórki interfejsu użytkownika umieszczamy w katalogu app/cells/ui. Każda komórka musi zawierać tylko jeden plik Ruby z przyrostkiem _cell.rb. Z technicznego punktu widzenia możesz pisać szablony bezpośrednio w Rubim za pomocą helpera content_tag, ale większość naszych komórek zawiera również odpowiedni szablon Haml, który znajduje się w folderze nazwanym przez komponent.

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

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

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 UI do modułu UI).

W szablonie możesz uzyskać dostęp do opcji przekazanych do komórki. Na przykład, jeśli komórka jest tworzona w widoku takim jak = cell("ui/slat", tytuł: "Tytuł", podtytuł: "Podtytuł", etykieta: "Etykieta"), możemy uzyskać dostęp do tych opcji za pośrednictwem obiektu opcji.

// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= options[:title]
      %p= options[:subtitle]
      = icon(options[:icon], color: "blue")

Wiele razy przenosimy proste elementy i ich wartości do metody w komórce, aby zapobiec renderowaniu pustych elementów, jeśli opcja nie jest obecna.

// cells/ui/slat_cell.rb
def title
  return unless options[:title]
  content_tag :h4, options[:title]
end

def subtitle
  return unless options[:subtitle]
  content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = title
      = subtitle

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

Po udowodnieniu koncepcji, że to może działać na dużą skalę, chciałem zająć się nieistotnymi znacznikami wymaganymi do wywołania komórki. To po prostu nie działa dobrze i jest trudne do zapamiętania. Więc zrobiliśmy do tego małego pomocnika! Teraz możemy po prostu wywołać = ui „nazwa_komponentu” i przekazać opcje inline.

= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"

Przekazywanie opcji jako blok zamiast inline

Idąc nieco dalej w narzędziu interfejsu użytkownika, szybko okazało się, że komórka z wieloma opcjami w jednym wierszu byłaby bardzo trudna do naśladowania i po prostu brzydka. Oto przykład komórki z wieloma opcjami zdefiniowanymi w tekście:

= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]

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

Oto przykład użycia klasy OptionProxy w naszej komórce:

module UI
  class SlatCell < ViewModel
    def show
      OptionProxy.new(self).yield!(options, &block)
      super()
    end
  end
end

Teraz, gdy już to mamy, możemy zmienić nasze niewygodne opcje inline w przyjemniejszy blok!

= ui "slat" do |slat|
  - slat.title = "Title"
  - slat.subtitle = "Subtitle"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tertiary"
  - slat.disabled = true
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

Przedstawiamy logikę

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

Pozostając przy naszym komponencie listew, musimy czasami renderować całość jako link, a czasami jako div, w zależności od tego, czy jest dostępna opcja linku. Uważam, że jest to jedyny komponent, który mamy, który można renderować jako div lub link, ale jest to całkiem zgrabny przykład mocy Cells.

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

Uwaga: To zostało zainspirowane i stworzone przez Adama Lasska, który miał ogromny wpływ na pomaganie nam w tworzeniu całej metody tworzenia interfejsu użytkownika za pomocą Cells.

def container(&block)
  tag =
    if options[:link]
      [:link_to, options[:link]]
    else
      [:content_tag, :div]
    end

  send(*tag, class: “slat__inner”, &block)
end

To pozwala nam zastąpić element .slat__inner w szablonie blokiem kontenera:

.slat
  = container do
  ...

Innym przykładem logiki w Cells, którego często używamy, jest warunkowe wyprowadzanie klas. Powiedzmy, że dodajemy wyłączoną opcję do komórki. Nic innego w wywołaniu zmian komórki, poza tym, że możesz teraz przekazać opcję disabled: true i obserwować, jak całość zmienia się w stan wyłączony (wyszarzony z linkami, których nie można kliknąć).

= ui "slat" do |slat|
  ...
  - slat.disabled = true

Gdy opcja disabled jest włączona, możemy ustawić klasy na elementach w szablonie, które są wymagane do uzyskania pożądanego wyglądu wyłączonego.

.slat{ class: possible_classes("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
      %p{ class: possible_classes("--alt": options[:disabled]) }=
      options[:subtitle]
      = icon(options[:icon], color: "gray")

Tradycyjnie musielibyśmy pamiętać (lub odwołać się do przewodnika po stylach), które poszczególne elementy potrzebował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 podnoszenie.

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


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

Chociaż podejście komórkowe jest niezwykle pomocne w przypadku naszej konkretnej aplikacji i sposobu, w jaki pracujemy, nie można powiedzieć, że rozwiązało ono 100% naszych problemów. Wciąż piszemy JavaScript (bardzo dużo) i budujemy całkiem sporo doświadczeń w Vue w całej naszej aplikacji. W 75% przypadków nasz szablon Vue nadal znajduje się w Haml i łączymy nasze instancje Vue z elementem zawierającym, co pozwala nam nadal korzystać z podejścia komórkowego.

Jednak w miejscach, w których 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 komponentach Cells, jak i Vue, więc jest w porządku, że niektóre komponenty są w 100% zbudowane z Vue, a niektóre z Cells. Jeśli komponent jest budowany za pomocą Vue, oznacza to, że do zbudowania go w DOM wymagany jest JavaScript i wykorzystujemy do tego framework Vue. Jednak w przypadku większości naszych innych komponentów nie wymagają one JavaScript, a jeśli tak, wymagają już zbudowanego DOM, a my po prostu podłączamy i dodajemy detektory zdarzeń.

W miarę postępów w podejściu komórkowym na pewno będziemy eksperymentować z kombinacją komponentów komórki i komponentów Vue, aby mieć jeden i tylko jeden sposób tworzenia i używania komponentów. Jeszcze nie wiem, jak to wygląda, więc przekroczymy ten most, kiedy tam dotrzemy!


Nasz wniosek

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

Nasz zespół projektowy jest bardziej niż kiedykolwiek przekonany, że komponenty i doświadczenia w naszej aplikacji są 1:1 z tym, co zaprojektowali w Adobe XD. Zmiany lub dodatki do komponentów są teraz obsługiwane wyłącznie poprzez interakcję z projektantem i programistą front-end, co sprawia, że ​​reszta zespołu jest skupiona i nie musi się martwić wiedząc, jak ulepszyć komponent, aby pasował do makiety projektu.

Nieustannie powtarzamy nasze podejście do ograniczania komponentów interfejsu użytkownika, ale mam nadzieję, że techniki zilustrowane w tym artykule dadzą ci wgląd w to, co działa u nas dobrze!


Przyjdź do pracy w Flywheel!

W firmie Flywheel każdy dział ma znaczący wpływ na naszych klientów i wyniki finansowe. Niezależnie od tego, czy jest to obsługa klienta, tworzenie oprogramowania, marketing, czy cokolwiek pomiędzy, wszyscy pracujemy nad naszą misją stworzenia firmy hostingowej, w której ludzie mogą się naprawdę zakochać.

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