iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
自我挑戰組

Re: 從 Next.js 開始的 React 生活系列 第 29

[Day29] 倒數第二天~集大成!Next.js + React + Bootstrap + Reactstrap 十八般武藝(?)樣樣來,勇敢的上吧!

前言

因為今天已經來到倒數第二天!
覺得應該要把之前介紹過的東西全部融合在一起展現出來,
也算是成果展吧XD

本日正文

今天要用 Next.js 搭配 Bootstrap + React + Reactstrap,
做出問答小遊戲!
然後今天原諒我可能沒辦法像前幾天那樣講得很細Q_Q

工欲善其事,必先利其器

之前我們只有把 Next.js 的環境佈好,
但今天還會用到 Bootstrap 跟 Reactstrap,
因此我們也需要在專案目錄下進行安裝,
先移到 nextjs-blog 資料夾底下,
執行以下指令:

npm install bootstrap@next
npm install reactstrap

https://ithelp.ithome.com.tw/upload/images/20211001/20129873UoJOVAGXzD.png

再來就是在 pages 資料夾下新增頁面 game.jsx
game.jsx 最前面記得 import,
像這樣:

import React, { useState, useEffect } from "react";
import { Container, Row, Col, Button } from "reactstrap"; //這邊放你會用到的 component
import 'bootstrap/dist/css/bootstrap.css';

不要想一步登天,在 React 以前先寫死資料把基本架構弄出來再說

雖然最後的 code 看起來已經用 React 的方式寫好了,
不過我個人習慣都會是先寫死,
先把架構弄出來,
之後再把它改成 React 語法就不會是太大的問題,
像這樣:

<Container className="w-50 my-4">
      <div className="d-flex flex-column align-items-center">
            <h3>本屆東京奧運台灣的表現相當亮眼,締造隊史最佳參賽成績。最終奪牌數以下何者正確?</h3>
            <img width="300rem" src={`img/01.jpg`}/>
              <Row md={2} className="mt-4">
                  <Col className="my-2">
                    <Button size="sm" color="info" className="w-50 px-2 py-3">
                      1金、2銀、3銅
                    </Button>
                  </Col>
                  <Col className="my-2">
                    <Button size="sm" color="info" className="w-50 px-2 py-3">
                      2金、4銀、6銅
                    </Button>
                  </Col>
                  <Col className="my-2">
                    <Button size="sm" color="info" className="w-50 px-2 py-3">
                      2金、4銀、8銅
                    </Button>
                  </Col>
                  <Col className="my-2">
                    <Button size="sm" color="info" className="w-50 px-2 py-3">
                      3金、3銀、6銅
                    </Button>
                  </Col>
              </Row>
		</div>
</Container>

https://ithelp.ithome.com.tw/upload/images/20211001/20129873WSnJ5Yh8wP.png

遊戲架構分成三階段

再來因為我想要分成三個畫面:開始遊戲前、遊戲中、遊戲結束,
所以我宣告了一個 status 的狀態,分別為:initial, start, end
(PS. 遊戲中才是遊戲的核心,所以之前的寫死資料我只試遊戲中的部份而已)

... (略)
const [status, setStatus] = useState('initial');
{status === 'initial' && (
... (這邊放開始遊戲前的歡迎畫面)
)}
{status === 'start' && (
... (這邊放開始遊戲的畫面)
)}
{status === 'end' && (
... (這邊放遊戲結束的畫面)
)}

準備題庫跟選項

再來我新增了data的資料夾,
裡面放 question.json
https://ithelp.ithome.com.tw/upload/images/20211001/20129873IX6AuF1USR.png

內容就是題庫跟選項,
像這樣:

[
    {
        "question": "本屆東京奧運台灣的表現相當亮眼,締造隊史最佳參賽成績。最終奪牌數以下何者正確?",
        "img": "01.jpg",
        "options": ["1金、2銀、3銅","2金、4銀、6銅","2金、4銀、8銅","3金、3銀、6銅"],
        "answer": 2
    },
    {
        "question": "以下運動項目與本屆奧運台灣出賽選手的配對,何者「有誤」?",
        "img": "02.png",
        "options": ["柔道─楊勇緯","舉重─郭婞淳","射箭─湯智鈞","桌球─戴資穎"],
        "answer": 4
    },
	... (略)
]

這樣的架構大家應該不陌生,裡面就是物件的陣列,
每個物件分別有 question, img, options, answer 這四個屬性,
分別為題目、圖片、選項、正確答案。

而在前面 import 的地方記得將 json 檔引入,

import questions from '../data/question.json';

利用 map 一個一個讀取題庫

既然前面都學過 map 了,我們當然不會想要 hard code 把每一題寫死在頁面上,
這邊我先貼這部份的程式,然後再進行說明:

{status === 'start' ? (
        <>
          {questions.map((q, index_q) => (
            <>
            {index_q === selectedIndex ? (
              <div className="d-flex flex-column align-items-center">
              <h3>Q{index_q+1}. {q.question}</h3>
              <img className="my-4" width="300rem" src={`img/${q.img}`}/>
                <Row md={2}>
                {q.options.map((o, index_o) => (
                  <>
                    <Col className="my-2">
                      <Button size="sm" color="info" value={index_o+1} className={`${styles.btn} px-2 py-3`}
                      onClick={(e) => {
                        handleResponse(e);
                        setSelectedIndex((prev) => prev+1);
                        }}>
                        {index_o+1}) {o}
                      </Button>
                    </Col>
                  </>
                ))}
                </Row>
              </div>
            ): null}
            </>
          ))}
        </>
      ): null}

