Theming und Theme-Switching mit React und styled-components

Avatar of Tapas Adhikary
Tapas Adhikary am

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

Ich hatte kürzlich ein Projekt mit der Anforderung, *Theming* auf der Website zu unterstützen. Es war eine etwas seltsame Anforderung, da die Anwendung hauptsächlich von einer Handvoll Administratoren genutzt wird. Eine noch größere Überraschung war, dass sie nicht nur zwischen vordefinierten Themes wählen wollten, sondern auch *eigene Themes erstellen* wollten. Ich schätze, die Leute wollen, was sie wollen!

Lassen Sie uns das zu einer vollständigen Liste detaillierterer Anforderungen destillieren, dann machen wir das!

  • Definieren Sie ein Theme (z.B. Hintergrundfarbe, Schriftfarbe, Buttons, Links usw.)
  • Erstellen und speichern Sie mehrere Themes
  • Wählen und **anwenden** Sie ein Theme
  • Wechseln Sie die Themes
  • Passen Sie ein Theme an

Wir haben genau das an unseren Kunden geliefert, und das Letzte, was ich gehört habe, war, dass sie es glücklich nutzten!

Lassen Sie uns genau das aufbauen. Wir werden React und styled-components verwenden. Der gesamte Quellcode, der im Artikel verwendet wird, ist im GitHub-Repository zu finden.

Das Setup

Lassen Sie uns ein Projekt mit React und styled-components einrichten. Dazu verwenden wir create-react-app. Es gibt uns die Umgebung, die wir benötigen, um React-Anwendungen schnell zu entwickeln und zu testen.

Öffnen Sie eine Eingabeaufforderung und verwenden Sie diesen Befehl, um das Projekt zu erstellen

npx create-react-app theme-builder

Das letzte Argument, theme-builder, ist einfach der Name des Projekts (und damit des Ordners). Sie können jeden beliebigen Namen verwenden.

Das kann eine Weile dauern. Wenn es fertig ist, navigieren Sie in der Kommandozeile mit cd theme-builder dorthin. Öffnen Sie die Datei src/App.js und ersetzen Sie den Inhalt durch Folgendes

import React from 'react';

function App() {
  return (
    <h1>Theme Builder</h1>
  );
}

export default App;

Dies ist eine einfache React-Komponente, die wir bald modifizieren werden. Führen Sie den folgenden Befehl vom Stammordner des Projekts aus, um die App zu starten

# Or, npm run start
yarn start

Sie können die App jetzt über die URL https://:3000 aufrufen.

A simple heading 1 that says Theme Builder in black on a white background.

create-react-app kommt mit der Testdatei für die App-Komponente. Da wir im Rahmen dieses Artikels keine Tests für die Komponenten schreiben werden, können Sie diese Datei löschen.

Wir müssen ein paar Abhängigkeiten für unsere App installieren. Installieren wir sie also gleich

# Or, npm i ...
yarn add styled-components webfontloader lodash

Hier ist, was wir bekommen

  • styled-components: Eine flexible Möglichkeit, React-Komponenten mit CSS zu gestalten. Es bietet standardmäßig Theming-Unterstützung mit einer Wrapper-Komponente namens <ThemeProvider>. Diese Komponente ist dafür verantwortlich, das Theme für alle anderen React-Komponenten bereitzustellen, die darin eingeschlossen sind. Das werden wir gleich in Aktion sehen.
  • Web Font Loader: Der Web Font Loader hilft beim Laden von Schriftarten aus verschiedenen Quellen, wie Google Fonts, Adobe Fonts usw. Wir werden diese Bibliothek verwenden, um Schriftarten zu laden, wenn ein Theme angewendet wird.
  • lodash: Dies ist eine JavaScript-Utility-Bibliothek für einige praktische kleine Extras.

Ein Theme definieren

Dies ist die erste unserer Anforderungen. Ein Theme sollte eine bestimmte Struktur haben, um das Erscheinungsbild zu definieren, einschließlich Farben, Schriftarten usw. Für unsere Anwendung definieren wir jedes Theme mit diesen Eigenschaften

  • eindeutiger Identifikator
  • Name des Themes
  • Farbbeschreibungen
  • Schriftarten
Screenshot of a code editor showing the organized structure of properties for a sea wave theme.
Ein Theme ist eine strukturierte Gruppe von Eigenschaften, die wir in der Anwendung verwenden werden.

Sie können mehr Eigenschaften und/oder andere Möglichkeiten zur Strukturierung haben, aber dies sind die Dinge, die wir für unser Beispiel verwenden werden.

Mehrere Themes erstellen und speichern

