项目演示地址
项目源码
视频教程
项目代码结构
前言
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做为我们整个应用的挂载点
/src
目录将包含我们所有的React代码。
要查看环境如何自动编译和更新您的React代码,请找到文件/src/App.js
:
将其中的
Learn React
修改为
和豆约翰 Learn React
保存文件后,您会注意到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 (
);
}
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.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.js
import React from "react";
import { Jumbotron, Container} from "reactstrap";
export default function ToggleHeader() {
return (
计算机知识小测验
);
}
https://opentdb.com/api.php接...
{
"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 && (
)}
{!toggleView &&
(
)}
>
);
使用index控制题目索引
var [index, setIndex] = useState(0);
修改Toggle.js
获取完远程数据,通过setToggleView(false);切换视图。
export const Toggle = ({
setQuestionData,
setToggleView,
setIndex,
}) => {
...
return (
<>
>
);
};
QuestionHeader.js代码:
同样的,点击 返回首页按钮 setToggleView(true),切换视图。
import React from "react";
import { Button } from "reactstrap";
export default function QuestionHeader({ setToggleView, category }) {
return (
<>
>
);
}
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 (
{decode}
);
};
3. 加载等待动画
新增LoadingSpin.js
import React from "react";
import { Spinner } from "reactstrap";
export default function LoadingSpin() {
return (
<>
>
);
}
修改Quiz.js
import LoadingSpin from "./LoadingSpin";
export const Quiz = () => {
const [isLoading, setLoading] = useState(false);
return (
<>
{toggleView && (
)}
{!toggleView &&
(isLoading ? (
) :
(
...
))}
>
);
};
修改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 (
);
};
修改Quiz.js,添加Answer组件:
import { Answer } from "./Answer";
...
{!toggleView &&
(isLoading ? (
) :
(
...
))}
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.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 (
);
};
修改Quiz.js
// import { Answer } from "./Answer";
import { AnswerList } from "./AnswerList";
export const Quiz = () => {
...
return (
<>
...
{!toggleView &&
(isLoading ? (
) :
(
...
))}
>
);
};
6. 记录用户成绩
修改quiz.js,添加setResult,并传递给AnswerList
export const Quiz = () => {
var [result, setResult] = useState(null);
...
return (
<>
...
{!toggleView &&
(isLoading ? (
) :
(
...
))}
>
);
};
修改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.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 (
);
};
修改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 && (
)}
{!toggleView &&
(isLoading ? (
) :
(
))}
>
);
};
新增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 ;
}
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 (
<>
<>
Correct Answers: {right}
Final Score: %{scoreFormatted(finalScore)}
>
谢谢使用
>
);
};
ScoreHeader.js
import React from "react";
import { Button } from "reactstrap";
export default function ScoreHeader({ setGameOver, setToggleView }) {
return (
);
}
修改Quiz.js,当gameIsOver 变量为true时,显示得分页面。
import { ScoreBoard } from "./ScoreBoard";
import ScoreHeader from "./ScoreHeader";
export const Quiz = () => {
...
return (
<>
{!toggleView &&
!gameIsOver &&
(isLoading ? (
) :
(
...
))}
{gameIsOver && (
)}
...
>
);
};