{questions.map((q, index_q) => (
questions 就是先前引入的 json 裡面的物件陣列,
而這邊希望是一題一題出現,
因此我在前面有宣告一個索引,用來記錄現在進行到哪一題:

const [selectedIndex, setSelectedIndex] = useState(0);

所以下面才會有 {index_q === selectedIndex ? (
例如現在進行到第1題,只要顯示第1題的內容。(索引值為0)

<h3>Q{index_q+1}. {q.question}</h3>
再來這邊是顯示題目內容,
這邊應該還算直覺,
再下來又出現一個 map 了!
原因是因為選項(options)也是一個陣列(每一題有4個選項),
題目都可以用 map 讀出來了,
選項當然也可以呀~
因此這邊才會這樣寫:

 {q.options.map((o, index_o) => (
 <>
... (略)
  </>
))}

每一題點擊選項後,進入下一題並計算得分

以下是選項的部份:

<Col className="my-2">
  <Button size="sm" color="info" value={index_o+1} className={`${styles.btn} px-2 py-3`}
  onClick={(e) => {
	handleResponse(e);
	setSelectedIndex((prev) => prev+1);
	}}>
	{index_o+1}) {o}
  </Button>
</Col>

首先看到 onClick 裡面有兩行,
setSelectedIndex((prev) => prev+1) 這行就是將目前進行的題目索引值+1,
再來是 handleResponse(e) 這個函數主要就是進行分數的計算,
這個函數我是這麼寫的:

const handleResponse = (event) => {
    if ( parseInt(event.target.value) === questions[selectedIndex].answer ){
      alert("恭喜答對!");
      setTotalpoints((prev) => prev+1);
    } else {
      alert("可惜!請再接再勵!");
    }
  }

event.target.value(點擊目標的 value) 等於 questions[selectedIndex].answer (現在進行題目的正確答案),
分數就要+1 setTotalpoints((prev) => prev+1)

當題目結束,要遊戲狀態切換到結束並讓畫面改變

這邊應該是這個遊戲最關鍵的地方,
當你前面寫完,發現每一題都很順利的進行,
可是結束最後一題後畫面變成空白了,

這是因為 questions 裡面的陣列已經全部結束了,
它沒有內容可以顯示只能顯示空白給你看啊XD

但是遊戲結束並不是點擊哪個按鈕之類就是結束遊戲,
因此不適合使用 useState,
所以這邊我們要用到 useEffect 了!

useEffect(() => {
    if ( selectedIndex === questions.length ){
      setStatus('end');
    }
  },[selectedIndex]);

偵測題目索引值的變化,
一旦 selectedIndex 等於 questions.length(題庫陣列長度) 表示遊戲結束了,
因此要把狀態切換成 end setStatus('end')
一旦狀態為 end,
就要顯示遊戲結束以及顯示總得分,
像這樣:

{status === 'end' && (
        <div className="d-flex flex-column align-items-center">
          <h2>遊戲結束!</h2>
          <img className="my-2" width="500rem" src="img/end.jpg" />
          <h3>
            共獲得 <span className="h1 text-danger">{totalpoints}</span> 分
          </h3>
          <Button color="secondary" onClick={resetGame}>
            重新遊戲
          </Button>
        </div>
      )}

一些「眉角」

1. img 放在 public 資料夾底下

前面的地方都有用到 <img> 因此你可能有發現 src 的設定不是網址,
而是相對位置的檔案位址 src="img/end.jpg"
介紹一下 Next.js 的檔案架構:
https://ithelp.ithome.com.tw/upload/images/20211001/20129873V9QgbWaGVk.png

圖片供外部存取的檔案都會放在 public 資料夾底下,
因此你可以直接使用 檔名.副檔名 就可以指到該檔案的位址,
至於前面多了 img/ 是因為我把圖片放在 img 的資料夾底下,
像這樣:
https://ithelp.ithome.com.tw/upload/images/20211001/201298730VGH9f8J4S.png

2. CSS 必須取名為XXX.module.css

我們之前在 Codesandbox import CSS 是這樣寫的:

import "./styles.css";

可是當你在 Next.js 引入 CSS 這樣寫卻會出錯:

import styles from '../styles/styles.css';

https://ithelp.ithome.com.tw/upload/images/20211001/20129873BLTIvFYYuD.png

原因是 XXX.css 在 Next.js 被視為是 Global CSS,
詳細可看官方文件說明→ Global CSS Must Be in Your Custom <App>
如果你真的要自己寫 CSS,
只能將檔案命名為 XXX.module.css 了,
像這樣:

import styles from '../styles/styles.module.css';

https://ithelp.ithome.com.tw/upload/images/20211001/201298735J4RIR3McP.png

而使用 CSS 的語法是這樣:

... className={`${styles.btn} px-2 py-3`} ...

如果 className 只有要放一個單獨自訂的 class,語法這樣寫就好:className={styles.btn}
上面這個寫法是因為還有跟其他 Bootstrap 的 class 擺在一起才要這樣寫。(ES6 的寫法)

一樣來玩玩看吧XD

[10/4-補充] 因為鐵人賽結束之後我應該不太會再打開這個服務了,
所以這邊還是補一下 CodeSandbox 的連結,
如果想要自己玩玩看的,
不想先被我破梗的可以右轉至此→ Day29 - React 奧運小學堂

------ 我是貼心的防雷設置 ------
------ 我是貼心的防雷設置 ------
------ 我是貼心的防雷設置 ------

然後下面是我試玩的畫面XD
(不過因為題目答案都是我自己用的,所以當然是全部答對XD)

後記

這個系列文可以用這個小遊戲當成結尾真是太好了!!!!!!!
中間一度以為自己寫不出來,
算是及格過關了XD
接著就開心迎接明天的完結篇吧XD
https://ithelp.ithome.com.tw/upload/images/20211001/20129873VmjysJSx7s.png


上一篇
[Day28] 又回到最初的起點 ~ Next.js
下一篇
[Day30] 終於來到了這一天 ~ 第二次鐵人賽完賽心得 && 梳理學習順序
系列文
Re: 從 Next.js 開始的 React 生活30

尚未有邦友留言

立即登入留言