Wie wir UI-Komponenten in Rails erstellen

Veröffentlicht: 2024-06-28

Die Aufrechterhaltung der visuellen Konsistenz in einer großen Webanwendung ist in vielen Unternehmen ein gemeinsames Problem. Die Hauptwebanwendung hinter unserem Flywheel-Produkt wird mit Ruby on Rails erstellt, und wir haben etwa mehrere Rails-Entwickler und drei Front-End-Entwickler, die jeden Tag Code dafür bereitstellen. Wir legen auch großen Wert auf Design (es ist einer unserer Grundwerte als Unternehmen) und haben drei Designer, die in unseren Scrum-Teams sehr eng mit den Entwicklern zusammenarbeiten.

Zwei Personen arbeiten gemeinsam an einem Website-Design

Unser Hauptziel besteht darin, sicherzustellen, dass jeder Entwickler eine responsive Seite ohne Hindernisse erstellen kann. Zu den Hürden gehörte im Allgemeinen, dass man nicht wusste, welche vorhandenen Komponenten man zum Erstellen eines Modells verwenden sollte (was dazu führte, dass die Codebasis mit sehr ähnlichen, redundanten Komponenten aufgebläht wurde) und nicht wusste, wann man die Wiederverwendbarkeit mit Designern besprechen sollte. Dies trägt zu inkonsistenten Kundenerlebnissen, Frustration der Entwickler und einer unterschiedlichen Designsprache zwischen Entwicklern und Designern bei.

Wir haben mehrere Iterationen von Styleguides und Methoden zum Erstellen/Pflegen von UI-Mustern und -Komponenten durchlaufen, und jede Iteration hat dazu beigetragen, die Probleme zu lösen, mit denen wir zu diesem Zeitpunkt konfrontiert waren. Wir sind zuversichtlich, dass wir mit unserem neuen Ansatz auch in Zukunft erfolgreich sein werden. Wenn Sie in Ihrer Rails-Anwendung auf ähnliche Probleme stoßen und Komponenten serverseitig angehen möchten, hoffe ich, dass Ihnen dieser Artikel einige Anregungen geben kann.

Ein bärtiger Mann lächelt in die Kamera, während er vor einem Computermonitor sitzt, auf dem Codezeilen angezeigt werden

In diesem Artikel gehe ich auf Folgendes ein:

  • Was wir lösen
  • Einschränkende Komponenten
  • Rendern von Komponenten auf der Serverseite
  • Wo wir keine serverseitigen Komponenten verwenden können

Was wir lösen

Wir wollten unsere UI-Komponenten vollständig einschränken und die Möglichkeit ausschließen, dass dieselbe UI auf mehr als eine Weise erstellt wird. Auch wenn ein Kunde es (zunächst) vielleicht nicht erkennen kann, führt das Fehlen von Einschränkungen bei Komponenten zu einer verwirrenden Entwicklererfahrung, macht die Wartung sehr schwierig und macht es schwierig, globale Designänderungen vorzunehmen.

Der traditionelle Weg, wie wir an Komponenten herangegangen sind, war unser Styleguide, der die gesamte Menge an Markup auflistete, die zum Erstellen einer bestimmten Komponente erforderlich war. So sah beispielsweise die Styleguide-Seite für unsere Lamellenkomponente aus:

Styleguide-Seite für Lamellenkomponente

Das funktionierte mehrere Jahre lang gut, aber als wir Varianten, Zustände oder alternative Möglichkeiten zur Verwendung der Komponente hinzufügten, begannen sich Probleme einzuschleichen. Bei einem komplexen Teil der Benutzeroberfläche war es umständlich, auf den Styleguide zu verweisen, um zu wissen, welche Klassen verwendet und welche vermieden werden sollten und in welcher Reihenfolge das Markup sein musste, um die gewünschte Variante auszugeben.

Oftmals nahmen Designer kleine Ergänzungen oder Optimierungen an einer bestimmten Komponente vor. Da der Styleguide dies nicht ganz unterstützte, wurden alternative Hacks, um diese Optimierung korrekt anzuzeigen (z. B. die unangemessene Kannibalisierung eines Teils einer anderen Komponente), irritierend häufig.

Beispiel für eine uneingeschränkte Komponente

Um zu veranschaulichen, wie Inkonsistenzen mit der Zeit auftauchen, verwende ich ein einfaches (und erfundenes), aber sehr häufiges Beispiel für eine unserer Komponenten in der Flywheel-App: Kartenköpfe.

Ausgehend von einem Designmodell sah ein Kartenkopf so aus. Es war ziemlich einfach mit einem Titel, einer Schaltfläche und einem unteren Rand.

 .card__header
  .card__header-left
    %h2 Backups
  .card__header-right
    = link_to "#" do
      = icon("plus_small")

Stellen Sie sich vor, ein Designer möchte nach dem Codieren links neben dem Titel ein Symbol hinzufügen. Standardmäßig gibt es keinen Abstand zwischen dem Symbol und dem Titel.

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

Idealerweise würden wir das im CSS für Kartenkopfzeilen lösen, aber für dieses Beispiel sagen wir mal, ein anderer Entwickler dachte: „Oh, ich weiß! Wir haben einige Margenhelfer. Ich werde dem Titel einfach eine Hilfsklasse hinzufügen.“

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

Nun ja, das sieht technisch gesehen genauso aus wie das Modell, oder?! Sicher, aber nehmen wir an, einen Monat später benötigt ein anderer Entwickler einen Kartenkopf, jedoch ohne das Symbol. Sie finden das letzte Beispiel, kopieren/fügen es ein und entfernen einfach das Symbol.

Wieder sieht es korrekt aus, oder? Aus dem Kontext gerissen, für jemanden ohne ein scharfes Auge für Design, klar! Aber schauen Sie es sich neben dem Original an. Der linke Rand des Titels ist immer noch vorhanden, weil ihnen nicht klar war, dass der linke Randhelfer entfernt werden musste!

Um dieses Beispiel noch einen Schritt weiter zu gehen, nehmen wir an, dass ein anderes Modell einen Kartenkopf ohne unteren Rand fordert. Man könnte einen Zustand finden, den wir im Styleguide haben und der „randlos“ heißt, und diesen anwenden. Perfekt!

Ein anderer Entwickler könnte dann versuchen, diesen Code wiederzuverwenden, aber in diesem Fall benötigt er tatsächlich einen Rahmen. Nehmen wir hypothetisch an, dass sie die im Styleguide dokumentierte ordnungsgemäße Verwendung ignorieren und nicht erkennen, dass das Entfernen der randlosen Klasse ihnen ihren Rand verleiht. Stattdessen fügen sie eine horizontale Regel hinzu. Am Ende gibt es etwas mehr Abstand zwischen dem Titel und dem Rand, also wenden sie eine Hilfsklasse auf hr an und voilà!

Mit all diesen Änderungen am ursprünglichen Kartenkopf haben wir jetzt ein Chaos im Code vor uns.

 .card__header.--randlos
  .card__header-left
    %h2.--ml-10 Sicherungen
  .card__header-right
    = link_to "#" do
      = icon("plus_small")
  %hr.--mt-0.--mb-0

Bedenken Sie, dass das obige Beispiel nur dazu dient, zu veranschaulichen, wie unbeschränkte Komponenten mit der Zeit unordentlich werden können. Wenn jemand in unserem Team versucht hat, eine Variation eines Kartenkopfes zu versenden, sollte dies durch eine Designüberprüfung oder Codeüberprüfung aufgedeckt werden. Aber solche Dinge passieren manchmal durch das Raster, deshalb müssen wir die Dinge kugelsicher machen!


Einschränkende Komponenten

