Kürzlich, während ich meinem Kind 🧒🏻 beim Spielen kostenloser Memory-Spiele auf ihrem Tablet zuschaute, bemerkte ich, dass sie mit einer überwältigenden Anzahl von Werbeanzeigen und nervigen Pop-up-Bannern zu kämpfen hatte.

Dies inspirierte mich dazu, ein ähnliches Spiel für sie zu erstellen. Da sie derzeit Anime mag, beschloss ich, das Spiel mit niedlichen Anime-Bildern zu erstellen.

In diesem Artikel werde ich Sie durch den Prozess führen, das Spiel für sich selbst oder Ihre Kinder 🎮 zu erstellen.

Wir werden damit beginnen, die Spielmerkmale zu erkunden, dann die Technologie-Stack und das Projektstruktur abdecken – beides ist unkompliziert. Abschließend werden wir Optimierungen und die Gewährleistung eines reibungslosen Spielablaufs auf mobilen Geräten 📱 diskutieren.

Wenn Sie das Lesen überspringen möchten, ist hier 💁 das GitHub-Repository 🙌. Und hier können Sie die Live-Demo sehen.

Inhaltsverzeichnis

Projektbeschreibung

In diesem Tutorial werden wir ein anspruchsvolles Memory-Kartenspiel mit React erstellen, das Ihre Erinnerungsfähigkeiten testet. Ihr Ziel ist es, eindeutige Anime-Bilder anzuklicken, ohne dasselbe Bild zweimal anzuklicken. Jeder eindeutige Klick bringt Ihnen Punkte ein, aber seien Sie vorsichtig – wenn Sie ein Bild zweimal anklicken, wird Ihr Fortschritt zurückgesetzt.

Spielmerkmale:

  • 🎯 Dynamisches Gameplay, das Ihr Gedächtnis herausfordert

  • 🔄 Karten werden nach jedem Klick gemischt, um die Schwierigkeit zu erhöhen

  • 🏆 Punkteverfolgung mit dauerhaft gespeichertem Bestwert

  • 😺 Niedliche Anime-Bilder aus der Nekosia-API

  • ✨ Reibungslose Ladeübergänge und Animationen

  • 📱 Responsive Design für alle Geräte

  • 🎨 Saubere, moderne Benutzeroberfläche

Das Spiel wird Ihnen helfen, Ihre Gedächtnisfähigkeiten zu testen, während Sie niedliche Anime-Bilder genießen. Können Sie die perfekte Punktzahl erreichen?

So wird gespielt

  1. Klicken Sie auf eine beliebige Karte, um zu beginnen

  2. Merken Sie sich, welche Karten Sie angeklickt haben

  3. Versuchen Sie, alle Karten genau einmal anzuklicken

  4. Beobachten Sie, wie Ihre Punktzahl mit jeder einzigartigen Auswahl wächst

  5. Dann spielen Sie weiter, um zu versuchen, Ihre Bestpunktzahl zu schlagen

Die Technologie-Stack

Hier ist eine Liste der Haupttechnologien, die wir verwenden werden:

  • NPM – Ein Paketmanager für JavaScript, der hilft, Abhängigkeiten und Skripte für das Projekt zu verwalten.

  • Vite – Ein Build-Tool, das eine schnelle Entwicklungsumgebung bietet, die besonders für moderne Webprojekte optimiert ist.

  • React – Eine beliebte JavaScript-Bibliothek zur Erstellung von Benutzeroberflächen, die effizientes Rendern und Zustandsverwaltung ermöglicht.

  • CSS Modules – Eine Styling-Lösung, die CSS auf einzelne Komponenten beschränkt, um Stilkonflikte zu verhindern und die Wartbarkeit zu gewährleisten.

Let’s Build the Game

Von diesem Punkt an werde ich Sie durch den Prozess führen, den ich beim Bau dieses Spiels befolgt habe.

Projektstruktur und Architektur

Beim Bau dieses Memory-Kartenspiels habe ich den Code sorgfältig organisiert, um Wartbarkeit, Skalierbarkeit und klare Trennung der Anliegen sicherzustellen. Lassen Sie uns die Struktur und die Gründe hinter jeder Entscheidung erkunden:

Komponentenbasierte Architektur

Ich habe mich aus mehreren Gründen für eine komponentenbasierte Architektur entschieden:

  • Modularität: Jede Komponente ist eigenständig mit ihrer eigenen Logik und Styles

  • Wiederverwendbarkeit: Komponenten wie Card und Loader können in der Anwendung wiederverwendet werden

  • Wartbarkeit: Einfacheres Debuggen und Modifizieren einzelner Komponenten

  • Testen: Komponenten können isoliert getestet werden