Wir haben gerade gesehen, wie man ein Theme definiert. Lassen Sie uns nun mehrere Themes erstellen, indem wir einen Ordner im Projekt unter src/theme und darin eine Datei namens schema.json hinzufügen. Hier ist, was wir in diese Datei einfügen können, um die Themes "light" und "sea wave" zu etablieren

{
  "data" : {
    "light" : {
      "id": "T_001",
      "name": "Light",
      "colors": {
        "body": "#FFFFFF",
        "text": "#000000",
        "button": {
          "text": "#FFFFFF",
          "background": "#000000"
        },
        "link": {
          "text": "teal",
          "opacity": 1
        }
      },
      "font": "Tinos"
    },
    "seaWave" : {
      "id": "T_007",
      "name": "Sea Wave",
      "colors": {
        "body": "#9be7ff",
        "text": "#0d47a1",
        "button": {
          "text": "#ffffff",
          "background": "#0d47a1"
        },
        "link": {
          "text": "#0d47a1",
          "opacity": 0.8
        }
      },
      "font": "Ubuntu"
    }
  }
}

Der Inhalt der Datei schema.json kann in einer Datenbank gespeichert werden, damit wir alle Themes zusammen mit der Theme-Auswahl persistent speichern können. Vorerst werden wir ihn einfach im localStorage des Browsers speichern. Dazu erstellen wir einen weiteren Ordner unter src/utils mit einer neuen Datei darin namens storage.js. Wir benötigen nur ein paar Zeilen Code, um localStorage einzurichten

export const setToLS = (key, value) => {
  window.localStorage.setItem(key, JSON.stringify(value));
}

export const getFromLS = key => {
  const value = window.localStorage.getItem(key);

  if (value) {
    return JSON.parse(value);
  }
}

Dies sind einfache Hilfsfunktionen zum Speichern von Daten im localStorage des Browsers und zum Abrufen von dort. Jetzt laden wir die Themes beim ersten Start der App in das localStorage des Browsers. Dazu öffnen wir die Datei index.js und ersetzen den Inhalt durch Folgendes,

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import * as themes from './theme/schema.json';
import { setToLS } from './utils/storage';

const Index = () => {
  setToLS('all-themes', themes.default);
  return(
    <App />
  )
}

ReactDOM.render(
  <Index />
  document.getElementById('root'),
);

Hier holen wir die Theme-Informationen aus der Datei schema.json und fügen sie mit dem Schlüssel all-themes zum localStorage hinzu. Wenn Sie die laufende App gestoppt haben, starten Sie sie bitte erneut und greifen Sie auf die Benutzeroberfläche zu. Sie können die Entwicklertools im Browser verwenden, um zu sehen, dass die Themes in localStorage geladen werden.

The theme with DevTools open and showing the theme properties in the console.
Alle Theme-Props sind ordnungsgemäß im localStorage des Browsers gespeichert, wie in den Entwicklertools unter Anwendung → Lokaler Speicher zu sehen ist.

Ein Theme auswählen und anwenden

Wir können nun die Theme-Struktur verwenden und das Theme-Objekt dem <ThemeProvider>-Wrapper übergeben.

Zuerst erstellen wir einen benutzerdefinierten React-Hook. Dieser verwaltet das ausgewählte Theme und weiß, ob ein Theme korrekt geladen wurde oder Probleme aufweist. Beginnen wir mit einer neuen Datei useTheme.js im Ordner src/theme mit folgendem Inhalt

import { useEffect, useState } from 'react';
import { setToLS, getFromLS } from '../utils/storage';
import _ from 'lodash';

export const useTheme = () => {
  const themes = getFromLS('all-themes');
  const [theme, setTheme] = useState(themes.data.light);
  const [themeLoaded, setThemeLoaded] = useState(false);

  const setMode = mode => {
    setToLS('theme', mode)
    setTheme(mode);
  };

  const getFonts = () => {
    const allFonts = _.values(_.mapValues(themes.data, 'font'));
    return allFonts;
  }

  useEffect(() =>{
    const localTheme = getFromLS('theme');
    localTheme ? setTheme(localTheme) : setTheme(themes.data.light);
    setThemeLoaded(true);
  }, []);

  return { theme, themeLoaded, setMode, getFonts };
};

Dieser benutzerdefinierte React-Hook gibt das ausgewählte Theme aus localStorage zurück und einen booleschen Wert, der anzeigt, ob das Theme korrekt aus dem Speicher geladen wurde. Er stellt auch eine Funktion, setMode, zur Verfügung, um ein Theme programmgesteuert anzuwenden. Wir werden darauf später zurückkommen. Damit erhalten wir auch eine Liste von Schriftarten, die wir später mit einem Webfont-Loader laden können.