Sie denken vielleicht, dass die oben aufgeführten Probleme mit Komponenten bereits eindeutig gelöst wurden. Das ist eine richtige Annahme! Frontend-Frameworks wie React und Vue sind genau für diesen Zweck sehr beliebt; Sie sind erstaunliche Tools zum Kapseln der Benutzeroberfläche. Es gibt jedoch einen Haken bei ihnen, der uns nicht immer gefällt: Sie erfordern, dass Ihre Benutzeroberfläche durch JavaScript gerendert wird.

Unsere Flywheel-Anwendung ist sehr backendlastig und besteht hauptsächlich aus vom Server gerendertem HTML – aber glücklicherweise können Komponenten in vielen Formen vorliegen. Letztendlich ist eine UI-Komponente eine Kapselung von Stilen und Designregeln, die Markup an einen Browser ausgibt. Mit dieser Erkenntnis können wir den gleichen Ansatz für Komponenten verfolgen, jedoch ohne den Overhead eines JavaScript-Frameworks.

Im Folgenden werden wir näher darauf eingehen, wie wir eingeschränkte Komponenten erstellen. Hier sind jedoch einige der Vorteile, die wir durch deren Verwendung festgestellt haben:

  • Es gibt nie wirklich die falsche Art, eine Komponente zusammenzusetzen.
  • Die Komponente erledigt das gesamte Design Thinking für Sie. (Sie geben einfach Optionen ein!)
  • Die Syntax zum Erstellen einer Komponente ist sehr konsistent und leicht zu begründen.
  • Wenn eine Designänderung an einer Komponente erforderlich ist, können wir diese einmal in der Komponente ändern und sicher sein, dass sie überall aktualisiert wird.

Rendern von Komponenten auf der Serverseite

Wovon reden wir also mit der Einschränkung von Komponenten? Lasst uns eintauchen!

Wie bereits erwähnt, möchten wir, dass jeder Entwickler, der an der Anwendung arbeitet, sich ein Designmodell einer Seite ansehen und diese Seite sofort und ohne Hindernisse erstellen kann. Das bedeutet, dass die Methode zur Erstellung der Benutzeroberfläche A) sehr gut dokumentiert und B) sehr deklarativ und frei von Vermutungen sein muss.

Partials zur Rettung (so dachten wir zumindest)

Ein erster Versuch, den wir in der Vergangenheit versucht haben, war die Verwendung von Rails-Partials. Partials sind das einzige Tool, das Rails für die Wiederverwendbarkeit in Vorlagen bereitstellt. Natürlich sind sie das Erste, wonach jeder greift. Es gibt jedoch erhebliche Nachteile, sich auf sie zu verlassen, denn wenn Sie Logik mit einer wiederverwendbaren Vorlage kombinieren müssen, haben Sie zwei Möglichkeiten: Duplizieren Sie die Logik auf jedem Controller, der den Teil verwendet, oder betten Sie die Logik in den Teil selbst ein.

Teiltexte verhindern Fehler beim Kopieren/Einfügen und funktionieren einwandfrei, wenn Sie zum ersten Mal etwas wiederverwenden müssen. Aber unserer Erfahrung nach werden die Teilbereiche bald mit der Unterstützung für immer mehr Funktionalität und Logik überfüllt. Aber Logik sollte nicht in Vorlagen leben!

Einführung in Zellen

Glücklicherweise gibt es eine bessere Alternative zu Partials, die es uns ermöglicht, Code wiederzuverwenden und die Logik aus der Ansicht herauszuhalten. Es heißt Cells, ein von Trailblazer entwickeltes Ruby-Juwel. Zellen gab es schon lange vor der Popularität von Front-End-Frameworks wie React und Vue und sie ermöglichen es Ihnen, gekapselte Ansichtsmodelle zu schreiben, die sowohl Logik als auch Vorlagen verarbeiten. Sie bieten eine Ansichtsmodellabstraktion, die Rails einfach nicht standardmäßig bietet. Wir verwenden Zellen in der Flywheel-App tatsächlich schon seit einiger Zeit, nur nicht in einem globalen, super wiederverwendbaren Maßstab.