Komponentenorganisation

  1. Card-Komponente
  • In ein eigenes Verzeichnis aufgeteilt, da es ein Kernspielelement ist

  • Enthält sowohl JSX- als auch SCSS-Module zur Kapselung

  • Behandelt individuelles Karten-Rendering, Ladezustände und Klickereignisse

  1. CardsGrid-Komponente
  • Verwaltet das Layout des Spielbretts

  • Behandelt Kartenmischen und -verteilung

  • Steuerung des responsiven Rasters für verschiedene Bildschirmgrößen

  1. Ladekomponente
  • Wiederverwendbarer Ladeindikator

  • Verbessert die Benutzererfahrung während des Bildladens

  • Kann von jedem Komponent verwendet werden, das Ladezustände benötigt

  1. Header/Footer/Untertitel-Komponenten
  • Strukturelle Komponenten für das App-Layout

  • Header zeigt den Spieltitel und Punkte an

  • Footer zeigt Copyright und Versionsinfo an

  • Untertitel liefert Spielanweisungen

CSS-Module-Ansatz

Ich habe CSS-Module (.module.scss-Dateien) aus mehreren Gründen verwendet:

  • Scoped Styling: Verhindert Stil-Lecks zwischen Komponenten

  • Name Collisions: Generiert automatisch eindeutige Klassenname
  • Wartbarkeit: Styles sind gemeinsam mit ihren Komponenten platziert

  • SCSS-Funktionen: Nutzt SCSS-Funktionen und behält dabei modulare Styles bei

Benutzerdefinierte Hooks

Das hooks-Verzeichnis enthält benutzerdefinierte Hooks wie useFetch:

  • Trennung von Anliegen: Isoliert die Datenabruflogik

  • Wiederverwendbarkeit: Kann von jeder Komponente verwendet werden, die Bilddaten benötigt

  • Status-Management: Behandelt Lade-, Fehler- und Datenstatus

  • Leistung: Implementiert Optimierungen wie die Steuerung der Bildgröße

Root-Level-Dateien

App.jsx:

  • Dient als Einstiegspunkt der Anwendung

  • Verwaltet globalen Status und Routing (falls erforderlich)

  • Koordiniert die Komponentenzusammensetzung

  • Behandelt Layouts auf höchster Ebene

Leistungsüberlegungen

Die Struktur unterstützt Leistungsoptimierungen:

  • Code-Splitting: Komponenten können bei Bedarf lazy-geladen werden

  • Memoisierung: Komponenten können effektiv memoisiert werden

  • Stil-Laden: CSS-Module ermöglichen effizientes Laden von Stilen

  • Asset-Verwaltung: Bilder und Ressourcen sind ordnungsgemäß organisiert

Skalierbarkeit

Diese Struktur ermöglicht einfaches Skalieren:

  • Neue Funktionen können als neue Komponenten hinzugefügt werden

  • Zusätzliche Hooks können für neue Funktionalitäten erstellt werden

  • Stile bleiben wartbar, wenn die App wächst

  • Tests können auf jeder Ebene implementiert werden

Entwicklungserfahrung

Die Struktur verbessert die Entwicklererfahrung:

  • Klare Dateiorganisation

  • Intuitive Komponentenpositionen

  • Leicht zu findende und zu modifizierende spezifische Funktionen

  • Unterstützt effiziente Zusammenarbeit

Diese Architektur erwies sich als besonders wertvoll beim Optimieren des Spiels für die Tablet-Nutzung, da sie es mir ermöglichte:

  1. Leistungsengpässe leicht zu identifizieren und zu optimieren

  2. Tablet-spezifische Stile hinzuzufügen, ohne andere Geräte zu beeinflussen

  3. Ladezustände implementieren für eine bessere mobile Erfahrung

  4. Saubere Trennung zwischen Spiellogik und UI-Komponenten beibehalten

Also, lasst uns jetzt mit dem Coden beginnen.

Schritt-für-Schritt-Bauanleitung

1. Projekt Setup

Einrichten der Entwicklungsumgebung

Um mit einem sauberen React-Projekt zu beginnen, öffnen Sie Ihre Terminal-App und führen Sie die folgenden Befehle aus (Sie können Ihren Projektordner nach Belieben benennen – in meinem Fall heißt er ‚memory-card‘):

npm create vite@latest memory-card -- --template react
cd memory-card
npm install

Installieren Sie die erforderlichen Abhängigkeiten