Es wäre gut, globale Stile zu verwenden, um Dinge wie die Hintergrundfarbe, die Schriftart, die Buttons usw. der Website zu steuern. styled-components bietet eine Komponente namens createGlobalStyle, die Theme-fähige globale Komponenten erstellt. Lassen Sie uns diese in einer Datei namens GlobalStyles.js im Ordner src/theme mit dem folgenden Code einrichten

import { createGlobalStyle} from "styled-components";

export const GlobalStyles = createGlobalStyle`
  body {
    background: ${({ theme }) => theme.colors.body};
    color: ${({ theme }) => theme.colors.text};
    font-family: ${({ theme }) => theme.font};
    transition: all 0.50s linear;
  }

  a {
    color: ${({ theme }) => theme.colors.link.text};
    cursor: pointer;
  }

  button {
    border: 0;
    display: inline-block;
    padding: 12px 24px;
    font-size: 14px;
    border-radius: 4px;
    margin-top: 5px;
    cursor: pointer;
    background-color: #1064EA;
    color: #FFFFFF;
    font-family: ${({ theme }) => theme.font};
  }

  button.btn {
    background-color: ${({ theme }) => theme.colors.button.background};
    color: ${({ theme }) => theme.colors.button.text};
  }
`;

Nur etwas CSS für den <body>, Links und Buttons, richtig? Wir können diese in der Datei App.js verwenden, um das Theme in Aktion zu sehen, indem wir den Inhalt darin damit ersetzen

// 1: Import
import React, { useState, useEffect } from 'react';
import styled, { ThemeProvider } from "styled-components";
import WebFont from 'webfontloader';
import { GlobalStyles } from './theme/GlobalStyles';
import {useTheme} from './theme/useTheme';

// 2: Create a cotainer
const Container = styled.div`
  margin: 5px auto 5px auto;
`;

function App() {
  // 3: Get the selected theme, font list, etc.
  const {theme, themeLoaded, getFonts} = useTheme();
  const [selectedTheme, setSelectedTheme] = useState(theme);

  useEffect(() => {
    setSelectedTheme(theme);
   }, [themeLoaded]);

  // 4: Load all the fonts
  useEffect(() => {
    WebFont.load({
      google: {
        families: getFonts()
      }
    });
  });

  // 5: Render if the theme is loaded.
  return (
    <>
    {
      themeLoaded && <ThemeProvider theme={ selectedTheme }>
        <GlobalStyles/>
        <Container style={{fontFamily: selectedTheme.font}}>
          <h1>Theme Builder</h1>
          <p>
            This is a theming system with a Theme Switcher and Theme Builder.
            Do you want to see the source code? <a href="https://github.com/atapas/theme-builder" target="_blank">Click here.</a>
          </p>
        </Container>
      </ThemeProvider>
    }
    </>
  );
}

export default App;

Hier passieren ein paar Dinge

  1. Wir importieren die React-Hooks useState und useEffect, die uns helfen, alle Zustandsvariablen und ihre Änderungen aufgrund von Nebenwirkungen zu verfolgen. Wir importieren ThemeProvider und styled von styled-components. WebFont wird ebenfalls importiert, um Schriftarten zu laden. Wir importieren auch das benutzerdefinierte Theme, useTheme, und die globale Stilkomponente GlobalStyles.
  2. Wir erstellen eine Container-Komponente unter Verwendung von CSS-Stilen und der styled-Komponente.
  3. Wir deklarieren die Zustandsvariablen und achten auf Änderungen.
  4. Wir laden alle Schriftarten, die für die App benötigt werden.
  5. Wir rendern eine Reihe von Texten und einen Link. Aber beachten Sie, dass wir den gesamten Inhalt mit dem <ThemeProvider>-Wrapper umschließen, der das ausgewählte Theme als Prop erhält. Wir übergeben auch die <GlobalStyles/>-Komponente.

Aktualisieren Sie die App, und wir sollten das standardmäßige "light"-Theme sehen.

The theme with a white background and black text.
Hey, schau dir dieses saubere, klare Design an!

Wir sollten wahrscheinlich prüfen, ob das Umschalten der Themes funktioniert. Öffnen wir also die Datei useTheme.js und ändern Sie diese Zeile

localTheme ? setTheme(localTheme) : setTheme(themes.data.light);

...zu

localTheme ? setTheme(localTheme) : setTheme(themes.data.seaWave);

Aktualisieren Sie die App erneut, und hoffentlich sehen wir das "sea wave"-Theme in Aktion.

The same theme in with a blue color scheme with a light blue background and dark blue text and a blue button.
Jetzt reiten wir die Wellen dieses blau-dominierten Themes.

Themes wechseln

Großartig! Wir können Themes korrekt anwenden. Wie wäre es, wenn wir eine Möglichkeit schaffen, Themes per Knopfdruck umzuschalten? Natürlich können wir das tun! Wir können auch eine Art Theme-Vorschau anbieten.

