Skip to content
← Wszystkie wpisy
5 min czytania Dawid Skłodowski

HAML i SASS: markup i style, które pozostają DRY

Dwa narzędzia, po które sięgamy w każdym projekcie Rails. Jak HAML czyni szablony czytelnymi, a SASS utrzymuje arkusze stylów w ryzach — ze wzorcami, które działają, i pułapkami (nadmierne zagnieżdżanie, @extend), które nie.

Dwa narzędzia siedzą na froncie niemal każdego projektu Rails, który budujemy: HAML do szablonów i SASS do arkuszy stylów. Żadne nie jest niezbędne — ERB i czysty CSS działają — ale oba zwracają się w ciągu tygodnia, usuwając kategorię szumu i powtórzeń, które inaczej narastają, aż twoje widoki i arkusze stylów stają się tą częścią bazy kodu, której nikt nie chce dotykać. Oto jak ich używamy i jakich błędów nauczyliśmy się unikać.

HAML: markup znika

ERB to HTML z wmieszanym Ruby, co oznacza, że piszesz — i czytasz — każdy nawias kątowy i każdy tag zamykający. HAML wyrzuca to wszystko. Wcięcie definiuje zagnieżdżenie, więc nie ma tagów zamykających do pomylenia; %tag deklaruje element, a skróty w stylu CSS .class i #id sprawiają, że typowy przypadek czyta się niemal jak selektory, które później wystylujesz:

.card
  .card__header
    %h2= post.title
    %span.card__meta= post.published_on.to_s(:long)
  .card__body
    = simple_format(post.body)
  - if post.comments.any?
    %footer.card__footer
      = pluralize(post.comments.count, "komentarz")

Odpowiednik w ERB jest o jedną trzecią dłuższy i znacznie bardziej zaszumiony, a łatwiej go pomylić — brakujący </div> w głęboko zagnieżdżonym ERB to klasyczny zabójca popołudnia, który po prostu nie może się zdarzyć w HAML, bo nie ma tagów zamykających. Dyscyplina, którą HAML narzuca, jest też realna: ponieważ wcięcie jest strukturą, niechlujne zagnieżdżenie staje się natychmiast widoczne, a twoje szablony niemal siłą wychodzą płytsze i lepiej rozłożone.

Kilka rzeczy wartych wiedzy, gdy miniesz podstawy:

  • = wypisuje wyrażenie, - uruchamia Ruby bez wypisywania. %h2= post.title drukuje; - if ... steruje przepływem.
  • Atrybuty to hash Ruby: %a{ href: post_path(post), class: "btn" }, a hash współgra z helperami Rails i atrybutami warunkowymi.
  • Filtry czysto obsługują osadzoną treść — :javascript, :css, :markdown — więc nie escapujesz się przez bloki inline.
  • HAML nagradza wydzielanie partiali i helperów, bo szablon będący samą strukturą sprawia, że zabłąkana grudka logiki rzuca się w oczy. Gdy widok zaczyna kiełkować warunkami, to HAML mówi ci, byś przeniósł logikę do helpera albo presentera.

Zarzut, który ludzie podnoszą, to wrażliwość na białe znaki, i jest słuszny: spartolone wcięcie to realny błąd. Ale to błąd, który widzisz od razu, w miejscu, gdzie się dzieje, a nie zagadka zniekształconego DOM, którą debugujesz w przeglądarce. Tę wymianę bierzemy za każdym razem.

SASS: arkusze stylów, które się skalują

CSS nie pozwala abstrahować. Nie nazwiesz koloru, nie podzielisz bloku deklaracji, nie zagnieździsz powiązanych reguł — więc rosnący arkusz staje się długą płaską listą z tym samym kodem hex skopiowanym czterdzieści razy i selektorami powtórzonymi dla każdego wariantu. SASS (używamy składni SCSS, będącej nadzbiorem CSS, więc każdy poprawny CSS jest poprawnym SCSS) daje brakujące narzędzia: zmienne, zagnieżdżanie, miksiny, dziedziczenie i partiale.

Zmienne i partiale: jedno źródło prawdy

// _variables.scss
$brand:        #b00020;
$ink:          #1a1a1a;
$space:        1rem;
$breakpoint-m: 48rem;

// application.scss
@import "variables";
@import "components/card";
@import "components/buttons";

Nazwanie palety i odstępów raz, w partialu, który wszystko importuje, to rzecz o najwyższej dźwigni, jaką daje SASS. Rebranding z jednego pliku; zmiana rytmu odstępów w jednym miejscu. Partiale @import (nazwy plików z prefiksem _) pozwalają też podzielić monolityczny arkusz na plik na komponent, dzięki czemu utrzymujesz go żeglownym, gdy rośnie.

Miksiny do reużycia, z argumentami

Miksin to reużywalny blok deklaracji, opcjonalnie parametryzowany — idealny do zupy prefiksów producentów z 2014 i do punktów łamania responsywności:

@mixin respond-to($min) {
  @media (min-width: $min) { @content; }
}

.card {
  padding: $space;
  @include respond-to($breakpoint-m) {
    padding: $space * 2;
  }
}

Blok @content jest tym, co sprawia, że responsywne miksiny wskakują na miejsce: nazywasz punkty łamania raz i piszesz media queries, które wszędzie indziej czyta się jak po angielsku.

Zagnieżdżanie: użyteczne, dopóki nie jest

Zagnieżdżanie to funkcja, po którą ludzie sięgają pierwszą i nadużywają najmocniej. Odzwierciedla strukturę markupu i trzyma powiązane reguły razem:

.card {
  &__header { font-weight: 600; }
  &__body   { color: $ink; }
  &:hover   { box-shadow: 0 2px 8px rgba(0,0,0,.1); }
}

& (selektor rodzica) to dobra część — w połączeniu z konwencją nazewniczą jak BEM (block__element--modifier) produkuje płaskie selektory o niskiej specyficzności, trzymając źródło schludnie zgrupowane. Pułapką jest zagnieżdżanie podążające za drzewem DOM kilka poziomów w głąb:

// NIE: kompiluje się do .sidebar .widget ul li a — kruche i nadmiernie specyficzne
.sidebar { .widget { ul { li { a { color: $brand; } } } } }

To generuje głęboko potomne, mocno specyficzne selektory, które są kruche (psują się, gdy markup się przesunie), trudne do nadpisania (kończysz w wojnie ze specyficznością, sięgając po !important) i wolne do czytania. Nasza zasada: zagnieżdżaj dla wzorca modyfikatora &, nie po to, by odwzorować HTML. Jeśli selektor jest głębszy niż dwa–trzy poziomy, spłaszcz go, zwykle nadając elementowi własną klasę.

@extend vs miksiny: wybierz miksin

SASS oferuje dwa sposoby współdzielenia deklaracji, a wybór liczy się bardziej, niż wygląda. @extend sprawia, że jeden selektor dziedziczy reguły innego, grupując selektory w wyniku; miksin kopiuje deklaracje. @extend wygląda elegancko i produkuje mniej CSS, ale ma zaskakujące efekty: może przestawić twój wynik, wyciągnąć selektory przez kaskadę w sposób zmieniający, która reguła wygrywa, i tworzyć rozległe listy przez przecinki, które trudno przewidzieć. Po ugryzieniu przez niespodzianki specyficzności wywołane przez @extend, naszym domyślnym wyborem jest miksin (albo selektor placeholder %foo z @extend tylko dla naprawdę współdzielonych, statycznych kawałków). Kilka dodatkowych bajtów jest warte przewidywalności.

Działają razem

Powodem, dla którego te dwa narzędzia tak dobrze się parują w projekcie Rails, jest to, że pchają w tym samym kierunku: struktura ponad szum. Element .card__header w HAML siedzi wprost nad regułą .card { &__header { ... } } w SASS, nazwy się zgadzają, a asset pipeline kompiluje oba bez ceremonii. Komponent to partial HAML plus partial SCSS, które dzielą nazwę i korzeń BEM — jednostka, którą czytasz, przenosisz i ogarniasz jako całość.

Żadne z narzędzi nie jest magią i oba da się nadużyć — HAML w sprytne jednolinijkowce, których nikt nie przeczyta, SASS w piramidy zagnieżdżeń i spaghetti @extend. Używane z odrobiną dyscypliny usuwają jednak harówkę z frontu i zostawiają ci szablony i arkusze stylów, które wciąż przyjemnie otworzyć rok później. W naszych projektach to cała gra: kod, którego nie boisz się zmieniać, to kod, który dalej trafia na produkcję.