البرنامج التعليمي: لعبة إكس-أو
ستقوم ببناء لعبة إكس-أو صغيرة خلال هذا البرنامج التعليمي. لا يفترض هذا البرنامج التعليمي أي معرفة مسبقة بـ React. التقنيات التي ستتعلمها في البرنامج التعليمي أساسية لبناء أي تطبيق React، وفهمها بشكل كامل سيمنحك فهماً عميقاً لـ React.
ملاحظة
تم تصميم هذا البرنامج التعليمي للأشخاص الذين يفضلونالتعلم بالممارسةويريدون تجربة صنع شيء ملموس بسرعة. إذا كنت تفضل تعلم كل مفهوم خطوة بخطوة، ابدأ بـوصف واجهة المستخدم.
ينقسم البرنامج التعليمي إلى عدة أقسام:
- إعداد البرنامج التعليميسيمنحكنقطة بدايةلمتابعة البرنامج التعليمي.
- نظرة عامةستعلّمكأساسياتالمكونات، الخصائص (props)، والحالة (state).
- إكمال اللعبةستعلّمكالتقنيات الأكثر شيوعاًفي تطوير React.
- إضافة السفر عبر الزمنستمنحكنظرة أعمقفي نقاط القوة الفريدة لـ React.
ماذا ستبني؟
في هذا البرنامج التعليمي، ستقوم ببناء لعبة إكس-أو تفاعلية باستخدام React.
يمكنك رؤية الشكل الذي ستكون عليه عند الانتهاء هنا:
إذا كان الكود غير مفهوم لك بعد، أو إذا كنت غير مألوف مع بناء جملة الكود، لا تقلق! الهدف من هذا البرنامج التعليمي هو مساعدتك على فهم React وبناء جملته.
نوصي بأن تتحقق من لعبة إكس-أو أعلاه قبل متابعة البرنامج التعليمي. إحدى الميزات التي ستلاحظها هي وجود قائمة مرقمة على يمين لوحة اللعبة. تعطيك هذه القائمة تاريخاً لجميع الحركات التي حدثت في اللعبة، ويتم تحديثها مع تقدم اللعبة.
بمجرد أن تتجول في لعبة إكس-أو المكتملة، استمر في التمرير. ستبدأ بقالب أبسط في هذا البرنامج التعليمي. خطوتنا التالية هي إعدادك حتى تتمكن من البدء في بناء اللعبة.
إعداد البرنامج التعليمي
في محرر الكود المباشر أدناه، انقر علىForkفي الزاوية اليمنى العليا لفتح المحرر في علامة تبويب جديدة باستخدام موقع CodeSandbox. يتيح لك CodeSandbox كتابة الكود في متصفحك ومعاينة كيف سيرى المستخدمون التطبيق الذي أنشأته. يجب أن تعرض علامة التبويب الجديدة مربعاً فارغاً وكود البداية لهذا البرنامج التعليمي.
ملاحظة
يمكنك أيضاً متابعة هذا البرنامج التعليمي باستخدام بيئة التطوير المحلية لديك. للقيام بذلك، تحتاج إلى:
- تثبيتNode.js
- في علامة تبويب CodeSandbox التي فتحتها سابقاً، اضغط على زر الزاوية اليسرى العليا لفتح القائمة، ثم اخترDownload Sandboxفي تلك القائمة لتنزيل أرشيف الملفات محلياً
- فك ضغط الأرشيف، ثم افتح طرفية و
cdإلى الدليل الذي فككت ضغطه فيه - تثبيت التبعيات باستخدام
npm install - شغّل
npm startلبدء خادم محلي واتبع التعليمات لعرض الكود يعمل في متصفح
إذا واجهتك مشكلة، لا تدع هذا يوقفك! تابع عبر الإنترنت بدلاً من ذلك وحاول الإعداد المحلي مرة أخرى لاحقاً.
نظرة عامة
الآن بعد أن أصبحت جاهزاً، دعنا نحصل على نظرة عامة على React!
فحص كود البداية
في CodeSandbox سترى ثلاثة أقسام رئيسية:

- قسمالملفاتمع قائمة بالملفات مثل
App.js،index.js،styles.cssفي مجلدsrcومجلد يسمىpublic - محررالكودحيث سترى الكود المصدري للملف الذي اخترته
- قسمالمتصفححيث سترى كيف سيتم عرض الكود الذي كتبته
يجب أن يكون ملفApp.jsمحددًا في قسمالملفات. محتويات ذلك الملف فيمحرر الكوديجب أن تكون:
قسمالمتصفحيجب أن يعرض مربعًا بداخله حرف X هكذا:

