לאחרונה, בזמן שצפה בילדתי 🧒🏻 משחקת משחקי זיכרון חינמיים בטאבלט שלה, שמתי לב שהיא מתמודדת עם מספר עצום של פרסומות ובאנרים מקישים.
זה השריע אותי לבניית משחק דומה עבורה. מאחר שהיא נמצאת בתקופה של התמכרות לאנימה, החלטתי ליצור את המשחק באמצעות תמונות בסגנון אנימה חמודות.
במאמר זה, אשתף אותך בתהליך בניית המשחק עבורך או עבור ילדיך 🎮.
נתחיל בחקירת תכונות המשחק, ולאחר מכן נכסה את הטכנולוגיות ואת מבנה הפרויקט—שניהם ישים. לבסוף, נדון באופטימיזציות ובהבטחת משחק חלק על מכשירים ניידים 📱.
אם ברצונך לדלג על הקריאה, כאן 💁 נמצא מאגר הקוד ב-GitHub 🙌. וכאן תוכל לראות את הדמו החי.
תוכן עניינים
תיאור הפרויקט
במדריך זה, נבנה משחק קלפים מאתגר עם React שבודק את יכולת הזיכרון שלך. המטרה שלך היא ללחוץ על תמונות אנימה ייחודיות מבלי ללחוץ על אותה תמונה פעמיים. כל לחיצה ייחודית מרוויחה עבורך נקודות, אך תהיה זהיר – לחיצה על תמונה פעמיים מאפסת את ההתקדמות שלך.
תכונות המשחק:
-
🎯 תהליך משחק דינמי שמאתגר את הזיכרון שלך
-
🔄 הקלפים מתערבבים לאחר כל לחיצה כדי להגביר את הקושי
-
🏆 מעקב אחר הניקוד עם שמירת הניקוד הטוב ביותר
-
😺 תמונות אנימה מקסימות מממשק ה- Nekosia API
-
✨ מעברים חלקים בזמן טעינה ואנימציות
-
📱 עיצוב רספונסיבי לכל המכשירים
-
🎨 ממשק נקי ומודרני
המשחק יעזור לך לבחון את מיומנויות הזיכרון שלך בזמן שאתה נהנה מתמונות אנימה חמודות. האם תצליח להשיג את הניקוד המושלם?
איך לשחק
-
לחץ על כל קלף כדי להתחיל
-
זכור אילו קלפים לחצת
-
נסה ללחוץ על כל הקלפים בדיוק פעם אחת
-
צפה בניקוד שלך גדל עם כל בחירה ייחודית
-
ואז המשך לשחק כדי לנסות להכריע את הניקוד הטוב ביותר שלך
הערכת הטכנולוגיות
להלן רשימה של הטכנולוגיות העיקריות שנשתמש בהן:
-
NPM – מנהל חבילות עבור JavaScript שעוזר לנהל תלות ותסריטים עבור הפרויקט.
-
Vite – כלי בנייה שמספק סביבה לפיתוח מהיר, במיוחד מותאם לפרויקטים מודרניים באינטרנט.
-
React – ספריית JavaScript פופולרית לבניית ממשקי משתמש, מאפשרת רינדור יעיל וניהול מצב.
-
CSS Modules – פתרון עיצוב שמגביל את ה-CSS לרכיבים בודדים, מונע קונפליקטים בעיצוב ומבטיח תחזוקה.
בואו נבנה את המשחק
מנקודה זו ואילך, אני אדריך אתכם בתהליך שבו פעלתי כאשר בניתי את המשחק הזה.
מבנה הפרויקט ואדריכלות
בעת בניית משחק זיכרון זה, ארגנתי בקפידה את בסיס הקוד כדי להבטיח תחזוקה, סקלאביליות והפרדת דאגות ברורה. בואו נחקור את המבנה ואת ההיגיון מאחורי כל החלטה:
אדריכלות מבוססת רכיבים
בחרתי באדריכלות מבוססת רכיבים מכמה סיבות:
-
ניתוח: כל רכיב הוא עצמאי עם לוגיקה ועיצוב עצמיים
-
חוזר: רכיבים כמו
כרטיס
ו־טוען
יכולים להיות משומשים בכל היישום -
ניהול: קל יותר לאתר ולשנות רכיבים יחידים
-
בדיקות: ניתן לבדוק רכיבים בידידות
ארגון רכיב
- רכיב כרטיס
-
מופרד לתיקייה נפרדת מכיוון שהוא רכיב מרכזי במשחק
-
מכיל גם מודולי JSX ו־SCSS לאיטום
-
מטפל בעיבוד כרטיסים יחידים, מצבי טעינה ואירועי לחיצה
- רכיב לוח כרטיסים
-
ניהול פריסת לוח המשחק
-
מטפל בערבוב והפצת הכרטיסים
-
שולט בפריסת רשת רספונסיבית עבור גדלי מסך שונים
- רכיב טעינה
-
אינדיקטור טעינה שניתן להשתמש בו שוב
-
משפר את חוויית המשתמש במהלך טעינת התמונות
-
ניתן לשימוש על ידי כל רכיב שזקוק למצבי טעינה
- ראש/תחתית/תת כותרת רכיבים
-
רכיבים מבניים לפריסת האפליקציה
-
הכותרת מציגה את כותרת המשחק וניקוד
-
התחתית מציגה זכויות יוצרים ומידע על גרסה
-
הכותרת המשנית מספקת הוראות משחק
גישת מודולי CSS
השתמשתי במודולים של CSS (.module.scss
) למספר יתרונות:
-
עיצוב מוגבל: מונע דליפת סגנונות בין רכיבים
-
התנגדות בשמות: יוצר באופן אוטומטי שמות ייחודיים למחלקות
-
ניתור: הסגנונות ממוקמים בצורה קו-קו עם הרכיבים שלהם
-
תכונות SCSS: משתמש בתכונות SCSS תוך שמירה על סגנונות מודולריים
פריחות מותאמות
התיקייה hooks
מכילה פריחות מותאמות כמו useFetch:
-
הפרדת תחומים: בודדת את לוגיקת האיסוף של הנתונים
-
ניתושיות: יכולה לשמש כל רכיב שזקוק לנתוני תמונה
-
ניהול מצב: עוסקת בטעינה, בשגיאה ובמצבי הנתונים
-
ביצועים: מיישמת אופטימיזציות כמו בקרת גודל התמונה
קבצים ברמת השורש
App.jsx:
-
פועל כנקודת הכניסה של היישום
-
ניהול מצב גלובלי וניתוב (אם נדרש)
-
מפרק את הרכיבים בצורה תיאום
-
מטפל בפריסות ברמה עליונה
שיקולים ביצועים
המבנה תומך באופטימיזציות ביצועים:
-
פיצול קוד: רכיבים יכולים להיטענו באופן עצל אם נדרש
-
ממוייזציה: רכיבים יכולים להיות ממוזים באופן יעיל
-
טעינת סגנון: מודולי CSS מאפשרים טעינת סגנון יעילה
-
ניהול נכסים: תמונות ומשאבים מאורגנים בצורה תקינה
נפילות
המבנה הזה מאפשר התרחבות קלה:
-
ניתן להוסיף תכונות חדשות כרכיבים חדשים
-
ניתן ליצור עוד תופסים לפונקציונליות חדשה
-
סגנונות נשמרים ניתנים לתחזוקה כשהאפליקציה גדלה
-
בדיקה ניתן ליישם בכל רמה
חוויית פיתוח
המבנה משפר את חוויית הפיתוח:
-
ארגון קבצים ברור
-
מיקומי רכיבים אינטואיטיביים
-
קל למצוא ולשנות תכונות ספציפיות
-
תומך בשיתוף פעולה יעיל
ארכיטקטורה זו התגלתה כמאוד ערכית במהלך אופטימיזציה של המשחק לשימוש בטאבלט, מאחר שהיא מאפשרת לי:
-
לזהות ולאופטימזציה בקלות של נקודות מצוקה בביצועים
-
להוסיף סגנונות מיוחדים לטאבלט מבלי להשפיע על התקנים אחרים
-
ליישם מצבי טעינה לחווית מובייל טובה יותר
-
לשמור על ניתוק נקי בין לוגיקת המשחק ורכיבי הממשק המשתמש
כעת, בואו נתחיל לכתוב קוד.
מדריך בניית צעדים
1. הגדרת הפרויקט
הגדרת סביבת הפיתוח
כדי להתחיל עם פרויקט React נקי, פתח את אפליקציית הטרמינל שלך והרץ את הפקודות הבאות (תוכל לתת שם לתיקיית הפרויקט שלך כפי שתרצה – במקרה שלי השם הוא 'memory-card'):
npm create vite@latest memory-card -- --template react
cd memory-card
npm install
התקן את התלויות הדרושות
התלויות היחידות שנשתמש בהן בפרויקט זה הן חבילת ההוק מהאתר UI.dev (בפנייה, כאן תוכל למצוא מאמר מסודר המסביר כיצד עיבוד ב-React עובד).
התלות השנייה היא מטמון ה-CSS המוכר, SASS, שנצטרך כדי להיות מסוגלים לכתוב את מודולי ה-CSS שלנו ב-SASS במקום ב-CSS רגיל.
npm install @uidotdev/usehooks sass
הגדר את Vite והגדרות הפרויקט
כאשר אנו מגדירים את הפרויקט שלנו, עלינו לבצע כמה התאמות בהגדרה ספציפית כדי לטפח אזהרות של SASS ולשפר את חוויית הפיתוח שלנו. הנה איך תוכל להגדיר את Vite:
// 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, // משתיק אזהרות תלות SASS
charset: false // מונע אזהרת charset בגרסאות SASS חדשות
}
}
}
});
שים לב כי רוב ההגדרות הללו נוצרות באופן אוטומטי עבורך כאשר אתה יוצר את הפרויקט עם Vite. הנה מה קורה:
-
הגדרת SASS:
-
quietDeps: true
: משתיק את האזהרות על תלותים שכבר לא מומלצות במודולי SASS. שימושי במיוחד כאשר עובדים עם קבצי SASS/SCSS מצד שלישי. -
charset: false
: מונע את האזהרה על "@charset" שמופיעה בגרסאות חדשות יותר של SASS כאשר משתמשים בתווים מיוחדים בקובץ הסגנון שלך.
-
-
הגדרות בדיקת מבחן:
-
globals: true
: הופך פונקציות בדיקת המבחן לזמינות באופן גלובלי בקבצי הבדיקה -
environment: 'jsdom'
: מספק סביבת DOM לצורך בדיקה -
setupFiles
: מצביע על קובץ ההגדרה של הבדיקה שלנו
-
ההגדרות הללו עוזרות ליצירת חוויית פיתוח נקייה על ידי הסרת הודעות אזהרה מיותרות בקונסול, הגדרת תצורות סביבת בדיקה נכונות, והבטחת שעיבוד SASS/SCSS עובד בצורה חלקה
עשויים להופיע אזהרות בקונסול שלך בלעדי אלו הגדרות כאשר:
-
משתמשים בתכונות SASS/SCSS או מייבאים קבצי SASS
-
מריצים בדיקות שדורשות טיפול ב-DOM
-
משתמשים בתווים מיוחדים בגיליונות הסגנון שלכם
2. בניית הרכיבים
יצירת רכיב הכרטיס
ראשית, בואו ניצור את רכיב הכרטיס הבסיסי שלנו שיציג תמונות יחידות:
// 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;
רכיב הכרטיס הוא גוש בנייה בסיסי של המשחק שלנו. הוא אחראי להצגת תמונות יחידות ולטיפול באינטראקציות של השחקן. בואו לפרק את המימוש שלו:
פירוט התכונות:
-
image
: (string)-
כתובת ה-URL של התמונה שיש להציג שמתקבלת משירות ה- API שלנו.
-
משמש ישירות ב- src של תגית ה- img.
-
-
id
: (string)-
זיהוי ייחודי עבור כל קלף שחשוב למעקב אחר הקלפים שנלחצו.
-
מועבר לקריאת הפונקציה
processTurn
כאשר נלחץ קלף.
-
-
category
: (string)-
מתאר את סוג התמונה (לדוגמה, "אנימה", "נקו"), ומשמש ב- alt לנגישות טובה יותר.
-
זה מסייע ב- SEO ובקוראי מסך.
-
-
processTurn
: (פונקציה)-
פונקציית קולבק שמועברת מרכיב ההורה שמטפלת בלוגיקת המשחק כאשר נלחץ כרטיס.
-
היא גם ניהול עדכוני ציון ושינויים במצב המשחק ומחליטה אם כרטיס נלחץ קודם.
-
-
isLoading
: (בוליאני)-
שולט באם להציג מצב טעינה. כאשר נכון, זה מציג רכיב Loader במקום התמונה.
-
זה משפר את חוויית המשתמש במהלך טעינת התמונה.
-
עיצוב הרכיב:
// 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%);
}
}
שימוש ברכיב:
<Card
key={getKey()}
imgUrl={item?.image?.original?.url || ""}
imageId={item?.id}
categoryName={item?.category}
processTurn={(imageId) => processTurn(imageId)}
/>
תכונות עיקריות:
-
אופטימיזצית ביצועים:
-
משתמש ב-
React.memo
כדי למנוע עידכונים לא נחוצים -
מיישם
useCallback
עבור סוגי אירועים -
ניהול מצב טעינה פנימי לחוויית משתמש משופרת
-
-
ניהול מצב טעינה:
-
המשתמש במשתנה
isLoading
הפנימי כדי לעקוב אחר טעינת התמונה -
מציג רכיב Loader עם הודעה במהלך הטעינה
-
מסתיר את התמונה עד שהיא נטענת במלואה באמצעות מחלקות CSS
-
-
טיפול באירועים:
-
handleImageLoad
: ניהול מעבר מצבי טעינה -
handleClick
: עיבוד תורי שחקנים דרך הקריאהprocessTurn
-
בניית רכיב CardsGrid
זהו רכיב המשחק העיקרי שלנו שמנהל את מצב המשחק, הלוגיקה של הניקוד ואינטראקציות הקלפים. בואו נפרט את היישום שלו:
// 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) {
// ניהול מצב
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);
// הוק מותאם אישית לאחזור תמונות
const { data: fetchedData, fetchData, error } = useFetch();
// עדכון תמונות כאשר מתקבלים נתונים חדשים
useEffect(() => {
if (fetchedData?.images) {
setImages(fetchedData.images);
setIsLoading(false);
// איפוס תמונות שהוקלקו כאשר נטען קבוצה חדשה
setClickedImages([]);
}
}, [fetchedData]);
// פונקצית עזר לעדכון התוצאה הטובה ביותר
function updateBestScore(currentScore) {
if (currentScore > bestScore) {
setBestScore(currentScore);
}
}
// לוגיקת המשחק העיקרית
function processTurn(imageId) {
const newClickedImages = [...clickedImages, imageId];
setClickedImages(newClickedImages);
// אם לוחצים על אותה תמונה פעמיים, אפס הכל
if (clickedImages.includes(imageId)) {
// עדכון התוצאה הטובה ביותר אם נדרש
updateBestScore(score);
setClickedImages([]);
setScore(0);
} else {
// טיפול בבחירת כרטיס מוצלחת
const newScore = score + 1;
setScore(newScore);
// בדיקת תוצאה מושלמת (כל הכרטיסים נלחצו פעם אחת)
if (newClickedImages.length === images.length) {
updateBestScore(newScore);
fetchData();
setClickedImages([]);
} else {
// לערבב את התמונות
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);
עיצוב הרכיב:
.container {
display: grid;
gap: 1rem 1rem;
grid-template-columns: auto; /* ברירת מחדל: עמוד אחד עבור מובייל */
background-color: #2196f3;
padding: 0.7rem;
cursor: pointer;
}
@media (min-width: 481px) {
.container {
grid-template-columns: auto auto; /* שני עמודים עבור טאבלטים ומעלה */
}
}
@media (min-width: 769px) {
.container {
grid-template-columns: auto auto auto; /* שלושה עמודים עבור שולחנות עבותים וגדולים יותר */
}
}
פירוט תכונות עיקריות:
-
ניהול מצב:
-
משתמש ב־
useState
עבור מצב ברמת רכיב -
מיישם
useLocalStorage
עבור נתוני משחק עמידים:-
clickedImages
: מעקב אחר הקלפים שנלחצו -
score
: ציון המשחק הנוכחי -
bestScore
: הציון הגבוה ביותר שנשגב
-
-
מנהל מצב טעינה עבור גיבוי תמונה
-
ערבב את הקלפים
-
-
לוגיקת משחק:
-
processTurn
: עובד עם מהלכי השחקנים-
מעקב אחר לחיצות כפולות
-
עדכון ציונים
-
ניהול תרחישי ציון מושלמים
-
-
updateBestScore
: עדכון תוצאת גבוהה כאשר נדרש -
מביא אוטומטית תמונות חדשות כאשר סיים סיבוב
-
-
איסוף נתונים:
-
משתמש בהוק מותאם
useFetch
עבור נתוני תמונה -
טופל סטטוסי טעינה ושגיאה
-
עדכון תמונות כאשר נתונים חדשים מתקבלים
-
-
אופטימיזצית ביצועים:
-
רכיב מעוטר ב-
React.memo
-
עדכוני מצב יעילים
-
פריסת רשת רספונסיבית
-
-
התמדיות:
-
מצב המשחק מתמיד לאורך טעינות דף מחדש
-
מעקב אחר ניקוד הטוב ביותר
-
שמירת התקדמות המשחק הנוכחית
-
דוגמת שימוש:
...
...
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;
רכיב ה- CardsGrid מהווה את לב משחק כרטיסי הזיכרון שלנו, ניהול:
-
מצב המשחק והלוגיקה
-
מעקב אחר ניקוד
-
אינטראקציות קלפים
-
טעינת תמונות ותצוגה
-
שילוב רספונסיבי
-
שמירת נתונים
מימוש זה מספק חוויית משחק חלקה תוך שמירה על קריאות קוד וניתוח דרך ניתוח ברור של התחומים וניהול מצב נכון.
3. מימוש שכבת ה- API
המשחק שלנו משתמש בשכבת API חזקה עם אפשרויות גיבוי מרובות כדי להבטיח משלוח תמונה אמין. בואו נממש כל שירות והמנגנון האלטרנטיבי.
הגדרת שירות ה-API הראשי:
// 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 };
}
יצירת שירות API גיבוי ראשון:
// 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();
// המרת המענה כך שיתאים לפורמט הצפוי שלנו
const transformedImages = result.results.map(item => ({
id: item.url.split('/').pop().split('.')[0], // חילוץ UUID מ-URL
image: {
original: {
url: item.url
}
},
artist: {
name: item.artist_name,
href: item.artist_href
},
source: item.source_url
}));
return { images: transformedImages };
}
יצירת שירות API גיבוי שני:
// 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();
// המרת המענה כך שיתאים לפורמט הצפוי שלנו
const transformedImages = result.items.map(item => ({
id: item.id,
image: {
original: {
url: item.image_url
}
}
}));
return { images: transformedImages };
}
בניית מנגנון גיבוי API:
// src/services/api/imageService.js
import { fetchNekosiaImages } from "./nekosiaApi";
import { fetchNekosImages } from "./nekosApi";
import { fetchNekosBestImages } from "./nekosBestApi";
export async function fetchImages() {
try {
// נסה תחילה את ה-API הראשי
return await fetchNekosiaImages();
} catch (error) {
console.warn("Primary API failed, trying fallback:", error);
// נסה את ה-API הגיבוי הראשון
try {
return await fetchNekosBestImages();
} catch (fallbackError) {
console.warn("First fallback API failed, trying second fallback:", fallbackError);
// נסה את ה-API הגיבוי השני
try {
return await fetchNekosImages();
} catch (secondFallbackError) {
console.error("All image APIs failed:", secondFallbackError);
throw new Error("All image APIs failed");
}
}
}
}
שימוש בשירות התמונות:
// 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,
};
}
תכונות מרכזיות של מימוש ה-API שלנו:
-
מקורות API מרובים:
-
API ראשי (Nekosia): מספק תמונות אנימה באיכות גבוהה
-
גיבוי ראשון (Nekos Best): כולל מידע על האמן
-
גיבוי שני (Nekos): גיבוי פשוט ואמין
-
-
פורמט נתונים עקבי:
- כל ה-APIs משנים את התגובות שלהם כך שיתאימו לפורמט הצפוי שלנו:
{
images: [
{
id: string,
image: {
original: {
url: string
}
}
}
]
}
-
טיפול בשגיאות יציב:
-
מאשר תגובות API חוקיות
-
בודק כתובות URL של תמונות חוקיות
-
מספק הודעות שגיאה מפורטות
-
מנגנון חלופי ונעים
-
-
תכונות בטיחות:
-
סינון תוכן בטוח (
rating=safe
) -
הגבלת מספר תמונות (21 תמונות)
-
אימות כתובת URL
-
אימות פורמט תגובה
-
-
התחשבויות בביצועים:
-
גדלי תמונות מותוחים
-
תגי תוכן מסוננים
-
המרת נתונים יעילה
-
מינימום קריאות ל- API
-
המימוש הזה מבטיח כי למשחק שלנו יהיה מקור אמין של תמונות תוך טיפול נאות בכשלי API אפשריים. התבנית הקבועה של הנתונים בכל ה-APis עושה את זה קל לעבור ביניהם בלי להשפיע על פונקציונליות המשחק.
בדיקת האפליקציה
בדיקה היא חלק חשוב ביישום כל פיתוח, ולמשחק כרטיסי זיכרון שלנו, ביצענו אסטרטגיה רחבה לבדיקות באמצעות כלים ושיטות מודרניים. בואו נתעוף לתוך כיצד קיבלנו את הבדיקות וכמה תבניות מרכזיות שהשתמשנו בהן.
ערימת בדיקות
-
Vitest: הפריימוורק לבדיקה שלנו, נבחר בגלל מהירותו ושילובו הקל עם Vite
-
ספריית בדיקות React: לבדיקת רכיבי React בגישה ממוקדת משתמש
-
@testing-library/user-event: לחיקוי התנהגות משתמש
-
jsdom: ליצירת סביבת DOM בבדיקות שלנו
תבניות בדיקות מרכזיות
בדיקות היו חלק קריטי בהבטחת האמינות והניתוח של משחק כרטיסי זיכרון זה. עיצבתי אסטרטגית בדיקה מקיפה באמצעות ספריית בדיקות React ו-Vitest, מתמקדת בכמה תחומים מרכזיים:
1. בדיקת רכיבים
כתבתי בדיקות מפורטות עבור רכיבי React שלי כדי לוודא שהם מתרים בצורה נכונה ומתנהגים כפי שצפוי. לדוגמה, הרכיב CardsGrid
, שהוא לב המשחק, כולל כיסוי בדיקות מפורט כולל:
-
מצבי תצוגה ראשוניים
-
מצבי טעינה
-
טיפול בשגיאות
-
מעקב אחר ציון
-
התנהגות אינטראקציה עם כרטיסים
2. מוקינג בדיקות
כדי לוודא בדיקות אמינות ומהירות, מימשתי מספר אסטרטגיות למוקינג:
-
פעולות אחסון מקומי באמצעות ה-hook useLocalStorage
-
קריאות API באמצעות ה-hook
useFetch
-
טפסי אירועים ועדכוני מצב
3. שיטות בדיקה מומלצות
במהלך היישום של בדיקותיי, עקבתי אחרי מספר שיטות בדיקה מומלצות:
-
שימוש בהוקים
beforeEach
ו-afterEach
כדי לאפס את המצב בין הבדיקות -
בדיקת אינטראקציות משתמש באמצעות
fireEvent
מספריית הבדיקות של React -
כתיבת בדיקות הדומות לאיך משתמשים מתנהגים עם האפליקציה
-
בדיקת תרחישי הצלחה ושגיאה כאחד
-
בידוד בדיקות באמצעות מסיכה תקנית
4. כלי בדיקה
הפרויקט משתמש בכלים וספריות לבדיקה מודרניים:
-
Vitest: כמנוהל הבדיקות
-
ספריית בדיקות ריאקט: לבדיקת רכיבי ריאקט
-
@testing-library/jest-dom: לאמת בדיקות DOM משודרגות
-
@testing-library/user-event: עבור סימולציה של פעולות משתמש
הגישה לבדיקה כוללת זו עזרה לי לתפוס באופן מוקדם באגים, הבטיחה איכות קוד, והפכה את תהליך השיפור לשקף וניהולי יותר בטוחים.
אופטימיזציות
כדי להבטיח ביצוע חלק, במיוחד על מכשירים ניידים, יישמנו מספר טכניקות אופטימיזציה:
-
המרת תגובה
-
פורמט נתונים אחיד בכל ה- APIs
-
חילוץ ID יעיל מכתובות URL
-
מטה-נתוני תמונה מובנים לגישה מהירה
-
-
אופטימיזציה של רשת
-
שימוש במצב
no-cors
במקומות המתאימים לטיפול יעיל בבעיות CORS -
טיפול בשגיאות עם קודי מצב מסוימים לצורך בידוד תקלות יותר טוב
-
מבנה תגובה עקבי בכל המימושים של ה- API
-
-
שיקולים ראשוניים לנייד
-
אסטרטגיית טעינת תמונות מאוחזרות
-
טיפול בשגיאות יעיל למניעת ניסיונות מיותרים
-
המרת נתונים משולבת כדי להפחית את העומס בעיבוד
-
שדרוגים עתידיים
ישנם כמה דרכים שבאפשרותנו לשפר עוד יותר את הפרויקט זה:
-
מטמון תגובת API
-
יישום מטמון אחסון מקומי עבור תמונות בשימוש תדיר
-
הוספת אסטרטגיית אתחול מטמון עבור תוכן חדש
-
יישום טעינת תמונה באופן פרוגרסיבי
-
-
אופטימיזציות בביצועים
-
הוספת טעינת תמונה עצלה לזמן טעינה ראשוני טוב יותר
-
יישום תור דרישות לניהול רוחב פס טוב יותר
-
הוספת דחיסת תגובה להעברת נתונים מהירה יותר
-
-
שיפורי אמינות
-
הוספת בדיקת בריאות API לפני ניסיונות
-
יישום מנגנוני ניסיון מחדש עם אטימה אקספוננציאלית
-
הוספת תבנית שבר ל-APIs שנכשלים
-
-
ניתוח ומעקב
-
מעקב אחר שיעורי הצלחת ה- API
-
מעקב אחר זמני התגובה
-
יישום החלפת API אוטומטית בהתבסס על מדדי ביצועים
-
יישום זה, היציב, מבטיח שהמשחק שלנו נשאר פונקציונלי ובעל ביצועים אף בתנאים רשת לא טובים או במקרה של אי זמינות של API, תוך שמירה על מרווח לשיפורים ואופטימיזציות בעתיד.
מסקנה
בניית משחק זיכרון זה הייתה יותר מיצירת אלטרנטיבה כיפית וללא פרסומות עבור ילדים – זה היה אימון ביישום שיטות הפיתוח המודרניות ברשת בפתרון בעיה ממשית.
הפרויקט מדגים כיצד שילוב של ארכיטקטורה מקודם למחשבה, בדיקות חזקות ומנגנונים אמינים לצמיחה יכולים לתת תוצאה באפליקציה מוכנה לייצור שהיא גם מהנה וחינוכית.
🗝️ נקודות מרכזיות
-
פיתוח שממוקד על המשתמש
-
התחיל עם בעיה ברורה (משחקים מלאי פרסומות שמשפיעים על חוויית המשתמש)
-
יישם תכונות שמשפרות את חוויית המשחק ללא הפרעות
-
שמר על מוקד על ביצועים ואמינות במכשירים שונים
-
-
מצוינות טכנולוגית
-
ניצל תבניות והוקים חדשניים של React לקוד נקי וניתן לתחזוקה
-
יישם אסטרטגיית בדיקות מקיפה המבטיחה אמינות
-
יצר מערכת גיבוי API חזקה למשחק בלתי מפוסק
-
-
ביצועים כמעלה
-
אימצתי גישה תחילית לנייד עם עיצוב רספונסיבי
-
טעינת תמונות מותות וטיפול מותח
-
יישמתי אסטרטגיות ניהול מצב יעילות ואחסון מטמון
-
📚 תוצאות למידה
הפרויקט מציג כיצד משחקים שגרתיים יכולים לשמש ככלי מצוין ליישום פתרונות טכניים מורכבים. מארכיטקטורת רכיבים ועד לגיבויי API, כל תכונה נבנתה עם דמיון ותחזוקה ביד נפש, מוכיחה כי גם פרויקטים תחביביים יכולים לשמור על איכות קוד מקצועית.
🔮 המשך קדימה
עודף המשחק בהצלחה להשיג את מטרתו העיקרית של לספק חוויית משחק נעימה וללא פרסומות, השיפורים העתידיים שתיעדו מספקים מפתח רואה לעתיד. בין אם זו הטמעת אופטימיזציות נוספות או הוספת תכונות חדשות, היסוד חזק ומוכן להרחבה.
משחק הזיכרון מהווה עדות לכך שפרויקטים אישיים יכולים גם לפתור בעיות בעולם האמיתי וגם לשמש כפלטפורמות ליישום שיטות עבודה מומלצות בפיתוח אתרים מודרני. אל תהססו לחקור את הקוד, לתרום, או להשתמש בו כהשראה לפרויקטים שלכם!
Source:
https://www.freecodecamp.org/news/how-to-build-a-memory-card-game-using-react/