Auf der einfachsten Ebene ermöglichen uns Zellen, einen Teil des Markups wie folgt zu abstrahieren (wir verwenden Haml als unsere Vorlagensprache):

 %div
  %h1 Hallo Welt!

In ein wiederverwendbares Ansichtsmodell (zu diesem Zeitpunkt sehr ähnlich zu Teilansichten) und verwandeln Sie es in Folgendes:

 = cell("hello_world")

Dies hilft uns letztendlich dabei, die Komponente darauf zu beschränken, dass Hilfsklassen oder falsche untergeordnete Komponenten nicht hinzugefügt werden können, ohne die Zelle selbst zu ändern.

Aufbau von Zellen

Wir legen alle unsere UI-Zellen in einem app/cells/ui-Verzeichnis ab. Jede Zelle darf nur eine Ruby-Datei mit dem Suffix _cell.rb enthalten. Sie können die Vorlagen technisch gesehen direkt in Ruby mit dem Hilfsprogramm „content_tag“ schreiben, aber die meisten unserer Zellen enthalten auch eine entsprechende Haml-Vorlage, die sich in einem Ordner befindet, der von der Komponente benannt wird.

Eine supereinfache Zelle ohne Logik sieht in etwa so aus:

 // Zellen/ui/slat_cell.rb
Modul-Benutzeroberfläche
  Klasse SlatCell < ViewModel
    auf jeden Fall zeigen
    Ende
  Ende
Ende

Die Show-Methode ist das, was gerendert wird, wenn Sie die Zelle instanziieren, und sie sucht automatisch nach einer entsprechenden show.haml-Datei im Ordner mit demselben Namen wie die Zelle. In diesem Fall ist es app/cells/ui/slat (wir beziehen alle unsere UI-Zellen auf das UI-Modul).

In der Vorlage können Sie auf die an die Zelle übergebenen Optionen zugreifen. Wenn die Zelle beispielsweise in einer Ansicht wie = cell(„ui/slat“, Titel: „Title“, Untertitel: „Subtitle“, Label: „Label“) instanziiert wird, können wir über das Optionsobjekt auf diese Optionen zugreifen.

 //cells/ui/slat/show.haml
.Lamelle
  .slat__inner
    .slat__content
      %h4= Optionen[:Titel]
      %p= Optionen[:subtitle]
      = icon(options[:icon], Farbe: „blau“)

Oft verschieben wir einfache Elemente und ihre Werte in eine Methode in der Zelle, um zu verhindern, dass leere Elemente gerendert werden, wenn keine Option vorhanden ist.

 //cells/ui/slat_cell.rb
definitiver Titel
  Rückkehr, es sei denn Optionen[:Titel]
  content_tag :h4, Optionen[:title]
Ende
auf jeden Fall Untertitel
  Rückkehr, es sei denn Optionen[:Untertitel]
  content_tag :p, Optionen[:Untertitel]
Ende
 //cells/ui/slat/show.haml
.Lamelle
  .slat__inner
    .slat__content
      = Titel
      = Untertitel

Zellen mit einem UI-Dienstprogramm umschließen

Nachdem ich bewiesen hatte, dass dies im großen Maßstab funktionieren kann, wollte ich mich mit dem überflüssigen Markup befassen, das zum Aufrufen einer Zelle erforderlich ist. Es fließt einfach nicht ganz richtig und ist schwer zu merken. Also haben wir einen kleinen Helfer dafür gemacht! Jetzt können wir einfach = ui „name_of_component“ aufrufen und Optionen inline übergeben.

 = ui „slat“, Titel: „Title“, Untertitel: „Subtitle“, Label: „Label“

Übergeben von Optionen als Block statt Inline

Wenn man das UI-Dienstprogramm etwas weiter betrachtet, wird schnell klar, dass eine Zelle mit einer Reihe von Optionen in einer Zeile sehr schwer zu verstehen und einfach nur hässlich ist. Hier ist ein Beispiel einer Zelle mit vielen inline definierten Optionen:

 = ui „slat“, Titel: „Title“, Untertitel: „Subtitle“, Label: „Label“, Link: „#“, tertiary_title: „Tertiary“, deaktiviert: true, checklist: [„Item 1“, „Item 2“, „Punkt 3“]

