Überlegungen zu Styling-Optionen für Web Components

Avatar of Chris Coyier
Chris Coyier am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Wo platziert man Styles in Web Components?

Ich gehe davon aus, dass wir hier den Shadow DOM verwenden, da dies für mich einer der großen Vorteile einer Web Component ist: ein Plattform-Feature, das einzigartig leistungsstark ist und nur von der Plattform selbst geleistet werden kann. Es geht also darum, Stile für eine Web Component so zu definieren, dass sie nicht nach außen dringen, und weniger darum, globale Stile nach innen dringen zu lassen (obwohl das ebenfalls sehr interessant ist und, wie wir später im Artikel sehen werden, über Custom Properties erfolgen kann).

Wenn Sie die Vorlage im JavaScript erstellen – was wegen Template-Literalen und der Möglichkeit, unsere Daten schön in die Vorlage einzufügen, vorteilhaft ist –, benötigen Sie Zugriff auf diese Stile in JavaScript.

const template = `
  <style>${styles}</style>
  <div class="${class}">
    <h2>${title}</h2>
    ${content}
  </div>
`;

Woher kommt diese Style-Variable? Vielleicht auch ein Template-Literal?

const style = `
  :host {
    background: white;
  }
  h2 {
    font: 900 1.5rem/1.1 -system-ui, sans-serif;
  }
`;

Ich denke, das ist in Ordnung, aber es führt zu einem großen, unübersichtlichen Codeblock, der irgendwo in der Klasse platziert wird, in der Sie diese Web Component erstellen möchten.

Eine andere Möglichkeit ist, die Vorlage in ein <template> zu packen und einen <style>-Block zu einem Teil davon zu machen.

<template id="card-template">
  <style>
    :host {
      background: white;
    }
    h2 {
      font: 900 1.5rem/1.1 -system-ui, sans-serif;
    }
  </style>

  <div id="card-hook">
    <h2 id="title-hook"></h2>
    <p id="desc-hook"></p>
  </div>
</template>

Ich kann den Reiz daran erkennen, da HTML in HTML bleibt. Was mir daran nicht gefällt, ist, dass man eine Menge manueller Arbeit wie shadowRoot.querySelector("#title-hook").innerHTML = myData.title; leisten muss, um diese Vorlage auszufüllen. Das fühlt sich nicht wie eine praktische Vorlage an. Außerdem gefällt mir nicht, dass man diese Vorlage einfach irgendwo in seinem HTML ablegen muss. Wo? Ich weiß es nicht. Einfach da reinwerfen. Einfach reinwerfen.

Auch der CSS-Code wird aus dem JavaScript herausgenommen, aber er wechselt nur von einem unpraktischen Ort zu einem anderen.

Wenn wir den CSS-Code in einer CSS-Datei behalten wollten, könnten wir das irgendwie so machen

<template id="card-template">
  <style>
    @import "/css/components/card.css";
  </style>

  <div id="card-hook">
    <h2 id="title-hook"></h2>
    <p id="desc-hook"></p>
  </div>
</template>

(Die Verwendung von <link rel="import" type="css" href=""> ist anscheinend veraltet.)

Jetzt haben wir @import, was eine zusätzliche HTTP-Anfrage bedeutet und bekanntermaßen die Performance beeinträchtigt. Ein Artikel von Steven Lambert besagt, dass dies eine halbe Sekunde langsamer war. Nicht ideal. Ich nehme nicht an, dass es viel besser wäre, stattdessen Folgendes zu tun

class MyComponent extends HTMLElement {
    
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    fetch('/css/components/card.css')
      .then(response => response.text())
      .then(data => {
        let node = document.createElement('style');
        node.innerHTML = data;
        document.body.appendChild(node);
      });
  }

  // ...
}

Das könnte potenziell zu einem Flash-of-Unstyled-Web-Component führen? Ich schätze, ich sollte mich aufraffen und es testen.

Jetzt, wo ich mich wieder damit beschäftige, scheint ::part an Bedeutung gewonnen zu haben (Erklärung). Ich kann also machen…

const template = `
  <div part="card">
    <h2>${title}</h2>
    ${content}
  </div>
`;

…und dann Stile in einem globalen Stylesheet schreiben, die nur innerhalb dieses Shadow DOM gelten, wie

my-card::part(card) {
  background: black;
  color: white;
}

…was ein wenig Browser-Unterstützung hat, aber vielleicht nicht genug?

Diese "part"-Selektoren können nur das genaue Element berühren, mit dem sie verbunden sind. Man müsste die gesamte Formatierung vornehmen, indem man jedem einzelnen DOM-Knoten einen Part-Namen zuweist und dann jeden einzeln formatiert. Das macht keinen Spaß, besonders weil der Reiz des Shadow DOM seine isolierte Styling-Umgebung ist, in der wir angeblich lockerere CSS-Selektoren schreiben und uns keine Sorgen machen sollten, dass unser h2 { }-Stil überall durchsickert.

Es sieht so aus, als wäre native CSS-Module, falls sie sich durchsetzen, die hilfreichste Entwicklung, die passieren könnte.

import styles from './styles.css';

class MyElement extends HTMLElement {
  constructor() {
    this.attachShadow({mode: open});
    this.shadowRoot.adoptedStyleSheets = [styles];
  }
}

Ich bin mir jedoch nicht sicher, ob dies eine Leistungssteigerung darstellt. Es scheint, als wäre es ein Nullsummenspiel zwischen diesem und @import. Ich muss sagen, ich bevorzuge die Klarheit und Syntax bei nativen CSS-Modulen. Es ist schön, JavaScript zu schreiben, wenn man mit JavaScript arbeitet.

Constructable Stylesheets sehen auch hilfreich aus, um ein Stylesheet über mehrere Komponenten hinweg zu teilen. Aber der Ansatz mit CSS-Modulen könnte das auch, da das Stylesheet zu diesem Zeitpunkt bereits zu einer Variablen geworden ist.