CSS Houdini - przyszłość CSSa czy niepotrzebna nowość?

CSS Houdini to zbiór API, udostępnionych przez przeglądarkę, dzięki którym, mamy bezpośredni dostęp do drzewka CSSOM(CSS Object Model). Pozwala nam to rozszerzać CSSa o nowe funkcjonalności, wpinać się do silnika renderującego i mówić przeglądarce, w jaki sposób ma wykorzystać CSSa podczas renderowania. A to wszystko z pomocą JavaScriptu!

Wsparcie przeglądarek dla Houdini różni się dla każdego API, jeśli chcesz zobaczyć pełen wykres wsparcia, zachęcam Cię do odwiedzenia https://ishoudinireadyyet.com/.

Dlaczego warto?

Po pierwsze, nowości wprowadzane do CSSa często potrzebują pollyfilli, mogą być one na bazie JavaScriptu, jednak te niosą ze sobą problemy z wydajnością. Dzieje się tak dlatego, że muszą one poczekać aż DOM i CSSOM się utworzą. Gdy drzewa się stworzą i nasz dokument zostanie załadowany, kończy się pierwszy cykl renderowania, dopiero po nim nasz pollyfill może zadziałać. W CSS Houdini nie czekamy na ten pierwszy cykl renderowania.

Następną zaletą jest, powiązaną już z poprzednią, jest fakt, iż nie musimy czekać na nowości, aż zostaną wprowadzone do przeglądarek.

Spis treści

TypedOM API

Jak możemy zmieniać CSSowe wartości w JavaScripcie?

Nic prostszego, pobieramy element, piszemy .style i wybraną wartość:

button.style.fontSize = 32 + "px";

Nie jest to przyjemny sposób, może rodzić wiele problemów i bugów.

computedStyleMap

W TypedOM manipulacja CSSem, jest bardziej logiczna, prostsza i szybsza. Zamiast stringów dostajemy obiekt CSSStyleValue z kluczami i wartościami:

{
  value: 20,
  unit: "px"
}
const button = document.querySelector(".button");

button.computedStyleMap().get("font-size");

Wykorzystujemy tutaj metodę computedStyleMap(), zwraca nam ona wszystkie style danego elementu z stylesheeta (computed styles). Dzięki niej mamy dostęp do metody get(), która zwraca daną własność. Oprócz niej mamy dostęp również do metod: set(),delete(),has() i append().

attributeStyleMap

Oprócz computedStyleMap() mamy również dostęp do wartości attributeStyleMap. Możemy, dzięki niej, pobierać, zamiast computed styles, wartości inlinowe.

let heightValue = element.attributeStyleMap.get("height");
heightValue.value++;
target.attributeStyleMap.set("height", heightValue);

CSSStyleValue

TypedOM udostępnia nam również klasę w której wszystkie CSSowe wartości są opisane. Dzięki temu mamy dostęp do jej subklas: CSSKeywordValue, CSSNumericValue,CSSTransformValue, CSSResourceValue.

Dzięki CSSKeywordValue do slów kluczowych, np. none:

element.attributeStyleMap.set("display", new CSSKeywordValue("none")));

Obiekty CSSNumericValue możemy podzielić na CSSUnitValues i CSSMathValues. Ta pierwsza reprezentuje numeryczne wartości wraz z jednostkami, np. CSSUnitValue(12, 'px'), ta druga zaś bardziej zaawansowane operacje, np. CSSMathSum(CSS.em(5), CSS.px(5)), odpowiada to znanemu już ze zwykłego CSSa calc(5em + 5px).

Dzięki CSSTransformValue możemy wpływać na wartości transform, a CSSResourceValue na np. background-image(za pomocą CSSImageValues).

Custom Properties And Values API

Custom Properties And Values pozwala nam rozszerzać zmienne CSSowe dodając do nich pewnie ciekawe ficzery. Żeby je stworzyć używamy specjalnej metody registerProperty(), metoda ta przyjmuje pewne argumenty:

  • name - nazwa
  • syntax - mówi przeglądarce jak ją parsować, mamy do dyspozycji np. <color>, <integer>, <number>, <percentage>
  • inherits - informacja o dziedziczeniu przez rodzica, możliwe opcje: true lub false
  • initialValue - początkowa wartość
