Cum construim componente UI în Rails
Publicat: 2024-06-28Menținerea consistenței vizuale într-o aplicație web mare este o problemă comună în multe organizații. Principala aplicație web din spatele produsului nostru Flywheel este construită cu Ruby on Rails și avem aproximativ mai mulți 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 poate construi o pagină receptivă fără blocaje. Obstacolele au inclus, în general, neștiința care sunt componentele existente să folosești 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ă discutăm 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 în acel moment. Suntem încrezători că noua noastră abordare ne va pregăti pentru mult timp de acum încolo. Dacă vă confruntați cu probleme similare în aplicația dvs. 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ă ne constrângem complet componentele 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 timp de 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 interfață complexă, a devenit greoaie să facem 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ă.
Deseori, 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-stânga %h2 copii de rezervă .card__header-dreapta = link_to „#” face = pictogramă(„plus_mic”)
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-stânga = pictogramă(„arrow_backup”, culoare: „gri25”) %h2 copii de rezervă ...
Ideal ar fi să rezolvăm asta în CSS pentru antetele cardurilor, dar, pentru acest exemplu, să spunem 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-stânga = pictogramă(„arrow_backup”, culoare: „gri25”) %h2.--ml-10 Backup-uri ...
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 hr și voila!
Cu toate aceste modificări ale antetului original al cardului, acum avem o mizerie pe mâini în cod.
.card__header.--fară margini .card__header-stânga %h2.--ml-10 Backup-uri .card__header-dreapta = link_to „#” face = pictogramă(„plus_mic”) %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ă trimită o variantă a antetului unui card, aceasta ar trebui să fie surprinsă de o revizuire a designului sau de cod. 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 interfața dvs. de utilizare să fie redată de JavaScript.
Aplicația noastră 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 folosindu-le:
- 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.
Componente de randare 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ție 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. Parțialele 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.
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 devin î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 o 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 Bună, lume!
Într-un model de vizualizare reutilizabil (foarte similar cu parțialele în acest moment) și transformați-l în acesta:
= celulă(„bună_lumea”)
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
Am plasat 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:
// celule/ui/slat_cell.rb modulul UI clasa SlatCell < ViewModel def show Sfârşit Sfârşit Sfârşit
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 app/cells/ui/slat (întindem toate celulele noastre 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.
// celule/ui/slat/show.haml .șipcă .slat__interior .slat__conținut %h4= opțiuni[:title] %p= opțiuni[:subtitle] = pictogramă(opțiuni[:icon], culoare: „albastru”)
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.
// celule/ui/slat_cell.rb titlu def returnează cu excepția cazului în care opțiuni[:title] content_tag :h4, opțiuni[:title] Sfârşit def subtitrare returnează cu excepția cazului în care opțiuni[:subtitle] content_tag :p, opțiuni[:subtitle] Sfârşit
// celule/ui/slat/show.haml .șipcă .slat__interior .slat__conținut = titlu = subtitrare
Împachetarea 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ă am făcut un mic ajutor pentru asta! Acum putem doar să apelăm = ui „name_of_component” și să transmitem opțiunile în linie.
= ui „slat”, titlu: „Titlu”, subtitrare: „Subtitlu”, etichetă: „Etichetă”
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”, titlu: „Titlu”, subtitlu: „Subtitlu”, etichetă: „Etichetă”, link: „#”, titlul_terțiar: „Tertiar”, dezactivat: adevărat, listă de verificare: [„Articol 1”, „Articol 2”, „Articol 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 vă faceți 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:
modulul UI clasa SlatCell < ViewModel def show OptionProxy.new(self).yield!(opțiuni, &block) super() Sfârşit Sfârşit Sfârşit
Acum, cu asta în loc, putem transforma opțiunile noastre greoaie în linie într-un bloc mai plăcut!
= ui „slat” do |slat| - slat.title = „Titlu” - slat.subtitle = „Subtitrare” - slat.label = „Etichetă” - slat.link = "#" - slat.tertiary_title = "Tertiar" - slat.disabled = adevărat - slat.checklist = ["Element 1", "Element 2", "Element 3"]
Prezentarea 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]
.
container def(&bloch) tag = opțiuni dacă[:link] [:link_to, opțiuni[:link]] altfel [:content_tag, :div] Sfârşit trimite(*etichetă, clasă: „slat__inner”, &block) sfârşitul
Acest lucru ne permite să înlocuim elementul .slat__inner din șablon cu un bloc container:
.șipcă = 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ă puteți trece acum 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 = adevărat
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: posibil_classes("--disabled": opțiuni[:disabled]) } .slat__interior .slat__conținut %h4{ class: posibil_classes("--alt": opțiuni[:dezactivat]) }= opțiuni[:titlu] %p{ class: posibil_classes("--alt": opțiuni[:dezactivat]) }= opțiuni[:subtitle] = pictogramă(opțiuni[:icon], culoare: „gri”)
Î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 componentele 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 a o construi în DOM și profităm de framework-ul Vue pentru a face acest lucru. Cu toate acestea, 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. Încă nu știu 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 conceput î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 ț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 cu noi!
Fiecare departament care lucrează la produsele noastre 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ți aici.