從零開始使用create-react-app + react + typescript 完成一個(gè)網(wǎng)站
在線示例
以下是一個(gè)已經(jīng)完成的成品,如圖所示:

你也可以點(diǎn)擊此處:https://www.eveningwater.com/my-web-projects/react/5/查看在線示例。
也許有人咋一看,看到這個(gè)網(wǎng)站有些熟悉,沒錯(cuò),這個(gè)網(wǎng)站來(lái)源于https://jsisweird.com/。我花了三天時(shí)間,用create-react-app + react + typescript重構(gòu)這個(gè)網(wǎng)站,與網(wǎng)站效果不同的是,我沒有加入任何的動(dòng)畫,并且我添加了中英文切換以及回到頂部的效果。
設(shè)計(jì)分析
觀看整個(gè)網(wǎng)站,其實(shí)整體的架構(gòu)也不復(fù)雜,就是一個(gè)首頁(yè),20道問題頁(yè)面以及一個(gè)解析頁(yè)面構(gòu)成。這些涉及到的問題也好,標(biāo)題也罷,其實(shí)都是一堆定義好的數(shù)據(jù),下面我們來(lái)一一查看這些數(shù)據(jù)的定義:
問題數(shù)據(jù)的定義
很顯然,問題數(shù)據(jù)是一個(gè)對(duì)象數(shù)組,我們來(lái)看結(jié)構(gòu)如下:
?export?const?questions?=?[];?
?//因?yàn)閱栴}本身不需要實(shí)現(xiàn)中英文切換,所以我們這里也不需要區(qū)分,數(shù)組項(xiàng)的結(jié)構(gòu)如:{question:"true?+?false",answer:["\"truefalse\"","1","NaN","SyntaxError"],correct:"1"},
數(shù)據(jù)的表示一眼就可以看出來(lái),question代表問題,answer代表回答選項(xiàng),correct代表正確答案。讓我們繼續(xù)。
解析數(shù)據(jù)的定義
解析數(shù)據(jù),需要進(jìn)行中英文切換,所以我們用一個(gè)對(duì)象表示,如下:
export?const?parseObject?=?{
????"en":{
????????output:"",//輸出文本
????????answer:"",//用戶回答文本:[],
????????successMsg:"",//用戶回答正確文本
????????errorMsg:"",//用戶回答錯(cuò)誤文本
????????detail:[],//問題答案解析文本
????????tabs:[],//中英文切換選項(xiàng)數(shù)組
????????title:"",//首頁(yè)標(biāo)題文本
????????startContent:"",//首頁(yè)段落文本
????????endContent:"",//解析頁(yè)段落文本
????????startBtn:"",//首頁(yè)開始按鈕文本
??????? endBtn:"",//解析頁(yè)重新開始文本
????},
????"zh":{
????????//選項(xiàng)同en屬性值一致
????}
}
更多詳情,請(qǐng)查看此處源碼:https://github.com/eveningwater/my-web-projects/blob/master/react/5/src/data/data.ts。
這其中,由于detail里的數(shù)據(jù)只是普通文本,我們需要將其轉(zhuǎn)換成HTML字符串,雖然有marked.js這樣的庫(kù)可以幫助我們,但是這里我們的轉(zhuǎn)換規(guī)則也比較簡(jiǎn)單,無(wú)需使用marked.js這樣的庫(kù),因此,我在這里封裝了一個(gè)簡(jiǎn)易版本的marked工具函數(shù),如下所示:
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 `${linkText}`;
}).replace(/\*\*\*([\s\S]*?)\*\*\*[\s]?/g,text => '' + text.slice(3,text.length - 4) + '');
return result;
}
轉(zhuǎn)換規(guī)則也比較簡(jiǎn)單,就是匹配a標(biāo)簽以及code標(biāo)簽,這里我們寫的是類似markdown的語(yǔ)法。比如a標(biāo)簽的寫法應(yīng)該是如下所示:
[xxx](xxx)
所以以上的轉(zhuǎn)換函數(shù),我們匹配的就是這種結(jié)構(gòu)的字符串,其正則表達(dá)式結(jié)構(gòu)如:
/\[.+?\]\(.+?\)/g;
這其中.+?表示匹配任意的字符,這個(gè)正則表達(dá)式就不言而喻了。除此之外,我們匹配代碼高亮的markdown的語(yǔ)法定義如下:
***//code***
為什么我要如此設(shè)計(jì)?這是因?yàn)槿绻乙彩褂?code style="font-size: 14px;border-radius: 4px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(155, 110, 35);background-color: rgb(255, 245, 227);padding: 3px;margin: 3px;">markdown的三個(gè)模板字符串符號(hào)來(lái)定義代碼高亮,會(huì)和js的模板字符串起沖突,所以為了不必要的麻煩,我改用了三個(gè)*來(lái)表示,所以以上的正則表達(dá)式才會(huì)匹配*。如下:
/\*\*\*([\s\S]*?)\*\*\*[\s]?/g
那么以上的正則表達(dá)式應(yīng)該如何理解呢?首先,我們需要確定的是\s以及\S代表什么意思,*在正則表達(dá)式中需要轉(zhuǎn)義,所以加了\,這個(gè)正則表達(dá)式的意思就是匹配***//code***這樣的結(jié)構(gòu)。
以上的源碼可以查看此處:https://github.com/eveningwater/my-web-projects/blob/master/react/5/src/utils/marked.ts。
其它文本的定義
還有2處的文本的定義,也就是問題選項(xiàng)的統(tǒng)計(jì)以及用戶回答問題的統(tǒng)計(jì),所以我們分別定義了2個(gè)函數(shù)來(lái)表示,如下:
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?}道題,您答對(duì)了?${?correctNum?}?道題!`;
}
這2個(gè)工具函數(shù)接受3個(gè)參數(shù),第一個(gè)參數(shù)代表語(yǔ)言類型,默認(rèn)值是"en"也就是英文模式,第二個(gè)代表當(dāng)前第幾題/正確題數(shù),第三個(gè)參數(shù)代表題的總數(shù)。然后根據(jù)這幾個(gè)參數(shù)返回一段文本,這個(gè)也沒什么好說(shuō)的。
實(shí)現(xiàn)思路分析
初始化項(xiàng)目
此處略過。可以參考文檔:https://reactjs.org/docs/create-a-new-react-app.html。
基礎(chǔ)組件的實(shí)現(xiàn)
接下來(lái),我們實(shí)際上可以將頁(yè)面分成三大部分,第一部分即首頁(yè),第二部分即問題選項(xiàng)頁(yè),第三部分則是問題解析頁(yè)面,在解析頁(yè)面由于解析內(nèi)容過多,所以我們需要一個(gè)回到頂部的效果。在提及這三個(gè)部分的實(shí)現(xiàn)之前,我們首先需要封裝一些公共的組件,讓我們來(lái)一起看一下吧!
中英文選項(xiàng)卡切換組件
不管是首頁(yè)也好,問題頁(yè)也罷,我們都會(huì)看到右上角有一個(gè)中英文切換的選項(xiàng)卡組件,效果自不比多說(shuō),讓我們來(lái)思考一下應(yīng)該如何實(shí)現(xiàn)。首先思考一下DOM結(jié)構(gòu)。我們可以很快就想到結(jié)構(gòu)如下:
<div?class="tab-container">
????<div?class="tab-item">endiv>
????<div?class="tab-item">zhdiv>
div>
在這里,我們應(yīng)該知道類名應(yīng)該會(huì)是動(dòng)態(tài)操作的,因?yàn)樾枰砑右粋€(gè)選中效果,暫定類名為active,我在這里使用的是事件代理,將事件代理到父元素tab-container上。并且它的文本也是動(dòng)態(tài)的,因?yàn)樾枰獏^(qū)分中英文。于是我們可以很快寫出如下的代碼:
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邏輯,我們可以看到我們通過父組件傳遞一個(gè)lang參數(shù)用來(lái)確定中英文模式,然后開始訪問定義數(shù)據(jù)上的tabs,即數(shù)組,react.js渲染列表通常都是使用map方法。事件代理,我們可以看到我們是通過獲取原生事件對(duì)象nativeEvent拿到類名,判斷元素是否含有tab-item類名,從而確定點(diǎn)擊的是子元素,然后調(diào)用this.setState更改當(dāng)前的索引項(xiàng),用來(lái)確定當(dāng)前是哪項(xiàng)被選中。由于只有兩項(xiàng),所以我們可以確定當(dāng)前索引項(xiàng)不是0就是1,并且我們也暴露了一個(gè)事件changeLang給父元素以便父元素可以實(shí)時(shí)的知道語(yǔ)言模式的值。
至于樣式,都是比較基礎(chǔ)的樣式,沒有什么好說(shuō)的,需要注意的就是我們是使用固定定位將選項(xiàng)卡組件固定在右上角的。以上的源碼可以查看此處。
接下來(lái),我們來(lái)看第二個(gè)組件的實(shí)現(xiàn)。
底部?jī)?nèi)容組件
底部?jī)?nèi)容組件比較簡(jiǎn)單,就是一個(gè)標(biāo)簽包裹內(nèi)容。代碼如下:
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;
}
也就是函數(shù)組件的寫法,采用固定定位定位在底部。以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
內(nèi)容組件的實(shí)現(xiàn)
該組件的實(shí)現(xiàn)也比較簡(jiǎn)單,就是用p標(biāo)簽包裝了一下。如下:
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;
}
以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
渲染HTML字符串的組件
這個(gè)組件其實(shí)也就是利用了react.js的dangerouslySetInnerHTML屬性來(lái)渲染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;
}
如代碼所示,我們可以看到其實(shí)我們就是dangerouslySetInnerHTML屬性綁定一個(gè)函數(shù),將模板字符串當(dāng)做參數(shù)傳入這個(gè)函數(shù)組件,在函數(shù)組件當(dāng)中,我們返回一個(gè)對(duì)象,結(jié)構(gòu)即:{ __html:template }。其它也就沒有什么好說(shuō)的。
以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
標(biāo)題組件的實(shí)現(xiàn)
標(biāo)題組件也就是對(duì)h1~h6標(biāo)簽的一個(gè)封裝,代碼如下:
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;
整體邏輯也不復(fù)雜,就是根據(jù)父元素傳入的一個(gè)level屬性從而確定是h1 ~ h6的哪個(gè)標(biāo)簽,也就是動(dòng)態(tài)組件的寫法。在這里,我們使用了Fragment來(lái)包裹了一下組件,關(guān)于Fragment組件的用法可以參考文檔。我的理解,它就是一個(gè)占位標(biāo)簽,由于react.js虛擬DOM的限制需要提供一個(gè)根節(jié)點(diǎn),所以這個(gè)占位標(biāo)簽的出現(xiàn)就是為了解決這個(gè)問題。當(dāng)然,如果是typescript,我們還需要顯示的定義一個(gè)類型,如下:
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的標(biāo)簽名
????let?TagName?=?`h${?props.level?}`?as?HeadingTag;
????return?(
????????<React.Fragment>
????????????<TagName>{?props.children?}TagName>
????????React.Fragment>
????)
}
export?default?TitleComponent;
以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
按鈕組件的實(shí)現(xiàn)
按鈕組件是一個(gè)最基本的組件,它的默認(rèn)樣式肯定是不符合我們的需求的,所以我們需要將它簡(jiǎn)單的封裝一下。如下所示:
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%;
}
這里對(duì)按鈕的封裝,主要是將按鈕分類,通過疊加類名的方式,給按鈕加各種類名,從而達(dá)到不同類型的按鈕的實(shí)現(xiàn)。然后暴露一個(gè)onClick事件。關(guān)于樣式代碼,這里是通過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;
}
以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
注意:這里的按鈕組件樣式事實(shí)上還沒有寫完,其它類型的樣式因?yàn)槲覀円獙?shí)現(xiàn)的網(wǎng)站沒有用到所以沒有去實(shí)現(xiàn)。
問題選項(xiàng)組件
實(shí)際上就是問題部分頁(yè)面的實(shí)現(xiàn),我們先來(lái)看實(shí)際的代碼:
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標(biāo)簽來(lái)顯示問題,四個(gè)選項(xiàng)都使用的按鈕標(biāo)簽,我們將按鈕標(biāo)簽選中的是哪一項(xiàng),通過暴露一個(gè)事件onSelect給傳遞出去。通過使用該組件的時(shí)候傳遞question數(shù)據(jù)就可以確定一組問題以及選項(xiàng)答案。所以實(shí)現(xiàn)效果如下圖所示:

這個(gè)組件里面可能比較復(fù)雜一點(diǎn)的是CSS布局,有采用彈性盒子布局以及背景色漸變動(dòng)畫等等,其它的也沒什么好說(shuō)的。
以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
解析組件
解析組件實(shí)際上就是解析頁(yè)面部分的一個(gè)封裝。我們先來(lái)看一下實(shí)現(xiàn)效果:

根據(jù)上圖,我們可以得知解析組件分為六大部分。第一部分首先是對(duì)用戶回答所作的一個(gè)正確統(tǒng)計(jì),實(shí)際上就是一個(gè)標(biāo)題組件,第二部分則同樣也是一個(gè)標(biāo)題組件,也就是題目信息。第三部分則是正確答案,第四部分則是用戶的回答,第五部分則是確定用戶回答是正確還是錯(cuò)誤,第六部分就是實(shí)際的解析。
我們來(lái)看一下實(shí)現(xiàn)代碼:
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;
}
可以看到根據(jù)我們前面分析的六大部分,我們已經(jīng)可以確定我們需要哪些組件,首先肯定是渲染一個(gè)列表,因?yàn)橛?0道題的解析,并且我們也知道根據(jù)傳遞的lang確定中英文模式。另外一個(gè)userAnswers則是用戶的回答,根據(jù)用戶的回答和正確答案做匹配,我們就可以知道用戶回答是正確還是錯(cuò)誤。這也就是如下這行代碼的意義:
const?setTypeClassName?=?(index)?=>?`answered-${?questions[index].correct?===?userAnswers[index]???'correctly'?:?'incorrectly'}`;
就是通過索引,確定返回的是正確的類名還是錯(cuò)誤的類名,通過類名來(lái)添加樣式,從而確定用戶回答是否正確。我們將以上代碼拆分一下,就很好理解了。如下:
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.答案解析
答案解析實(shí)際上就是渲染HTML字符串,所以我們就可以通過使用之前封裝好的組件。
<RenderHTMLComponent?template={?marked(content)?}>RenderHTMLComponent>
這個(gè)組件完成之后,實(shí)際上,我們的整個(gè)項(xiàng)目的大部分就已經(jīng)完成了,接下來(lái)就是一些細(xì)節(jié)的處理。
以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
讓我們繼續(xù),下一個(gè)組件的實(shí)現(xiàn)也是最難的,也就是回到頂部效果的實(shí)現(xiàn)。
回到頂部按鈕組件
回到頂部組件的實(shí)現(xiàn)思路其實(shí)很簡(jiǎn)單,就是通過監(jiān)聽滾動(dòng)事件確定回到頂部按鈕的顯隱狀態(tài),當(dāng)點(diǎn)擊回到頂部按鈕的時(shí)候,我們需要通過定時(shí)器以一定增量來(lái)進(jìn)行計(jì)算scrollTop,從而達(dá)到平滑回到頂部的效果。請(qǐng)看代碼如下:
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;
}
整個(gè)回到頂部按鈕組件分為了兩個(gè)部分,第一個(gè)部分我們是使用svg的圖標(biāo)作為回到頂部的點(diǎn)擊按鈕。首先是第一個(gè)組件TopButtonComponent,我們主要做了2個(gè)工作,第一個(gè)工作就是使用React.forwardRef API來(lái)將ref屬性進(jìn)行轉(zhuǎn)發(fā),或者說(shuō)是將ref屬性用于通信。關(guān)于這個(gè)API的詳情可查看文檔 forwardRef API。然后就是通過ref屬性拿到svg標(biāo)簽下面的所有子元素,通過setAttribute方法來(lái)為svg標(biāo)簽添加懸浮改變字體色的功能。這就是以下這個(gè)函數(shù)的作用:
const?setPathElementFill?=?(paths,?color)?=>?{
???//將顏色值和path標(biāo)簽數(shù)組作為參數(shù)傳入,然后設(shè)置fill屬性值
???if?(paths)?{
?????Array.from(paths).forEach((path)?=>?path.setAttribute("fill",?color));
???}
};
第二部分就是在鉤子函數(shù)useEffect中去監(jiān)聽元素的滾動(dòng)事件,從而確定回到頂部按鈕的顯隱狀態(tài)。并且封裝了一個(gè)更新scrollTop值的函數(shù)。
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);
????}
};
采用定時(shí)器來(lái)遞歸實(shí)現(xiàn)動(dòng)態(tài)更改scrollTop。其它也就沒有什么好說(shuō)的呢。
以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。
app組件的實(shí)現(xiàn)
實(shí)際上該組件就是將所有封裝的公共組件的一個(gè)拼湊。我們來(lái)看詳情代碼:
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;
以上代碼涉及到了一個(gè)工具函數(shù),如下所示:
export?function?computeSameAnswer(correct?=?0,userAnswer,correctAnswers,index)?{
????if(userAnswer?===?correctAnswers[index?-?1]?&&?correct?<=?25){
????????correct++;
????}
????return?correct;
}
可以看到,這個(gè)函數(shù)的作用就是計(jì)算用戶回答的正確數(shù)的。
另外,我們通過使用context.provider來(lái)將lang這個(gè)值傳遞給每一個(gè)組件,所以我們首先是需要?jiǎng)?chuàng)建一個(gè)context如下所示:
import?{?createContext?}?from?"react";
export?let?lang?=?"en";
export?const?LangContext?=?createContext(lang);
代碼也非常簡(jiǎn)單,就是調(diào)用React.createContext API來(lái)創(chuàng)建一個(gè)上下文,更多關(guān)于這個(gè)API的描述可以查看文檔。
除此之外,我們還封裝了一個(gè)reducer函數(shù),如下所示:
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的一種數(shù)據(jù)通信模式,狀態(tài)與行為(或者說(shuō)叫載荷),是的我們可以通過調(diào)用一個(gè)方法來(lái)修改數(shù)據(jù)。比如這一段代碼就是這么使用的:
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);
??}
然后就是我們通過一個(gè)狀態(tài)值或者說(shuō)是數(shù)據(jù)值order值從而決定頁(yè)面是渲染哪一部分的頁(yè)面。order <= 0的時(shí)候則是渲染首頁(yè),order > 0 && order <= 25的時(shí)候則是渲染問題選項(xiàng)頁(yè)面,order > 25則是渲染解析頁(yè)面。
以上的源碼可以查看此處:https://github.com/eveningwater/my-web-projects/blob/master/react/5/src/views/App.tsx。
關(guān)于這個(gè)網(wǎng)站,我用vue3.X也實(shí)現(xiàn)了一遍,感興趣可以參考源碼:https://github.com/eveningwater/my-web-projects/tree/master/vue/21/。
來(lái)自:夕水
https://segmentfault.com/a/1190000040677455
