Wie wir UI-Komponenten in Rails bei Flywheel erstellen

Veröffentlicht: 2019-11-16

Die Aufrechterhaltung der visuellen Konsistenz in einer großen Webanwendung ist ein gemeinsames Problem in vielen Organisationen. Bei Flywheel sind wir nicht anders. Unsere Haupt-Webanwendung wird mit Ruby on Rails erstellt und wir haben ungefähr 15 Rails-Entwickler und drei Front-End-Entwickler, die an jedem Tag Code dafür schreiben. Wir legen auch großen Wert auf Design (das ist einer unserer Grundwerte als Unternehmen) und haben drei Designer, die sehr eng mit den Entwicklern in unseren Scrum-Teams zusammenarbeiten.

Ein Hauptziel von uns ist es sicherzustellen, dass jeder Flywheel-Entwickler eine ansprechende Seite ohne Hindernisse erstellen kann. Zu den Hindernissen gehörte im Allgemeinen, nicht zu wissen, welche vorhandenen Komponenten zum Erstellen eines Modells verwendet werden sollten (was dazu führte, dass die Codebasis mit sehr ähnlichen, redundanten Komponenten aufgebläht wurde) und nicht zu wissen, wann die Wiederverwendbarkeit mit Designern besprochen werden sollte. Dies trägt zu inkonsistenten Kundenerlebnissen, Entwicklerfrust 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 dieser Zeit konfrontiert waren. Wir verfolgen jetzt einen (für uns) neuen Ansatz, von dem ich überzeugt bin, dass er uns für eine lange Zeit rüsten wird. Wenn Sie ähnliche Probleme in Ihrer Rails-Anwendung haben und Komponenten von der Serverseite aus angehen möchten, hoffe ich, dass dieser Artikel Ihnen einige Ideen geben kann.

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. Während ein Kunde (zunächst) vielleicht nicht in der Lage ist, dies zu erkennen, führt das Fehlen von Einschränkungen bei Komponenten zu einer verwirrenden Entwicklererfahrung, macht die Dinge sehr schwer zu warten und macht es schwierig, globale Designänderungen vorzunehmen.

Die herkömmliche Herangehensweise an Komponenten war unser Styleguide, der die gesamte Menge an Markup auflistete, die zum Erstellen einer bestimmten Komponente erforderlich ist. So sah beispielsweise die Styleguide-Seite für unsere Lamellenkomponente aus:

Das funktionierte gut und war mehrere Jahre lang für uns geeignet, aber Probleme begannen sich einzuschleichen, als wir Varianten, Zustände oder alternative Möglichkeiten zur Verwendung der Komponente hinzufügten. Bei einem komplexen Teil der Benutzeroberfläche wurde 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 Variation auszugeben. Und oft 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 Anpassung korrekt anzuzeigen (wie das unangemessene Ausschlachten eines Teils einer anderen Komponente), irritierend häufig.

Beispiel für eine uneingeschränkte Komponente

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

Ausgehend von einem Design-Mockup sah eine Kartenkopfzeile so aus. Es war ziemlich einfach mit einem Titel, einer Schaltfläche und einem unteren Rahmen.

.card__header
  .card__header-left
    %h2 Backups

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

Stellen Sie sich nach der Codierung einen Designer vor, der links neben dem Titel ein Symbol hinzufügen möchte. Ab Werk gibt es keinen Rand zwischen dem Symbol und dem Titel.