Das ist sehr umständlich, weshalb wir eine Klasse namens OptionProxy erstellt haben, die die Cells-Setter-Methoden abfängt und sie in Hash-Werte übersetzt, die dann in Optionen zusammengeführt werden. Wenn das kompliziert klingt, machen Sie sich keine Sorgen – für mich ist es auch kompliziert. Hier ist ein Kerninhalt der OptionProxy-Klasse, die Adam, einer unserer leitenden Softwareentwickler, geschrieben hat.

Hier ist ein Beispiel für die Verwendung der OptionProxy-Klasse in unserer Zelle:

 Modul-Benutzeroberfläche
  Klasse SlatCell < ViewModel
    auf jeden Fall zeigen
      OptionProxy.new(self).yield!(Optionen, &block)
      super()
    Ende
  Ende
Ende

Jetzt können wir unsere umständlichen Inline-Optionen in einen angenehmeren Block verwandeln!

 = ui „slat“ do |slat|
  - Slat.title = "Titel"
  - Slat.subtitle = "Untertitel"
  - Slat.label = "Label"
  -lat.link = "#"
  -lat.tertiary_title = "Tertiär"
  -lat.disabled = true
  -lat.checklist = ["Item 1", "Item 2", "Item 3"]

Einführung in die Logik

Bisher enthielten die Beispiele keine Logik für die Anzeige der Ansicht. Das ist eines der besten Dinge, die Cells zu bieten hat, also lasst uns darüber reden!

Wenn wir bei unserer Slat-Komponente bleiben, müssen wir manchmal das Ganze als Link und manchmal als Div rendern, je nachdem, ob eine Link-Option vorhanden ist oder nicht. Ich glaube, das ist die einzige Komponente, die wir haben, die als Div oder Link gerendert werden kann, aber es ist ein ziemlich schönes Beispiel für die Leistungsfähigkeit von Zellen.

Die folgende Methode ruft abhängig vom Vorhandensein der Optionen [:link] entweder einen link_to- oder einen content_tag-Helfer auf.

 def Container(&block)
  tag =
    if Optionen[:link]
      [:link_to, Optionen[:link]]
    anders
      [:content_tag, :div]
    Ende
  send(*tag, Klasse: „slat__inner“, &block)
Ende

Dadurch können wir das .slat__inner-Element in der Vorlage durch einen Containerblock ersetzen:

 .Lamelle
  = Container tun
  ...

Ein weiteres Beispiel für Logik in Zellen, das wir häufig verwenden, ist die bedingte Ausgabe von Klassen. Nehmen wir an, wir fügen der Zelle eine deaktivierte Option hinzu. Beim Aufruf der Zelle ändert sich nichts, außer dass Sie jetzt eine Option „disabled: true“ übergeben und zusehen können, wie das Ganze in einen deaktivierten Zustand übergeht (ausgegraut mit nicht anklickbaren Links).

 = ui „slat“ do |slat|
  ...
  -lat.disabled = true

