Cum construim componente UI în Rails at Flywheel

Publicat: 2019-11-16

Menținerea consistenței vizuale într-o aplicație web mare este o problemă comună în multe organizații. La Flywheel, nu suntem altfel. Aplicația noastră web principală este construită cu Ruby on Rails și avem aproximativ 15 dezvoltatori Rails și trei dezvoltatori front-end care angajează cod pentru aceasta în orice zi. Suntem mari și în design (este una dintre valorile noastre de bază ca companie) și avem trei designeri care lucrează foarte strâns cu dezvoltatorii din echipele noastre Scrum.

Un obiectiv major al nostru este să ne asigurăm că orice dezvoltator Flywheel poate construi o pagină receptivă fără blocaje. Obstacolele au inclus, în general, neștirea care sunt componentele existente să folosească pentru a construi o machetă (ceea ce duce la umflarea bazei de cod cu componente foarte asemănătoare, redundante) și neștiind când să discute despre reutilizare cu designerii. Acest lucru contribuie la experiențele inconsecvente ale clienților, frustrarea dezvoltatorilor și un limbaj de design disparat între dezvoltatori și designeri.

Am trecut prin mai multe iterații de ghiduri de stil și metode de construire/menținere a modelelor și componentelor UI, iar fiecare iterație a ajutat la rezolvarea problemelor cu care ne confruntam la acel moment. Suntem acum la o nouă abordare (pentru noi) care sunt încrezător că ne va stabili pentru mult timp de acum încolo. Dacă vă confruntați cu probleme similare în aplicația Rails și doriți să abordați componentele din partea serverului, sper că acest articol vă poate oferi câteva idei.

În acest articol, mă voi scufunda în:

  • Pentru ce rezolvăm
  • Componente de constrângere
  • Redarea componentelor pe partea serverului
  • Unde nu putem folosi componente pe partea serverului


Pentru ce rezolvăm

Am vrut să constrângem complet componentele noastre UI și să eliminăm posibilitatea ca aceeași IU să fie creată în mai multe moduri. Deși un client s-ar putea să nu poată spune (la început), lipsa de constrângeri asupra componentelor duce la o experiență confuză pentru dezvoltatori, face lucrurile foarte greu de întreținut și îngreunează efectuarea modificărilor globale de design.

Modul tradițional în care am abordat componentele a fost prin ghidul nostru de stil, care a enumerat întreaga cantitate de markup necesară pentru a construi o anumită componentă. De exemplu, iată cum arăta pagina de ghid de stil pentru componenta noastră cu șipci:

Acest lucru a funcționat bine și ni s-a potrivit pentru câțiva ani, dar problemele au început să apară când am adăugat variante, stări sau moduri alternative de a folosi componenta. Cu o componentă complexă a interfeței de utilizare, a devenit greoaie să faci referire la ghidul de stil pentru a ști ce clase să folosești și pe care să le eviti și în ce ordine trebuia să fie marcarea pentru a scoate variația dorită. Și de multe ori, designerii ar face mici completări sau modificări la o anumită componentă. Deoarece ghidul de stil nu a susținut tocmai acest lucru, hack-urile alternative pentru a face ca modificarea să se afișeze corect (cum ar fi canibalizarea inadecvată a unei părți a unei alte componente) au devenit iritant de comune.

Exemplu de componentă neconstrânsă

Pentru a ilustra modul în care neconcordanțe apar în timp, voi folosi un exemplu simplu (și născocit) dar foarte comun al uneia dintre componentele noastre din aplicația Flywheel: anteturile cardului.

Pornind de la o machetă de design, așa arăta antetul unui card. A fost destul de simplu, cu un titlu, un buton și un chenar de jos.

.card__header
  .card__header-left
    %h2 Backups

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

După ce a fost codificat, imaginați-vă un designer care dorește să adauge o pictogramă în stânga titlului. Din cutie, nu va exista nicio marjă între pictogramă și titlu.

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

În mod ideal, am rezolva asta în CSS pentru antetele cardurilor, dar pentru acest exemplu, să presupunem că un alt dezvoltator s-a gândit „Oh, știu! Avem niște ajutoare de marjă. O să dau doar o clasă de ajutor pe titlu.”

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

Ei bine, din punct de vedere tehnic , arată ca și macheta, nu?! Sigur, dar să spunem că o lună mai târziu, un alt dezvoltator are nevoie de un antet de card, dar fără pictogramă. Ei găsesc ultimul exemplu, îl copiază/lipesc și pur și simplu elimină pictograma.