الآن دعنا نلقي نظرة على الملفات في الكود الأولي.
App.js
الكود فيApp.jsينشئمكونًا. في React، المكون هو قطعة كود قابلة لإعادة الاستخدام تمثل جزءًا من واجهة المستخدم. تُستخدم المكونات لعرض وإدارة وتحديث عناصر واجهة المستخدم في تطبيقك. دعنا ننظر إلى المكون سطرًا بسطر لنرى ما يحدث:
السطر الأول يعرّف دالة تسمىSquare. كلمة JavaScript الرئيسيةexportتجعل هذه الدالة قابلة للوصول خارج هذا الملف. كلمةdefaultتخبر الملفات الأخرى التي تستخدم كودك أنها الدالة الرئيسية في ملفك.
السطر الثاني يُرجع زرًا. كلمة JavaScript الرئيسيةreturnتعني أن ما يأتي بعدها يُعاد كقيمة لمن يستدعي الدالة.<button>هوعنصر JSX. عنصر JSX هو مزيج من كود JavaScript ووسوم HTML يصف ما تريد عرضه.className="square"هي خاصية زر أوخاصية (prop)تخبر CSS بكيفية تنسيق الزر.Xهو النص المعروض داخل الزر و</button>يغلق عنصر JSX للإشارة إلى أن أي محتوى لاحق لا يجب وضعه داخل الزر.
styles.css
انقر على الملف المسمىstyles.cssفي قسمالملفاتفي CodeSandbox. هذا الملف يعرّف الأنماط لتطبيق React الخاص بك. أولمحددين CSS(* و body) يحددان نمط أجزاء كبيرة من تطبيقك بينما محدد.squareيحدد نمط أي مكون حيث تم تعيين خاصيةclassName إلى square. في كودك، سيطابق ذلك الزر من مكون Square الخاص بك في ملفApp.js.
index.js
انقر على الملف المسمىindex.jsفي قسمالملفاتفي CodeSandbox. لن تقوم بتحرير هذا الملف خلال البرنامج التعليمي لكنه الجسر بين المكون الذي أنشأته في ملفApp.jsومتصفح الويب.
الأسطر من 1 إلى 5 تجمع كل القطع الضرورية معًا:
- React
- مكتبة React للتواصل مع متصفحات الويب (React DOM)
- الأنماط (styles) لمكوناتك
- المكون الذي أنشأته في
App.js.
بقية الملف تجمع كل القطع معًا وتحقن المنتج النهائي فيindex.htmlداخل مجلدpublic.
بناء اللوحة
لنعد إلىApp.js. هذا هو المكان الذي ستقضي فيه بقية البرنامج التعليمي.
حاليًا، اللوحة هي مربع واحد فقط، لكنك تحتاج إلى تسعة! إذا حاولت نسخ ولصق مربعك لإنشاء مربعين مثل هذا:
ستحصل على هذا الخطأ:
وحدة التحكم
/src/App.js: يجب أن تكون عناصر JSX المتجاورة مغلفة داخل علامة محيطة. هل تريد جزء JSX<>...</>؟
يجب أن تعيد مكونات React عنصر JSX واحدًا وليس عناصر JSX متجاورة متعددة مثل زرين. لإصلاح هذا، يمكنك استخدامالأجزاء (Fragments)(<> و </>) لتغليف عناصر JSX المتجاورة المتعددة مثل هذا:
الآن يجب أن ترى:

عظيم! الآن تحتاج فقط إلى النسخ واللصق عدة مرات لإضافة تسعة مربعات و…

أوه لا! المربعات كلها في خط واحد، وليس في شبكة كما تحتاج للوحة. لإصلاح هذا، ستحتاج إلى تجميع مربعاتك في صفوف باستخدامdivوإضافة بعض فئات CSS. أثناء قيامك بذلك، ستعطي كل مربع رقمًا للتأكد من معرفة مكان عرض كل مربع.
في ملفApp.js، قم بتحديث مكونSquareليبدو هكذا:
تنسيقات CSS المعرفة فيstyles.cssتنسق عناصر div ذاتclassName board-row. الآن بعد أن قمت بتجميع مكوناتك في صفوف باستخدام عناصرdivالمنسقة، أصبح لديك لوحة تيك تاك تو:

لكن لديك الآن مشكلة. مكونك المسمىSquare، لم يعد مربعًا حقًا. دعنا نصلح ذلك بتغيير الاسم إلىBoard:
في هذه المرحلة، يجب أن يبدو الكود الخاص بك شيئًا كهذا:
ملاحظة
ششش… هذا كثير للكتابة! لا بأس بنسخ ولصق الكود من هذه الصفحة. ومع ذلك، إذا كنت مستعدًا لتحدٍ صغير، نوصي بنسخ الكود الذي كتبته يدويًا مرة واحدة على الأقل بنفسك.
تمرير البيانات عبر الخصائص (props)
بعد ذلك، سترغب في تغيير قيمة المربع من فارغ إلى "X" عندما ينقر المستخدم على المربع. مع طريقة بناء اللوحة حتى الآن، ستحتاج إلى نسخ ولصق الكود الذي يحدث المربع تسع مرات (مرة لكل مربع لديك)! بدلاً من النسخ واللصق، تتيح لك بنية مكونات React إنشاء مكون قابل لإعادة الاستخدام لتجنب كود فوضوي ومكرر.
أولاً، ستقوم بنسخ السطر الذي يحدد مربعك الأول (<button className="square">1</button>) من مكونBoardإلى مكونSquareجديد:
ثم ستقوم بتحديث مكونBoardSquare ليعرض مكون باستخدام صيغة JSX:
لاحظ كيف أن مكوناتك الخاصةdivفي المتصفح.Board و Squareيجب أن تبدأ بحرف كبير، على عكس عناصر
لنلقي نظرة:

أوه لا! لقد فقدت المربعات المرقمة التي كانت لديك من قبل. الآن كل مربع يعرض "1". لإصلاح هذا، ستستخدمالخصائص (props)لتمرير القيمة التي يجب أن يحتويها كل مربع من المكون الأب (Board) إلى مكونه الفرعي (Square).
قم بتحديث مكونSquareلقراءة خاصيةvalueالتي ستمررها من مكونBoard:
function Square({ value })تشير إلى أن مكون Square يمكن تمرير خاصية إليه تسمىvalue.
الآن تريد عرض تلكvalueبدلاً من1داخل كل مربع. حاول القيام بذلك هكذا:
عذراً، هذا ليس ما أردته:

كنت تريد عرض متغير JavaScript المسمىvalueمن مكونك، وليس كلمة "value". للـ"هروب إلى JavaScript" من JSX، تحتاج إلى الأقواس المعقوفة. أضف أقواساً معقوفة حولvalueفي JSX هكذا:
في الوقت الحالي، يجب أن ترى لوحة فارغة:

هذا لأن مكونBoardلم يمرر خاصيةvalueإلى كل مكونSquareيقوم بعرضه بعد. لإصلاح ذلك ستضيف خاصيةvalueإلى كل مكونSquareيتم عرضه بواسطة مكونBoard:
الآن يجب أن ترى شبكة من الأرقام مرة أخرى:

يجب أن يبدو الكود المحدث الخاص بك هكذا:
إنشاء مكون تفاعلي
دعنا نملأ مكونSquare بحرف Xعند النقر عليه. قم بتعريف دالة تسمىhandleClickداخل مكونSquare. ثم، أضفonClickإلى خصائص عنصر الزر JSX الذي يعيده مكونSquare:
إذا نقرت على مربع الآن، يجب أن ترى سجلًا في"clicked!"في علامة تبويبConsoleفي الجزء السفلي من قسمBrowserفي CodeSandbox. النقر على المربع أكثر من مرة سيسجل"clicked!"مرة أخرى. السجلات المتكررة في الكونسول بنفس الرسالة لن تنشئ المزيد من الأسطر في الكونسول. بدلاً من ذلك، سترى عدادًا متزايدًا بجوار سجل"clicked!"الأول.
ملاحظة
إذا كنت تتابع هذا البرنامج التعليمي باستخدام بيئة التطوير المحلية الخاصة بك، فأنت بحاجة إلى فتح كونسول المتصفح. على سبيل المثال، إذا كنت تستخدم متصفح Chrome، يمكنك عرض الكونسول باستخدام اختصار لوحة المفاتيحShift + Ctrl + J(على Windows/Linux) أوOption + ⌘ + J(على macOS).
كخطوة تالية، تريد أن "يتذكر" مكون Square أنه تم النقر عليه، ويملأه بعلامة "X". "لتذكر" الأشياء، تستخدم المكوناتالحالة.
يوفر React دالة خاصة تسمىuseStateيمكنك استدعاؤها من مكونك لتمكينه من "تذكر" الأشياء. دعنا نخزن القيمة الحالية للمربعSquareفي الحالة، ونغيرها عند النقر علىSquare.
استوردuseStateفي أعلى الملف. أزل خاصيةvalueمن مكونSquare. بدلاً من ذلك، أضف سطرًا جديدًا في بدايةSquareيستدعيuseState. اجعله يُرجع متغير حالة يسمىvalue:
valueيخزن القيمة وsetValueهي دالة يمكن استخدامها لتغيير القيمة. القيمةnullالمُمررة إلىuseStateتُستخدم كقيمة أولية لمتغير الحالة هذا، لذا تبدأvalueهنا مساوية لـnull.
بما أن مكونSquareلم يعد يقبل الخصائص بعد الآن، ستزيل خاصيةvalueمن جميع المربعات التسعة التي أنشأها مكون Board:
الآن ستغيرSquareلعرض "X" عند النقر. استبدل معالج الحدثconsole.log("clicked!"); بـ setValue('X');. الآن يبدو مكونSquareالخاص بك هكذا:
عن طريق استدعاء دالةsetهذه من معالجonClick، فإنك تخبر React بإعادة عرض ذلكSquareكلما تم النقر على<button>الخاص به. بعد التحديث، ستكونSquare valueمساوية لـ'X'، لذا سترى "X" على لوحة اللعبة. انقر على أي مربع، ويجب أن تظهر "X":

كل مربع له حالته الخاصة: القيمةvalueالمخزنة في كل مربع مستقلة تمامًا عن الأخرى. عندما تستدعي دالةsetفي مكون، يقوم React تلقائيًا بتحديث المكونات الفرعية داخله أيضًا.
بعد إجراء التغييرات المذكورة أعلاه، سيبدو الكود الخاص بك هكذا:
أدوات مطور React
تتيح لك React DevTools فحص الخصائص والحالة لمكونات React الخاصة بك. يمكنك العثور على علامة تبويب React DevTools في أسفل قسمالمتصفحفي CodeSandbox:

لفحص مكون معين على الشاشة، استخدم الزر في الزاوية العلوية اليسرى من React DevTools:

إكمال اللعبة
في هذه المرحلة، لديك جميع اللبنات الأساسية للعبة تيك تاك تو الخاصة بك. للحصول على لعبة كاملة، تحتاج الآن إلى التناوب على وضع "X" و "O" على اللوحة، وتحتاج إلى طريقة لتحديد الفائز.
رفع الحالة لأعلى
حاليًا، يحتفظ كل مكونSquareبجزء من حالة اللعبة. للتحقق من وجود فائز في لعبة تيك تاك تو، يحتاج مكونBoardبطريقة ما إلى معرفة حالة كل من المكونات التسعةSquare.
كيف ستتعامل مع ذلك؟ في البداية، قد تخمن أن مكونBoardيحتاج إلى "سؤال" كل مكونSquareعن حالة ذلك المكونSquare. على الرغم من أن هذا النهج ممكن تقنيًا في React، إلا أننا لا نشجع عليه لأن الكود يصبح صعب الفهم، وعرضة للأخطاء، وصعب إعادة الهيكلة. بدلاً من ذلك، أفضل نهج هو تخزين حالة اللعبة في المكون الأبBoardبدلاً من تخزينها في كل مكونSquare. يمكن لمكونBoardإخبار كل مكونSquareبما يجب عرضه عن طريق تمرير خاصية، كما فعلت عندما مررت رقمًا إلى كل Square.
لجمع البيانات من عدة أبناء، أو لجعل مكونين فرعيين يتواصلان مع بعضهما البعض، قم بتعريف الحالة المشتركة في المكون الأب بدلاً من ذلك. يمكن للمكون الأب تمرير تلك الحالة إلى الأبناء عبر الخصائص. هذا يحافظ على تزامن المكونات الفرعية مع بعضها البعض ومع المكون الأب.
رفع الحالة إلى مكون أصل أمر شائع عند إعادة هيكلة مكونات React.
لنستغل هذه الفرصة لتجربتها. قم بتعديل مكونBoardبحيث يعلن عن متغير حالة يُسمىsquaresويُعين افتراضيًا إلى مصفوفة من 9 قيم null تتوافق مع المربعات التسعة:
Array(9).fill(null)ينشئ مصفوفة تحتوي على تسعة عناصر ويُعين كل منها إلىnull. استدعاءuseState()حولها يعلن عن متغير حالةsquaresالذي يتم تعيينه في البداية إلى تلك المصفوفة. كل إدخال في المصفوفة يتوافق مع قيمة مربع. عندما تملأ اللوحة لاحقًا، ستبدو مصفوفةsquares هكذا:
الآن يحتاج مكونBoardالخاص بك إلى تمرير خاصيةvalueإلى كل مكونSquareيقوم بتصييره:
بعد ذلك، ستقوم بتعديل مكونSquareلاستقبال خاصيةvalueمن مكون Board. سيتطلب ذلك إزالة تتبع مكون Square الخاص لحالةvalueوخاصية الزرonClick:
في هذه المرحلة يجب أن ترى لوحة لعبة إكس-أو فارغة:

ويجب أن يبدو الكود الخاص بك هكذا:
كل مربع سيستقبل الآن خاصيةvalueوالتي ستكون إما'X'، أو'O'، أوnullللمربعات الفارغة.
بعد ذلك، تحتاج إلى تغيير ما يحدث عند النقر علىSquare. مكونBoardيحتفظ الآن بالمربعات المملوءة. ستحتاج إلى إنشاء طريقة تسمح لمكونSquareبتحديث حالةBoard. بما أن الحالة خاصة بالمكون الذي يعرفها، لا يمكنك تحديث حالةBoardمباشرة منSquare.
بدلاً من ذلك، ستمرر دالة من مكونBoardإلى مكونSquare، وستجعل Squareتستدعي تلك الدالة عند النقر على مربع. ستبدأ بالدالة التي سيستدعيها مكونSquareعند النقر عليه. ستسمي تلك الدالةonSquareClick:
بعد ذلك، ستضيف دالةonSquareClickإلى خاصيات مكونSquare:
الآن ستقوم بتوصيل خاصيةonSquareClickبدالة في مكونBoard ستسميها handleClick. لتوصيلonSquareClickبـhandleClickستمرر دالة إلى خاصيةonSquareClickالخاصة بأول مكونSquare:
أخيرًا، ستحدد دالةhandleClickداخل مكون Board لتحديث مصفوفةsquaresالتي تحتفظ بحالة اللوحة:
دالةhandleClickتنشئ نسخة من مصفوفةsquares (nextSquares) باستخدام دالة جافاسكريبتslice()الخاصة بالمصفوفات. ثم تقومhandleClickبتحديث مصفوفةnextSquaresلإضافةXإلى المربع الأول (المؤشر[0]).
استدعاء دالةsetSquaresيخبر React أن حالة المكون قد تغيرت. سيؤدي هذا إلى إعادة عرض المكونات التي تستخدم حالةsquares (Board) وكذلك مكوناتها الفرعية (مكوناتSquareالتي تشكل اللوحة).
ملاحظة
جافاسكريبت تدعمالإغلاقات (Closures)مما يعني أن الدالة الداخلية (مثلhandleClick) لها حق الوصول إلى المتغيرات والدوال المعرفة في دالة خارجية (مثلBoard). دالةhandleClickيمكنها قراءة حالةsquaresواستدعاء دالةsetSquaresلأن كلاهما معرف داخل دالةBoard.
الآن يمكنك إضافة X إلى اللوحة... ولكن فقط إلى المربع العلوي الأيسر. دالةhandleClickالخاصة بك محددة مسبقًا لتحديث المؤشر الخاص بالمربع العلوي الأيسر (0). دعنا نقوم بتحديثhandleClickلتكون قادرة على تحديث أي مربع. أضف وسيطًاiإلى دالةhandleClickيأخذ مؤشر المربع المراد تحديثه:
بعد ذلك، ستحتاج إلى تمرير ذلكiإلىhandleClick. يمكنك محاولة تعيين خاصيةonSquareClickللمربع لتكونhandleClick(0)مباشرةً في JSX هكذا، لكن ذلك لن يعمل:
إليك سبب عدم عمل ذلك. استدعاءhandleClick(0)سيكون جزءًا من عملية عرض مكون اللوحة. لأنhandleClick(0)يغير حالة مكون اللوحة عن طريق استدعاءsetSquares، سيتم إعادة عرض مكون اللوحة بأكمله مرة أخرى. لكن هذا يشغلhandleClick(0)مرة أخرى، مما يؤدي إلى حلقة لا نهائية:
وحدة التحكم
عدد كبير جدًا من عمليات إعادة العرض. يحد React من عدد عمليات العرض لمنع حدوث حلقة لا نهائية.
لماذا لم تحدث هذه المشكلة في وقت سابق؟
عندما كنت تمررonSquareClick={handleClick}، كنت تمرر دالةhandleClickكخاصية. لم تكن تستدعيها! لكنك الآنتستدعيتلك الدالة على الفور—لاحظ الأقواس فيhandleClick(0)—وهذا هو سبب تشغيلها مبكرًا جدًا. أنت لاتريداستدعاءhandleClickحتى ينقر المستخدم!
يمكنك إصلاح ذلك عن طريق إنشاء دالة مثلhandleFirstSquareClickتستدعيhandleClick(0)، ودالة مثلhandleSecondSquareClickتستدعيhandleClick(1)، وهكذا. ستمرر (بدلاً من استدعاء) هذه الدوال كخصائص مثلonSquareClick={handleFirstSquareClick}. هذا سيحل مشكلة الحلقة اللانهائية.
ومع ذلك، تعريف تسع دوال مختلفة وإعطاء كل منها اسم هو أمر مطول جدًا. بدلاً من ذلك، لنفعل هذا:
لاحظ بناء الجملة الجديد() =>. هنا،() => handleClick(0) هي دالة سهمية،وهي طريقة أقصر لتعريف الدوال. عند النقر على المربع، سيتم تشغيل الكود بعد=>"السهم"، مما يستدعيhandleClick(0).
أنت الآن بحاجة إلى تحديث المربعات الثمانية الأخرى لاستدعاءhandleClickمن الدوال السهمية التي تمررها. تأكد من أن الوسيط لكل استدعاء للدالةhandleClickيتوافق مع فهرس المربع الصحيح:
يمكنك الآن مرة أخرى إضافة علامات X إلى أي مربع على اللوحة عن طريق النقر عليها:

لكن هذه المرة تتم إدارة جميع الحالة بواسطة مكونBoard!
هذا ما يجب أن يبدو عليه الكود الخاص بك:
الآن بعد أن أصبحت إدارة حالتك في مكونBoard، يمرر المكون الأصليBoardالخصائص إلى المكونات الفرعيةSquareبحيث يمكن عرضها بشكل صحيح. عند النقر علىSquare، يطلب المكون الفرعيSquareالآن من المكون الأصليBoardتحديث حالة اللوحة. عندما تتغير حالةBoard، يتم إعادة عرض كل من مكونBoardوكل مكون فرعيSquareتلقائيًا. سيسمح الاحتفاظ بحالة جميع المربعات في مكونBoardبتحديد الفائز في المستقبل.
دعنا نلخص ما يحدث عندما ينقر المستخدم على المربع العلوي الأيسر في لوحتك لإضافةXإليه:
- النقر على المربع العلوي الأيسر يشغل الدالة التي تلقاها
buttonكخاصيةonClickمن المكونSquare. تلقى مكونSquareتلك الدالة كخاصيةonSquareClickمن المكونBoard. عرّف مكونBoardتلك الدالة مباشرة في JSX. إنه يستدعيhandleClickمع وسيط0. handleClickيستخدم الوسيط (0) لتحديث العنصر الأول من مصفوفةsquaresمنnullإلىX.- تم تحديث حالة
squaresالخاصة بمكونBoard، لذا فإن مكونBoardوجميع مكوناته الفرعية يعيدون التصيير. هذا يتسبب في تغيير خاصيةvalueالخاصة بمكونSquareذي الفهرس0منnullإلىX.
في النهاية يرى المستخدم أن المربع العلوي الأيسر قد تغير من فارغ إلى احتوائه علىXبعد النقر عليه.
ملاحظة
لعنصر DOM<button>السمةonClickلها معنى خاص في React لأنه مكون مدمج. بالنسبة للمكونات المخصصة مثل Square، فإن التسمية متروكة لك. يمكنك إعطاء أي اسم لخاصيةSquareالمسماةonSquareClickأو دالةBoardالمسماةhandleClick، وسيعمل الكود بنفس الطريقة. في React، من المعتاد استخدام أسماءonSomethingللخصائص التي تمثل أحداثًا وأسماءhandleSomethingلتعريفات الدوال التي تتعامل مع تلك الأحداث.
لماذا الثبات (Immutability) مهم
لاحظ كيف فيhandleClick، تستدعي.slice()لإنشاء نسخة من مصفوفةsquaresبدلاً من تعديل المصفوفة الحالية. لشرح السبب، نحتاج إلى مناقشة الثبات ولماذا من المهم تعلمه.
هناك عمومًا نهجان لتغيير البيانات. النهج الأول هوتحويرالبيانات عن طريق تغيير قيم البيانات مباشرة. النهج الثاني هو استبدال البيانات بنسخة جديدة تحتوي على التغييرات المطلوبة. إليك كيف سيبدو الأمر إذا قمت بتحوير مصفوفةsquares:
وهنا كيف سيبدو إذا قمت بتغيير البيانات دون تحوير مصفوفةsquares:
النتيجة هي نفسها ولكن بعدم التحوير (تغيير البيانات الأساسية) مباشرة، تكتسب عدة فوائد.
يجعل الثبات الميزات المعقدة أسهل بكثير في التنفيذ. لاحقًا في هذا البرنامج التعليمي، ستقوم بتنفيذ ميزة "السفر عبر الزمن" التي تتيح لك مراجعة تاريخ اللعبة و"العودة" إلى النقلات السابقة. هذه الوظيفة ليست خاصة بالألعاب - فالقدرة على التراجع وإعادة تنفيذ إجراءات معينة هي متطلب شائع للتطبيقات. تجنب تحوير البيانات مباشرة يتيح لك الاحتفاظ بالإصدارات السابقة من البيانات سليمة، وإعادة استخدامها لاحقًا.
هناك أيضًا فائدة أخرى للثبات. بشكل افتراضي، جميع المكونات الفرعية تعيد التصيير تلقائيًا عندما تتغير حالة المكون الأب. وهذا يشمل حتى المكونات الفرعية التي لم تتأثر بالتغيير. على الرغم من أن إعادة التصيير بحد ذاتها ليست ملحوظة للمستخدم (لا يجب أن تحاول تجنبها بنشاط!)، فقد ترغب في تخطي إعادة تصيير جزء من الشجرة الذي لم يتأثر بالتغيير بوضوح لأسباب تتعلق بالأداء. يجعل الثبات من الرخيص جدًا للمكونات مقارنة ما إذا كانت بياناتها قد تغيرت أم لا. يمكنك معرفة المزيد حول كيفية اختيار React متى تعيد تصيير مكون فيمرجع واجهة برمجة التطبيقات memo.
أخذ الأدوار
حان الوقت الآن لإصلاح عيب رئيسي في لعبة تيك تاك تو هذه: لا يمكن وضع علامات "O" على اللوحة.
ستضبط النقلة الأولى لتكون "X" بشكل افتراضي. دعنا نتابع هذا عن طريق إضافة قطعة أخرى من الحالة إلى مكون Board:
في كل مرة يتحرك فيها لاعب، سيتم قلب قيمةxIsNext(وهي قيمة منطقية) لتحديد اللاعب التالي وسيتم حفظ حالة اللعبة. ستقوم بتحديث دالةBoardالمسماةhandleClick لقلب قيمة xIsNext:
الآن، عند النقر على مربعات مختلفة، ستتناوب بينX و O، كما ينبغي!
ولكن انتظر، هناك مشكلة. حاول النقر على نفس المربع عدة مرات:

تمت الكتابة فوقX بواسطة O! بينما قد يضيف هذا لمسة مثيرة للاهتمام للعبة، سنلتزم بالقواعد الأصلية في الوقت الحالي.
عند وضع علامةX أو Oفي مربع، لا تقوم أولاً بالتحقق مما إذا كان المربع يحتوي بالفعل على قيمةXأوO. يمكنك إصلاح ذلك عن طريقالخروج مبكرًا. ستتحقق مما إذا كان المربع يحتوي بالفعل علىX أو O. إذا كان المربع ممتلئًا بالفعل، ستقوم بـreturnفي دالةhandleClickمبكرًا — قبل أن تحاول تحديث حالة اللوحة.
الآن يمكنك فقط إضافةXأوOإلى المربعات الفارغة! إليك ما يجب أن يبدو عليه الكود في هذه المرحلة:
إعلان الفائز
الآن بعد أن أصبح بإمكان اللاعبين التناوب، سترغب في إظهار متى يتم الفوز باللعبة ولا توجد المزيد من الأدوار. للقيام بذلك، ستضيف دالة مساعدة تسمىcalculateWinnerتأخذ مصفوفة من 9 مربعات، تتحقق من وجود فائز وتعيد'X'أو'O'أوnullحسب الاقتضاء. لا تقلق كثيرًا بشأن دالةcalculateWinner؛ فهي ليست خاصة بـ React:
ملاحظة
لا يهم ما إذا قمت بتعريفcalculateWinnerقبل أو بعدBoard. دعنا نضعها في النهاية حتى لا تضطر للتمرير عليها في كل مرة تقوم فيها بتعديل مكوناتك.
ستقوم باستدعاءcalculateWinner(squares)في دالةBoardالمسماةhandleClickللتحقق مما إذا كان لاعب قد فاز. يمكنك إجراء هذا التحقق في نفس الوقت الذي تتحقق فيه مما إذا كان المستخدم قد نقر على مربع يحتوي بالفعل علىX أو O. نود الخروج مبكرًا في كلتا الحالتين:
لإعلام اللاعبين بانتهاء اللعبة، يمكنك عرض نص مثل "الفائز: X" أو "الفائز: O". للقيام بذلك ستضيف قسمstatusإلى مكونBoard. سيعرض الحالة الفائز إذا انتهت اللعبة، وإذا كانت اللعبة مستمرة فستعرض دور أي لاعب هو التالي:
تهانينا! لديك الآن لعبة تيك تاك تو تعمل. وقد تعلمت أساسيات React أيضًا. إذنأنتالفائز الحقيقي هنا. إليك كيف يجب أن يبدو الكود:
إضافة السفر عبر الزمن
كتمرين أخير، دعنا نجعل من الممكن "العودة بالزمن" إلى النقلات السابقة في اللعبة.
تخزين سجل النقلات
إذا قمت بتغيير مصفوفةsquares، سيكون تنفيذ السفر عبر الزمن صعبًا جدًا.
ومع ذلك، استخدمتslice()لإنشاء نسخة جديدة من مصفوفةsquaresبعد كل نقلة، وعاملتها على أنها غير قابلة للتغيير. سيسمح لك ذلك بتخزين كل نسخة سابقة من مصفوفةsquares، والتنقل بين الأدوار التي حدثت بالفعل.
ستقوم بتخزين مصفوفاتsquaresالسابقة في مصفوفة أخرى تسمىhistory، والتي ستخزنها كمتغير حالة جديد. تمثل مصفوفةhistoryجميع حالات اللوحة، من النقلة الأولى إلى الأخيرة، ولها شكل مثل هذا:
رفع الحالة للأعلى مرة أخرى
ستكتب الآن مكونًا جديدًا من المستوى الأعلى يسمىGameلعرض قائمة بالنقلات السابقة. هذا هو المكان الذي ستضع فيه حالةhistoryالتي تحتوي على سجل اللعبة بأكمله.
سيسمح وضع حالةhistoryفي مكونGameبإزالة حالةsquaresمن مكونه الفرعيBoard. تمامًا كما "رفعت الحالة للأعلى" من مكونSquareإلى مكونBoard، ستقوم الآن برفعها منBoardإلى مكون المستوى الأعلىGame. هذا يمنح مكونGameسيطرة كاملة على بياناتBoardويتيح له توجيهBoardلعرض الأدوار السابقة منhistory.
أولاً، أضف مكونGame مع export default. اجعله يعرض مكونBoardوبعض الترميز:
لاحظ أنك تقوم بإزالة الكلمات المفتاحيةexport defaultقبل تعريفfunction Board() {وإضافتها قبل تعريفfunction Game() {. هذا يخبر ملفindex.jsالخاص بك باستخدام مكونGameكمكون المستوى الأعلى بدلاً من مكونBoardالخاص بك. عناصرdivالإضافية التي يُرجعها مكونGameتُعد مكانًا لمعلومات اللعبة التي ستضيفها إلى اللوحة لاحقًا.
أضف بعض الحالة إلى مكونGameلتتبع اللاعب التالي وتاريخ الحركات:
لاحظ كيف أن[Array(9).fill(null)]عبارة عن مصفوفة تحتوي على عنصر واحد، وهو نفسه مصفوفة من 9 قيمnull.
لتقديم المربعات الخاصة بالحركة الحالية، ستحتاج إلى قراءة آخر مصفوفة مربعات منhistory. لا تحتاج إلىuseStateلهذا—لديك بالفعل معلومات كافية لحسابها أثناء التقديم:
بعد ذلك، أنشئ دالةhandlePlayداخل مكونGameوالتي سيتم استدعاؤها بواسطة مكونBoardلتحديث اللعبة. مررxIsNextوcurrentSquares وhandlePlayكخصائص إلى مكونBoard:
لنجعل مكونBoardخاضعًا بالكامل للخصائص التي يتلقاها. غيّر مكونBoardليأخذ ثلاث خصائص:xIsNextوsquaresودالة جديدةonPlayيمكن لمكونBoardاستدعاؤها بمصفوفة المربعات المحدثة عندما يقوم اللاعب بحركة. بعد ذلك، أزل السطرين الأولين من دالةBoardاللذين يستدعيانuseState:
الآن استبدل استدعاءاتsetSquares وsetXIsNext في handleClickداخل مكونBoardباستدعاء واحد لدالتك الجديدةonPlayحتى يتمكن مكونGameمن تحديثBoardعندما ينقر المستخدم على مربع:
مكونBoardخاضع بالكامل للخصائص التي يمررها إليه مكونGame. تحتاج إلى تنفيذ دالةhandlePlayفي مكونGameلجعل اللعبة تعمل مرة أخرى.
ماذا يجب أن تفعلhandlePlayعند استدعائها؟ تذكر أن مكون Board كان يستدعيsetSquaresبمصفوفة محدثة؛ الآن يمرر مصفوفةsquaresالمحدثة إلىonPlay.
تحتاج دالةhandlePlayإلى تحديث حالةGameلإحداث إعادة تقديم، لكن لم تعد لديك دالةsetSquaresيمكنك استدعاؤها بعد الآن—أنت الآن تستخدم متغير الحالةhistoryلتخزين هذه المعلومات. سترغب في تحديثhistoryعن طريق إلحاق مصفوفةsquaresالمحدثة كمدخل جديد في السجل. كما تريد تبديلxIsNext، تمامًا كما كان يفعل مكون Board سابقًا:
هنا،[...history, nextSquares]تنشئ مصفوفة جديدة تحتوي على جميع العناصر فيhistory، متبوعة بـnextSquares. (يمكنك قراءة...historyبناء الجملة الانتشاريعلى أنه "تعداد جميع العناصر فيhistory".)
على سبيل المثال، إذا كانتhistoryهي[[null,null,null], ["X",null,null]] و nextSquaresهي["X",null,"O"]، فإن المصفوفة الجديدة[...history, nextSquares]ستكون[[null,null,null], ["X",null,null], ["X",null,"O"]].
في هذه المرحلة، قمت بنقل الحالة لتعيش في مكونGame، ويجب أن تكون واجهة المستخدم تعمل بالكامل، تمامًا كما كانت قبل إعادة الهيكلة. إليك ما يجب أن يبدو عليه الكود في هذه المرحلة:
عرض الحركات السابقة
بما أنك تقوم بتسجيل تاريخ لعبة تيك تاك تو، يمكنك الآن عرض قائمة بالحركات السابقة للاعب.
عناصر React مثل<button>هي كائنات JavaScript عادية؛ يمكنك تمريرها في تطبيقك. لعرض عناصر متعددة في React، يمكنك استخدام مصفوفة من عناصر React.
لديك بالفعل مصفوفة من حركاتhistoryفي الحالة، لذا تحتاج الآن إلى تحويلها إلى مصفوفة من عناصر React. في JavaScript، لتحويل مصفوفة إلى أخرى، يمكنك استخدامطريقة map للمصفوفات:
ستستخدمmapلتحويلhistoryمن الحركات إلى عناصر React تمثل أزرارًا على الشاشة، وعرض قائمة من الأزرار للـ"قفز" إلى الحركات السابقة. دعنا نستخدمmapعلىhistoryفي مكون Game:
يمكنك رؤية ما يجب أن يبدو عليه الكود أدناه. لاحظ أنه يجب أن ترى خطأً في وحدة تحكم أدوات المطور يقول:
ستصلح هذا الخطأ في القسم التالي.
أثناء تكرارك عبر مصفوفةhistoryداخل الدالة التي مررتها إلىmap، يمر الوسيطsquaresعبر كل عنصر من عناصرhistory، ويمر الوسيطmoveعبر كل فهرس في المصفوفة:0،1،2، …. (في معظم الحالات، ستحتاج إلى عناصر المصفوفة الفعلية، ولكن لتقديم قائمة بالحركات ستحتاج فقط إلى الفهارس.)
لكل حركة في سجل لعبة تيك تاك تو، تنشئ عنصر قائمة<li>يحتوي على زر<button>. يحتوي الزر على معالجonClickيستدعي دالة تسمىjumpTo(والتي لم تنفذها بعد).
في الوقت الحالي، يجب أن ترى قائمة بالحركات التي حدثت في اللعبة وخطأ في وحدة تحكم أدوات المطور. دعنا نناقش ما يعنيه خطأ "المفتاح".
اختيار مفتاح
عند تقديم قائمة، يقوم React بتخزين بعض المعلومات حول كل عنصر قائمة مُقدم. عند تحديث قائمة، يحتاج React إلى تحديد ما الذي تغير. ربما تكون قد أضفت أو أزلت أو أعيد ترتيب أو قمت بتحديث عناصر القائمة.
تخيل الانتقال من
إلى
بالإضافة إلى الأعداد المحدثة، قد يقول قارئ بشري أنك قمت بتبديل ترتيب أليكسا وبن وإدراج كلوديا بين أليكسا وبن. ومع ذلك، فإن React هو برنامج حاسوبي ولا يعرف ما كنت تنوي، لذا تحتاج إلى تحديد خاصيةkeyلكل عنصر قائمة لتمييز كل عنصر قائمة عن أشقائه. إذا كانت بياناتك من قاعدة بيانات، يمكن استخدام معرفات قاعدة بيانات أليكسا وبن وكلوديا كمفاتيح.
عند إعادة تقديم قائمة، يأخذ React مفتاح كل عنصر قائمة ويبحث في عناصر القائمة السابقة عن مفتاح مطابق. إذا كانت القائمة الحالية تحتوي على مفتاح لم يكن موجودًا من قبل، فإن React ينشئ مكونًا. إذا كانت القائمة الحالية تفتقد مفتاحًا كان موجودًا في القائمة السابقة، فإن React يدمر المكون السابق. إذا تطابق مفتاحان، يتم نقل المكون المقابل.
تخبر المفاتيح React عن هوية كل مكون، مما يسمح لـ React بالحفاظ على الحالة بين عمليات إعادة التقديم. إذا تغير مفتاح المكون، فسيتم تدمير المكون وإعادة إنشائه بحالة جديدة.
keyهي خاصية خاصة ومحجوزة في React. عند إنشاء عنصر، يستخرج React خاصيةkeyويخزن المفتاح مباشرة على العنصر المُعاد. على الرغم من أنkeyقد تبدو وكأنها تم تمريرها كـ props، فإن React يستخدمkeyتلقائيًا لتحديد المكونات التي سيتم تحديثها. لا توجد طريقة للمكون لمعرفة أيkeyحدده المكون الأصلي.
يوصى بشدة بتعيين مفاتيح مناسبة كلما قمت ببناء قوائم ديناميكية.إذا لم يكن لديك مفتاح مناسب، فقد ترغب في النظر في إعادة هيكلة بياناتك بحيث يكون لديك واحد.
إذا لم يتم تحديد مفتاح، سيقوم React بالإبلاغ عن خطأ واستخدام فهرس المصفوفة كمفتاح افتراضيًا. استخدام فهرس المصفوفة كمفتاح يمثل مشكلة عند محاولة إعادة ترتيب عناصر القائمة أو إدراج/إزالة عناصر القائمة. تمريرkey={i}بشكل صريح يكتم الخطأ ولكن له نفس مشاكل فهارس المصفوفة ولا يوصى به في معظم الحالات.
لا تحتاج المفاتيح إلى أن تكون فريدة عالميًا؛ فهي تحتاج فقط إلى أن تكون فريدة بين المكونات وأشقائها.
تنفيذ السفر عبر الزمن
في سجل لعبة تيك تاك تو، لكل حركة سابقة معرف فريد مرتبط بها: وهو الرقم التسلسلي للحركة. لن يتم إعادة ترتيب الحركات أو حذفها أو إدراجها في المنتصف أبدًا، لذا من الآمن استخدام فهرس الحركة كمفتاح.
في دالةGame، يمكنك إضافة المفتاح كـ<li key={move}>، وإذا قمت بإعادة تحميل اللعبة المُقدمة، يجب أن يختفي خطأ "المفتاح" في React:
قبل أن تتمكن من تنفيذjumpTo، تحتاج مكونGameإلى تتبع الخطوة التي يشاهدها المستخدم حاليًا. للقيام بذلك، عرّف متغير حالة جديد يسمىcurrentMove، ويأخذ القيمة الافتراضية0:
بعد ذلك، قم بتحديث دالةjumpTo داخل GameلتحديثcurrentMove. ستقوم أيضًا بتعيينxIsNextإلىtrueإذا كان الرقم الذي تقوم بتغييرcurrentMoveإليه زوجيًا.
ستقوم الآن بإجراء تغييرين على دالةGameالمسماةhandlePlayوالتي يتم استدعاؤها عند النقر على مربع.
- إذا "عدت بالزمن إلى الوراء" ثم قمت بتنفيذ حركة جديدة من تلك النقطة، فأنت تريد الاحتفاظ بالسجل فقط حتى تلك النقطة. بدلاً من إضافة
nextSquaresبعد جميع العناصر (بناء جملة الانتشار...) فيhistory، ستضيفها بعد جميع العناصر فيhistory.slice(0, currentMove + 1)بحيث تحتفظ فقط بذلك الجزء من السجل القديم. - في كل مرة يتم فيها تنفيذ حركة، تحتاج إلى تحديث
currentMoveللإشارة إلى أحدث إدخال في السجل.
أخيرًا، ستقوم بتعديل مكونGameلتقديم الحركة المحددة حاليًا، بدلاً من تقديم الحركة الأخيرة دائمًا:
إذا نقرت على أي خطوة في سجل اللعبة، يجب أن يتم تحديث لوحة تيك تاك تو على الفور لإظهار شكل اللوحة بعد حدوث تلك الخطوة.
التنظيف النهائي
إذا نظرت إلى الكود عن كثب، قد تلاحظ أنxIsNext === trueعندما يكونcurrentMoveزوجيًا وxIsNext === falseعندما يكونcurrentMoveفرديًا. بعبارة أخرى، إذا كنت تعرف قيمةcurrentMove، فيمكنك دائمًا معرفة ما يجب أن تكون عليهxIsNext.
لا يوجد سبب لتخزين كلاهما في الحالة. في الواقع، حاول دائمًا تجنب الحالة الزائدة عن الحاجة. تبسيط ما تخزنه في الحالة يقلل من الأخطاء ويجعل كودك أسهل في الفهم. قم بتغييرGameبحيث لا يخزنxIsNextكمتغير حالة منفصل وبدلاً من ذلك يحسبه بناءً علىcurrentMove:
لم تعد بحاجة إلى تعريف حالةxIsNextأو استدعاءاتsetXIsNext. الآن، لا توجد فرصة لأن يصبحxIsNextغير متزامن معcurrentMove، حتى لو ارتكبت خطأ أثناء برمجة المكونات.
الختام
تهانينا! لقد أنشأت لعبة تيك تاك تو التي:
- تتيح لك لعب تيك تاك تو،
- تشير عندما يفوز لاعب باللعبة،
- تخزن تاريخ اللعبة مع تقدمها،
- تسمح للاعبين بمراجعة تاريخ اللعبة ورؤية النسخ السابقة من لوحة اللعبة.
عمل رائع! نأمل أن تشعر الآن بأن لديك فهمًا جيدًا لكيفية عمل React.
تحقق من النتيجة النهائية هنا:
إذا كان لديك وقت إضافي أو ترغب في ممارسة مهاراتك الجديدة في React، إليك بعض الأفكار لتحسينات يمكنك إجراؤها على لعبة تيك تاك تو، مدرجة بترتيب صعوبة متزايدة:
- للنقلة الحالية فقط، اعرض "أنت في النقلة رقم..." بدلاً من زر.
- أعد كتابة
Boardلاستخدام حلقتين لإنشاء المربعات بدلاً من ترميزها يدويًا. - أضف زر تبديل يسمح لك بترتيب النقلات بترتيب تصاعدي أو تنازلي.
- عندما يفوز أحد، قم بتظليل المربعات الثلاثة التي تسببت في الفوز (وعندما لا يفوز أحد، اعرض رسالة تفيد بأن النتيجة تعادل).
- اعرض موقع كل نقلة بالتنسيق (صف، عمود) في قائمة سجل النقلات.
خلال هذا البرنامج التعليمي، تطرقت إلى مفاهيم React بما في ذلك العناصر والمكونات والخصائص والحالة. الآن بعد أن رأيت كيف تعمل هذه المفاهيم عند بناء لعبة، تحقق منالتفكير في Reactلترى كيف تعمل نفس مفاهيم React عند بناء واجهة مستخدم تطبيق.