...
  .card__header-left
    = icon("arrow_backup", color: "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 Randhelfer. Ich schlage einfach eine Helferklasse auf den Titel.“

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

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

Wieder sieht es richtig aus, oder? Aus dem Zusammenhang gerissen, für jemanden ohne ein scharfes Auge für Design, sicher! Aber schau es dir neben dem Original an. Der linke Rand des Titels ist immer noch da, weil sie nicht erkannt haben, dass der Helfer für den linken Rand entfernt werden muss!

Um dieses Beispiel noch einen Schritt weiter zu gehen, nehmen wir an, ein anderes Mockup forderte eine Kartenkopfzeile ohne unteren Rand. Man könnte einen Zustand finden, den wir im Styleguide „randlos“ haben, und ihn anwenden. Perfekt!

Ein anderer Entwickler könnte dann versuchen, diesen Code wiederzuverwenden, aber in diesem Fall benötigen sie 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 Klasse Borderless ihnen ihren Rahmen gibt. Stattdessen fügen sie eine horizontale Regel hinzu. Am Ende gibt es eine zusätzliche Polsterung zwischen dem Titel und der Grenze, also wenden sie eine Hilfsklasse auf die hr an und voila!

Mit all diesen Änderungen am ursprünglichen Kartenheader haben wir jetzt ein Durcheinander im Code.

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

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

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

Denken Sie daran, dass das obige Beispiel nur veranschaulichen soll, wie unbeschränkte Komponenten im Laufe der Zeit unordentlich werden können. Wenn jemand in unserem Team versucht hat, eine Variation eines Kartenheaders zu liefern, sollte dies von einer Design- oder Codeüberprüfung erfasst werden. Aber solche Dinge rutschen manchmal durch die Ritzen, daher müssen wir Dinge kugelsicher machen!


Einschränkende Komponenten

Sie denken vielleicht, dass die oben aufgeführten Probleme bereits eindeutig mit Komponenten gelöst wurden. Das ist eine richtige Annahme! Front-End-Frameworks wie React und Vue sind für genau diesen Zweck sehr beliebt; Sie sind erstaunliche Werkzeuge zum Einkapseln von Benutzeroberflächen. Allerdings gibt es einen Haken bei ihnen, den wir nicht immer mögen – sie erfordern, dass Ihre Benutzeroberfläche von JavaScript gerendert wird.

Die Flywheel-Anwendung ist sehr Back-End-lastig mit hauptsächlich servergerendertem HTML – aber zum Glück für uns 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 verwenden, jedoch ohne den Overhead eines JavaScript-Frameworks.

Wir werden unten darauf eingehen, wie wir eingeschränkte Komponenten erstellen, aber hier sind einige der Vorteile, die wir durch ihre Verwendung festgestellt haben:

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

Rendern von Komponenten auf der Serverseite

Worüber reden wir also, wenn wir Komponenten beschränken? Lassen Sie uns graben!

Wie bereits erwähnt, möchten wir, dass jeder Entwickler, der in der Flywheel-Anwendung arbeitet, in der Lage ist, sich ein Design-Mockup einer Seite anzusehen und diese Seite sofort ohne Hindernisse zu erstellen. Das bedeutet, dass die Methode zur Erstellung der Benutzeroberfläche A) sehr gut dokumentiert und B) sehr aussagekräftig und frei von Vermutungen sein muss.

Partials zur Rettung (so dachten wir)

Ein erster Versuch, den wir in der Vergangenheit versucht haben, war die Verwendung von Rails-Partials. Partials sind das einzige Tool, das Rails Ihnen für die Wiederverwendbarkeit in Templates zur Verfügung stellt. 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 über jeden Controller, der das Partial verwendet, oder betten Sie die Logik in das Partial selbst ein.

Partials verhindern Kopieren/Einfügen-Duplizierungsfehler und funktionieren die ersten Male, wenn Sie etwas wiederverwenden müssen, einwandfrei. Aber unserer Erfahrung nach werden die Partials bald mit Unterstützung für immer mehr Funktionalität und Logik überladen. Aber Logik sollte nicht in Vorlagen leben!

Einführung in die 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 Ruby-Juwel, das von Trailblazer entwickelt wurde. Zellen gab es schon lange vor dem Anstieg der Popularität in 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 View-Model-Abstraktion, die Rails nicht wirklich standardmäßig hat. Wir verwenden Cells schon seit einiger Zeit in der Flywheel-App, 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 für unsere Templating-Sprache):

%div
  %h1 Hello, world!

In ein wiederverwendbares Ansichtsmodell (an dieser Stelle sehr ähnlich zu Partials) und daraus Folgendes machen:

= cell("hello_world")

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

Zellen konstruieren