// JS
CSS.registerProperty({
  name: "--box__gradient--position",
  syntax: "<percentage>",
  initialValue: "60%",
  inherits: false,
});
// CSS
.box {
  width: 20rem;
  height: 20rem;
  background: linear-gradient(
    45deg,
    rgba(255, 255, 255, 1) 0%,
    var(--box__color) var(--box__gradient--position)
  );
  transition: --box__color 0.5s ease, --box__gradient--position 1s 0.5s ease;
}

.box:hover {
  --box__color: #baebae;
  --box__gradient--position: 0%;
}

Tym sposobem osiągnęliśmy nieosiągalne w CSS - zanimowaliśmy gradient.

Paint API

Zanim zaczniemy, Paint API jest Workletem. A co to ten Worklet? Worklety to moduły czy też skrypty działające w osobnym wątku JavaScriptu. Mają imitować natywną funkcjonalność przeglądarki. Worklet wywołujemy specjalną funkcją addModule, która jest obietnicą.

await demoWorklet.addModule("path/to/script.js");

Promise.all([
  demoWorklet1.addModule("script1.js"),
  demoWorklet2.addModule("script2.js"),
]).then((results) => {...});

Okej to tyle odnośnie Workletów, przejdźmy do Paint API!

Dzięki Paint API możemy rysować, za pomocą context(tak, to ten context z canvasa ), bezpośrednio do właściwości elementu takich jak background-image. Jeżeli znacie canvasa będziecie czuli się jak w domu.

Tworzymy Worklet!

registerPaint(
  "paintWorketExample",
  class {
    static get inputProperties() {
      return ["--myVariable"];
    }
    static get inputArguments() {
      return ["<color>"];
    }
    static get contextOptions() {
      return { alpha: true };
    }

    paint(ctx, size, properties, args) {
      /* ... */
    }
  }
);
  • inputProperties - tablica custom properties, których Worklet ma śledzić
  • <color> - tablica argumentów, jakie mogą być podane podczas wywołania
  • contextOptions - pozwala nam ustawić aplpę, czyli takie opacity dla kolorów, jeśli wartość będzie false, wszystkie kolory będą miały 100% opacity
  • paint - tutaj dzieję się cała magia, funkcja może przyjąć kilka parametrów, ctx jest praktycznie tym samym jak ctx w canvasie, size jest obiektem składającym się z rozmiarów elementu width i height,properties są definiowane przez inputProperties, a args przez inputArguments.

Rejestracja Workleta w główym pliku .js:

CSS.paintWorklet.addModule("ścieżka_do_workleta.js");

Użycie w CSS:

.exampleElement {
  background-image: paint(paintWorketExample, blue);
}

Wywołujemy tutaj wcześniej napisaną funkcję paint(), argument paintWorketExample to nazwa Workletu, a blue to podane argumenty.

Przykład wykorzystania Paint API

Pełny kod tego przykładu ☝️ możecie znaleźć na Githubie GoogleChromeLabs

Animation API

Ten Worklet pozwala nam nasłuchiwać na przeróżne eventy takie jak scroll,hover czy click. Dodatkowo, wpływa bardzo dobrze na wydajność(w porównaniu chociażby do requestAnimationFrame) ponieważ działa na osobnym wątku.

registerAnimator(
  "animationWorkletExample",
  class {
    constructor(options) {
      /* ... */
    }
    animate(currentTime, effect) {
      /* ... */
    }
  }
);
  • constructor - ustawiamy tutaj setup naszego Workleta, odpalany przy stworzeniu nowej instancji
  • animate - tutaj trafia cała logika, currentTime to aktualna wartość czasu dla animacji, effect jest tablicą efektów, których używa animacja

Wywołanie Workleta w głowyn pliku .js:

async function init() {
  await CSS.animationWorklet.addModule("ścieżka_do_workleta.js");

  const effect = new KeyframeEffect(
    document.querySelector("#rotation"),
    [
      {
        transform: "rotateZ(0deg) ",
      },
      {
        transform: "rotateZ(-280deg)",
      },
    ],
    {
      duration: 3000,
      iterations: 5,
    }
  );

  new WorkletAnimation(
    "animationWorkletExample",
    effect,
    document.timeline,
    {}
  ).play();
}

init();

Wyjaśnijmy sobie powyższy przykład, tak jak zawsze, na początku pobieramy nasz Worklet, następnie inicjujemy klasę WorkletAnimation. A w niej pojawia się nasz efekt - KeyframeEffect. Pierwszy podajemy element, który ma być animowany. Następnie podajemy tablicę obiektów z keyframesami i dodatkowe opcje takie jak: czas trwania animacji i liczba iteracji. Później już tylko oś czasu currentTime i dodatkowe opcje dla konstruktora.

Przykład wykorzystania Animation API

Pełny kod tego przykładu ☝️ możecie znaleźć na Githubie houdini-examples

Layout API

Ostatni już w Workletów, rozszerza nam możliwości jakie daje nam przeglądarka w ramach tworzenia layoutu. Dzięki temu możemy stworzyć własną wartość dla display, na przykład masonry.

registerLayout(
  "exampleLayout",
  class {
    static get inputProperties() {
      return ["--exampleVariable"];
    }

    static get childrenInputProperties() {
      return ["--exampleChildVariable"];
    }

    static get layoutOptions() {
      return {
        childDisplay: "normal",
        sizing: "block-like",
      };
    }

    intrinsicSizes(children, edges, styleMap) {
      /* ... */
    }

    layout(children, edges, constraints, styleMap, breakToken) {
      /* ... */
    }
  }
);
  • inputProperties - tak jak w przypadku Paint API, tylko w tym przypadku Worklet śledzi custom properties, które przynależą do rodzica elementu
  • childrenInputProperties - podobnie do inputProperties, tym razem śledzimy custom properties dla dzieci elementu
  • layoutOptions - znajdziemy tutaj childDisplay, który definiuje w jaki sposób mają zostać wyświetlone dzieci elementu, jako block(blokowo), czy też normal(inlinowo). Za to sizing może mieć pre-definiowane wartość block-like lub manual. Mówi to przeglądarce czy ma przekalkulować rozmiar elementu, czy też nie.
  • intrinsicSizes - definiuje w jaki sposób kontener lub jego dzieci mają się zachowywać w kontekście layoutu.
  • layout - główna funkcja do tworzenia naszego layoutu, możemy wykorzystać tutaj dzieci elementu, ustawić krawędzie i ograniczenia.

Odpalenie Workleta w głównym pliku .js:

async function init() {
  await CSS.animationWorklet.addModule("ścieżka_do_workleta.js");
}
init();

Wykorzystanie w CSS:

.exampleElement {
  display: layout(exampleLayout);
}
  • exampleLayout - nazwa Workletu Przykład wykorzystania Layout API

Pełny kod tego przykładu ☝️ możecie znaleźć na Githubie houdini-examples

Wsparcie przeglądarek

Wsparcie przeglądarek dla CSS Houdini jest w tym momencie raczej średnie. Ale jestem pewien, że w najbliżej przyszłości sytuacja znacznie się poprawiać.

Warto jednak dać Houdini szansę i wykorzystywać tą technologię w nurcie Progressive Enhancement.

Jeżeli chcecie na bieżąco śledzić wsparcie dla CSS Houdini, to polecam Wam rzucić okiem na ishoudinireadyyet 👇

Wspracie przeglądarek dla CSS Houdini

Przyszłość CSS czy niepotrzebna nowość?

Dla mnie jest to zdecydowanie przyszłość CSSa. Mamy dostęp do kilku świetnych API, możemy dodawać nowe funkcjonalności dla naszych styli, jednym słowem Houdini to przyszłość!

Poza tym, przez fakt, że możemy wpinać się do procesu renderowania, nasze strony i aplikacje stają się szybsze i wydajniejsze.

Podsumowanie

Jak się Wam podoba CSS Houdini? Dajcie znać 👇

Nie jest to na pewno łatwe rozwiązanie, największe problemy może sprawiać Layout API, jednak warto się pobawić, chociażby w tym playgroudzie.

Zostawiam, jak zawsze, przydatne linki i źródła, tym samym zachęcam do głębszego poznawania Houdini!

Do usłyszenia!

Źródła

© Olaf Sulich 2020