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
-
Clicca su qualsiasi carta per iniziare
-
Ricorda quali carte hai cliccato
-
Prova a cliccare tutte le carte esattamente una volta
-
Guarda il tuo punteggio crescere con ogni selezione unica
-
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
eLoader
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
- 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
- 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
- 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
- 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:
-
Identificare e ottimizzare facilmente i colli di bottiglia delle prestazioni
-
Aggiungere stili specifici per tablet senza influenzare altri dispositivi
-
Implementare stati di caricamento per una migliore esperienza mobile
-
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:
-
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.
-
-
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à:
-
image
: (string)-
L’URL dell’immagine da visualizzare ricevuta dal nostro servizio API.
-
È utilizzato direttamente nell’attributo src del tag img.
-
-
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.
-
-
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.
-
-
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.
-
-
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:
-
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
-
-
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
-
-
Gestione degli eventi:
-
handleImageLoad
: Gestisce la transizione dello stato di caricamento -
handleClick
: Elabora le mosse del giocatore tramite il callbackprocessTurn
-
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:
-
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
-
-
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
-
-
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
-
-
Ottimizzazione delle prestazioni:
-
Componente avvolto in
React.memo
-
Aggiornamenti di stato efficienti
-
Layout a griglia reattivo
-
-
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:
-
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
-
-
Formato dati coerente:
- Tutte le API trasformano le loro risposte per adattarsi al nostro formato previsto:
{
images: [
{
id: string,
image: {
original: {
url: string
}
}
}
]
}
-
Gestione degli Errori Robusta:
-
Valida le risposte API
-
Controlla la validità degli URL delle immagini
-
Fornisce messaggi di errore dettagliati
-
Meccanismo di fallback elegante
-
-
Caratteristiche di Sicurezza:
-
Filtraggio dei contenuti sicuri (
rating=safe
) -
Limitazione del numero di immagini (21 immagini)
-
Validazione degli URL
-
Validazione del formato di risposta
-
-
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
eafterEach
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:
-
Trasformazione della Risposta
-
Formato dati standardizzato in tutte le API
-
Estrazione efficiente degli ID dagli URL
-
Metadata delle immagini strutturati per un accesso rapido
-
-
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
-
-
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:
-
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
-
-
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
-
-
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
-
-
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
-
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
-
-
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
-
-
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!
Source:
https://www.freecodecamp.org/news/how-to-build-a-memory-card-game-using-react/