35

从0开始,手把手教你使用React开发答题App

 3 years ago
source link: http://www.cnblogs.com/songboriceboy/p/13278805.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

项目演示地址

项目演示地址

项目源码

项目源码

其他版本教程

Vue版本

小程序版本

项目代码结构

6JZnem7.png!web

前言

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应用程序将弹出一个新窗口。

Bn2yQnf.png!web

项目目录结构

右键react-exam目录,使用vscode打开该目录。

react-exam项目目录中有一个/public和/src目录,以及node_modules,.gitignore,README.md,和package.json。

niye2am.png!web

在目录 /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 编译并刷新了新数据。

yEnQVfi.png!web

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"]
	}]
}

程序运行效果:

fQfyymn.png!web

当前项目目录结构为:

mQfm6zn.png!web

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>
  );
};

程序运行效果:

首页

fQfyymn.png!web

点击开始后,显示问题:

eIzAraa.png!web

当前项目目录结构为:

BbuqMv.png!web

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);
    }
  };

 ...
};

运行效果:

ArUZnuv.png!web

目前代码结构:

JJzeam6.png!web

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>

        ))}

运行效果:

FvAnya6.png!web

点击下一题:

aIvqQni.png!web

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>

        ))}
    </>
  );
};

运行效果:

BfyuErM.png!web

项目结构:

faErYjj.png!web

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>
      )}
      
   ...
    </>
  );
};

最后

UzINFzr.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK