[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


<<:  找LeetCode上简单的题目来撑过30天啦(DAY16)

>>:  Day 16: LeetCode 698. Partition to K Equal Sum Subsets

DAY2 序

第一页、第六页、第七页 序。 序放在第一页後面也是完全OKです! 因为第一天讲太多废话了,完全没有讲...

json档删除符合条件的特定事件该怎麽做?

大家好,我是用python程序 在输出json档之後想做两件事但没有头绪,希望有人可以帮我解惑。 以...

Day3 用基本功写一支简单的程序

今天我们要实作一支程序------计算平行四边形面积的程序。 题目如下:假设三角形的 底为a,高为b...

模型架构--1

GoogLeNet Google提出的GoogLeNet,层数比较多,运算的效率相当好,超参数数量比...

Day11-动态元件

这章节是延伸v-if和v-show管理元件,如何用更简便的方式做tab页签。 v-bind:is 做...