從0開始,手把手教你使用React開發答題App
專案演示地址
專案原始碼
其他版本教程
專案程式碼結構
前言
React 框架的優雅不言而喻,元件化的程式設計思想使得React框架開發的專案程式碼簡潔,易懂,但早期 React 類元件的寫法略顯繁瑣。React Hooks 是 React 16.8 釋出以來最吸引人的特性之一,她簡化了原有程式碼的編寫,是未來 React 應用的主流寫法。
本文通過一個實戰小專案,手把手從零開始帶領大家快速入門React Hooks。本專案線上演示地址:
在本專案中,會用到以下知識點:
- React 元件化設計思想
- React State 和 Props
- React 函式式元件的使用
- React Hooks useState 的使用
- React Hooks useEffect 的使用
- React 使用 Axios 請求遠端介面獲取問題及答案
- React 使用Bootstrap美化介面
Hello React
(1)安裝node.js 官網連結
(2)安裝vscode 官網連結
(3)安裝 creat-react-app 功能元件,該元件可以用來初始化一個專案, 即按照一定的目錄結構,生成一個新專案。
開啟cmd 視窗 輸入:
npm install --g create-react-app
npm install --g yarn
(-g 代表全域性安裝)
如果安裝失敗或較慢。需要換源,可以使用淘寶NPM映象,設定方法為:
npm config set registry https://registry.npm.taobao.org
設定完成後,重新執行
npm install --g create-react-app
npm install --g yarn
(4)在你想建立專案的目錄下 例如 D:/project/ 開啟cmd命令 輸入
create-react-app react-exam
去使用creat-react-app命令建立名字是react-exam的專案
安裝完成後,移至新建立的目錄並啟動專案
cd react-exam
yarn start
一旦執行此命令,localhost:3000新的React應用程式將彈出一個新視窗。
專案目錄結構
右鍵react-exam目錄,使用vscode開啟該目錄。
react-exam專案目錄中有一個/public和/src目錄,以及node_modules,.gitignore,README.md,和package.json。
在目錄/public
中,重要檔案是index.html
,其中一行程式碼最重要
<div id="root"></div>
該div做為我們整個應用的掛載點
/src
目錄將包含我們所有的React程式碼。
要檢視環境如何自動編譯和更新您的React程式碼,請找到檔案/src/App.js
:
將其中的
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
修改為
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
和豆約翰 Learn React
</a>
儲存檔案後,您會注意到localhost:3000
編譯並重新整理了新資料。
React-Exam專案實戰
1. 首頁製作
1.安裝專案依賴,在package.json中新增:
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"axios": "^0.19.2",
"bootstrap": "^4.5.0",
"he": "^1.2.0",
"react-loading": "^2.0.3",
"reactstrap": "^8.4.1"
},
執行命令:
yarn install
修改index.js,匯入bootstrap樣式
import "bootstrap/dist/css/bootstrap.min.css";
修改App.css程式碼
html {
width: 80%;
margin-left: 10%;
margin-top: 2%;
}
.ansButton {
margin-right: 4%;
margin-top: 4%;
}
修改App.js,引入Quiz元件
import React from 'react';
import './App.css'
import { Quiz } from './Exam/Quiz';
function App() {
return (
<div className = 'layout'>
<Quiz></Quiz>
</div>
);
}
export default App;
在專案src目錄下新增Exam目錄,Exam目錄中新建Quiz.js
Quiz元件的定義如下:
Quiz.js,引入開始頁面元件Toggle。
import React, { useState } from "react";
import { Toggle } from "./Toggle";
export const Quiz = () => {
const [questionData, setQuestionData] = useState([]);
const questions = questionData.map(({ question }) => [question]);
const answers = questionData.map(({ incorrect_answers, correct_answer }) =>
[correct_answer, incorrect_answers].flat()
);
return (
<>
<Toggle
setQuestionData={setQuestionData}
/>
</>
);
};
Toggle.js,點選開始按鈕,通過axios訪問遠端介面,獲得題目及答案。
import React from "react";
import axios from "axios";
import ToggleHeader from "./ToggleHeader";
import {
Button,
Form,
} from "reactstrap";
export const Toggle = ({
setQuestionData,
}) => {
const getData = async () => {
try {
const incomingData = await axios.get(
`https://opentdb.com/api.php?amount=10&category=18&difficulty=easy&type=multiple`
);
setQuestionData(incomingData.data.results);
} catch (err) {
console.error(err);
}
};
return (
<>
<ToggleHeader />
<Form
onSubmit={(e) => {
e.preventDefault();
getData();
}}
>
<Button color="primary">開始</Button>
</Form>
</>
);
};
ToggleHeader.js
import React from "react";
import { Jumbotron, Container} from "reactstrap";
export default function ToggleHeader() {
return (
<Jumbotron fluid>
<Container fluid>
<h1 className="display-4">計算機知識小測驗</h1>
</Container>
</Jumbotron>
);
}
https://opentdb.com/api.php介面返回的json資料格式為
{
"response_code": 0,
"results": [{
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "The numbering system with a radix of 16 is more commonly referred to as ",
"correct_answer": "Hexidecimal",
"incorrect_answers": ["Binary", "Duodecimal", "Octal"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "This mobile OS held the largest market share in 2012.",
"correct_answer": "iOS",
"incorrect_answers": ["Android", "BlackBerry", "Symbian"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "How many values can a single byte represent?",
"correct_answer": "256",
"incorrect_answers": ["8", "1", "1024"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "In computing, what does MIDI stand for?",
"correct_answer": "Musical Instrument Digital Interface",
"incorrect_answers": ["Musical Interface of Digital Instruments", "Modular Interface of Digital Instruments", "Musical Instrument Data Interface"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "In computing, what does LAN stand for?",
"correct_answer": "Local Area Network",
"incorrect_answers": ["Long Antenna Node", "Light Access Node", "Land Address Navigation"]
}]
}
程式執行效果:
當前專案目錄結構為:
2. 問題展示頁面
Quiz.js,新增toggleView變數用來切換檢視。
const [toggleView, setToggleView] = useState(true);
Quiz.js,其中Question和QuestionHeader 元件,參見後面。
import { Question } from "./Question";
import { Jumbotron } from "reactstrap";
import QuestionHeader from "./QuestionHeader";
...
export const Quiz = () => {
var [index, setIndex] = useState(0);
const [questionData, setQuestionData] = useState([]);
...
return (
<>
{toggleView && (
<Toggle
setIndex={setIndex}
setQuestionData={setQuestionData}
setToggleView={setToggleView}
/>
)}
{!toggleView &&
(
<Jumbotron>
<QuestionHeader
setToggleView={setToggleView}
/>
<Question question={questions[index]} />
</Jumbotron>
)}
</>
);
使用index控制題目索引
var [index, setIndex] = useState(0);
修改Toggle.js
獲取完遠端資料,通過setToggleView(false);切換檢視。
export const Toggle = ({
setQuestionData,
setToggleView,
setIndex,
}) => {
...
return (
<>
<ToggleHeader />
<Form
onSubmit={(e) => {
e.preventDefault();
getData();
setToggleView(false);
setIndex(0);
}}
>
<Button color="primary">開始</Button>
</Form>
</>
);
};
QuestionHeader.js程式碼:
同樣的,點選 返回首頁按鈕 setToggleView(true),切換檢視。
import React from "react";
import { Button } from "reactstrap";
export default function QuestionHeader({ setToggleView, category }) {
return (
<>
<Button color="link" onClick={() => setToggleView(true)}>
返回首頁
</Button>
</>
);
}
Question.js程式碼
接受父元件傳過來的question物件,並顯示。
其中he.decode是對字串中的特殊字元進行轉義。
import React from "react";
import he from "he";
export const Question = ({ question }) => {
// he is a oddly named library that decodes html into string values
var decode = he.decode(String(question));
return (
<div>
<hr className="my-2" />
<h1 className="display-5">
{decode}
</h1>
<hr className="my-2" />
<br />
</div>
);
};
程式執行效果:
首頁
點選開始後,顯示問題:
當前專案目錄結構為:
3. 載入等待動畫
新增LoadingSpin.js
import React from "react";
import { Spinner } from "reactstrap";
export default function LoadingSpin() {
return (
<>
<Spinner type="grow" color="primary" />
<Spinner type="grow" color="secondary" />
<Spinner type="grow" color="success" />
<Spinner type="grow" color="danger" />
</>
);
}
修改Quiz.js
import LoadingSpin from "./LoadingSpin";
export const Quiz = () => {
const [isLoading, setLoading] = useState(false);
return (
<>
{toggleView && (
<Toggle
...
setLoading={setLoading}
/>
)}
{!toggleView &&
(isLoading ? (
<LoadingSpin />
) :
(
...
))}
</>
);
};
修改Toggle.js
export const Toggle = ({
...
setLoading,
}) => {
const getData = async () => {
try {
setLoading(true);
const incomingData = await axios.get(
`https://opentdb.com/api.php?amount=10&category=18&difficulty=easy&type=multiple`
);
setQuestionData(incomingData.data.results);
setLoading(false);
} catch (err) {
console.error(err);
}
};
...
};
執行效果:
目前程式碼結構:
4. 實現下一題功能
新增Answer.js,使用者點選下一題按鈕,修改index,觸發主介面重新整理,顯示下一題:
import React from "react";
import { Button } from "reactstrap";
export const Answer = ({ setIndex, index }) => {
function answerResult() {
setIndex(index + 1);
}
return (
<Button className="ansButton" onClick={answerResult}>
下一題
</Button>
);
};
修改Quiz.js,新增Answer元件:
import { Answer } from "./Answer";
...
{!toggleView &&
(isLoading ? (
<LoadingSpin />
) :
(
<Jumbotron>
...
<Answer
setIndex={setIndex}
index={index}
/>
</Jumbotron>
))}
執行效果:
點選下一題:
5. 實現選項展示
新增AnswerList.js。
通過屬性answers傳進來的選項列表,需要被打亂順序(shuffle )
import React from "react";
import { Answer } from "./Answer";
export const AnswerList = ({ answers, index, setIndex }) => {
if (answers) var correctAns = answers[0];
const shuffle = (array) => {
return array.sort(() => Math.random() - 0.5);
};
const arrayCheck = (arg) => {
return Array.isArray(arg) ? arg : [];
};
return (
<>
{shuffle(arrayCheck(answers)).map((text,ind) => (
<Answer
text={text}
correct={correctAns}
setIndex={setIndex}
index={index}
key={ind}
/>
))}
</>
);
};
修改Answer.js
import React from "react";
import he from "he";
import { Button } from "reactstrap";
export const Answer = ({ text, correct, setIndex, index }) => {
function answerResult() {
setIndex(index + 1);
}
var decode = he.decode(String(text));
return (
<Button className="ansButton" onClick={answerResult}>
{decode}
</Button>
);
};
修改Quiz.js
// import { Answer } from "./Answer";
import { AnswerList } from "./AnswerList";
export const Quiz = () => {
...
return (
<>
...
{!toggleView &&
(isLoading ? (
<LoadingSpin />
) :
(
...
<AnswerList
answers={answers[index]}
index={index}
setIndex={setIndex}
/>
</Jumbotron>
))}
</>
);
};
執行效果:
專案結構:
6. 記錄使用者成績
修改quiz.js,新增setResult,並傳遞給AnswerList
export const Quiz = () => {
var [result, setResult] = useState(null);
...
return (
<>
...
{!toggleView &&
(isLoading ? (
<LoadingSpin />
) :
(
<Jumbotron>
...
<AnswerList
answers={answers[index]}
index={index}
setIndex={setIndex}
setResult={setResult}
/>
</Jumbotron>
))}
</>
);
};
修改AnswerList.js,傳遞setResult
import React from "react";
import { Answer } from "./Answer";
export const AnswerList = ({ answers, index,setResult, setIndex }) => {
...
return (
<>
{shuffle(arrayCheck(answers)).map((text,ind) => (
<Answer
text={text}
correct={correctAns}
setIndex={setIndex}
setResult={setResult}
index={index}
key={ind}
/>
))}
</>
);
};
修改Answer.js,使用者點選選項,回撥setResult,通知Quiz元件,本次選擇是對是錯。
import React from "react";
import { Button } from "reactstrap";
import he from 'he'
export const Answer = ({ text, correct, setResult,setIndex, index }) => {
function answerResult() {
setIndex(index + 1);
correct === text ? setResult(true) : setResult(false);
}
var decode = he.decode(String(text));
return (
<Button className="ansButton" onClick={answerResult}>
{decode}
</Button>
);
};
修改Quiz.js,放一個隱藏的GameOver元件,每當index發生變化的時候,觸發GameOver中的useEffect程式碼,累計使用者答對題目的數目(setRight)
import GameOver from "./GameOver";
export const Quiz = () => {
const [right, setRight] = useState(0);
const [gameIsOver, setGameOver] = useState(false);
return (
<>
{toggleView && (
<Toggle
setIndex={setIndex}
setQuestionData={setQuestionData}
setToggleView={setToggleView}
setLoading={setLoading}
/>
)}
{!toggleView &&
(isLoading ? (
<LoadingSpin />
) :
(
<Jumbotron>
<QuestionHeader
setToggleView={setToggleView}
/>
<Question question={questions[index]} />
<AnswerList
answers={answers[index]}
index={index}
setIndex={setIndex}
setResult={setResult}
/>
</Jumbotron>
))}
<GameOver
right={right}
setRight={setRight}
quizLength={questions.length}
setGameOver={setGameOver}
result={result}
index={index}
/>
</>
);
};
新增GameOver.js元件,當index === quizLength && index時,setGameOver(true)設定遊戲結束,顯示使用者得分。
import React, { useEffect } from "react";
export default function GameOver({
right,
setRight,
setGameOver,
index,
quizLength,
result,
}) {
useEffect(() => {
if (result === true) {
setRight(right + 1);
}
if (index === quizLength && index) {
setGameOver(true);
}
}, [index]);
return <div></div>;
}
7. 遊戲結束,展示使用者得分
新增ScoreBoard.js
import React from "react";
export const ScoreBoard = ({ finalScore, right }) => {
// if index === 0 then right === 0 --> this way when index is reset in toggle so is right answers
const scoreFormatted = score => {
if (score === 1) {
return 100;
} else if (score === 0) {
return 0;
} else {
return score.toFixed(2) * 100;
}
}
return (
<>
<>
<h1 className="display-4">Correct Answers: {right}</h1>
<hr className="my-2" />
<h1 className="display-4">
Final Score: %{scoreFormatted(finalScore)}
</h1>
<hr className="my-2" />
</>
<p>謝謝使用 </p>
</>
);
};
ScoreHeader.js
import React from "react";
import { Button } from "reactstrap";
export default function ScoreHeader({ setGameOver, setToggleView }) {
return (
<Button
color="link"
onClick={() => {
setGameOver(false);
setToggleView(true);
}}
>
返回首頁
</Button>
);
}
修改Quiz.js,當gameIsOver 變數為true時,顯示得分頁面。
import { ScoreBoard } from "./ScoreBoard";
import ScoreHeader from "./ScoreHeader";
export const Quiz = () => {
...
return (
<>
{!toggleView &&
!gameIsOver &&
(isLoading ? (
<LoadingSpin />
) :
(
...
))}
{gameIsOver && (
<Jumbotron>
<ScoreHeader
setToggleView={setToggleView}
setGameOver={setGameOver}
/>
<ScoreBoard right={right} finalScore={right / index} />
</Jumbotron>
)}
...
</>
);
};