Di recente, mentre guardavo mia figlia 🧒🏻 giocare a giochi di memoria gratuiti sul suo tablet, ho notato che si trovava in difficoltà a causa di un numero travolgente di annunci e fastidiosi banner pop-up.

Questo mi ha ispirato a costruire un gioco simile per lei. Poiché attualmente è appassionata di anime, ho deciso di creare il gioco utilizzando immagini carine in stile anime.

In questo articolo, ti guiderò attraverso il processo di costruzione del gioco per te stesso o per i tuoi bambini 🎮.

Inizieremo esplorando le caratteristiche del gioco, poi tratteremo la tecnologia e la struttura del progetto, entrambe semplici. Infine, discuteremo delle ottimizzazioni e di come garantire un gameplay fluido sui dispositivi mobili 📱.

Se vuoi saltare la lettura, qui 💁 c’è il repository GitHub 🙌. E qui puoi vedere la demo dal vivo.

Indice

Descrizione del Progetto

In questo tutorial, costruiremo un impegnativo gioco di carte memory con React che mette alla prova le tue capacità di richiamo. Il tuo obiettivo è cliccare immagini uniche di anime senza cliccare la stessa due volte. Ogni clic unico ti guadagna punti, ma fai attenzione: cliccare un’immagine due volte resetta i tuoi progressi.

Caratteristiche del gioco:

  • 🎯 Gioco dinamico che sfida la tua memoria

  • 🔄 Le carte si mescolano dopo ogni clic per aumentare la difficoltà

  • 🏆 Monitoraggio del punteggio con persistenza del punteggio migliore

  • 😺 Immagini anime adorabili dall’API di Nekosia

  • ✨ Transizioni e animazioni di caricamento fluide

  • 📱 Design reattivo per tutti i dispositivi

  • 🎨 Interfaccia utente pulita e moderna

Il gioco ti aiuterà a testare le tue abilità di memoria mentre ti godi immagini anime carine. Riuscirai a ottenere il punteggio perfetto?

Come giocare

  1. Clicca su qualsiasi carta per iniziare

  2. Ricorda quali carte hai cliccato

  3. Prova a cliccare tutte le carte esattamente una volta

  4. Guarda il tuo punteggio crescere con ogni selezione unica

  5. Poi continua a giocare per cercare di battere il tuo punteggio migliore

Il Tech Stack

Ecco un elenco delle principali tecnologie che utilizzeremo:

  • NPM – Un gestore di pacchetti per JavaScript che aiuta a gestire le dipendenze e gli script per il progetto.

  • Vite – Uno strumento di build che fornisce un ambiente di sviluppo veloce, particolarmente ottimizzato per progetti web moderni.

  • React – Una popolare libreria JavaScript per la creazione di interfacce utente, che consente un rendering efficiente e una gestione dello stato.

  • CSS Modules – Una soluzione di styling che limita il CSS a componenti individuali, prevenendo conflitti di stile e garantendo la manutenibilità.

Costruire il Gioco

Da questo punto in poi, ti guiderò attraverso il processo che ho seguito per costruire questo gioco.

Struttura e Architettura del Progetto

Quando ho costruito questo gioco di memoria, ho organizzato attentamente la base di codice per garantire manutenibilità, scalabilità e chiara separazione delle preoccupazioni. Esploriamo la struttura e il ragionamento dietro ogni decisione:

Architettura Basata su Componenti

Ho scelto un’architettura basata su componenti per diversi motivi:

  • Modularità: Ogni componente è autonomo con la propria logica e stili

  • Riutilizzabilità: Componenti come Card e Loader possono essere riutilizzati nell’applicazione

  • Manutenibilità: Più facile eseguire il debug e modificare componenti singoli

  • Testing: I componenti possono essere testati in isolamento

Organizzazione dei Componenti

  1. Componente Card
  • Separato nella propria directory perché è un elemento centrale del gioco

  • Contiene sia moduli JSX che SCSS per l’incapsulamento

  • Gestisce il rendering delle singole carte, gli stati di caricamento e gli eventi di clic

  1. Componente CardsGrid
  • Gestisce il layout della plancia di gioco

  • Gestisce la mescolanza e la distribuzione delle carte

  • Controlla il layout della griglia reattiva per diverse dimensioni dello schermo

  1. Componente Loader
  • Indicatore di caricamento riutilizzabile

  • Migliora l’esperienza utente durante il caricamento delle immagini

  • Può essere utilizzato da qualsiasi componente che necessita di stati di caricamento

  1. Componenti Intestazione/Piede/Sottotitolo
  • Componenti strutturali per il layout dell’app

  • L’intestazione visualizza il titolo del gioco e i punteggi

  • Il piè di pagina mostra le informazioni sul copyright e sulla versione

  • Il sottotitolo fornisce istruzioni per il gioco

Approccio ai Moduli CSS

Ho utilizzato i Moduli CSS (.module.scss file) per diversi vantaggi:

  • Stile Scoperto: Previene perdite di stile tra i componenti

  • Collisioni di Nome: Genera automaticamente nomi di classe unici

  • Mantenibilità: Gli stili sono co-locati con i loro componenti

  • Caratteristiche SCSS: Sfrutta le caratteristiche di SCSS mantenendo gli stili modulari

Hook Personalizzati

La directory hooks contiene hook personalizzati come useFetch:

  • Separazione delle Preoccupazioni: Isola la logica di recupero dei dati

  • Riutilizzabilità: Può essere utilizzato da qualsiasi componente che necessiti di dati di immagine

  • Gestione dello Stato: Gestisce stati di caricamento, errore e dati

  • Prestazioni: Implementa ottimizzazioni come il controllo delle dimensioni delle immagini

File di Livello Radice

App.jsx:

  • Funziona come punto di ingresso dell’applicazione

  • Gestisce lo stato globale e il routing (se necessario)

  • Coordina la composizione dei componenti

  • Gestisce i layout di alto livello

Considerazioni sulle prestazioni

La struttura supporta ottimizzazioni delle prestazioni:

  • Code Splitting: I componenti possono essere caricati in modo lazy se necessario

  • Memoization: I componenti possono essere memorizzati efficacemente

  • Caricamento degli stili: I CSS Modules consentono un caricamento efficiente degli stili

  • Gestione delle risorse: Le immagini e le risorse sono organizzate correttamente

Scalabilità

Questa struttura consente una facile scalabilità:

  • Nuove funzionalità possono essere aggiunte come nuovi componenti

  • Hook aggiuntivi possono essere creati per nuove funzionalità

  • Gli stili rimangono gestibili man mano che l’app cresce

  • Il testing può essere implementato a qualsiasi livello

Esperienza di Sviluppo

La struttura migliora l’esperienza dello sviluppatore:

  • Chiarezza nell’organizzazione dei file

  • Posizioni dei componenti intuitive

  • Facilità di trovare e modificare funzionalità specifiche

  • Supporta una collaborazione efficiente

Questa architettura si è rivelata particolarmente utile durante l’ottimizzazione del gioco per l’uso su tablet, in quanto mi ha permesso di:

  1. Identificare e ottimizzare facilmente i colli di bottiglia delle prestazioni

  2. Aggiungere stili specifici per tablet senza influenzare altri dispositivi

  3. Implementare stati di caricamento per una migliore esperienza mobile

  4. Mantenere una chiara separazione tra la logica di gioco e i componenti UI

Va bene, ora iniziamo a programmare.

Guida alla Costruzione Passo-Passo

1. Configurazione del Progetto

Impostare l’Ambiente di Sviluppo

Per iniziare con un progetto React pulito, apri la tua app terminale e esegui i seguenti comandi (puoi nominare la tua cartella di progetto come preferisci – nel mio caso il nome è ‘memory-card’):

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

Installa le dipendenze richieste

L’unica dipendenza che utilizzeremo in questo progetto è il pacchetto hook da UI.dev (tra l’altro, qui puoi trovare un articolo ben spiegato su come funziona il rendering in React).

L’altra dipendenza è il famoso preprocessore CSS, SASS, di cui avremo bisogno per poter scrivere i nostri moduli CSS in SASS invece del normale CSS.

npm install @uidotdev/usehooks sass

Configura Vite e le impostazioni del progetto

Quando impostiamo il nostro progetto, dobbiamo fare alcune regolazioni specifiche di configurazione per gestire gli avvisi SASS e migliorare la nostra esperienza di sviluppo. Ecco come puoi configurare Vitest:

// vitest.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,  // Silenzia gli avvisi di dipendenza SASS
        charset: false    // Previene l'avviso di charset nelle recenti versioni di SASS
      }
    }
  }
});

Ricorda che la maggior parte di queste configurazioni vengono generate automaticamente per te quando crei il progetto con Vite. Ecco cosa sta succedendo:

  1. Configurazione SASS:

    • quietDeps: true: Questo silenzia gli avvisi riguardanti le dipendenze deprecate nei moduli SASS. Particolarmente utile quando si lavora con file SASS/SCSS di terze parti.

    • charset: false: Previene l’avviso “@charset” che appare nelle versioni più recenti di SASS quando si utilizzano caratteri speciali nei fogli di stile.

  2. Configurazione Test:

    • globals: true: Rende le funzioni di test globalmente disponibili nei file di test

    • environment: 'jsdom': Fornisce un ambiente DOM per i test

    • setupFiles: Punta al nostro file di impostazione dei test

Queste configurazioni aiutano a creare un’esperienza di sviluppo più pulita rimuovendo messaggi di avviso non necessari nella console, impostando configurazioni appropriate per l’ambiente di test e garantendo che l’elaborazione SASS/SCSS funzioni senza problemi.

Potresti vedere avvisi nella tua console senza queste configurazioni quando:

  • Utilizzi funzionalità SASS/SCSS o importi file SASS

  • Esegui test che richiedono manipolazione del DOM

  • Utilizzi caratteri speciali nei tuoi fogli di stile

2. Costruire i Componenti

Crea il Componente Card

Prima di tutto, creiamo il nostro componente card di base che visualizzerà immagini singole:

// 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;

Il componente Card è un blocco fondamentale del nostro gioco. È responsabile della visualizzazione delle immagini singole e della gestione delle interazioni dei giocatori. Analizziamo la sua implementazione:

Analisi delle proprietà:

  1. image: (string)

    • L’URL dell’immagine da visualizzare ricevuta dal nostro servizio API.

    • È utilizzato direttamente nell’attributo src del tag img.

  2. id: (string)

    • Identificatore unico per ogni carta, fondamentale per tracciare quali carte sono state cliccate.

    • Viene passato al callback processTurn quando una carta viene cliccata.

  3. category: (string)

    • Descrive il tipo di immagine (ad esempio, “anime”, “neko”) ed è utilizzato nell’attributo alt per una migliore accessibilità.

    • Aiuta con SEO e lettori di schermo.

  4. processTurn: (funzione)

    • Funzione di callback passata dal componente genitore che gestisce la logica di gioco quando viene cliccata una carta.

    • Gestisce anche gli aggiornamenti del punteggio e i cambiamenti di stato del gioco e determina se una carta è stata cliccata in precedenza.

  5. isLoading: (booleano)

    • Controlla se mostrare uno stato di caricamento. Quando è vero, visualizza un componente Loader invece dell’immagine.

    • Migliora l’esperienza utente durante il caricamento dell’immagine.

Stile del componente:

// 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%);
  }
}

Utilizzo nel componente:

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

Caratteristiche principali:

  1. Ottimizzazione delle prestazioni:

    • Utilizza React.memo per prevenire re-render inutili

    • Implementa useCallback per i gestori di eventi

    • Gestisce internamente lo stato di caricamento per una migliore UX

  2. Gestione dello stato di caricamento:

    • Lo stato interno isLoading tiene traccia del caricamento dell’immagine

    • Mostra un componente Loader con un messaggio durante il caricamento

    • Nasconde l’immagine fino a quando non è completamente caricata utilizzando classi CSS

  3. Gestione degli eventi:

    • handleImageLoad: Gestisce la transizione dello stato di caricamento

    • handleClick: Elabora le mosse del giocatore tramite il callback processTurn

Costruire il componente CardsGrid

Questo è il nostro principale componente di gioco che gestisce lo stato del gioco, la logica di punteggio e le interazioni con le carte. Analizziamo la sua implementazione:


// 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) {
  // Gestione dello stato
  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);

  // Hook personalizzato per il recupero delle immagini
  const { data: fetchedData, fetchData, error } = useFetch();

  // Aggiorna le immagini quando vengono recuperati nuovi dati
  useEffect(() => {
    if (fetchedData?.images) {
      setImages(fetchedData.images);
      setIsLoading(false);
      // Resetta le immagini cliccate quando viene caricata un'altra serie
      setClickedImages([]);
    }
  }, [fetchedData]);

  // Funzione di supporto per aggiornare il punteggio migliore
  function updateBestScore(currentScore) {
    if (currentScore > bestScore) {
      setBestScore(currentScore);
    }
  }

  // Logica principale del gioco
  function processTurn(imageId) {
    const newClickedImages = [...clickedImages, imageId];
    setClickedImages(newClickedImages);

    // Se si clicca sulla stessa immagine due volte, resetta tutto
    if (clickedImages.includes(imageId)) {
      // Aggiorna il punteggio migliore se necessario
      updateBestScore(score);

      setClickedImages([]);
      setScore(0);
    } else {
      // Gestisci la selezione della carta con successo
      const newScore = score + 1;
      setScore(newScore);

      // Controlla il punteggio perfetto (tutte le carte cliccate una volta)
       if (newClickedImages.length === images.length) {
        updateBestScore(newScore);
        fetchData();
        setClickedImages([]);
      } else {
        // Mescola le immagini
        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);

Stile del componente:

.container {
  display: grid;
  gap: 1rem 1rem;
  grid-template-columns: auto; /* Predefinito: una colonna per il mobile-first */
  background-color: #2196f3;
  padding: 0.7rem;
  cursor: pointer;
}

@media (min-width: 481px) {
  .container {
    grid-template-columns: auto auto; /* Due colonne per tablet e oltre */
  }
}

@media (min-width: 769px) {
  .container {
    grid-template-columns: auto auto auto; /* Tre colonne per desktop e più grandi */
  }
}

Analisi delle funzionalità chiave:

  1. Gestione dello Stato:

    • Utilizza useState per lo stato a livello di componente

    • Implementa useLocalStorage per i dati di gioco persistenti:

      • clickedImages: Traccia quali carte sono state cliccate

      • score: Punteggio attuale del gioco

      • bestScore: Punteggio più alto ottenuto

    • Gestisce lo stato di caricamento per il recupero delle immagini

    • Mischia le carte

  2. Logica di Gioco:

    • processTurn: Gestisce le mosse dei giocatori

      • Monitora i clic duplicati

      • Aggiorna i punteggi

      • Gestisce scenari di punteggio perfetto

    • updateBestScore: Aggiorna il punteggio più alto quando necessario

    • Recupera automaticamente nuove immagini quando un turno è completato

  3. Recupero Dati:

    • Utilizza un hook personalizzato useFetch per i dati delle immagini

    • Gestisce stati di caricamento e errore

    • Aggiorna le immagini quando nuovi dati vengono recuperati

  4. Ottimizzazione delle prestazioni:

    • Componente avvolto in React.memo

    • Aggiornamenti di stato efficienti

    • Layout a griglia reattivo

  5. Persistenza:

    • Lo stato del gioco persiste attraverso i ricaricamenti della pagina

    • Tracciamento del punteggio migliore

    • Salvataggio del progresso attuale del gioco

Esempio di utilizzo:

...
...

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;

Il componente CardsGrid funge da cuore del nostro gioco di carte di memoria, gestendo:

  • Stato e logica del gioco

  • Tracciamento del punteggio

  • Interazioni delle carte

  • Caricamento e visualizzazione delle immagini

  • Layout reattivo

  • Persistenza dei dati

Questa implementazione offre un’esperienza di gioco fluida mantenendo la leggibilità e la manutenibilità del codice attraverso una chiara separazione delle preoccupazioni e una corretta gestione dello stato.

3. Implementazione del livello API

Il nostro gioco utilizza un robusto livello API con diverse opzioni di fallback per garantire una consegna affidabile delle immagini. Implementiamo ogni servizio e il meccanismo di fallback.

Imposta il servizio API principale:

// 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 };
}

Crea il primo servizio API di fallback:

// 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();

  // Trasforma la risposta per adattarla al nostro formato atteso
  const transformedImages = result.results.map(item => ({
    id: item.url.split('/').pop().split('.')[0], // Estrai UUID dall'URL
    image: {
      original: {
        url: item.url
      }
    },
    artist: {
      name: item.artist_name,
      href: item.artist_href
    },
    source: item.source_url
  }));

  return { images: transformedImages };
}

Crea il secondo servizio API di fallback:

// 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();

  // Trasforma la risposta per adattarla al nostro formato atteso
  const transformedImages = result.items.map(item => ({
    id: item.id,
    image: {
      original: {
        url: item.image_url
      }
    }
  }));

  return { images: transformedImages };
}

Costruisci il meccanismo di fallback per l’API:

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

export async function fetchImages() {
  try {
    // Prova prima l'API principale
    return await fetchNekosiaImages();
  } catch (error) {
    console.warn("Primary API failed, trying fallback:", error);

    // Prova la prima API di fallback
    try {
      return await fetchNekosBestImages();
    } catch (fallbackError) {
      console.warn("First fallback API failed, trying second fallback:", fallbackError);

      // Prova la seconda API di fallback
      try {
        return await fetchNekosImages();
      } catch (secondFallbackError) {
        console.error("All image APIs failed:", secondFallbackError);
        throw new Error("All image APIs failed");
      }
    }
  }
}

Usa il servizio immagini:

// 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,
  };
}

Caratteristiche principali della nostra implementazione API:

  1. Multiple fonti API:

    • API principale (Nekosia): Fornisce immagini anime di alta qualità

    • Primo fallback (Nekos Best): Include informazioni sugli artisti

    • Secondo fallback (Nekos): Backup semplice e affidabile

  2. Formato dati coerente:

    • Tutte le API trasformano le loro risposte per adattarsi al nostro formato previsto:
    {
      images: [
        {
          id: string,
          image: {
            original: {
              url: string
            }
          }
        }
      ]
    }
  1. Gestione degli Errori Robusta:

    • Valida le risposte API

    • Controlla la validità degli URL delle immagini

    • Fornisce messaggi di errore dettagliati

    • Meccanismo di fallback elegante

  2. Caratteristiche di Sicurezza:

    • Filtraggio dei contenuti sicuri (rating=safe)

    • Limitazione del numero di immagini (21 immagini)

    • Validazione degli URL

    • Validazione del formato di risposta

  3. Considerazioni sulle Prestazioni:

    • Dimensioni delle immagini ottimizzate

    • Tag di contenuto filtrati

    • Trasformazione dei dati efficiente

    • Chiamate API minime

Questa implementazione garantisce che il nostro gioco abbia una fonte affidabile di immagini gestendo elegantemente le potenziali interruzioni delle API. Il formato di dati coerente attraverso tutte le API rende facile passare da una all’altra senza influenzare la funzionalità del gioco.

Testare l’App

Il testing è una parte cruciale dello sviluppo di qualsiasi applicazione e, per il nostro Gioco delle Carte Memoria, abbiamo implementato una strategia di testing completa utilizzando strumenti e pratiche moderne. Esploriamo come abbiamo strutturato i nostri test e alcuni schemi di testing chiave che abbiamo utilizzato.

Stack di testing

  • Vitest: Il nostro framework di testing principale, scelto per la sua velocità e integrazione senza soluzione di continuità con Vite

  • React Testing Library: Per testare i componenti React con un approccio incentrato sull’utente

  • @testing-library/user-event: Per simulare le interazioni degli utenti

  • jsdom: Per creare un ambiente DOM nei nostri test

Modelli di Test Chiave

Il testing è stato una parte cruciale per garantire l’affidabilità e la manutenibilità di questo Memory Card Game. Ho implementato una strategia di testing completa utilizzando React Testing Library e Vitest, focalizzandomi su diverse aree chiave:

1. Test dei Componenti

Ho scritto ampi test per i miei componenti React per assicurarmi che vengano renderizzati correttamente e si comportino come previsto. Ad esempio, il componente CardsGrid, che è il cuore del gioco, ha una copertura di test approfondita che include:

  • Stati di rendering iniziali

  • Stati di caricamento

  • Gestione degli errori

  • Tracciamento del punteggio

  • Comportamento delle interazioni con le carte

2. Mocking dei Test

Per garantire test affidabili e veloci, ho implementato diverse strategie di mocking:

  • Operazioni di storage locale utilizzando l’hook useLocalStorage

  • Chiamate API utilizzando l’hook useFetch

  • Gestori degli eventi e aggiornamenti dello stato

3. Migliori pratiche di test

Durante la mia implementazione dei test, ho seguito diverse migliori pratiche:

  • Utilizzare i hook beforeEach e afterEach per ripristinare lo stato tra i test

  • Testare le interazioni dell’utente utilizzando fireEvent dalla React Testing Library

  • Scrivere test che somigliano a come gli utenti interagiscono con l’app

  • Testare sia scenari di successo che scenari di errore

  • Isolare i test utilizzando un mocking appropriato

4. Strumenti di test

Il progetto sfrutta strumenti e librerie di test moderni:

  • Vitest: Come runner di test

  • React Testing Library: Per testare i componenti React

  • @testing-library/jest-dom: Per asserzioni di test DOM avanzate

  • @testing-library/user-event: Per simulare interazioni utente

Questo approccio completo ai test mi ha aiutato a catturare bug precocemente, garantire la qualità del codice e rendere il refactoring più sicuro e gestibile.

Ottimizzazioni

Per garantire prestazioni fluide, specialmente sui dispositivi mobili, abbiamo implementato diverse tecniche di ottimizzazione:

  1. Trasformazione della Risposta

    • Formato dati standardizzato in tutte le API

    • Estrazione efficiente degli ID dagli URL

    • Metadata delle immagini strutturati per un accesso rapido

  2. Ottimizzazione della Rete

    • Utilizzo della modalità no-cors dove appropriato per gestire in modo efficiente i problemi CORS

    • Gestione degli errori con codici di stato specifici per un migliore debug

    • Struttura della risposta coerente in tutte le implementazioni API

  3. Considerazioni Mobile-First

    • Strategia di caricamento delle immagini ottimizzata

    • Gestione degli errori efficiente per prevenire tentativi non necessari

    • Trasformazione dei dati semplificata per ridurre il sovraccarico di elaborazione

Miglioramenti futuri

Ci sono alcuni modi in cui potremmo ulteriormente migliorare questo progetto:

  1. Cache delle risposte API

    • Implementare la cache di archiviazione locale per le immagini utilizzate frequentemente

    • Aggiungere una strategia di invalidazione della cache per contenuti freschi

    • Implementare il caricamento progressivo delle immagini

  2. Ottimizzazioni delle Prestazioni

    • Aggiungere il caricamento “lazy” delle immagini per un miglior tempo di caricamento iniziale

    • Implementare la coda delle richieste per una migliore gestione della larghezza di banda

    • Aggiungere la compressione delle risposte per un trasferimento dati più veloce

  3. Miglioramenti dell’Affidabilità

    • Aggiungere il controllo dello stato dell’API prima dei tentativi

    • Implementare meccanismi di ripetizione con backoff esponenziale

    • Aggiungere il pattern del circuito interruttore per API in errore

  4. Analisi e Monitoraggio

    • Monitorare i tassi di successo delle API

    • Monitorare i tempi di risposta

    • Implementare il passaggio automatico delle API basato su metriche di performance

Questa implementazione robusta assicura che il nostro gioco rimanga funzionale e performante anche in condizioni di rete avverse o in caso di indisponibilità delle API, mantenendo comunque spazio per futuri miglioramenti e ottimizzazioni.

Conclusione

Costruire questo Gioco di Carte della Memoria è stato più di creare un’alternativa divertente e priva di pubblicità per i bambini: è stata un’esercitazione nell’implementare le migliori pratiche di sviluppo web moderno mentre si risolveva un problema del mondo reale.

Il progetto dimostra come combinare un’architettura pensata, test robusti e meccanismi di fallback affidabili possa risultare in un’applicazione pronta per la produzione che sia sia intrattenente che educativa.

🗝️ Punti Chiave

  1. Sviluppo Centrico sull’Utente

    • Iniziato con un problema chiaro (giochi pieni di pubblicità che influenzano l’esperienza dell’utente)

    • Implementate funzionalità che migliorano il gameplay senza interruzioni

    • Mantenuto il focus su prestazioni e affidabilità su tutti i dispositivi

  2. Eccellenza Tecnica

    • Sfruttato modelli e hook moderni di React per un codice pulito e manutenibile

    • Implementata una strategia di testing completa per garantire l’affidabilità

    • Creata un robusto sistema di fallback API per un gameplay ininterrotto

  3. Prestazioni Prima

    • Adottato un approccio mobile-first con design reattivo

    • Ottimizzato il caricamento e la gestione delle immagini

    • Implementate strategie efficienti di gestione dello stato e caching

📚 Risultati di Apprendimento

Questo progetto dimostra come giochi apparentemente semplici possano essere eccellenti veicoli per implementare soluzioni tecniche complesse. Dall’architettura dei componenti ai fallback API, ogni funzionalità è stata costruita tenendo conto della scalabilità e della manutenibilità, dimostrando che anche i progetti amatoriali possono mantenere una qualità del codice professionale.

🔮 Guardando Avanti

Mentre il gioco raggiunge con successo il suo obiettivo primario di fornire un’esperienza senza pubblicità e piacevole, i miglioramenti futuri documentati offrono una chiara roadmap per l’evoluzione. Che si tratti di implementare ottimizzazioni aggiuntive o di aggiungere nuove funzionalità, le fondamenta sono solide e pronte per l’espansione.

Il Gioco delle Carte della Memoria è una testimonianza di come i progetti personali possano risolvere problemi reali e fungere da piattaforme per implementare le migliori pratiche nello sviluppo web moderno. Sentiti libero di esplorare il codice, contribuire o usarlo come ispirazione per i tuoi progetti!