Die einzigen Abhängigkeiten, die wir in diesem Projekt verwenden werden, sind das Hook-Paket von UI.dev (übrigens finden Sie hier einen gut erklärten Artikel darüber, wie das Rendern in React funktioniert).

Die andere Abhängigkeit ist der bekannte CSS-Präprozessor, SASS, den wir benötigen, um unsere CSS-Module in SASS anstelle von regulärem CSS schreiben zu können.

npm install @uidotdev/usehooks sass

Konfigurieren Sie Vite und Projekt-Einstellungen

Bei der Einrichtung unseres Projekts müssen wir einige spezifische Konfigurationsanpassungen vornehmen, um SASS-Warnungen zu handhaben und unser Entwicklungserlebnis zu verbessern. So können Sie Vite konfigurieren:

// vite.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/setupTests.js'],
    css: {
      modules: {
        classNameStrategy: 'non-scoped'
      }
    },
    preprocessors: {
      '**/*.scss': 'sass'
    },
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/setupTests.js',
        'src/main.jsx',
        'src/vite-env.d.ts',
      ],
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        quietDeps: true,  // Unterdrückt SASS-Abhängigkeitswarnungen
        charset: false    // Verhindert Charset-Warnungen in neueren SASS-Versionen
      }
    }
  }
});

Beachten Sie, dass die meisten dieser Konfigurationen automatisch für Sie generiert werden, wenn Sie das Projekt mit Vite erstellen. Hier ist, was passiert:

  1. SASS-Konfiguration:

    • quietDeps: true: Dies unterdrückt die Warnungen über veraltete Abhängigkeiten in SASS-Modulen. Besonders nützlich beim Arbeiten mit SASS/SCSS-Dateien von Drittanbietern.

    • charset: false: Verhindert die „@charset“-Warnung, die in neueren Versionen von SASS erscheint, wenn Sonderzeichen in Ihren Stylesheets verwendet werden.

  2. Testkonfiguration:

    • globals: true: Macht Testfunktionen global in Testdateien verfügbar

    • environment: 'jsdom': Stellt eine DOM-Umgebung für Tests bereit

    • setupFiles: Verweist auf unsere Test-Setup-Datei

Diese Konfigurationen helfen, eine sauberere Entwicklungserfahrung zu schaffen, indem unnötige Warnmeldungen in der Konsole entfernt, geeignete Testumgebungs-Konfigurationen eingerichtet und sichergestellt wird, dass die SASS/SCSS-Verarbeitung reibungslos funktioniert

Sie sehen möglicherweise Warnungen in Ihrer Konsole ohne diese Konfigurationen, wenn:

  • SASS/SCSS-Funktionen verwendet werden oder SASS-Dateien importiert werden
  • Tests ausgeführt werden, die DOM-Manipulation erfordern
  • Spezielle Zeichen in Ihren Stylesheets verwendet werden

2. Erstellen der Komponenten

Erstellen Sie die Kartenkomponente

Zunächst erstellen wir unsere grundlegende Kartenkomponente, die einzelne Bilder anzeigt:

// src/components/Card/Card.jsx
import React, { useState, useCallback } from "react";
import Loader from "../Loader";
import styles from "./Card.module.scss";

const Card = React.memo(function Card({ imgUrl, imageId, categoryName, processTurn }) {
  const [isLoading, setIsLoading] = useState(true);

  const handleImageLoad = useCallback(() => {
    setIsLoading(false);
  }, []);

  const handleClick = useCallback(() => {
    processTurn(imageId);
  }, [processTurn, imageId]);

  return (
    <div className={styles.container} onClick={handleClick}>
      {isLoading && (
        <div className={styles.loaderContainer}>
          <Loader message="Loading..." />
        </div>
      )}
      <img
        src={imgUrl}
        alt={categoryName}
        onLoad={handleImageLoad}
        className={`${styles.image} ${isLoading ? styles.hidden : ''}`}
      />
    </div>
  );
});

export default Card;

Die Kartenkomponente ist ein grundlegender Baustein unseres Spiels. Sie ist dafür verantwortlich, einzelne Bilder anzuzeigen und Spielerinteraktionen zu handhaben. Lassen Sie uns die Implementierung aufschlüsseln:

Requisiten Aufschlüsselung:

  1. image: (string)

    • Die URL des Bildes, das aus unserem API-Service erhalten wird und angezeigt werden soll.

    • Wird direkt im src-Attribut des img-Tags verwendet.

  2. id: (string)

    • Eindeutiger Bezeichner für jede Karte, der entscheidend ist, um zu verfolgen, welche Karten angeklickt wurden.

    • Wird an den processTurn-Rückruf übergeben, wenn eine Karte angeklickt wird.

  3. Kategorie: (string)

    • Beschreibt den Typ des Bildes (zum Beispiel „Anime“, „Neko“) und wird im alt-Attribut für eine bessere Zugänglichkeit verwendet.

    • Hilft bei SEO und Bildschirmlesern.

  4. processTurn: (Funktion)

    • Rückruffunktion, die vom übergeordneten Komponenten übergeben wird und die Spiellogik behandelt, wenn eine Karte angeklickt wird.

    • Sie verwaltet auch Score-Updates und Spielstatusänderungen und bestimmt, ob eine Karte zuvor angeklickt wurde.

  5. isLoading: (Boolean)

    • Steuerung, ob ein Ladezustand angezeigt werden soll. Wenn true, wird anstelle des Bildes eine Loader-Komponente angezeigt.

    • Es verbessert die Benutzererfahrung während des Bildladens.

Komponentenstil:

// src/components/Card/Card.module.scss
.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(255, 255, 255, 0.8);
  border: 1px solid rgba(0, 0, 0, 0.8);
  padding: 20px;
  font-size: 30px;
  text-align: center;
  min-height: 200px;
  position: relative;
  cursor: pointer;
  transition: transform 0.2s ease;

  &:hover {
    transform: scale(1.02);
  }

  .image {
    width: 10rem;
    height: auto;
    opacity: 1;
    transition: opacity 0.3s ease;

    &.hidden {
      opacity: 0;
    }
  }

  .loaderContainer {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
}

Verwendung in der Komponente:

<Card
    key={getKey()}
    imgUrl={item?.image?.original?.url || ""}
    imageId={item?.id}
    categoryName={item?.category}
    processTurn={(imageId) => processTurn(imageId)} 
/>

Hauptmerkmale:

  1. Leistungsoptimierung:

    • Verwendet React.memo, um unnötige Neurenderings zu verhindern

    • Implementiert useCallback für Ereignishandler

    • Verwaltet den Ladezustand intern für eine bessere Benutzererfahrung

  2. Ladezustandsverwaltung:

    • Interner isLoading-Zustand verfolgt das Laden des Bildes

    • Zeigt eine Loader-Komponente mit einer Nachricht während des Ladens an

    • Blendet das Bild aus, bis es vollständig geladen ist, unter Verwendung von CSS-Klassen

  3. Ereignisbehandlung:

    • handleImageLoad: Verwaltet den Übergang des Ladezustands

    • handleClick: Verarbeitet die Züge der Spieler über den processTurn Callback

Erstelle die CardsGrid-Komponente

Dies ist unsere Hauptspielkomponente, die den Spielzustand, die Punktelogik und die Karteninteraktionen verwaltet. Lassen Sie uns die Implementierung aufschlüsseln:


// src/components/CardsGrid/CardsGrid.jsx
import React, { useState, useEffect } from "react";
import { useLocalStorage } from "@uidotdev/usehooks";
import Card from "../Card";
import Loader from "../Loader";
import styles from "./CardsGrid.module.scss";
import useFetch from "../../hooks/useFetch";

function CardsGrid(data) {
  // Zustandsverwaltung
  const [images, setImages] = useState(data?.data?.images || []);
  const [clickedImages, setClickedImages] = useLocalStorage("clickedImages", []);
  const [score, setScore] = useLocalStorage("score", 0);
  const [bestScore, setBestScore] = useLocalStorage("bestScore", 0);
  const [isLoading, setIsLoading] = useState(!data?.data?.images?.length);

  // Benutzerdefinierter Hook zum Abrufen von Bildern
  const { data: fetchedData, fetchData, error } = useFetch();

  // Bilder aktualisieren, wenn neue Daten abgerufen werden
  useEffect(() => {
    if (fetchedData?.images) {
      setImages(fetchedData.images);
      setIsLoading(false);
      // Zurücksetzen angeklickter Bilder bei Laden neuer Stapel
      setClickedImages([]);
    }
  }, [fetchedData]);

  // Hilfsfunktion zum Aktualisieren des besten Punktestands
  function updateBestScore(currentScore) {
    if (currentScore > bestScore) {
      setBestScore(currentScore);
    }
  }

  // Kernspiellogik
  function processTurn(imageId) {
    const newClickedImages = [...clickedImages, imageId];
    setClickedImages(newClickedImages);

    // Bei zweimaligem Klicken auf dasselbe Bild alles zurücksetzen
    if (clickedImages.includes(imageId)) {
      // Den besten Punktestand bei Bedarf aktualisieren
      updateBestScore(score);

      setClickedImages([]);
      setScore(0);
    } else {
      // Erfolgreiche Kartenauswahl behandeln
      const newScore = score + 1;
      setScore(newScore);

      // Auf perfekten Punktestand prüfen (alle Karten einmal angeklickt)
       if (newClickedImages.length === images.length) {
        updateBestScore(newScore);
        fetchData();
        setClickedImages([]);
      } else {
        // Bilder mischen
        const shuffled = [...images].sort(() => Math.random() - 0.5);
        setImages(shuffled);
      }
    }
  }

 if (error) {
    return <p>Failed to fetch data</p>;
  }

  if (isLoading) {
    return <Loader message="Loading new images..." />;
  }

  return (
    <div className={styles.container}>
      {images.map((item) => (
        <Card
          key={getKey()}
          imgUrl={item?.image?.original?.url || ""}
          imageId={item?.id}
          categoryName={item?.category}
          processTurn={(imageId) => processTurn(imageId)}
        />
      ))}
    </div>
  );
}