Wir legen alle unsere UI-Zellen in einem app/cells/ui-Verzeichnis ab. Jede Zelle muss nur eine Ruby-Datei mit dem Suffix _cell.rb enthalten. Technisch gesehen können Sie die Templates direkt in Ruby mit dem content_tag-Helfer schreiben, aber die meisten unserer Cells enthalten auch ein entsprechendes Haml-Template, das sich in einem von der Komponente benannten Ordner befindet.

Eine Super-Basiszelle ohne Logik darin sieht etwa so aus:

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

Die Show-Methode wird gerendert, 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 richten alle unsere UI-Zellen auf das UI-Modul aus).

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", title: "Title", subtitle: "Subtitle", label: "Label") instanziiert wird, können wir auf diese Optionen über das Optionsobjekt zugreifen.

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

Häufig 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
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

Umhüllen von Zellen mit einem UI-Dienstprogramm

Nachdem ich das Konzept bewiesen hatte, dass dies in großem Umfang funktionieren könnte, wollte ich mich mit dem irrelevanten 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", title: "Title", subtitle: "Subtitle", label: "Label"

Übergeben von Optionen als Block statt Inline

Als wir das UI-Dienstprogramm ein wenig weiterführten, wurde schnell klar, dass eine Zelle mit einer Reihe von Optionen in einer Zeile sehr schwer zu verfolgen und einfach nur hässlich wäre. Hier ist ein Beispiel für eine Zelle mit vielen inline definierten Optionen:

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

Es ist sehr umständlich, was uns dazu veranlasst hat, eine Klasse namens OptionProxy zu erstellen, die die Setter-Methoden von Cells abfängt und sie in Hash-Werte übersetzt, die dann in Optionen zusammengeführt werden. Wenn das kompliziert klingt, keine Sorge – für mich ist es auch kompliziert. Hier ist ein Überblick über die OptionProxy-Klasse, die Adam, einer unserer leitenden Softwareingenieure, geschrieben hat.

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

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

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

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

Logik vorstellen

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

Um bei unserer Slat-Komponente zu bleiben, müssen wir das Ganze manchmal 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 nettes Beispiel für die Leistungsfähigkeit von Cells.

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

Hinweis: Dies wurde von Adam Lassek inspiriert und erstellt, der uns maßgeblich dabei unterstützt hat, diese gesamte Methode der UI-Entwicklung mit Cells zu entwickeln.

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

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

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

.slat
  = container do
  ...

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

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

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

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

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

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


Wo wir keine serverseitigen Komponenten verwenden können

Während der Zellansatz für unsere spezielle Anwendung und unsere Arbeitsweise äußerst hilfreich ist, würde ich nachlässig sagen, dass er 100 % unserer Probleme gelöst hat. Wir schreiben immer noch JavaScript (viel davon) und bauen in unserer App einige 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 den Zellansatz nutzen können.

An Stellen, an denen es jedoch sinnvoller ist, eine Komponente vollständig als Vue-Instanz mit einer einzigen Datei einzuschränken, können wir Cells nicht verwenden. Unsere Auswahllisten zum Beispiel sind alle Vue. Aber das finde ich in Ordnung! 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 wurden. Wenn eine Komponente mit Vue erstellt wird, bedeutet dies, 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, und wenn dies der Fall ist, muss das DOM bereits erstellt sein, und wir hängen uns einfach ein und fügen Ereignis-Listener hinzu.

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


Unser Fazit

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

Unser Designteam ist mehr denn je davon überzeugt, dass die Komponenten und Erfahrungen 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 eine Interaktion mit einem Designer und Front-End-Entwickler gehandhabt, wodurch der Rest des Teams konzentriert bleibt und sich keine Gedanken darüber machen muss, wie man eine Komponente an ein Design-Mockup anpasst.

Wir wiederholen unseren Ansatz zur Einschränkung von UI-Komponenten ständig, aber ich hoffe, die in diesem Artikel veranschaulichten Techniken geben Ihnen einen Einblick in das, was für uns gut funktioniert!


Komm und arbeite bei Flywheel!

Bei Flywheel hat jede einzelne Abteilung 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.

Bereit, unserem Team beizutreten? Wir stellen ein! Hier bewerben.