Din nou pare corect, nu? În afara contextului, pentru cineva fără un ochi aprofundat pentru design, sigur! Dar uită-te lângă original. Marja din stânga de pe titlu este încă acolo, pentru că nu și-au dat seama că ajutorul de margine din stânga trebuie eliminat!

Luând acest exemplu cu un pas mai departe, să presupunem că o altă machetă a cerut un antet de card fără margine de jos. S-ar putea găsi o stare pe care o avem în ghidul de stil numită „fără margini” și să o aplici. Perfect!

Un alt dezvoltator ar putea încerca apoi să refolosească acel cod, dar în acest caz, au nevoie de fapt de o chenar. Să spunem ipotetic că ei ignoră utilizarea corectă documentată în ghidul de stil și nu își dau seama că eliminarea clasei fără margini le va oferi chenarul lor. În schimb, adaugă o regulă orizontală. Sfârșește prin a fi o umplutură suplimentară între titlu și chenar, așa că aplică o clasă de ajutor pentru ora și voila!

Cu toate aceste modificări ale antetului original al cardului, acum avem o mizerie pe mâini în cod.

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

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

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

Rețineți că exemplul de mai sus este doar pentru a ilustra modul în care componentele neconstrânse pot deveni dezordonate în timp. Dacă cineva din echipa noastră a încercat să livreze o variantă a antetului unui card, aceasta ar trebui să fie surprinsă printr-o revizuire a designului sau a codului. Dar lucruri de genul acesta se strecoară uneori printre crăpături, de unde și nevoia noastră de a proteja lucrurile!


Componente de constrângere

S-ar putea să vă gândiți că problemele enumerate mai sus au fost deja rezolvate clar cu componente. Aceasta este o presupunere corectă! Frame-urile front-end precum React și Vue sunt foarte populare în acest scop; sunt instrumente uimitoare pentru încapsularea interfeței de utilizare. Cu toate acestea, există un sughiț cu ele care nu ne place întotdeauna - necesită ca UI să fie redată de JavaScript.

Aplicația Flywheel este foarte grea pentru back-end, cu HTML redat în principal pe server – dar, din fericire pentru noi, componentele pot veni în multe forme. La sfârșitul zilei, o componentă a interfeței de utilizare este o încapsulare a stilurilor și a regulilor de proiectare care trimite markup către un browser. Cu această realizare, putem adopta aceeași abordare a componentelor, dar fără suprasolicitarea unui cadru JavaScript.

Vom aborda mai jos modul în care construim componente constrânse, dar iată câteva dintre beneficiile pe care le-am găsit prin utilizarea lor:

  • Nu există niciodată o modalitate greșită de a pune împreună o componentă.
  • Componenta face toată gândirea de design pentru tine. (Doar treceți opțiunile!)
  • Sintaxa pentru crearea unei componente este foarte consistentă și ușor de raționat.
  • Dacă este necesară o modificare de design pentru o componentă, o putem schimba o dată în componentă și să fim siguri că este actualizată peste tot.

Redarea componentelor pe partea serverului

Deci despre ce vorbim prin constrângerea componentelor? Să pătrundem!

După cum am menționat mai devreme, dorim ca orice dezvoltator care lucrează în aplicația Flywheel să poată privi o machetă de design a unei pagini și să poată construi imediat acea pagină fără impedimente. Aceasta înseamnă că metoda de creare a interfeței de utilizare trebuie să fie A) foarte bine documentată și B) foarte declarativă și fără presupuneri.

Parțiale la salvare (sau așa credeam noi)

O primă încercare pe care am încercat-o în trecut a fost să folosim parțialele Rails. Partialele sunt singurul instrument pe care ți-l oferă Rails pentru reutilizare în șabloane. Desigur, ele sunt primul lucru la care toată lumea ajunge. Dar există dezavantaje semnificative în a te baza pe ele, deoarece dacă trebuie să combinați logica cu un șablon reutilizabil, aveți două opțiuni: duplicați logica pe fiecare controler care utilizează parțial sau încorporați logica în parțial în sine.

Parțialele previn greșelile de copiere/lipire de duplicare și funcționează bine pentru primele două ori în care trebuie să reutilizați ceva. Dar, din experiența noastră, parțialele sunt în curând aglomerate cu suport pentru tot mai multă funcționalitate și logică. Dar logica nu ar trebui să trăiască în șabloane!

Introducere în celule