Wenn die Option „disabled“ auf „true“ gesetzt ist, können wir Klassen für Elemente in der Vorlage festlegen, die erforderlich sind, um das gewünschte deaktivierte Aussehen zu erhalten.

 .slat{ Klasse: mögliche_Klassen("--disabled": Optionen[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ Klasse: mögliche_Klassen("--alt": Optionen[:disabled]) }= Optionen[:Titel]
      %p{ Klasse: mögliche_Klassen("--alt": Optionen[:disabled]) }=
      Optionen[:Untertitel]
      = icon(options[:icon], Farbe: „grau“)

Traditionell hätten wir uns merken müssen (oder auf den Styleguide verweisen), welche einzelnen Elemente zusätzliche Klassen benötigten, damit das Ganze im deaktivierten Zustand korrekt funktionierte. Zellen ermöglichen es uns, eine Option zu deklarieren und dann die schwere Arbeit für uns zu erledigen.

Hinweis: Mögliche_Klassen ist eine Methode, die wir erstellt haben, um die bedingte Anwendung von Klassen in Haml auf nette Weise zu ermöglichen.


Wo wir keine serverseitigen Komponenten verwenden können

Obwohl der Zellenansatz für unsere spezielle Anwendung und unsere Arbeitsweise äußerst hilfreich ist, würde ich nicht sagen, dass er 100 % unserer Probleme gelöst hat. Wir schreiben immer noch JavaScript (viel davon) und bauen in unserer App eine ganze Reihe von Erfahrungen in Vue auf. In 75 % der Fälle befindet sich unsere Vue-Vorlage immer noch in Haml und wir binden unsere Vue-Instanzen an das enthaltende Element, wodurch wir weiterhin die Vorteile des Zellansatzes nutzen können.

An Stellen, an denen es sinnvoller ist, eine Komponente vollständig als Vue-Instanz mit einer einzelnen Datei einzuschränken, können wir jedoch keine Zellen verwenden. Unsere Auswahllisten zum Beispiel stammen alle von Vue. Aber ich denke, das ist okay! Wir sind nicht wirklich auf die Notwendigkeit gestoßen, doppelte Versionen von Komponenten sowohl in Cells- als auch in Vue-Komponenten zu haben, daher ist es in Ordnung, dass einige Komponenten zu 100 % mit Vue und andere mit Cells erstellt werden.

Wenn eine Komponente mit Vue erstellt wird, bedeutet das, dass JavaScript erforderlich ist, um sie im DOM zu erstellen, und wir nutzen dazu das Vue-Framework. Für die meisten unserer anderen Komponenten ist jedoch kein JavaScript erforderlich. Wenn dies der Fall ist, muss das DOM bereits erstellt sein. Wir schließen uns einfach an und fügen Ereignis-Listener hinzu.

Während wir den Zellansatz weiterentwickeln, werden wir auf jeden Fall mit der Kombination von Zellkomponenten und Vue-Komponenten experimentieren, damit wir eine und nur eine Möglichkeit haben, Komponenten zu erstellen und zu verwenden. Ich weiß noch nicht, wie das aussieht, also werden wir die Brücke überqueren, wenn wir dort ankommen!


Unser Fazit

Bisher haben wir etwa dreißig unserer am häufigsten verwendeten visuellen Komponenten in Zellen konvertiert. Es hat uns einen enormen Produktivitätsschub beschert und Entwicklern das Gefühl der Bestätigung gegeben, dass die Erfahrungen, die sie erstellen, korrekt und nicht zusammengehackt sind.

Unser Designteam ist zuversichtlicher denn je, dass die Komponenten und Erlebnisse in unserer App 1:1 mit dem übereinstimmen, was sie in Adobe XD entworfen haben. Änderungen oder Ergänzungen an Komponenten werden jetzt ausschließlich durch die Interaktion mit einem Designer und Front-End-Entwickler abgewickelt, sodass der Rest des Teams konzentriert bleibt und sich keine Gedanken darüber machen muss, wie eine Komponente an ein Designmodell angepasst werden kann.

Wir arbeiten ständig an unserem Ansatz zur Einschränkung von UI-Komponenten, aber ich hoffe, dass die in diesem Artikel dargestellten Techniken Ihnen einen Einblick in das geben, was bei uns gut funktioniert!


Kommen Sie und arbeiten Sie mit uns!

Jede einzelne Abteilung, die an unseren Produkten arbeitet, hat einen bedeutenden Einfluss auf unsere Kunden und unser Endergebnis. Ob Kundensupport, Softwareentwicklung, Marketing oder irgendetwas dazwischen, wir arbeiten alle gemeinsam an unserer Mission, ein Hosting-Unternehmen aufzubauen, in das sich die Leute wirklich verlieben können.

Sind Sie bereit, unserem Team beizutreten? Wir stellen ein! Hier bewerben.