乐趣区

关于前端:从零开始使用createreactapp-react-typescript-完成一个网站

在线示例

以下是一个曾经实现的成品,如图所示:

你也能够点击此处查看在线示例。

兴许有人咋一看,看到这个网站有些相熟,没错,这个网站来源于 https://jsisweird.com/。我花了三天工夫,用 create-react-app + react + typescript 重构这个网站,与网站成果不同的是,我没有退出任何的动画,并且我增加了中英文切换以及回到顶部的成果。

设计剖析

观看整个网站,其实整体的架构也不简单,就是一个首页,20 道问题页面以及一个解析页面形成。这些波及到的问题也好,题目也罢,其实都是一堆定义好的数据,上面咱们来一一查看这些数据的定义:

问题数据的定义

很显然,问题数据是一个对象数组,咱们来看构造如下:

 export const questions = []; 
 // 因为问题自身不须要实现中英文切换,所以咱们这里也不须要辨别,数组项的构造如:{question:"true + false",answer:["\"truefalse\"","1","NaN","SyntaxError"],correct:"1"},

数据的示意一眼就可以看进去,question代表问题,answer代表答复选项,correct代表正确答案。让咱们持续。

解析数据的定义

解析数据,须要进行中英文切换,所以咱们用一个对象示意,如下:

export const parseObject = {
    "en":{
        output:"",// 输入文本
        answer:"",// 用户答复文本:[],
        successMsg:"",// 用户答复正确文本
        errorMsg:"",// 用户答复谬误文本
        detail:[],// 问题答案解析文本
        tabs:[],// 中英文切换选项数组
        title:"",// 首页题目文本
        startContent:"",// 首页段落文本
        endContent:"",// 解析页段落文本
        startBtn:"",// 首页开始按钮文本
        endBtn:"",// 解析页从新开始文本
    },
    "zh":{// 选项同 en 属性值统一}
}

更多详情,请查看此处源码。

这其中,因为 detail 里的数据只是一般文本,咱们须要将其转换成 HTML 字符串,尽管有marked.js 这样的库能够帮忙咱们,然而这里咱们的转换规则也比较简单,无需应用 marked.js 这样的库,因而,我在这里封装了一个繁难版本的 marked 工具函数,如下所示:

export function marked(template) {
    let result = "";
    result = template.replace(/\[.+?\]\(.+?\)/g,word => {const link = word.slice(word.indexOf('(') + 1, word.indexOf(')'));
        const linkText = word.slice(word.indexOf('[') + 1, word.indexOf(']'));
        return `<a href="${link}" target="blank">${linkText}</a>`;
    }).replace(/\*\*\*([\s\S]*?)\*\*\*[\s]?/g,text => '<code>' + text.slice(3,text.length - 4) + '</code>');
    return result;
}

转换规则也比较简单,就是匹配 a 标签以及 code 标签,这里咱们写的是相似 markdown 的语法。比方 a 标签的写法应该是如下所示:

[xxx](xxx)

所以以上的转换函数,咱们匹配的就是这种构造的字符串,其正则表达式构造如:

/\[.+?\]\(.+?\)/g;

这其中 .+? 示意匹配任意的字符,这个正则表达式就显而易见了。除此之外,咱们匹配代码高亮的 markdown 的语法定义如下:

***//code***

为什么我要如此设计?这是因为如果我也应用 markdown三个模板字符串符号 来定义代码高亮,会和 js 的 模板字符串起抵触 ,所以为了不必要的麻烦,我改用了三个* 来示意,所以以上的正则表达式才会匹配*。如下:

/\*\*\*([\s\S]*?)\*\*\*[\s]?/g

那么以上的正则表达式应该如何了解呢?首先,咱们须要确定的是 \s 以及 \S 代表什么意思,*在正则表达式中须要本义,所以加了 \, 这个正则表达式的意思就是匹配***//code*** 这样的构造。

以上的源码能够查看此处。

其它文本的定义

还有 2 处的文本的定义,也就是问题选项的统计以及用户答复问题的统计,所以咱们别离定义了 2 个函数来示意,如下:

export function getCurrentQuestion(lang="en",order= 1,total = questions.length){return lang === 'en' ? `Question ${ order} of ${total}` : ` 第 ${order}题,共 ${total}题 `;
}
export function getCurrentAnswers(lang = "en",correctNum = 0,total= questions.length){return lang === 'en' ? `You got ${ correctNum} out of ${total} correct!` : ` 共 ${total}道题,您答对了 ${correctNum} 道题!`;
}

这 2 个工具函数承受 3 个参数,第一个参数代表语言类型,默认值是 ”en” 也就是英文模式,第二个代表以后第几题 / 正确题数,第三个参数代表题的总数。而后依据这几个参数返回一段文本,这个也没什么好说的。

实现思路剖析

初始化我的项目

此处略过。能够参考文档。

根底组件的实现

接下来,咱们实际上能够将页面分成三大部分,第一局部即首页,第二局部即问题选项页,第三局部则是问题解析页面,在解析页面因为解析内容过多,所以咱们须要一个回到顶部的成果。在提及这三个局部的实现之前,咱们首先须要封装一些公共的组件,让咱们来一起看一下吧!

中英文选项卡切换组件

不论是首页也好,问题页也罢,咱们都会看到右上角有一个中英文切换的选项卡组件,成果自不比多说,让咱们来思考一下应该如何实现。首先思考一下 DOM 构造。咱们能够很快就想到构造如下:

<div class="tab-container">
    <div class="tab-item">en</div>
    <div class="tab-item">zh</div>
</div>

在这里,咱们应该晓得类名应该会是动静操作的,因为须要增加一个选中成果,暂定类名为 active, 我在这里应用的是事件代理,将事件代理到父元素tab-container 上。并且它的文本也是动静的,因为须要辨别中英文。于是咱们能够很快写出如下的代码:

import React from "react";
import {parseObject} from '../data/data';
import "../style/lang.css";
export default class LangComponent extends React.Component {constructor(props){super(props);
        this.state = {activeIndex:0};
    }
    onTabHandler(e){const { nativeEvent} = e;
        const {classList} = nativeEvent.target;
        if(classList.contains('tab-item') && !classList.contains('tab-active')){const { activeIndex} = this.state;
            let newActiveIndex = activeIndex === 0 ? 1 : 0;
            this.setState({activeIndex:newActiveIndex});
            this.props.changeLang(newActiveIndex);
        }
    }
    render(){const { lang} = this.props;
        const {activeIndex} = this.state;
        return (<div className="tab-container" onClick = { this.onTabHandler.bind(this) }>
                {parseObject[lang]["tabs"].map((tab,index) => 
                        (<div className={`tab-item ${ activeIndex === index ? 'tab-active' : ''}`}  key={tab}>{tab}</div>
                        )
                    )
                }
            </div>
        )
    }
}

css 款式代码如下:

.tab-container {
    display: flex;
    align-items: center;
    justify-content: center;
    border:1px solid #f2f3f4;
    border-radius: 5px;
    position: fixed;
    top: 15px;
    right: 15px;
}
.tab-container > .tab-item {
    padding: 8px 15px;
    color: #e7eaec;
    cursor: pointer;
    background: linear-gradient(to right,#515152,#f3f3f7);
    transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.tab-container > .tab-item:first-child {
    border-top-left-radius: 5px;
    border-bottom-left-radius:5px;
}
.tab-container > .tab-item:last-child {
    border-top-right-radius: 5px;
    border-bottom-right-radius:5px;
}
.tab-container > .tab-item.tab-active,.tab-container > .tab-item:hover {
    color: #fff;
    background: linear-gradient(to right,#53b6e7,#0c6bc9);
}

js逻辑,咱们能够看到咱们通过父组件传递一个 lang 参数用来确定中英文模式,而后开始拜访定义数据上的 tabs, 即数组,react.js 渲染列表通常都是应用 map 办法。事件代理,咱们能够看到咱们是通过获取原生事件对象 nativeEvent 拿到类名,判断元素是否含有 tab-item 类名,从而确定点击的是子元素,而后调用 this.setState 更改以后的索引项,用来确定以后是哪项被选中。因为只有两项,所以咱们能够确定以后索引项不是 0 就是 1,并且咱们也裸露了一个事件changeLang 给父元素以便父元素能够实时的晓得语言模式的值。

至于款式,都是比拟根底的款式,没有什么好说的, 须要留神的就是咱们是应用固定定位将选项卡组件固定在右上角的。以上的源码能够查看此处。

接下来,咱们来看第二个组件的实现。

底部内容组件

底部内容组件比较简单,就是一个标签包裹内容。代码如下:

import React from "react";
import "../style/bottom.css";
const BottomComponent = (props) => {
    return (<div className="bottom" id="bottom">{ props.children}</div>
    )
}
export default BottomComponent;

CSS 代码如下:

.bottom {
    position: fixed;
    bottom: 5px;
    left: 50%;
    transform: translateX(-50%);
    color: #fff;
    font-size: 18px;
}

也就是函数组件的写法,采纳固定定位定位在底部。以上的源码能够查看此处。让咱们看下一个组件的实现。

内容组件的实现

该组件的实现也比较简单,就是用 p 标签包装了一下。如下:

import React from "react";
import "../style/content.css";
const ContentComponent = (props) => {
    return (<p className="content">{ props.children}</p>
    )
}
export default ContentComponent;

CSS 款式代码如下:

.content {
    max-width: 35rem;
    width: 100%;
    line-height: 1.8;
    text-align: center;
    font-size: 18px;
    color: #fff;
}

以上的源码能够查看此处。让咱们看下一个组件的实现。

渲染 HTML 字符串的组件

这个组件其实也就是利用了 react.jsdangerouslySetInnerHTML属性来渲染 html 字符串的。代码如下:

import "../style/render.css";
export function createMarkup(template) {return { __html: template};
}
const RenderHTMLComponent = (props) => {const { template} = props;
    let renderTemplate = typeof template === 'string' ? template : "";
    return <div dangerouslySetInnerHTML={createMarkup( renderTemplate)} className="render-content"></div>;
}
export default RenderHTMLComponent;

CSS 款式代码如下:

.render-content a,.render-content{color: #fff;}
.render-content a {
    border-bottom:1px solid #fff;
    text-decoration: none;
}
.render-content code {
    color: #245cd4;
    background-color: #e5e2e2;
    border-radius: 5px;
    font-size: 16px;
    display: block;
    white-space: pre;
    padding: 15px;
    margin: 15px 0;
    word-break: break-all;
    overflow: auto;
}
.render-content a:hover {
    color:#efa823;
    border-color: #efa823;
}

如代码所示,咱们能够看到其实咱们就是 dangerouslySetInnerHTML 属性绑定一个函数,将模板字符串当做参数传入这个函数组件,在函数组件当中,咱们返回一个对象,构造即:{__html:template}。其它也就没有什么好说的。

以上的源码能够查看此处。让咱们看下一个组件的实现。

题目组件的实现

题目组件也就是对 h1~h6 标签的一个封装,代码如下:

import React from "react";
const TitleComponent = (props) => {let TagName = `h${ props.level || 1}`;
    return (
        <React.Fragment>
            <TagName>{props.children}</TagName>
        </React.Fragment>
    )
}
export default TitleComponent;

整体逻辑也不简单,就是依据父元素传入的一个 level 属性从而确定是 h1 ~ h6 的哪个标签,也就是动静组件的写法。在这里,咱们应用了 Fragment 来包裹了一下组件,对于 Fragment 组件的用法能够参考文档。我的了解,它就是一个占位标签,因为 react.js 虚构 DOM 的限度须要提供一个根节点,所以这个占位标签的呈现就是为了解决这个问题。当然,如果是typescript,咱们还须要显示的定义一个类型,如下:

import React, {FunctionComponent,ReactNode}from "react";
interface propType {
    level:number,
    children?:ReactNode
}
// 这一行代码是须要的
type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const TitleComponent:FunctionComponent<propType> = (props:propType) => {
    // 这里断言一下只能是 h1~h6 的标签名
    let TagName = `h${props.level}` as HeadingTag;
    return (
        <React.Fragment>
            <TagName>{props.children}</TagName>
        </React.Fragment>
    )
}
export default TitleComponent;

以上的源码能够查看此处。让咱们看下一个组件的实现。

按钮组件的实现

按钮组件是一个最根本的组件,它的默认款式必定是不合乎咱们的需要的,所以咱们须要将它简略的封装一下。如下所示:

import React from "react";
import "../style/button.css";
export default class ButtonComponent extends React.Component {constructor(props){super(props);
        this.state = {typeArr:["primary","default","danger","success","info"],
            sizeArr:["mini",'default',"medium","normal","small"]
        }
    }
    onClickHandler(){this.props.onClick && this.props.onClick();
    }
    render(){const { nativeType,type,long,size,className,forwardedRef} = this.props;
        const {typeArr,sizeArr} = this.state;
        const buttonType = type && typeArr.indexOf(type) > -1 ? type : 'default';
        const buttonSize = size && sizeArr.indexOf(size) > -1 ? size : 'default';
        let longClassName = '';
        let parentClassName = '';
        if(className){parentClassName = className;}
        if(long){longClassName = "long-btn";}
        return (
            <button
                ref={forwardedRef}
                type={nativeType} 
                className={`btn btn-${ buttonType} ${longClassName} btn-size-${buttonSize} ${parentClassName}`} 
                onClick={this.onClickHandler.bind(this)}
            >{this.props.children}</button>
        )
    }
}

CSS 款式代码如下:

.btn {
    padding: 14px 18px;
    outline: none;
    display: inline-block;
    border: 1px solid var(--btn-default-border-color);
    color: var(--btn-default-font-color);
    border-radius: 8px;
    background-color: var(--btn-default-color);
    font-size: 14px;
    letter-spacing: 2px;
    cursor: pointer;
}
.btn.btn-size-default {padding: 14px 18px;}
.btn.btn-size-mini {padding: 6px 8px;}
.btn:not(.btn-no-hover):hover,.btn:not(.btn-no-active):active,.btn.btn-active {border-color: var(--btn-default-hover-border-color);
    background-color: var(--btn-default-hover-color);
    color:var(--btn-default-hover-font-color);
}
.btn.long-btn {width: 100%;}

这里对按钮的封装,次要是将按钮分类,通过叠加类名的形式,给按钮加各种类名,从而达到不同类型的按钮的实现。而后裸露一个 onClick 事件。对于款式代码,这里是通过 CSS 变量的形式。代码如下:

:root {
    --btn-default-color:transparent;
    --btn-default-border-color:#d8dbdd;
    --btn-default-font-color:#ffffff;
    --btn-default-hover-color:#fff;
    --btn-default-hover-border-color:#a19f9f;
    --btn-default-hover-font-color:#535455;
    /* 1 */
    --bg-first-radial-first-color:rgba(50, 4, 157, 0.271);
    --bg-first-radial-second-color:rgba(7,58,255,0);
    --bg-first-radial-third-color:rgba(17, 195, 201,1);
    --bg-first-radial-fourth-color:rgba(220,78,78,0);
    --bg-first-radial-fifth-color:#09a5ed;
    --bg-first-radial-sixth-color:rgba(255,0,0,0);
    --bg-first-radial-seventh-color:#3d06a3;
    --bg-first-radial-eighth-color:#7eb4e6;
    --bg-first-radial-ninth-color:#4407ed;
    /* 2 */
    --bg-second-radial-first-color:rgba(50, 4, 157, 0.41);
    --bg-second-radial-second-color:rgba(7,58,255,0.1);
    --bg-second-radial-third-color:rgba(17, 51, 201,1);
    --bg-second-radial-fourth-color:rgba(220,78,78,0.2);
    --bg-second-radial-fifth-color:#090ded;
    --bg-second-radial-sixth-color:rgba(255,0,0,0.1);
    --bg-second-radial-seventh-color:#0691a3;
    --bg-second-radial-eighth-color:#807ee6;
    --bg-second-radial-ninth-color:#07ede1;
    /* 3 */
    --bg-third-radial-first-color:rgba(50, 4, 157, 0.111);
    --bg-third-radial-second-color:rgba(7,58,255,0.21);
    --bg-third-radial-third-color:rgba(118, 17, 201, 1);
    --bg-third-radial-fourth-color:rgba(220,78,78,0.2);
    --bg-third-radial-fifth-color:#2009ed;
    --bg-third-radial-sixth-color:rgba(255,0,0,0.3);
    --bg-third-radial-seventh-color:#0610a3;
    --bg-third-radial-eighth-color:#c07ee6;
    --bg-third-radial-ninth-color:#9107ed;
    /* 4 */
    --bg-fourth-radial-first-color:rgba(50, 4, 157, 0.171);
    --bg-fourth-radial-second-color:rgba(7,58,255,0.2);
    --bg-fourth-radial-third-color:rgba(164, 17, 201, 1);
    --bg-fourth-radial-fourth-color:rgba(220,78,78,0.1);
    --bg-fourth-radial-fifth-color:#09deed;
    --bg-fourth-radial-sixth-color:rgba(255,0,0,0);
    --bg-fourth-radial-seventh-color:#7106a3;
    --bg-fourth-radial-eighth-color:#7eb4e6;
    --bg-fourth-radial-ninth-color:#ac07ed;
}

以上的源码能够查看此处。让咱们看下一个组件的实现。

留神: 这里的按钮组件款式事实上还没有写完,其它类型的款式因为咱们要实现的网站没有用到所以没有去实现。

问题选项组件

实际上就是问题局部页面的实现,咱们先来看理论的代码:

import React from "react";
import {QuestionArray} from "../data/data";
import ButtonComponent from './buttonComponent';
import TitleComponent from './titleComponent';
import "../style/quiz-wrapper.css";
export default class QuizWrapperComponent extends React.Component {constructor(props:PropType){super(props);
        this.state = {}}
    onSelectHandler(select){this.props.onSelect && this.props.onSelect(select);
    }
    render(){const { question} = this.props;
        return (
            <div className="quiz-wrapper flex-center flex-direction-column">
                <TitleComponent level={1}>{question.question}</TitleComponent>
                <div className="button-wrapper flex-center flex-direction-column">
                    {question.answer.map((select,index) => (
                            <ButtonComponent 
                                nativeType="button" 
                                onClick={this.onSelectHandler.bind(this,select)}
                                className="mt-10 btn-no-hover btn-no-active"
                                key={select}
                                long
                            >{select}</ButtonComponent>
                        ))
                    }
                </div>
            </div>
        )
    }
}

css 款式代码如下:

.quiz-wrapper {
    width: 100%;
    height: 100vh;
    padding: 1rem;
    max-width: 600px;
}
.App {
  height: 100vh;
  overflow:hidden;
}
.App h1 {
  color: #fff;
  font-size: 32px;
  letter-spacing: 2px;
  margin-bottom: 15px;
  text-align: center;
}
.App .button-wrapper {
  max-width: 25rem;
  width: 100%;
  display: flex;
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  height:100vh;
  overflow: hidden;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-first-radial-first-color) 0,var(--bg-first-radial-second-color) 100%),
                    radial-gradient(113% 91% at 17% -2%,var(--bg-first-radial-third-color) 1%,var(--bg-first-radial-fourth-color) 99%),
                    radial-gradient(142% 91% at 83% 7%,var(--bg-first-radial-fifth-color) 1%,var(--bg-first-radial-sixth-color) 99%),
                    radial-gradient(142% 91% at -6% 74%,var(--bg-first-radial-seventh-color) 1%,var(--bg-first-radial-sixth-color) 99%),
                    radial-gradient(142% 91% at 111% 84%,var(--bg-first-radial-eighth-color) 0,var(--bg-first-radial-ninth-color) 100%);
  animation:background 50s linear infinite;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}
.mt-10 {margin-top: 10px;}
.ml-5 {margin-left: 5px;}
.text-align {text-align: center;}
.flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}
.flex-direction-column {flex-direction: column;}
.w-100p {width: 100%;}
::-webkit-scrollbar {
  width: 5px;
  height: 10px;
  background: linear-gradient(45deg,#e9bf89,#c9a120,#c0710a);
}
::-webkit-scrollbar-thumb {
   width: 5px;
   height: 5px;
   background: linear-gradient(180deg,#d33606,#da5d4d,#f0c8b8);
}
@keyframes background {
    0% {background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-first-radial-first-color) 0,var(--bg-first-radial-second-color) 100%),
                        radial-gradient(113% 91% at 17% -2%,var(--bg-first-radial-third-color) 1%,var(--bg-first-radial-fourth-color) 99%),
                        radial-gradient(142% 91% at 83% 7%,var(--bg-first-radial-fifth-color) 1%,var(--bg-first-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at -6% 74%,var(--bg-first-radial-seventh-color) 1%,var(--bg-first-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at 111% 84%,var(--bg-first-radial-eighth-color) 0,var(--bg-first-radial-ninth-color) 100%);
    }
    25%,50% {background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-second-radial-first-color) 0,var(--bg-second-radial-second-color) 100%),
                        radial-gradient(113% 91% at 17% -2%,var(--bg-second-radial-third-color) 1%,var(--bg-second-radial-fourth-color) 99%),
                        radial-gradient(142% 91% at 83% 7%,var(--bg-second-radial-fifth-color) 1%,var(--bg-second-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at -6% 74%,var(--bg-second-radial-seventh-color) 1%,var(--bg-second-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at 111% 84%,var(--bg-second-radial-eighth-color) 0,var(--bg-second-radial-ninth-color) 100%);
    }
    50%,75% {background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-third-radial-first-color) 0,var(--bg-third-radial-second-color) 100%),
                        radial-gradient(113% 91% at 17% -2%,var(--bg-third-radial-third-color) 1%,var(--bg-third-radial-fourth-color) 99%),
                        radial-gradient(142% 91% at 83% 7%,var(--bg-third-radial-fifth-color) 1%,var(--bg-third-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at -6% 74%,var(--bg-third-radial-seventh-color) 1%,var(--bg-third-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at 111% 84%,var(--bg-third-radial-eighth-color) 0,var(--bg-third-radial-ninth-color) 100%);
    }
    100% {background-image: radial-gradient(49% 81% at 45% 47%, var(--bg-fourth-radial-first-color) 0,var(--bg-fourth-radial-second-color) 100%),
                        radial-gradient(113% 91% at 17% -2%,var(--bg-fourth-radial-third-color) 1%,var(--bg-fourth-radial-fourth-color) 99%),
                        radial-gradient(142% 91% at 83% 7%,var(--bg-fourth-radial-fifth-color) 1%,var(--bg-fourth-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at -6% 74%,var(--bg-fourth-radial-seventh-color) 1%,var(--bg-fourth-radial-sixth-color) 99%),
                        radial-gradient(142% 91% at 111% 84%,var(--bg-fourth-radial-eighth-color) 0,var(--bg-fourth-radial-ninth-color) 100%);
    }
}

能够看到,咱们应用 h1 标签来显示问题,四个选项都应用的按钮标签,咱们将按钮标签选中的是哪一项,通过裸露一个事件 onSelect 给传递进来。通过应用该组件的时候传递 question 数据就能够确定一组问题以及选项答案。所以实现成果如下图所示:

这个组件外面可能比较复杂一点的是 CSS 布局, 有采纳弹性盒子布局以及背景色突变动画等等,其它的也没什么好说的。

以上的源码能够查看此处。让咱们看下一个组件的实现。

解析组件

解析组件实际上就是解析页面局部的一个封装。咱们先来看一下实现成果:

依据上图,咱们能够得悉解析组件分为六大部分。第一局部首先是对用户答复所作的一个正确统计,实际上就是一个题目组件,第二局部则同样也是一个题目组件,也就是题目信息。第三局部则是正确答案,第四局部则是用户的答复,第五局部则是确定用户答复是正确还是谬误,第六局部就是理论的解析。

咱们来看一下实现代码:

import React from "react";
import {parseObject,questions} from "../data/data";
import {marked} from "../utils/marked";
import RenderHTMLComponent from './renderHTML';
import "../style/parse.css";
export default class ParseComponent extends React.Component {constructor(props){super(props);
        this.state = {};}
    render(){const { lang,userAnswers} = this.props;
        const setTypeClassName = (index) => 
        `answered-${questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;
        return (
            <ul className="result-list">
                {parseObject[lang].detail.map((content,index) => (
                        <li 
                            className={`result-item ${ setTypeClassName(index) }`} key={content}>
                            <span className="result-question">
                                <span className="order">{(index + 1)}.</span>
                                {questions[index].question }
                            </span>
                            <div className="result-item-wrapper">
                                <span className="result-correct-answer">
                                    {parseObject[lang].output }:<span className="ml-5 result-correct-answer-value">{questions[index].correct }</span>
                                </span>
                                <span className="result-user-answer">
                                    {parseObject[lang].answer }:<span className="ml-5 result-user-answer-value">{userAnswers[index]}</span>
                                </span>
                                <span 
                                    className={`inline-answer ${ setTypeClassName(index) }`}>
                                    {questions[index].correct === userAnswers[index] 
                                        ? parseObject[lang].successMsg 
                                        : parseObject[lang].errorMsg
                                    }
                                </span>
                                <RenderHTMLComponent template={marked(content) }></RenderHTMLComponent>
                            </div>
                        </li>
                    ))
                }
            </ul>
        )
    }
}

CSS 款式代码如下:

.result-wrapper {
  width: 100%;
  height: 100%;
  padding: 60px 15px 40px;
  overflow-x: hidden;
  overflow-y: auto;
}
.result-wrapper .result-list {
  list-style: none;
  padding-left: 0;
  width: 100%;
  max-width: 600px;
}
.result-wrapper .result-list .result-item {
  background-color: #020304;
  border-radius: 4px;
  margin-bottom: 2rem;
  color: #fff;
}
.result-content .render-content {
  max-width: 600px;
  line-height: 1.5;
  font-size: 18px;
}
.result-wrapper .result-question {
    padding:25px;
    background-color: #1b132b;
    font-size: 22px;
    letter-spacing: 2px;
    border-radius: 4px 4px 0 0;
}
.result-wrapper .result-question .order {margin-right: 8px;}
.result-wrapper .result-item-wrapper,.result-wrapper .result-list .result-item {
    display: flex;
    flex-direction: column;
}
.result-wrapper .result-item-wrapper {padding: 25px;}
.result-wrapper .result-item-wrapper .result-user-answer {letter-spacing: 1px;}
.result-wrapper .result-item-wrapper .result-correct-answer .result-correct-answer-value,
.result-wrapper .result-item-wrapper .result-user-answer .result-user-answer-value {
   font-weight: bold;
   font-size: 20px;
}
.result-wrapper .result-item-wrapper .inline-answer {
    padding:15px 25px;
    max-width: 250px;
    margin:1rem 0;
    border-radius: 5px;
}
.result-wrapper .result-item-wrapper .inline-answer.answered-incorrectly {background-color: #d82323;}
.result-wrapper .result-item-wrapper .inline-answer.answered-correctly {background-color: #4ee24e;}

能够看到依据咱们后面剖析的六大部分,咱们曾经能够确定咱们须要哪些组件,首先必定是渲染一个列表,因为有 20 道题的解析,并且咱们也晓得依据传递的 lang 确定中英文模式。另外一个 userAnswers 则是用户的答复,依据用户的答复和正确答案做匹配,咱们就能够晓得用户答复是正确还是谬误。这也就是如下这行代码的意义:

const setTypeClassName = (index) => `answered-${questions[index].correct === userAnswers[index] ? 'correctly' : 'incorrectly'}`;

就是通过索引,确定返回的是正确的类名还是谬误的类名,通过类名来增加款式,从而确定用户答复是否正确。咱们将以上代码拆分一下,就很好了解了。如下:

1. 题目信息

<span className="result-question">
     <span className="order">{(index + 1)}.</span>
     {questions[index].question }
</span>

2. 正确答案

 <span className="result-correct-answer">
    {parseObject[lang].output }:
    <span className="ml-5 result-correct-answer-value">{questions[index].correct }</span>
</span>

3. 用户答复

<span className="result-user-answer">
  {parseObject[lang].answer }:
  <span className="ml-5 result-user-answer-value">{userAnswers[index]}</span>
</span>

4. 提示信息

<span className={`inline-answer ${ setTypeClassName(index) }`}>
     {questions[index].correct === userAnswers[index] 
         ? parseObject[lang].successMsg 
         : parseObject[lang].errorMsg
     }
</span>

5. 答案解析

答案解析实际上就是渲染 HTML 字符串,所以咱们就能够通过应用之前封装好的组件。

<RenderHTMLComponent template={marked(content) }></RenderHTMLComponent>

这个组件实现之后,实际上,咱们的整个我的项目的大部分就曾经实现了,接下来就是一些细节的解决。

以上的源码能够查看此处。让咱们看下一个组件的实现。

让咱们持续,下一个组件的实现也是最难的,也就是回到顶部成果的实现。

回到顶部按钮组件

回到顶部组件的实现思路其实很简略,就是通过监听滚动事件确定回到顶部按钮的显隐状态,当点击回到顶部按钮的时候,咱们须要通过定时器以肯定增量来进行计算scrollTop,从而达到平滑回到顶部的成果。请看代码如下:

import React, {useEffect} from "react";
import ButtonComponent from "./buttonComponent";
import "../style/top.css";
const TopButtonComponent = React.forwardRef((props, ref) => {const svgRef = React.createRef();
    const setPathElementFill = (paths, color) => {if (paths) {Array.from(paths).forEach((path) => path.setAttribute("fill", color));
      }
    };
    const onMouseEnterHandler = () => {
      const svgPaths = svgRef?.current?.children;
      if (svgPaths) {setPathElementFill(svgPaths, "#2396ef");
      }
    };
    const onMouseLeaveHandler = () => {
      const svgPaths = svgRef?.current?.children;
      if (svgPaths) {setPathElementFill(svgPaths, "#ffffff");
      }
    };
    const onTopHandler = () => {props.onClick && props.onClick();
    };
    return (
      <ButtonComponent
        onClick={onTopHandler.bind(this)}
        className="to-Top-btn btn-no-hover btn-no-active"
        size="mini"
        forwardedRef={ref}
      >
        {props.children ? ( props.children) : (
          <svg
            className="icon"
            viewBox="0 0 1024 1024"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            p-id="4158"
            onMouseEnter={onMouseEnterHandler.bind(this)}
            onMouseLeave={onMouseLeaveHandler.bind(this)}
            ref={svgRef}
          >
            <path
              d="M508.214279 842.84615l34.71157 0c0 0 134.952598-188.651614 134.952598-390.030088 0-201.376427-102.047164-339.759147-118.283963-357.387643-12.227486-13.254885-51.380204-33.038464-51.380204-33.038464s-37.809117 14.878872-51.379181 33.038464C443.247638 113.586988 338.550111 251.439636 338.550111 452.816063c0 201.378473 134.952598 390.030088 134.952598 390.030088L508.214279 842.84615zM457.26591 164.188456l50.948369 0 50.949392 0c9.344832 0 16.916275 7.522324 16.916275 16.966417 0 9.377578-7.688099 16.966417-16.916275 16.966417l-50.949392 0-50.948369 0c-9.344832 0-16.917298-7.556093-16.917298-16.966417C440.347588 171.776272 448.036711 164.188456 457.26591 164.188456zM440.347588 333.852624c0-37.47859 30.387078-67.865667 67.865667-67.865667s67.865667 30.387078 67.865667 67.865667-30.387078 67.865667-67.865667 67.865667S440.347588 371.331213 440.347588 333.852624z"
              p-id="4159"
              fill={props.color}
            ></path>
            <path
              d="M460.214055 859.812567c-1.87265 5.300726-2.90005 11.000542-2.90005 16.966417 0 12.623505 4.606925 24.189935 12.244882 33.103956l21.903869 37.510312c1.325182 8.052396 8.317433 14.216793 16.750499 14.216793 8.135284 0 14.929014-5.732561 16.585747-13.386892l0.398066 0 24.62177-42.117237c5.848195-8.284687 9.29469-18.425651 9.29469-29.325909 0-5.965875-1.027399-11.665691-2.90005-16.966417L460.214055 859.81359z"
              p-id="4160"
              fill={props.color}
            ></path>
            <path
              d="M312.354496 646.604674c-18.358113 3.809769-28.697599 21.439288-23.246447 39.399335l54.610782 179.871647c3.114944 10.304693 10.918677 19.086707 20.529569 24.454972l8.036024-99.843986c1.193175-14.745842 11.432377-29.226648 24.737404-36.517705-16.502859-31.912827-34.381042-71.079872-49.375547-114.721835L312.354496 646.604674z"
              p-id="4161"
              fill={props.color}
            ></path>
            <path
              d="M711.644481 646.604674l-35.290761-7.356548c-14.994506 43.641963-32.889061 82.810031-49.374524 114.721835 13.304004 7.291057 23.544229 21.770839 24.737404 36.517705l8.036024 99.843986c9.609869-5.368264 17.397229-14.150278 20.529569-24.454972L734.890928 686.004009C740.34208 668.043962 730.003618 650.414443 711.644481 646.604674z"
              p-id="4162"
              fill={props.color}
            ></path>
          </svg>
        )}
      </ButtonComponent>
    );
  }
);
const TopComponent = (props) => {const btnRef = React.createRef();
  let scrollElement= null;
  let top_value = 0,timer = null;
  const updateTop = () => {
        top_value -= 20;
        scrollElement && (scrollElement.scrollTop = top_value);
        if (top_value < 0) {if (timer) clearTimeout(timer);
            scrollElement && (scrollElement.scrollTop = 0);
            btnRef.current && (btnRef.current.style.display = "none");
        } else {timer = setTimeout(updateTop, 1);
        }
  };
  const topHandler = () => {
        scrollElement = props.scrollElement?.current || document.body;
        top_value = scrollElement.scrollTop;
        updateTop();
        props.onClick && props.onClick();};
  useEffect(() => {
    const scrollElement = props.scrollElement?.current || document.body;
    // listening the scroll event
    scrollElement && scrollElement.addEventListener("scroll", (e: Event) => {const { scrollTop} = e.target;
        if (btnRef.current) {btnRef.current.style.display = scrollTop > 50 ? "block" : "none";}
    });
  });
  return (<TopButtonComponent ref={btnRef} {...props} onClick={topHandler.bind(this)}></TopButtonComponent>);
};
export default TopComponent;

CSS 款式代码如下:

.to-Top-btn {
    position: fixed;
    bottom: 15px;
    right: 15px;
    display: none;
    transition: all .4s ease-in-out;
}
.to-Top-btn .icon {
    width: 35px;
    height: 35px;
}

整个回到顶部按钮组件分为了两个局部,第一个局部咱们是应用 svg 的图标作为回到顶部的点击按钮。首先是第一个组件 TopButtonComponent,咱们次要做了 2 个工作,第一个工作就是应用React.forwardRef API 来将 ref 属性进行转发,或者说是将 ref 属性用于通信。对于这个 API 的详情可查看文档 forwardRef API。而后就是通过 ref 属性拿到 svg 标签上面的所有子元素,通过 setAttribute 办法来为 svg 标签增加悬浮扭转字体色的性能。这就是以下这个函数的作用:

const setPathElementFill = (paths, color) => {
   // 将色彩值和 path 标签数组作为参数传入,而后设置 fill 属性值
   if (paths) {Array.from(paths).forEach((path) => path.setAttribute("fill", color));
   }
};

第二局部就是在钩子函数 useEffect 中去监听元素的滚动事件,从而确定回到顶部按钮的显隐状态。并且封装了一个更新 scrollTop 值的函数。

const updateTop = () => {
    top_value -= 20;
    scrollElement && (scrollElement.scrollTop = top_value);
    if (top_value < 0) {if (timer) clearTimeout(timer);
        scrollElement && (scrollElement.scrollTop = 0);
        btnRef.current && (btnRef.current.style.display = "none");
    } else {timer = setTimeout(updateTop, 1);
    }
};

采纳定时器来递归实现动静更改scrollTop。其它也就没有什么好说的呢。

以上的源码能够查看此处。让咱们看下一个组件的实现。

app 组件的实现

实际上该组件就是将所有封装的公共组件的一个拼凑。咱们来看详情代码:

import React, {useReducer, useState} from "react";
import "../style/App.css";
import LangComponent from "../components/langComponent";
import TitleComponent from "../components/titleComponent";
import ContentComponent from "../components/contentComponent";
import ButtonComponent from "../components/buttonComponent";
import BottomComponent from "../components/bottomComponent";
import QuizWrapperComponent from "../components/quizWrapper";
import ParseComponent from "../components/parseComponent";
import RenderHTMLComponent from '../components/renderHTML';
import TopComponent from '../components/topComponent';
import {getCurrentQuestion, parseObject,questions,getCurrentAnswers,QuestionArray} from "../data/data";
import {LangContext, lang} from "../store/lang";
import {OrderReducer, initOrder} from "../store/count";
import {marked} from "../utils/marked";
import {computeSameAnswer} from "../utils/same";
let collectionUsersAnswers [] = [];
let collectionCorrectAnswers [] = questions.reduce((v,r) => {v.push(r.correct);
  return v;
},[]);
let correctNum = 0;
function App() {const [langValue, setLangValue] = useState(lang);
  const [usersAnswers,setUsersAnswers] = useState(collectionUsersAnswers);
  const [correctTotal,setCorrectTotal] = useState(0);
  const [orderState,orderDispatch] = useReducer(OrderReducer,0,initOrder);
  const changeLangHandler = (index: number) => {
    const value = index === 0 ? "en" : "zh";
    setLangValue(value);
  };
  const startQuestionHandler = () => orderDispatch({ type:"reset",payload:1});
  const endQuestionHandler = () => {orderDispatch({ type:"reset",payload:0});
    correctNum = 0;
  };
  const onSelectHandler = (select:string) => {// console.log(select)
    orderDispatch({type:"increment"});
    if(orderState.count > 25){orderDispatch({ type:"reset",payload:25});
    }
    if(select){collectionUsersAnswers.push(select);
    }
    correctNum = computeSameAnswer(correctNum,select,collectionCorrectAnswers,orderState.count);
    setCorrectTotal(correctNum);
    setUsersAnswers(collectionUsersAnswers);
  }
  const {count:order} = orderState;
  const wrapperRef = React.createRef();
  return (
    <div className="App flex-center">
      <LangContext.Provider value={langValue}>
        <LangComponent lang={langValue} changeLang={changeLangHandler}></LangComponent>
        {
          order > 0 ? order <= 25 ? 
            (
                <div className="flex-center flex-direction-column w-100p">
                  <QuizWrapperComponent 
                      question={questions[(order - 1 < 0 ? 0 : order - 1)] } 
                      onSelect={onSelectHandler}
                    >
                    </QuizWrapperComponent>
                  <BottomComponent lang={langValue}>{getCurrentQuestion(langValue, order)}</BottomComponent>
                </div>
            ) 
            : 
            (<div className="w-100p result-wrapper" ref={wrapperRef}>
                 <div className="flex-center flex-direction-column result-content">
                    <TitleComponent level={1}>{getCurrentAnswers(langValue,correctTotal)}</TitleComponent>
                    <ParseComponent lang={langValue} userAnswers={usersAnswers}></ParseComponent>
                    <RenderHTMLComponent template={marked(parseObject[langValue].endContent)}></RenderHTMLComponent>
                    <div className="button-wrapper mt-10">
                      <ButtonComponent nativeType="button" long onClick={endQuestionHandler}>
                        {parseObject[langValue].endBtn}
                      </ButtonComponent>
                    </div>
                 </div>
                 <TopComponent scrollElement={wrapperRef} color="#ffffff"></TopComponent>
              </div>
            )
            : 
            (
              <div className="flex-center flex-direction-column">
                <TitleComponent level={1}>{parseObject[langValue].title}</TitleComponent>
                <ContentComponent>{parseObject[langValue].startContent}</ContentComponent>
                <div className="button-wrapper mt-10">
                  <ButtonComponent nativeType="button" long onClick={startQuestionHandler}>
                    {parseObject[langValue].startBtn}
                  </ButtonComponent>
                </div>
              </div>
            )
        }
      </LangContext.Provider>
    </div>
  );
}
export default App;

以上代码波及到了一个工具函数,如下所示:

export function computeSameAnswer(correct = 0,userAnswer,correctAnswers,index) {if(userAnswer === correctAnswers[index - 1] && correct <= 25){correct++;}
    return correct;
}

能够看到,这个函数的作用就是计算用户答复的正确数的。

另外,咱们通过应用 context.provider 来将 lang 这个值传递给每一个组件,所以咱们首先是须要创立一个 context 如下所示:

import {createContext} from "react";
export let lang = "en";
export const LangContext = createContext(lang);

代码也非常简单,就是调用 React.createContext API 来创立一个上下文,更多对于这个 API 的形容能够查看文档。

除此之外,咱们还封装了一个 reducer 函数,如下所示:

export function initOrder(initialCount) {return { count: initialCount};
}
export function OrderReducer(state, action) {switch (action.type) {
    case "increment":
      return {count: state.count + 1};
    case "decrement":
      return {count: state.count - 1};
    case "reset":
      return initOrder(action.payload ? action.payload : 0);
    default:
      throw new Error();}
}

这也是 react.js 的一种数据通信模式,状态与行为(或者说叫载荷),是的咱们能够通过调用一个办法来批改数据。比方这一段代码就是这么应用的:

const startQuestionHandler = () => orderDispatch({ type:"reset",payload:1});
  const endQuestionHandler = () => {orderDispatch({ type:"reset",payload:0});
    correctNum = 0;
  };
  const onSelectHandler = (select:string) => {// console.log(select)
    orderDispatch({type:"increment"});
    if(orderState.count > 25){orderDispatch({ type:"reset",payload:25});
    }
    if(select){collectionUsersAnswers.push(select);
    }
    correctNum = computeSameAnswer(correctNum,select,collectionCorrectAnswers,orderState.count);
    setCorrectTotal(correctNum);
    setUsersAnswers(collectionUsersAnswers);
  }

而后就是咱们通过一个状态值或者说是数据值 order 值从而决定页面是渲染哪一部分的页面。order <= 0的时候则是渲染首页,order > 0 && order <= 25的时候则是渲染问题选项页面,order > 25则是渲染解析页面。

以上的源码能够查看此处。

对于这个网站,我用 vue3.X 也实现了一遍,感兴趣能够参考源码。

退出移动版