<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從零開始使用create-react-app + react + typescript 完成一個(gè)網(wǎng)站

          共 26869字,需瀏覽 54分鐘

           ·

          2021-10-11 15:05

          在線示例

          以下是一個(gè)已經(jīng)完成的成品,如圖所示:

          img

          你也可以點(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.jsdangerouslySetInnerHTML屬性來(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)效果如下圖所示:

          img

          這個(gè)組件里面可能比較復(fù)雜一點(diǎn)的是CSS布局,有采用彈性盒子布局以及背景色漸變動(dòng)畫等等,其它的也沒什么好說(shuō)的。

          以上的源碼可以查看此處。讓我們看下一個(gè)組件的實(shí)現(xiàn)。

          解析組件

          解析組件實(shí)際上就是解析頁(yè)面部分的一個(gè)封裝。我們先來(lái)看一下實(shí)現(xiàn)效果:

          img

          根據(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


          最后


          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對(duì)你有幫助,在看」是最大的支持
          ?》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持
          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本人妻の乱孕妇 | 五月丁香好婷婷网站入口 | 黄片www.| 亚洲中文字幕电影 | 日韩人妻无码精品视频 |