export default React.memo(CardsGrid);

Komponenten-Styling:

.container {
  display: grid;
  gap: 1rem 1rem;
  grid-template-columns: auto; /* Standard: eine Spalte für mobile Geräte */
  background-color: #2196f3;
  padding: 0.7rem;
  cursor: pointer;
}

@media (min-width: 481px) {
  .container {
    grid-template-columns: auto auto; /* Zwei Spalten für Tablets und größere Geräte */
  }
}

@media (min-width: 769px) {
  .container {
    grid-template-columns: auto auto auto; /* Drei Spalten für Desktops und größere Bildschirme */
  }
}

Übersicht der wichtigsten Funktionen:

  1. Zustandsverwaltung:

    • Verwendet useState für den Zustand auf Komponentenebene

    • Implementiert useLocalStorage für persistente Spieldaten:

      • clickedImages: Verfolgt, welche Karten angeklickt wurden

      • score: Aktuelle Spielpunktzahl

      • bestScore: Höchste erreichte Punktzahl

    • Verwaltet Ladezustand für das Abrufen von Bildern

    • Mischt die Karten

  2. Spiellogik:

    • processTurn: Behandelt die Spielzüge der Spieler

      • Verfolgt doppelte Klicks

      • Aktualisiert Punktestände

      • Verwaltet Szenarien mit perfekten Punktzahlen

    • updateBestScore: Aktualisiert den Highscore bei Bedarf

    • Holt automatisch neue Bilder ab, wenn eine Runde abgeschlossen ist

  3. Datenabruf:

    • Verwendet benutzerdefinierten useFetch-Hook für Bilddaten

    • Behandelt Lade- und Fehlerzustände

    • Aktualisiert Bilder, wenn neue Daten abgerufen werden

  4. Leistungsoptimierung:

    • Komponente in React.memo eingehüllt

    • Effiziente Zustandsaktualisierungen

    • Reaktionsfähiges Rasterlayout

  5. Beständigkeit:

    • Spielzustand bleibt erhalten, auch nachdem die Seite neu geladen wurde

    • Verfolgung des besten Ergebnisses

    • Aktuelles Spielfortschritt speichern

Beispiel für die Verwendung:

...
...

function App() {
  const { data, loading, error } = useFetch();

  if (loading) return <Loader />;
  if (error) return <p>Error: {error}</p>;

  return (
    <div className={styles.container}>
      <Header />
      <Subtitle />
      <CardsGrid data={data} />
      <Footer />
    </div>
  );
}
export default App;

Die CardsGrid-Komponente bildet das Herzstück unseres Memory-Kartenspiels und verwaltet:

  • Spielzustand und Logik

  • Punkteverfolgung

  • Karteninteraktionen

  • Bild laden und anzeigen

  • Responsive Layout

  • Datenpersistenz

Diese Implementierung bietet ein reibungsloses Spielerlebnis, während die Codelesbarkeit und Wartbarkeit durch klare Trennung der Anliegen und ordnungsgemäßes Zustandsmanagement gewährleistet werden.

3. Implementierung der API-Schicht

Unser Spiel verwendet eine robuste API-Schicht mit mehreren Ausfalloptionen, um eine zuverlässige Bildlieferung zu gewährleisten. Lassen Sie uns jeden Dienst und den Ausfallmechanismus implementieren.

Richten Sie den primären API-Dienst ein:

// src/services/api/nekosiaApi.js
const NEKOSIA_API_URL = "https://api.nekosia.cat/api/v1/images/catgirl";

export async function fetchNekosiaImages() {
  const response = await fetch(
    `${NEKOSIA_API_URL}?count=21&additionalTags=white-hair,uniform&blacklistedTags=short-hair,sad,maid&width=300`
  );

  if (!response.ok) {
    throw new Error(`Nekosia API error: ${response.status}`);
  }

  const result = await response.json();

  if (!result.images || !Array.isArray(result.images)) {
    throw new Error('Invalid response format from Nekosia API');
  }

  const validImages = result.images.filter(item => item?.image?.original?.url);

  if (validImages.length === 0) {
    throw new Error('No valid images received from Nekosia API');
  }

  return { ...result, images: validImages };
}

Erstellen Sie den ersten Ausfall-API-Dienst:

// src/services/api/nekosBestApi.js
const NEKOS_BEST_API_URL = "https://nekos.best/api/v2/neko?amount=21";

export async function fetchNekosBestImages() {
  const response = await fetch(NEKOS_BEST_API_URL, {
    method: "GET",
    mode: "no-cors"
  });

  if (!response.ok) {
    throw new Error(`Nekos Best API error: ${response.status}`);
  }

  const result = await response.json();

  // Transformieren Sie die Antwort, um unserem erwarteten Format zu entsprechen
  const transformedImages = result.results.map(item => ({
    id: item.url.split('/').pop().split('.')[0], // Extrahieren Sie die UUID aus der URL
    image: {
      original: {
        url: item.url
      }
    },
    artist: {
      name: item.artist_name,
      href: item.artist_href
    },
    source: item.source_url
  }));

  return { images: transformedImages };
}

Erstellen Sie den zweiten Ausfall-API-Dienst:

// src/services/api/nekosApi.js
const NEKOS_API_URL = "https://api.nekosapi.com/v3/images/random?limit=21&rating=safe";

export async function fetchNekosImages() {
  const response = await fetch(NEKOS_API_URL, {
    method: "GET",
  });

  if (!response.ok) {
    throw new Error(`Nekos API error: ${response.status}`);
  }

  const result = await response.json();

  // Transformieren Sie die Antwort, um unserem erwarteten Format zu entsprechen
  const transformedImages = result.items.map(item => ({
    id: item.id,
    image: {
      original: {
        url: item.image_url
      }
    }
  }));

  return { images: transformedImages };
}

Erstellen Sie den API-Ausweichmechanismus:

// src/services/api/imageService.js
import { fetchNekosiaImages } from "./nekosiaApi";
import { fetchNekosImages } from "./nekosApi";
import { fetchNekosBestImages } from "./nekosBestApi";

export async function fetchImages() {
  try {
    // Versuchen Sie zuerst die primäre API
    return await fetchNekosiaImages();
  } catch (error) {
    console.warn("Primary API failed, trying fallback:", error);

    // Versuchen Sie die erste Ausweich-API
    try {
      return await fetchNekosBestImages();
    } catch (fallbackError) {
      console.warn("First fallback API failed, trying second fallback:", fallbackError);

      // Versuchen Sie die zweite Ausweich-API
      try {
        return await fetchNekosImages();
      } catch (secondFallbackError) {
        console.error("All image APIs failed:", secondFallbackError);
        throw new Error("All image APIs failed");
      }
    }
  }
}

Verwenden Sie den Bilderdienst:

// src/hooks/useFetch.js
import { useState, useEffect } from "react";
import { fetchImages } from "../services/api/imageService";

export default function useFetch() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    setLoading(true);
    setError(null);

    try {
      const result = await fetchImages();
      setData(result);
    } catch (err) {
      setError(err.message || 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return {
    data,
    loading,
    error,
    fetchData,
  };
}

Schlüsselfunktionen unserer API-Implementierung:

  1. Mehrere API-Quellen:

    • Primäre API (Nekosia): Bietet hochwertige Anime-Bilder

    • Erste Ausweich-API (Nekos Best): Enthält Künstlerinformationen

    • Zweite Ausweich-API (Nekos): Einfache und zuverlässige Sicherung

  2. Konsistentes Datenformat:

    • Alle APIs transformieren ihre Antworten, um unserem erwarteten Format zu entsprechen:
    {
      images: [
        {
          id: string,
          image: {
            original: {
              url: string
            }
          }
        }
      ]
    }
  1. Robuste Fehlerbehandlung:

    • Überprüft API-Antworten

    • Überprüft gültige Bild-URLs

    • Bietet detaillierte Fehlermeldungen

    • Anmutiger Ausweichmechanismus

  2. Sicherheitsfunktionen:

    • Sichere Inhaltsfilterung (rating=safe)

    • Begrenzung der Bildanzahl (21 Bilder)

    • URL-Validierung

    • Validierung des Antwortformats

  3. Leistungsüberlegungen:

    • Optimierte Bildgrößen

    • Gefilterte Inhalts-Tags

    • Effiziente Datenumwandlung

    • Minimale API-Aufrufe

Diese Implementierung stellt sicher, dass unser Spiel eine zuverlässige Quelle für Bilder hat und mögliche API-Ausfälle souverän behandelt. Das einheitliche Datenformat aller APIs erleichtert es, zwischen ihnen zu wechseln, ohne die Funktionalität des Spiels zu beeinträchtigen.

App-Testen

Tests sind ein wesentlicher Bestandteil jeder Anwendungs­entwicklung, und für unser Memory Card Game haben wir eine umfassende Teststrategie mit modernen Tools und Praktiken implementiert. Lassen Sie uns untersuchen, wie wir unsere Tests strukturiert haben und einige wichtige Testmuster, die wir verwendet haben.

Test-Stack

  • Vitest: Unser primäres Testframework, ausgewählt wegen seiner Geschwindigkeit und nahtlosen Integration mit Vite

  • React Testing Library: Zum Testen von React-Komponenten mit einem nutzerzentrierten Ansatz

  • @testing-library/user-event: Zum Simulieren von Benutzerinteraktionen

  • jsdom: Zum Erstellen einer DOM-Umgebung in unseren Tests

Schlüsseltestmuster

Tests waren ein entscheidender Teil, um die Zuverlässigkeit und Wartbarkeit dieses Memory Card-Spiels sicherzustellen. Ich habe eine umfassende Teststrategie unter Verwendung von React Testing Library und Vitest implementiert, wobei ich mich auf mehrere Schlüsselbereiche konzentriert habe:

1. Komponententests

Ich habe umfangreiche Tests für meine React-Komponenten geschrieben, um sicherzustellen, dass sie korrekt gerendert werden und sich wie erwartet verhalten. Zum Beispiel hat die CardsGrid-Komponente, die das Herzstück des Spiels ist, eine gründliche Testabdeckung, einschließlich:

  • Anfangszustände des Renderings

  • Ladezustände

  • Fehlerbehandlung

  • Punkteverfolgung

  • Verhalten bei der Karteninteraktion

2. Test-Mocking

Um zuverlässige und schnelle Tests sicherzustellen, habe ich verschiedene Mocking-Strategien implementiert:

  • Operationen des lokalen Speichers unter Verwendung des useLocalStorage-Hooks

  • API-Aufrufe unter Verwendung des useFetch-Hooks

  • Ereignisbehandler und Zustandsaktualisierungen

3. Best Practices für Tests

Während meiner Testimplementierung habe ich mehrere bewährte Praktiken befolgt:

  • Verwendung der beforeEach– und afterEach-Hooks zum Zurücksetzen des Zustands zwischen den Tests

  • Testen von Benutzerinteraktionen mit fireEvent von React Testing Library

  • Schreiben von Tests, die der Interaktion der Benutzer mit der App ähneln

  • Testen sowohl von Erfolgs- als auch von Fehlerszenarien

  • Isolierung von Tests unter Verwendung von ordnungsgemäßem Mocking

4. Testwerkzeuge

Das Projekt nutzt moderne Testwerkzeuge und Bibliotheken:

  • Vitest: Als Testrunner

  • React Testing Library: Zum Testen von React-Komponenten

  • @testing-library/jest-dom: Für erweiterte DOM-Testaussagen

  • @testing-library/user-event: Zum Simulieren von Benutzerinteraktionen

Dieser umfassende Testansatz hat mir geholfen, Fehler frühzeitig zu erkennen, die Codequalität zu gewährleisten und Refactoring sicherer und einfacher zu machen.

Optimierungen

Um eine reibungslose Leistung zu gewährleisten, insbesondere auf mobilen Geräten, haben wir verschiedene Optimierungstechniken implementiert:

  1. Antworttransformation

    • Standardisiertes Datenformat für alle APIs

    • Effiziente Extraktion von IDs aus URLs

    • Strukturierte Bildmetadaten für schnellen Zugriff

  2. Netzwerkoptimierung

    • Verwendung des Modus no-cors zur effizienten Behandlung von CORS-Problemen, wo angebracht

    • Fehlerbehandlung mit spezifischen Statuscodes für eine bessere Fehlerbehebung

    • Konsistente Antwortstruktur für alle API-Implementierungen

  3. Mobile-First-Überlegungen

    • Optimierte Bildlade-Strategie

    • Effiziente Fehlerbehandlung zur Vermeidung unnötiger Wiederholungen

    • Vereinfachte Datenumwandlung zur Reduzierung der Verarbeitungsüberhead

Zukünftige Verbesserungen

Es gibt einige Möglichkeiten, wie wir dieses Projekt weiter verbessern könnten:

  1. API-Antwort-Caching

    • Implementierung von lokalem Speicher-Caching für häufig verwendete Bilder

    • Hinzufügen einer Cache-Invalidierungsstrategie für frische Inhalte

    • Implementierung des progressiven Bildladens

  2. Leistungsverbesserungen

    • Fügen Sie das verzögerte Laden von Bildern für eine bessere anfängliche Ladezeit hinzu

    • Implementieren Sie Warteschlangen für eine bessere Bandbreitenverwaltung

    • Fügen Sie die Antwortkomprimierung für einen schnelleren Datentransfer hinzu

  3. Zuverlässigkeitsverbesserungen

    • Fügen Sie die API-Health-Checks vor den Versuchen hinzu

    • Implementieren Sie Wiederholungsmechanismen mit exponentiellem Backoff

    • Fügen Sie das Schaltungsschaltermuster für fehlerhafte APIs hinzu

  4. Analytics und Überwachung

    • Verfolgen Sie die Erfolgsrate der APIs

    • Überwachen Sie die Antwortzeiten

    • Implementieren Sie automatisches Umschalten der APIs basierend auf Leistungsmetriken

Diese robuste Implementierung gewährleistet, dass unser Spiel auch unter ungünstigen Netzwerkbedingungen oder nicht verfügbaren APIs funktionsfähig und leistungsfähig bleibt, während gleichzeitig Raum für zukünftige Verbesserungen und Optimierungen bleibt.

Fazit

Das Erstellen dieses Memory Card-Spiels war mehr als nur die Schaffung einer unterhaltsamen, werbefreien Alternative für Kinder – es war eine Übung in der Implementierung moderner bewährter Praktiken für die Webentwicklung bei der Lösung eines realen Problems.

Das Projekt zeigt, wie durch die Kombination einer durchdachten Architektur, umfangreichen Tests und zuverlässigen Fallback-Mechanismen eine produktionsbereite Anwendung entstehen kann, die sowohl unterhaltsam als auch lehrreich ist.

🗝️ Wichtige Erkenntnisse

  1. Benutzerzentrierte Entwicklung

    • Begann mit einem klaren Problem (werbefinanzierte Spiele beeinträchtigen das Benutzererlebnis)

    • Implementierte Funktionen, die das Gameplay ohne Unterbrechungen verbessern

    • Fokus auf Leistung und Zuverlässigkeit über Geräte hinweg beibehalten

  2. Technische Exzellenz

    • Nutzte moderne React-Pattern und Hooks für sauberen, wartbaren Code

    • Implementierte eine umfassende Teststrategie zur Sicherstellung der Zuverlässigkeit

    • Schuf ein robustes API-Backup-System für unterbrechungsfreies Gameplay

  3. Leistung zuerst

    • Mobile-First-Ansatz mit responsivem Design übernommen

    • Optimiertes Laden und Handhaben von Bildern

    • Effiziente Zustandsverwaltung und Zwischenspeicherungsstrategien implementiert

📚 Lernerfolge

Dieses Projekt zeigt, wie scheinbar einfache Spiele hervorragende Möglichkeiten bieten, komplexe technische Lösungen umzusetzen. Von der Komponentenarchitektur bis hin zu API-Ausfällen wurde jede Funktion unter Berücksichtigung von Skalierbarkeit und Wartbarkeit entwickelt, was zeigt, dass auch Hobbyprojekte professionelle Codequalität aufrechterhalten können.

🔮 Ausblick

Obwohl das Spiel erfolgreich sein Hauptziel erreicht, nämlich ein werbefreies und unterhaltsames Erlebnis zu bieten, zeigen die dokumentierten zukünftigen Verbesserungen einen klaren Fahrplan für die Weiterentwicklung. Ob es um die Implementierung zusätzlicher Optimierungen oder das Hinzufügen neuer Funktionen geht, das Fundament ist solide und bereit für die Erweiterung.

Das Memory Card Spiel ist ein Zeugnis dafür, wie persönliche Projekte sowohl reale Probleme lösen als auch als Plattformen für die Umsetzung bewährter Praktiken in der modernen Webentwicklung dienen können. Fühlen Sie sich frei, den Code zu erkunden, beizutragen oder ihn als Inspiration für Ihre eigenen Projekte zu nutzen!