Din fericire, există o alternativă mai bună la parțiale, care ne permite atât să reutilizam codul, cât și să păstrăm logica în afara vederii. Se numește Cells, o bijuterie Ruby dezvoltată de Trailblazer. Celulele au existat cu mult înainte ca popularitatea să crească în cadrele front-end precum React și Vue și vă permit să scrieți modele de vizualizare încapsulate care se ocupă atât de logică, cât și de șabloane. Ele oferă o abstractizare a modelului de vizualizare, pe care Rails pur și simplu nu o are din cutie. De fapt, folosim Cells în aplicația Flywheel de ceva vreme, doar că nu la scară globală, super reutilizabilă.

La cel mai simplu nivel, Cells ne permit să abstragem o bucată de markup ca acesta (folosim Haml pentru limbajul nostru de șabloane):

%div
  %h1 Hello, world!

Într-un model de vizualizare reutilizabil (foarte similar cu parțialele în acest moment) și transformați-l în acesta:

= cell("hello_world")

Acest lucru ne ajută în cele din urmă să constrângem componenta acolo unde clasele de ajutor sau componentele copil incorecte nu pot fi adăugate fără a modifica celula în sine.

Construirea celulelor

Punem toate celulele noastre UI într-un director app/cells/ui. Fiecare celulă trebuie să conțină un singur fișier Ruby, cu sufixul _cell.rb. Din punct de vedere tehnic, puteți scrie șabloanele chiar în Ruby cu ajutorul helper_tag, dar majoritatea celulelor noastre conțin și un șablon Haml corespunzător care se află într-un folder numit de componentă.

O celulă super de bază fără logică arată cam așa:

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

Metoda show este ceea ce este redat atunci când instanțiați celula și va căuta automat un fișier show.haml corespunzător în folderul cu același nume ca celula. În acest caz, este aplicația/celulele/ui/slat (acoperim toate celulele UI la modulul UI).

În șablon, puteți accesa opțiunile transmise în celulă. De exemplu, dacă celula este instanțiată într-o vizualizare de genul = cell(„ui/slat”, titlu: „Titlu”, subtitlu: „Subtitlu”, etichetă: „Etichetă”), putem accesa acele opțiuni prin obiectul opțiuni.

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

De multe ori vom muta elemente simple și valorile lor într-o metodă din celulă pentru a preveni redarea elementelor goale dacă nu este prezentă o opțiune.

// 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

Încheierea celulelor cu un utilitar UI

După ce am demonstrat că acest lucru ar putea funcționa la scară largă, am vrut să abordez markupul străin necesar pentru a apela o celulă. Pur și simplu nu curge destul de bine și este greu de reținut. Așa că i-am făcut un mic ajutor! Acum putem doar să apelăm = ui „name_of_component” și să transmitem opțiunile în linie.

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

Transmiterea opțiunilor ca bloc în loc de inline

Ducând utilitarul UI puțin mai departe, a devenit rapid evident că o celulă cu o grămadă de opțiuni pe o singură linie ar fi foarte greu de urmărit și pur și simplu urâtă. Iată un exemplu de celulă cu o mulțime de opțiuni definite în linie:

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

Este foarte greoi, ceea ce ne determină să creăm o clasă numită OptionProxy care interceptează metodele Cells setter și le transpune în valori hash, care sunt apoi îmbinate în opțiuni. Dacă sună complicat, nu-ți face griji – este complicat și pentru mine. Iată o esențială a clasei OptionProxy pe care Adam, unul dintre inginerii noștri seniori de software, a scris-o.

Iată un exemplu de utilizare a clasei OptionProxy în interiorul celulei noastre:

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

Acum, cu acest lucru, putem transforma opțiunile noastre greoaie în linie într-un bloc mai plăcut!

= 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"]

Introducerea logicii

Până în acest moment, exemplele nu au inclus nicio logică în ceea ce privește ceea ce afișează vizualizarea. Acesta este unul dintre cele mai bune lucruri pe care le oferă Cells, așa că hai să vorbim despre asta!

Rămânând cu componenta noastră slat, avem nevoie să redăm uneori întregul lucru ca link și uneori să îl redăm ca div, în funcție de faptul că este prezentă sau nu o opțiune de link. Cred că aceasta este singura componentă pe care o avem care poate fi redată ca div sau link, dar este un exemplu destul de frumos al puterii Cells.

Metoda de mai jos apelează fie un ajutor link_to, fie un helper content_tag, în funcție de prezența opțiunilor [:link] .