A heading instructs the user to select a theme and two card components are beneath the heading, side-by-side, showing previews of the light theme and the sea wave theme.
Eine Vorschau jedes Themes wird in der Liste der Optionen angezeigt.

Nennen wir jede dieser Boxen eine ThemeCard und richten wir sie so ein, dass sie ihre Theme-Definition als Prop aufnehmen können. Wir gehen alle Themes durch, iterieren sie und füllen jedes als ThemeCard-Komponente auf.

{
  themes.length > 0 && 
  themes.map(theme =>(
    <ThemeCard theme={data[theme]} key={data[theme].id} />
  ))
}

Wenden wir uns nun dem Markup für eine ThemeCard zu. Ihre kann anders aussehen, aber beachten Sie, wie wir ihre eigenen Farb- und Schrifteigenschaften extrahieren und sie dann anwenden

const ThemeCard = props => {
  return(
    <Wrapper 
      style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.body}`, color: `${data[_.camelCase(props.theme.name)].colors.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
      <span>Click on the button to set this theme</span>
      <ThemedButton
        onClick={ (theme) => themeSwitcher(props.theme) }
        style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.button.background}`, color: `${data[_.camelCase(props.theme.name)].colors.button.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
        {props.theme.name}
      </ThemedButton>
    </Wrapper>
  )
}

Als Nächstes erstellen wir eine Datei namens ThemeSelector.js in unserem src-Ordner. Kopieren Sie den Inhalt von hier und fügen Sie ihn in die Datei ein, um unseren Theme-Selektor einzurichten, den wir in App.js importieren müssen

import ThemeSelector from './ThemeSelector';

Jetzt können wir ihn innerhalb der Container-Komponente verwenden

<Container style={{fontFamily: selectedTheme.font}}>
  // same as before
  <ThemeSelector setter={ setSelectedTheme } />
</Container>

Lassen Sie uns jetzt den Browser aktualisieren und sehen, wie das Umschalten der Themes funktioniert.

An animated screenshot showing the theme changing when it is selected from the list of theme card options.

Der lustige Teil ist, dass Sie so viele Themes wie gewünscht in die Datei schema.json einfügen können, um sie in der Benutzeroberfläche zu laden und zu wechseln. Schauen Sie sich diese schema.json-Datei für weitere Themes an. Bitte beachten Sie, dass wir auch die Informationen über das angewendete Theme in localStorage speichern, sodass die Auswahl beim nächsten Öffnen der App erhalten bleibt.

Selected theme stored in the Local Storage.

Ein Theme anpassen

Vielleicht mögen Ihre Benutzer einige Aspekte des einen Themes und einige Aspekte des anderen. Warum sollten sie sich zwischen ihnen entscheiden müssen, wenn sie die Möglichkeit haben, die Theme-Eigenschaften selbst zu definieren! Wir können eine einfache Benutzeroberfläche erstellen, die es Benutzern ermöglicht, die gewünschten Erscheinungsbildoptionen auszuwählen und sogar ihre Präferenzen zu speichern.

Animated screenshot showing a modal opening with a list of theme options to customize the appearance, including the them name, background color, text color, button text color, link color, and font.

Wir werden die Erklärung des Theme-Erstellungscodes nicht im Detail behandeln, aber es sollte einfach sein, dem Code im GitHub-Repo zu folgen. Die Hauptquellendatei ist CreateThemeContent.js und sie wird von App.js verwendet. Wir erstellen das neue Theme-Objekt, indem wir die Werte aus jedem Eingabeelement-Änderungsereignis sammeln und das Objekt zur Sammlung der Theme-Objekte hinzufügen. Das ist alles.

Bevor wir enden...

Danke fürs Lesen! Ich hoffe, Sie finden das hier Besprochene für etwas Nützliches, an dem Sie gerade arbeiten. Theming-Systeme machen Spaß! Tatsächlich machen CSS Custom Properties das immer mehr zu einem Ding. Sehen Sie sich zum Beispiel diesen Ansatz für Farben von Dieter Raber und diese Zusammenfassung von Chris an. Es gibt auch ein Setup von Michelle Barker, das auf Custom Properties mit Tailwind CSS basiert. Hier ist noch eine weitere Methode von Andrés Galente.

Wo all diese großartige Beispiele für die Erstellung von Themes sind, hoffe ich, dass dieser Artikel dazu beiträgt, dieses Konzept auf die nächste Stufe zu heben, indem Eigenschaften gespeichert, einfach zwischen Themes gewechselt, Benutzern die Möglichkeit gegeben wird, ein Theme anzupassen, und diese Präferenzen gespeichert werden.

Lassen Sie uns verbinden! Sie können mir mit Kommentaren auf Twitter eine DM senden oder mir gerne folgen.