Notă: Acesta a fost inspirat și creat de Adam Lassek, care a fost extrem de influent în a ne ajuta să construim această metodă de dezvoltare a interfeței de utilizare cu Cells.

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

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

Acest lucru ne permite să înlocuim elementul .slat__inner din șablon cu un bloc container:

.slat
  = container do
  ...

Un alt exemplu de logică în Cells pe care îl folosim foarte mult este cel al claselor de ieșire condiționată. Să presupunem că adăugăm o opțiune dezactivată în celulă. Nimic altceva în invocarea celulei nu se modifică, în afară de faptul că acum puteți trece o opțiune dezactivată: adevărată și urmăriți cum totul se transformă într-o stare dezactivată (îngrijită cu link-uri neclickabile).

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

Când opțiunea dezactivată este adevărată, putem seta clase pe elementele din șablon care sunt necesare pentru a obține aspectul dezactivat dorit.

.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")

În mod tradițional, ar fi trebuit să ne amintim (sau să facem referire la ghidul de stil) care elemente individuale aveau nevoie de clase suplimentare pentru a face ca totul să funcționeze corect în starea dezactivată. Celulele ne permit să declarăm o opțiune și apoi să facem munca grea pentru noi.

Notă: posibil_classes este o metodă pe care am creat-o pentru a permite aplicarea condiționată a claselor în Haml într-un mod frumos.


Unde nu putem folosi componente pe partea serverului

În timp ce abordarea celulară este extrem de utilă pentru aplicația noastră specială și pentru modul în care lucrăm, aș fi neglijent să spun că a rezolvat 100% din problemele noastre. Încă scriem JavaScript (o mulțime) și construim destul de multe experiențe în Vue în aplicația noastră. 75% din timp, șablonul nostru Vue încă trăiește în Haml și legăm instanțele noastre Vue la elementul care le conține, ceea ce ne permite să profităm în continuare de abordarea celulară.

Cu toate acestea, în locurile în care este mai logic să constrângem complet o componentă ca o instanță Vue cu un singur fișier, nu putem folosi Cells. Listele noastre selectate, de exemplu, sunt toate Vue. Dar cred că e în regulă! Nu ne-am confruntat cu nevoia de a avea versiuni duplicate ale componentelor atât în ​​componente Cells, cât și în Vue, așa că este în regulă că unele componente sunt 100% construite cu Vue, iar altele sunt cu Cells. Dacă o componentă este construită cu Vue, înseamnă că este necesar JavaScript pentru ao construi în DOM și profităm de framework-ul Vue pentru a face acest lucru. Totuși, pentru majoritatea celorlalte componente ale noastre, nu necesită JavaScript și, dacă o fac, necesită ca DOM-ul să fie deja construit și doar ne conectăm și adăugăm ascultători de evenimente.

Pe măsură ce continuăm să progresăm cu abordarea celulară, cu siguranță vom experimenta cu combinația de componente ale celulei și componente Vue, astfel încât să avem un singur mod de a crea și utiliza componente. Nu știu încă cum arată, așa că vom trece acel pod când ajungem acolo!


Concluzia noastră

Până acum am convertit aproximativ treizeci dintre cele mai utilizate componente vizuale ale noastre în Cells. Ne-a oferit o explozie uriașă de productivitate și oferă dezvoltatorilor un sentiment de validare că experiențele pe care le construiesc sunt corecte și nu piratate împreună.

Echipa noastră de proiectare este mai încrezătoare ca niciodată că componentele și experiențele din aplicația noastră sunt 1:1 cu ceea ce au proiectat în Adobe XD. Modificările sau completările la componente sunt acum gestionate doar printr-o interacțiune cu un designer și un dezvoltator front-end, ceea ce menține restul echipei concentrat și fără griji pentru a ști cum să modifice o componentă pentru a se potrivi cu o machetă de design.

Repetăm ​​în mod constant abordarea noastră de a constrânge componentele UI, dar sper că tehnicile ilustrate în acest articol vă oferă o privire asupra a ceea ce funcționează bine pentru noi!


Vino să lucrezi la Flywheel!

La Flywheel, fiecare departament are un impact semnificativ asupra clienților noștri și asupra rezultatului final. Fie că este vorba de asistență pentru clienți, dezvoltare de software, marketing sau orice altceva, lucrăm cu toții împreună pentru misiunea noastră de a construi o companie de găzduire de care oamenii se pot îndrăgosti cu adevărat.

Sunteți gata să vă alăturați echipei noastre? Angajam! Aplica aici.