整合營(yíng)銷(xiāo)服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢(xún)熱線:

          JavaScript 如何工作:對(duì)引擎、運(yùn)行時(shí)、調(diào)用

          JavaScript 如何工作:對(duì)引擎、運(yùn)行時(shí)、調(diào)用堆棧的概述

          著JavaScript越來(lái)越流行,越來(lái)越多的團(tuán)隊(duì)廣泛的把JavaScript應(yīng)用到前端、后臺(tái)、hybrid 應(yīng)用、嵌入式等等領(lǐng)域。

          這篇文章旨在深入挖掘JavaScript,以及向大家解釋JavaScript是如何工作的。我們通過(guò)了解它的底層構(gòu)建以及它是怎么發(fā)揮作用的,可以幫助我們寫(xiě)出更好的代碼與應(yīng)用。據(jù) GitHut 統(tǒng)計(jì)顯示,JavaScript 長(zhǎng)期占據(jù)GitHub中 Active Repositories 和 Total Pushes 的榜首,并且在其他的類(lèi)別中也不會(huì)落后太多。

          如果一個(gè)項(xiàng)目越來(lái)越依賴(lài) JavaScript,這就意味著開(kāi)發(fā)人員必須利用這些語(yǔ)言和生態(tài)系統(tǒng)提供更深層次的核心內(nèi)容去構(gòu)建一個(gè)令人振奮的應(yīng)用。然而,事實(shí)證明,有很多的開(kāi)發(fā)者每天都在使用 JavaScript,但是卻不知道在底層 JavaScript 是怎么運(yùn)作的。

          概述

          幾乎每個(gè)人聽(tīng)說(shuō)過(guò) V8 引擎的概念,而且,大多數(shù)人都知道 JavaScript 是單線程的,或者是它是使用回調(diào)隊(duì)列的。

          在這篇文章中,我們將詳細(xì)的介紹這些概念,并解釋 JavaScript 是怎么工作的。通過(guò)了解這些細(xì)節(jié),你就能利用這些提供的 API 來(lái)寫(xiě)出更好的,非阻塞的應(yīng)用來(lái)。如果你對(duì) JavaScript 比較陌生,那么這篇文章將幫助您理解為什么 JavaScript 相較于其他語(yǔ)言顯得如此“怪異”。如果您是一位經(jīng)驗(yàn)豐富的 JavaScript 開(kāi)發(fā)人員,希望它能給你帶來(lái)一些新的見(jiàn)解,說(shuō)明 JavaScript 的運(yùn)行時(shí),盡管你可能每天都會(huì)用到它。

          JavaScript 引擎

          JavaScript 引擎說(shuō)起來(lái)最流行的當(dāng)然是谷歌的 V8 引擎了, V8 引擎使用在 Chrome 以及 Node 中,下面有個(gè)簡(jiǎn)單的圖能說(shuō)明他們的關(guān)系:

          這個(gè)引擎主要由兩部分組成:

          • 內(nèi)存堆:這是內(nèi)存分配發(fā)生的地方
          • 調(diào)用棧:這是你的代碼執(zhí)行時(shí)的地方

          運(yùn)行時(shí)

          有些瀏覽器的 API 經(jīng)常被使用到(比如說(shuō):setTimeout),但是,這些 API 卻不是引擎提供的。那么,他們是從哪兒來(lái)的呢?事實(shí)上這里面實(shí)際情況有點(diǎn)復(fù)雜。

          所以說(shuō)我們還有很多引擎之外的 API,我們把這些稱(chēng)為瀏覽器提供的 Web API,比如說(shuō) DOM、AJAX、setTimeout等等。

          然后我們還擁有如此流行的事件循環(huán)和回調(diào)隊(duì)列。

          調(diào)用棧

          JavaScript 是一門(mén)單線程的語(yǔ)言,這意味著它只有一個(gè)調(diào)用棧,因此,它同一時(shí)間只能做一件事。

          調(diào)用棧是一種數(shù)據(jù)結(jié)構(gòu),它記錄了我們?cè)诔绦蛑械奈恢谩H绻覀冞\(yùn)行到一個(gè)函數(shù),它就會(huì)將其放置到棧頂。當(dāng)從這個(gè)函數(shù)返回的時(shí)候,就會(huì)將這個(gè)函數(shù)從棧頂彈出,這就是調(diào)用棧做的事情。

          讓我們來(lái)看一看下面的例子:

           function multiply(x, y) {
           return x * y;
           }
           function printSquare(x) {
           var s=multiply(x, x);
           console.log(s);
           }
           printSquare(5);
          
          

          當(dāng)程序開(kāi)始執(zhí)行的時(shí)候,調(diào)用棧是空的,然后,步驟如下:

          每一個(gè)進(jìn)入調(diào)用棧的都稱(chēng)為_(kāi)_調(diào)用幀__。

          這能清楚的知道當(dāng)異常發(fā)生的時(shí)候堆棧追蹤是怎么被構(gòu)造的,堆棧的狀態(tài)是如何的。讓我們看一下下面的代碼:

           function foo() {
           throw new Error('SessionStack will help you resolve crashes :)');
           }
           function bar() {
           foo();
           }
           function start() {
           bar();
           }
           start();
          
          

          如果這發(fā)生在 Chrome 里(假設(shè)這段代碼實(shí)在一個(gè)名為 foo.js 的文件中),那么將會(huì)生成以下的堆棧追蹤:

          "堆棧溢出",當(dāng)你達(dá)到調(diào)用棧最大的大小的時(shí)候就會(huì)發(fā)生這種情況,而且這相當(dāng)容易發(fā)生,特別是在你寫(xiě)遞歸的時(shí)候卻沒(méi)有全方位的測(cè)試它。我們來(lái)看看下面的代碼:

           function foo() {
           foo();
           }
           foo();
          
          

          當(dāng)我們的引擎開(kāi)始執(zhí)行這段代碼的時(shí)候,它從 foo 函數(shù)開(kāi)始。然后這是個(gè)遞歸的函數(shù),并且在沒(méi)有任何的終止條件的情況下開(kāi)始調(diào)用自己。因此,每執(zhí)行一步,就會(huì)把這個(gè)相同的函數(shù)一次又一次地添加到調(diào)用堆棧中。然后它看起來(lái)就像是這樣的:

          然后,在某一時(shí)刻,調(diào)用棧中的函數(shù)調(diào)用的數(shù)量超過(guò)了調(diào)用棧的實(shí)際大小,瀏覽器決定干掉它,拋出一個(gè)錯(cuò)誤,它看起來(lái)就像是這樣:

          在單個(gè)線程上運(yùn)行代碼很容易,因?yàn)槟悴槐靥幚碓诙嗑€程環(huán)境中出現(xiàn)的復(fù)雜場(chǎng)景——例如死鎖。但是在一個(gè)線程上運(yùn)行也非常有限制。由于 JavaScript 只有一個(gè)調(diào)用堆棧,當(dāng)某段代碼運(yùn)行變慢時(shí)會(huì)發(fā)生什么?

          并發(fā)與事件循環(huán)

          調(diào)用棧中的函數(shù)調(diào)用需要大量的時(shí)間來(lái)處理,那么這會(huì)發(fā)生什么情況呢?例如,假設(shè)你想在瀏覽器中使用 JavaScript 進(jìn)行一些復(fù)雜的圖片轉(zhuǎn)碼。

          你可能會(huì)問(wèn)?這算什么問(wèn)題?事實(shí)上,問(wèn)題是當(dāng)調(diào)用棧有函數(shù)要執(zhí)行,瀏覽器就不能做任何事,它會(huì)被堵塞住。這意味著瀏覽器不能渲染,不能運(yùn)行其他的代碼,它被卡住了。如果你想在應(yīng)用里讓 UI 很流暢的話,這就會(huì)產(chǎn)生問(wèn)題。

          而且這不是唯一的問(wèn)題,一旦你的瀏覽器開(kāi)始處理調(diào)用棧中的眾多任務(wù),它可能會(huì)停止響應(yīng)相當(dāng)長(zhǎng)一段時(shí)間。大多數(shù)瀏覽器都會(huì)這么做,報(bào)一個(gè)錯(cuò)誤,詢(xún)問(wèn)你是否想終止 web 頁(yè)面。

          這樣看來(lái),這并不是最好的用戶(hù)體驗(yàn),不是嗎?

          作者:小烜同學(xué)

          鏈接:https://juejin.im/post/5a05b4576fb9a04519690d42

          .HTML 介紹

          是網(wǎng)頁(yè)的后綴,txt 后綴是文本 ,py 后綴是 python ,html 后綴就是網(wǎng)頁(yè)的意思。我們?nèi)绻雱?chuàng)建一個(gè)網(wǎng)頁(yè)的話,可以直接將文本的后綴改為 html 。HTMLSHI 超文本標(biāo)記語(yǔ)言,是一種標(biāo)識(shí)性的語(yǔ)言。它包括一系列標(biāo)記標(biāo)簽,通過(guò)這些標(biāo)記標(biāo)簽可以將網(wǎng)絡(luò)上的文檔格式統(tǒng)一,使分散的Internet資源連接為一個(gè)邏輯整體。

          1.html 的介紹

          頁(yè)面整體分為兩部分:

          一部分是head部分,主要是頁(yè)面的整體信息和配置,內(nèi)容不會(huì)出現(xiàn)在瀏覽器內(nèi)部。

          一部分是body部分,這部分內(nèi)容則會(huì)在瀏覽器中展示出來(lái)

          我們使用 pycharm 創(chuàng)建一個(gè) html ,打開(kāi)后就是下圖模樣。


          (1)文檔類(lèi)型聲明(默認(rèn)的可以不用設(shè)置)

          <!DOCTYPE html>

          (2)開(kāi)始標(biāo)簽和結(jié)束標(biāo)簽

          一般的標(biāo)簽是成對(duì)出現(xiàn)的,一般稱(chēng)第一個(gè)標(biāo)簽是開(kāi)始標(biāo)簽,第二個(gè)是結(jié)束標(biāo)簽。開(kāi)始和結(jié)束標(biāo)簽也稱(chēng)為開(kāi)放標(biāo)簽和閉合標(biāo)簽。

          開(kāi)始標(biāo)簽:

          <html lang="en">

          其中的 html 為根元素,是所有元素的基礎(chǔ)。lang 表示語(yǔ)言,en 表示英文。

          結(jié)束標(biāo)簽:

          </html>

          (3)頭部標(biāo)簽

          <head>
              <meta charset="UTF-8">
              <title>Title</title>
          </head>

          其中 utf-8 表示字符編碼格式,如果沒(méi)有寫(xiě)這個(gè)就會(huì)發(fā)生亂碼。Title 表示文檔的標(biāo)題。

          (4)身體標(biāo)簽

          <body>
          
          </body>

          身體標(biāo)簽是文檔的主題,可視化區(qū)域,所有的音頻,視頻,圖片,文字都可在其中搭建,相當(dāng)于我們打開(kāi)網(wǎng)頁(yè)時(shí)所看到內(nèi)容。

          (5)標(biāo)簽的特點(diǎn)

          標(biāo)簽是由一對(duì)尖括號(hào)包裹單詞構(gòu)成的,標(biāo)簽要使用小寫(xiě)。 一般的標(biāo)簽是成對(duì)出現(xiàn)的,一般稱(chēng)第一個(gè)標(biāo)簽是開(kāi)始標(biāo)簽,第二個(gè)是結(jié)束標(biāo)簽。開(kāi)始和結(jié)束標(biāo)簽也稱(chēng)為開(kāi)放標(biāo)簽和閉合標(biāo)簽。

          二.標(biāo)簽

          標(biāo)簽分為塊級(jí)標(biāo)簽和內(nèi)聯(lián)標(biāo)簽(運(yùn)行時(shí)點(diǎn)擊右上角的谷歌模式的小圓圈就可以)

          1.內(nèi)容的書(shū)寫(xiě)

          (1)塊級(jí)標(biāo)簽(p)

          兩個(gè) p 中間可隨意書(shū)寫(xiě)內(nèi)容

          <p>故事和酒,淘寶都有</p>

          (2)內(nèi)聯(lián)標(biāo)簽(span)

          <span>故事和酒,淘寶都有</span>

          完整代碼:

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>Title</title>
          </head>
          <body>
          <!-- 塊級(jí)標(biāo)簽--> 
          <p>故事和酒,淘寶都有</p>
          <!--內(nèi)聯(lián)標(biāo)簽-->
          <span>故事和酒,淘寶都有</span>
          </body>
          </html>

          運(yùn)行后:


          運(yùn)行后看不出塊級(jí)標(biāo)簽和內(nèi)聯(lián)標(biāo)簽的區(qū)別,所有我們使用檢查。右擊后點(diǎn)擊檢查


          在點(diǎn)擊下圖中左上角的方框箭頭,變成藍(lán)色說(shuō)明正在運(yùn)行,之后就可以查看有關(guān)的數(shù)據(jù)了


          無(wú)需點(diǎn)擊,只要將箭頭放在文字上就會(huì)出現(xiàn)相關(guān)內(nèi)容



          上面兩圖可以明顯看出兩句話的寬度不相同。

          塊級(jí)標(biāo)簽:在不設(shè)置寬度的情況下,寬度始終和瀏覽器寬度保持一致。

          內(nèi)聯(lián)標(biāo)簽:寬度和內(nèi)容有關(guān)

          2.設(shè)置高度寬度

          <p style="width: 500px;height: 50px;">故事和酒,淘寶都有</p>
          
          <span style="width: 500px;height: 50px;">故事和酒,淘寶都有</span>
          



          如圖所示,只有塊級(jí)標(biāo)簽寬高改變了,內(nèi)聯(lián)標(biāo)簽不改變。由此可得,塊級(jí)標(biāo)簽設(shè)置寬高有效,內(nèi)聯(lián)標(biāo)簽設(shè)置寬高無(wú)效。

          3.多個(gè)標(biāo)簽同時(shí)存在

          <body>
          <!-- 塊級(jí)標(biāo)簽-->
          <p>故事和酒,淘寶都有</p>
          <p>故事和酒,淘寶都有</p>
          <!--內(nèi)聯(lián)標(biāo)簽-->
          <span>故事和酒,淘寶都有22</span>
          <span>故事和酒,淘寶都有22</span>
          </body>


          多個(gè)塊級(jí)標(biāo)簽同時(shí)存在的情況下,排列方式從上往下
          多個(gè)內(nèi)聯(lián)標(biāo)簽同時(shí)存在的情況下,排列方式從左往右

          4.是否包含

          <body>
          <!-- 塊級(jí)標(biāo)簽-->
          <p>故事和酒,淘寶都有
              <span>故事和酒,淘寶都有22</span>
          </p>
          
          <!--內(nèi)聯(lián)標(biāo)簽-->
          <span>故事和酒,淘寶都有22
              <p>故事和酒,淘寶都有</p>
          </span>
          
          </body>


          由此可知,塊級(jí)標(biāo)簽可以包含內(nèi)聯(lián)標(biāo)簽,但內(nèi)聯(lián)標(biāo)簽不可以包含塊級(jí)標(biāo)簽,只可以包含內(nèi)聯(lián)標(biāo)簽。

          5.塊級(jí)標(biāo)簽與內(nèi)聯(lián)標(biāo)簽相互轉(zhuǎn)換

          (1)塊級(jí)轉(zhuǎn)內(nèi)聯(lián)

          <body>
          <!--將塊級(jí)標(biāo)簽轉(zhuǎn)化成內(nèi)聯(lián)標(biāo)簽-->
          <p style="display: inline">故事和酒,淘寶都有</p>
          <p style="display: inline">故事和酒,淘寶都有</p>
          
          </body>


          (2)內(nèi)聯(lián)轉(zhuǎn)塊級(jí)(display: block)

          內(nèi)聯(lián)轉(zhuǎn)為塊級(jí)之后,具有了塊級(jí)的性質(zhì)。

          <span style="display: block">故事和酒,淘寶都有222</span>
          <span style="display: block">故事和酒,淘寶都有222</span>


          (3)內(nèi)聯(lián)塊元素(display: inline-block)

          內(nèi)聯(lián)塊元素包含了內(nèi)聯(lián)標(biāo)簽和塊級(jí)標(biāo)簽的部分特性。

          <span style="display: inline-block">故事和酒,淘寶都有333</span>
          <span style="display: inline-block;height: 50px">故事和酒,淘寶都有333</span>


          (4)段落標(biāo)簽(p)

          <!--段落標(biāo)簽-->
          <p></p>

          (5)標(biāo)題標(biāo)簽(h)


          們現(xiàn)在正處于可以構(gòu)建一個(gè) Web 應(yīng)用程序的階段,該應(yīng)用程序可以使用不同的方法和數(shù)據(jù)管理一系列 HTTP 請(qǐng)求。 這很有用,特別是當(dāng)我們?yōu)槲⒎?wù)構(gòu)建服務(wù)器時(shí)。 然而,我們也希望非程序員能夠與我們的應(yīng)用程序交互來(lái)使用它。 為了使非程序員能夠使用我們的應(yīng)用程序,我們必須創(chuàng)建一個(gè)圖形用戶(hù)界面。 不過(guò),必須注意的是,本章包含的 Rust 內(nèi)容并不多。 這是因?yàn)榇嬖谄渌Z(yǔ)言來(lái)呈現(xiàn)圖形用戶(hù)界面。 我們將主要使用 HTML、JavaScript 和 CSS。 這些工具已經(jīng)成熟并廣泛用于前端 Web 開(kāi)發(fā)。 雖然我個(gè)人很喜歡 Rust(否則我不會(huì)寫(xiě)一本關(guān)于它的書(shū)),但我們必須使用正確的工具來(lái)完成正確的工作。 在撰寫(xiě)本書(shū)時(shí),我們可以使用 Yew 框架在 Rust 中構(gòu)建前端應(yīng)用程序。 然而,能夠?qū)⒏墒斓墓ぞ呷诤系轿覀兊?Rust 技術(shù)堆棧中是一項(xiàng)更有價(jià)值的技能。

          本章將涵蓋以下主題:

          使用 Rust 提供 HTML、CSS 和 JavaScript 服務(wù)

          構(gòu)建連接到 Rust 服務(wù)器的 React 應(yīng)用程序

          將我們的 React 應(yīng)用程序轉(zhuǎn)換為要安裝在計(jì)算機(jī)上的桌面應(yīng)用程序

          在上一版本(Rust Web 編程:使用 Rust 編程語(yǔ)言開(kāi)發(fā)快速、安全的 Web 應(yīng)用程序的實(shí)踐指南)中,我們只是直接從 Rust 提供前端資產(chǎn)。 然而,由于反饋和修訂,這不能很好地?cái)U(kuò)展,導(dǎo)致大量重復(fù)。 由于使用這種方法的非結(jié)構(gòu)化性質(zhì),由 Rust 直接提供的原始 HTML、CSS 和 JavaScript 也容易出錯(cuò),這就是為什么在第二版中,我們將介紹 React 并簡(jiǎn)要介紹如何提供前端資產(chǎn) 直接使用 Rust。 到本章結(jié)束時(shí),您將能夠在沒(méi)有任何依賴(lài)的情況下編寫(xiě)基本的前端圖形界面,并了解低依賴(lài)前端解決方案和完整前端框架(例如 React)之間的權(quán)衡。 您不僅會(huì)了解何時(shí)使用它們,而且還能夠在項(xiàng)目需要時(shí)實(shí)施這兩種方法。 因此,您將能夠?yàn)檎_的工作選擇正確的工具,并在后端使用 Rust 并在前端使用 JavaScript 構(gòu)建端到端產(chǎn)品。

          使用 Rust 提供 HTML、CSS 和 JavaScript 服務(wù)

          在上一章中,我們以 JSON 的形式返回了所有數(shù)據(jù)。 在本節(jié)中,我們將返回 HTML 數(shù)據(jù)供用戶(hù)查看。 在此 HTML 數(shù)據(jù)中,我們將具有按鈕和表單,使用戶(hù)能夠與我們?cè)谏弦徽轮卸x的 API 端點(diǎn)進(jìn)行交互,以創(chuàng)建、編輯和刪除待辦事項(xiàng)。 為此,我們需要構(gòu)建自己的應(yīng)用程序視圖模塊,該模塊采用以下結(jié)構(gòu):

          views
          ├── app
          │   ├── items.rs
          │   └── mod.rs

          提供基本的 HTML

          在我們的 items.rs 文件中,我們將定義顯示待辦事項(xiàng)的主視圖。 但是,在此之前,我們應(yīng)該探索在 items.rs 文件中返回 HTML 的最簡(jiǎn)單方法:

          use actix_web::HttpResponse;
          pub async fn items() -> HttpResponse {
              HttpResponse::Ok()
                  .content_type("text/html; charset=utf-8")
                  .body("<h1>Items</h1>")
          }

          在這里,我們簡(jiǎn)單地返回一個(gè) HttpResponse 結(jié)構(gòu),該結(jié)構(gòu)具有 HTML 內(nèi)容類(lèi)型和 <h1>Items</h1> 主體。 要將 HttpResponse 傳遞到應(yīng)用程序中,我們必須在 app/views/mod.rs 文件中定義我們的工廠,如下所示:

          use actix_web::web;
          mod items;
          pub fn app_views_factory(app: &mut web::ServiceConfig) {
              app.route("/", web::get().to(items::items));
          }

          在這里,我們可以看到,我們只是為應(yīng)用程序定義了一條路由,而不是構(gòu)建服務(wù)。 這是因?yàn)檫@是登陸頁(yè)面。 如果我們要定義服務(wù)而不是路由,我們將無(wú)法在沒(méi)有前綴的情況下定義服務(wù)的視圖。

          一旦我們定義了app_views_factory,我們就可以在views/mod.rs 文件中調(diào)用它。 然而,首先,我們必須在views/mod.rs文件的頂部定義app模塊:

          mod app;

          一旦我們定義了應(yīng)用程序模塊,我們就可以在同一文件中的views_factory函數(shù)中調(diào)用應(yīng)用程序工廠:

          app::app_views_factory(app);

          現(xiàn)在我們的 HTML 服務(wù)視圖是我們應(yīng)用程序的一部分,我們可以運(yùn)行它并在瀏覽器中調(diào)用主 URL,給出以下輸出:


          圖 5.1 – 第一個(gè)呈現(xiàn)的 HTML 視圖

          我們可以看到我們的 HTML 已渲染! 根據(jù)圖 5.1 中的內(nèi)容,我們可以推斷出我們可以在響應(yīng)正文中返回一個(gè)字符串,其中包含以下內(nèi)容:

          HttpResponse::Ok()
              .content_type("text/html; charset=utf-8")
              .body("<h1>Items</h1>")

          如果字符串是 HTML 格式,則會(huì)呈現(xiàn) HTML。 根據(jù)這個(gè)啟示,您認(rèn)為我們?nèi)绾螐?Rust 服務(wù)器提供的 HTML 文件中渲染 HTML? 在繼續(xù)之前,想一想——這將鍛煉你解決問(wèn)題的能力。

          從文件中讀取基本 HTML

          如果我們有一個(gè) HTML 文件,我們只需將該 HTML 文件準(zhǔn)備為一個(gè)字符串并將該字符串插入到 HttpResponse 的正文中即可呈現(xiàn)它。 是的,就是這么簡(jiǎn)單。 為了實(shí)現(xiàn)這一目標(biāo),我們將構(gòu)建一個(gè)內(nèi)容加載器。

          要構(gòu)建基本的內(nèi)容加載器,首先在views/app/content_loader.rs文件中構(gòu)建HTML文件讀取函數(shù):

          use std::fs;
          pub fn read_file(file_path: &str) -> String {
              let data: String=fs::read_to_string(
                  file_path).expect("Unable to read file");
              return data
          }

          我們?cè)谶@里要做的就是返回一個(gè)字符串,因?yàn)檫@就是我們響應(yīng)正文所需的全部?jī)?nèi)容。 然后,我們必須在views/app/mod.rs文件中使用mod content_loader定義加載器; 文件頂部的行。

          現(xiàn)在我們有了加載功能,我們需要一個(gè) HTML 目錄。 這可以與稱(chēng)為 templates 的 src 目錄一起定義。 在 templates 目錄中,我們可以添加一個(gè)名為 templates/main.html 的 HTML 文件,其中包含以下內(nèi)容:

          <!DOCTYPE html>
          <html lang="en">
              <head>
                  <meta charSet="UTF-8"/>
                  <meta name="viewport"
                        content="width=device-width, initial-
                                                     scale=1.0"/>
                  <meta httpEquiv="X-UA-Compatible"
                        content="ie=edge"/>
                  <meta name="description"
                        content="This is a simple to do app"/>
                  <title>To Do App</title>
              </head>
              <body>
                  <h1>To Do Items</h1>
              </body>
          </html>

          在這里,我們可以看到我們的 body 標(biāo)簽具有與我們之前呈現(xiàn)的內(nèi)容相同的內(nèi)容 - 即 <h1>To Do Items</h1>。 然后,我們有一個(gè) head 標(biāo)簽,它定義了一系列元標(biāo)簽。 我們可以看到我們定義了視口。 這告訴瀏覽器如何處理頁(yè)面內(nèi)容的尺寸和縮放。 縮放很重要,因?yàn)槲覀兊膽?yīng)用程序可以通過(guò)一系列不同的設(shè)備和屏幕尺寸來(lái)訪問(wèn)。 通過(guò)這個(gè)視口,我們可以將頁(yè)面的寬度設(shè)置為與設(shè)備屏幕相同的寬度。 然后,我們可以將訪問(wèn)的頁(yè)面的初始比例設(shè)置為1.0。 轉(zhuǎn)到 httpEquiv 標(biāo)簽,我們將其設(shè)置為 X-UA-Compatible,這意味著我們支持舊版瀏覽器。 最終標(biāo)簽只是搜索引擎可以使用的頁(yè)面的描述。 我們的標(biāo)題標(biāo)簽確保待辦事項(xiàng)應(yīng)用程序顯示在瀏覽器標(biāo)簽上。 這樣,我們的正文中就有了標(biāo)準(zhǔn)的標(biāo)題標(biāo)題。

          提供從文件加載的基本 HTML

          現(xiàn)在我們已經(jīng)定義了 HTML 文件,我們必須加載并提供它。 回到我們的 src/views/app/items.rs 文件,我們必須加載 HTML 文件并使用以下代碼提供服務(wù):

          use actix_web::HttpResponse;
          use super::content_loader::read_file;
          pub async fn items() -> HttpResponse {
              let html_data=read_file(
                  "./templates/main.html");
              HttpResponse::Ok()
                  .content_type("text/html; charset=utf-8")
                  .body(html_data)
          }

          如果我們運(yùn)行我們的應(yīng)用程序,我們將得到以下輸出:


          圖 5.2 – 加載 HTML 頁(yè)面的視圖

          在圖 5.2 中,我們可以看到輸出與之前相同。 這并不奇怪; 但是,我們必須注意到,圖 5.2 中的選項(xiàng)卡現(xiàn)在顯示了“To Do App”,這意味著 HTML 文件中的元數(shù)據(jù)正在加載到視圖中。 沒(méi)有什么可以阻止我們充分利用 HTML 文件。 現(xiàn)在我們的 HTML 文件已經(jīng)提供,我們可以繼續(xù)我們的下一個(gè)目標(biāo),即向我們的頁(yè)面添加功能。

          將 JavaScript 添加到 HTML 文件

          如果前端用戶(hù)無(wú)法對(duì)我們的待辦事項(xiàng)狀態(tài)執(zhí)行任何操作,那么這對(duì)前端用戶(hù)來(lái)說(shuō)就沒(méi)有用。 在修改之前,我們需要通過(guò)查看下圖來(lái)了解 HTML 文件的布局:


          圖 5.3 – HTML 文件的一般布局

          在圖 5.3 中,我們可以看到我們可以在標(biāo)頭中定義元標(biāo)記。 然而,我們也可以看到我們可以在標(biāo)題中定義樣式標(biāo)簽。 在標(biāo)題下方的樣式標(biāo)簽中,我們可以將 CSS 插入到樣式中。 在主體下方,還有一個(gè)腳本部分,我們可以在其中注入 JavaScript。 該 JavaScript 在瀏覽器中運(yùn)行并與正文中的元素交互。 由此,我們可以看到,提供加載了 CSS 和 JavaScript 的 HTML 文件提供了一個(gè)功能齊全的前端單頁(yè)應(yīng)用程序。 至此,我們可以反思一下本章的介紹。 雖然我喜歡 Rust,并且強(qiáng)烈希望告訴你用它來(lái)編寫(xiě)所有內(nèi)容,但這對(duì)于軟件工程中的任何語(yǔ)言來(lái)說(shuō)都不是一個(gè)好主意。 現(xiàn)在,我們可以輕松地使用 JavaScript 提供功能性前端視圖,使其成為滿足您前端需求的最佳選擇。

          使用 JavaScript 與我們的服務(wù)器通信

          現(xiàn)在我們知道了將 JavaScript 插入到 HTML 文件中的位置,我們可以測(cè)試我們的方向了。 在本節(jié)的其余部分中,我們將在 HTML 正文中創(chuàng)建一個(gè)按鈕,將其融合到 JavaScript 函數(shù),然后讓瀏覽器在按下該按鈕時(shí)打印出帶有輸入消息的警報(bào)。 這對(duì)我們的后端應(yīng)用程序沒(méi)有任何作用,但它將證明我們對(duì) HTML 文件的理解是正確的。 我們可以將以下代碼添加到 templates/main.html 文件中:

          <body>
              <h1>To Do Items</h1>
              <input type="text" id="name" placeholder="create to do 
                   item">
              <button id="create-button" value="Send">Create</button>
          </body>
          <script>
              let createButton=document.getElementById("create-
                  button");
              createButton.addEventListener("click", postAlert);
              function postAlert() {
                  let titleInput=document.getElementById("name");
                  alert(titleInput.value);
                  titleInput.value=null;
              }
          </script>

          在我們的正文部分,我們可以看到我們定義了一個(gè)輸入和一個(gè)按鈕。 我們?yōu)檩斎牒桶粹o屬性提供唯一的 ID 名稱(chēng)。 然后,我們使用按鈕的 ID 添加事件監(jiān)聽(tīng)器。 之后,我們將 postAlert 函數(shù)綁定到該事件偵聽(tīng)器,以便在單擊按鈕時(shí)觸發(fā)。 當(dāng)我們觸發(fā) postAlert 函數(shù)時(shí),我們使用其 ID 獲取輸入并打印出警報(bào)中的輸入值。 然后,我們將input的值設(shè)置為null,以便用戶(hù)可以填寫(xiě)另一個(gè)要處理的值。 提供新的 main.html 文件,在輸入中進(jìn)行測(cè)試,然后單擊按鈕將產(chǎn)生以下輸出:


          圖 5.4 – 連接到 JavaScript 中的警報(bào)時(shí)單擊按鈕的效果


          我們的 JavaScript 不必停止讓元素在主體中交互。 我們還可以使用 JavaScript 對(duì)后端 Rust 應(yīng)用程序執(zhí)行 API 調(diào)用。 然而,在我們匆忙將整個(gè)應(yīng)用程序?qū)懭?main.html 文件之前,我們必須停下來(lái)思考一下。 如果我們這樣做,main.html 文件就會(huì)膨脹成一個(gè)巨大的文件。 調(diào)試起來(lái)會(huì)很困難。 此外,這可能會(huì)導(dǎo)致代碼重復(fù)。 如果我們想在其他視圖中使用相同的 JavaScript 怎么辦? 我們必須將其復(fù)制并粘貼到另一個(gè) HTML 文件中。 這無(wú)法很好地?cái)U(kuò)展,如果我們需要更新某個(gè)函數(shù),我們可能會(huì)面臨忘記更新某些重復(fù)函數(shù)的風(fēng)險(xiǎn)。 這就是 React 等 JavaScript 框架派上用場(chǎng)的地方。 我們將在本章后面探討 React,但現(xiàn)在,我們將通過(guò)提出一種將 JavaScript 與 HTML 文件分離的方法來(lái)完成我們的低依賴(lài)前端。

          必須警告的是,我們實(shí)際上是使用此 JavaScript 手動(dòng)動(dòng)態(tài)重寫(xiě) HTML。 人們可以將其描述為“hacky”解決方案。 然而,在探索 React 之前,重要的是要先掌握我們的方法,才能真正體會(huì)到不同方法的好處。 在繼續(xù)下一部分之前,我們必須在 src/views/to_do/create.rs 文件中重構(gòu)我們的創(chuàng)建視圖。 這是一個(gè)很好的機(jī)會(huì)來(lái)回顧我們?cè)谇皫渍轮虚_(kāi)發(fā)的內(nèi)容。 您必須本質(zhì)上轉(zhuǎn)換創(chuàng)建視圖,以便它返回待辦事項(xiàng)的當(dāng)前狀態(tài)而不是字符串。 嘗試此操作后,解決方案應(yīng)如下所示:

          use actix_web::HttpResponse;
          use serde_json::Value;
          use serde_json::Map;
          use actix_web::HttpRequest;
          use crate::to_do::{to_do_factory, enums::TaskStatus};
          use crate::json_serialization::to_do_items::ToDoItems;
          use crate::state::read_file;
          use crate::processes::process_input;
          pub async fn create(req: HttpRequest) -> HttpResponse {
              let state: Map<String, Value>= read_file("./state.json");
              let title: String=req.match_info().get("title"
              ).unwrap().to_string();
              let item=to_do_factory(&title.as_str(), 
                  TaskStatus::PENDING);
              process_input(item, "create".to_string(), &state);
              return HttpResponse::Ok().json(ToDoItems::get_state())
          }

          現(xiàn)在,我們所有的待辦事項(xiàng)均已更新并正常運(yùn)行。 現(xiàn)在我們可以進(jìn)入下一部分,我們將讓前端調(diào)用后端。

          將 JavaScript 注入 HTML

          完成本節(jié)后,我們將擁有一個(gè)不太漂亮但功能齊全的主視圖,我們可以在其中使用 JavaScript 調(diào)用 Rust 服務(wù)器來(lái)添加、編輯和刪除待辦事項(xiàng)。 但是,您可能還記得,我們沒(méi)有添加刪除 API 端點(diǎn)。 要將 JavaScript 注入到 HTML 中,我們必須執(zhí)行以下步驟:

          創(chuàng)建刪除項(xiàng)目 API 端點(diǎn)。

          添加 JavaScript 加載功能,并將 HTML 數(shù)據(jù)中的 JavaScript 標(biāo)簽替換為主項(xiàng) Rust 視圖中加載的 JavaScript 數(shù)據(jù)。

          在 HTML 文件中添加 JavaScript 標(biāo)簽,并在 HTML 組件中添加 ID,以便我們可以在 JavaScript 中引用組件。

          在 JavaScript 中為我們的待辦事項(xiàng)構(gòu)建一個(gè)渲染函數(shù),并通過(guò) ID 將其綁定到我們的 HTML。

          在 JavaScript 中構(gòu)建一個(gè) API 調(diào)用函數(shù)來(lái)與后端對(duì)話。

          在 JavaScript 中構(gòu)建獲取、刪除、編輯和創(chuàng)建函數(shù),供我們的按鈕使用。

          讓我們?cè)敿?xì)看看這一點(diǎn)。

          添加刪除端點(diǎn)

          現(xiàn)在添加刪除 API 端點(diǎn)應(yīng)該很簡(jiǎn)單。 如果您愿意,建議您自己嘗試并實(shí)現(xiàn)此視圖,因?yàn)槟F(xiàn)在應(yīng)該已經(jīng)熟悉此過(guò)程了:

          如果您遇到困難,我們可以通過(guò)將以下第三方依賴(lài)項(xiàng)導(dǎo)入到views/to_do/delete.rs 文件中來(lái)實(shí)現(xiàn)此目的:

          use actix_web::{web, HttpResponse};
          
          use serde_json::value::Value;
          
          use serde_json::Map;

          這些并不新鮮,您應(yīng)該熟悉它們并知道我們需要在哪里使用它們。

          然后,我們必須使用以下代碼導(dǎo)入結(jié)構(gòu)和函數(shù):

          use crate::to_do::{to_do_factory, enums::TaskStatus};
          
          use crate::json_serialization::{to_do_item::ToDoItem, 
          
              to_do_items::ToDoItems};
          
          use crate::processes::process_input;
          
          use crate::jwt::JwToken;
          
          use crate::state::read_file;

          在這里,我們可以看到我們正在使用 to_do 模塊來(lái)構(gòu)建我們的待辦事項(xiàng)。 通過(guò)我們的 json_serialization 模塊,我們可以看到我們正在接受 ToDoItem 并返回 ToDoItems。 然后,我們使用 process_input 函數(shù)執(zhí)行項(xiàng)目的刪除。 我們也不希望任何可以訪問(wèn)我們頁(yè)面的人刪除我們的項(xiàng)目。 因此,我們需要 JwToken 結(jié)構(gòu)。 最后,我們使用 read_file 函數(shù)讀取項(xiàng)目的狀態(tài)。

          現(xiàn)在我們已經(jīng)擁有了所需的一切,我們可以使用以下代碼定義刪除視圖:

          pub async fn delete(to_do_item: web::Json<ToDoItem>, 
          
              token: JwToken) -> HttpResponse {
          
              . . .
          
          }

          在這里,我們可以看到我們已經(jīng)接受了 JSON 形式的 ToDoItem,并且我們已經(jīng)為視圖附加了 JwToken,以便用戶(hù)必須有權(quán)訪問(wèn)它。 此時(shí),我們只有 JwToken 附加一條消息; 我們將在第 7 章“管理用戶(hù)會(huì)話”中管理 JwToken 的身份驗(yàn)證邏輯。

          在刪除視圖中,我們可以通過(guò)使用以下代碼讀取 JSON 文件來(lái)獲取待辦事項(xiàng)的狀態(tài):

          let state: Map<String, Value>=read_file("./state.json");

          然后,我們可以檢查具有該標(biāo)題的項(xiàng)目是否處于該狀態(tài)。 如果不是,那么我們返回一個(gè)未找到的 HTTP 響應(yīng)。 如果是,我們就會(huì)傳遞狀態(tài),因?yàn)槲覀冃枰獦?biāo)題和狀態(tài)來(lái)構(gòu)建項(xiàng)目。 我們可以使用以下代碼來(lái)實(shí)現(xiàn)這種檢查和狀態(tài)提取:

          let status: TaskStatus;
          
          match &state.get(&to_do_item.title) {
          
              Some(result)=> {
          
                  status=TaskStatus::from_string
          
                           (result.as_str().unwrap().to_string()                 );
          
              }
          
              None=> {
          
                  return HttpResponse::NotFound().json(
          
                      format!("{} not in state", 
          
                               &to_do_item.title))
          
              }
          
          }

          現(xiàn)在我們有了待辦事項(xiàng)的狀態(tài)和標(biāo)題,我們可以構(gòu)建我們的項(xiàng)目并使用刪除命令將其傳遞到 process_input 函數(shù)。 這將從 JSON 文件中刪除我們的項(xiàng)目:

          let existing_item=to_do_factory(to_do_item.title.as_    str(),
          
              status.clone());
          
          process_input(existing_item, "delete".    to_owned(), 
          
              &state);

          請(qǐng)記住,我們?yōu)?ToDoItems 結(jié)構(gòu)實(shí)現(xiàn)了 Responder 特征,并且 ToDoItems::get_state() 函數(shù)返回一個(gè) ToDoItems 結(jié)構(gòu),其中填充了 JSON 文件中的項(xiàng)目。 因此,我們可以從刪除視圖中得到以下返回語(yǔ)句:

          return HttpResponse::Ok().json(ToDoItems::get_state())

          現(xiàn)在我們的刪除視圖已經(jīng)定義了,我們可以將其添加到我們的 src/views/to_do/mod.rs 文件中,導(dǎo)致我們的視圖工廠如下所示:

          mod create;
          
          mod get;
          
          mod edit;
          
          mod delete;
          
          use actix_web::web::{ServiceConfig, post, get, scope};
          
          pub fn to_do_views_factory(app: &mut ServiceConfig) {
          
              app.service(
          
                  scope("v1/item")
          
                  .route("create/{title}", 
          
                          post().to(create::create))
          
                  .route("get", get().to(get::get))
          
                  .route("edit", post().to(edit::edit))
          
                  .route("delete", post().to(delete::delete))
          
              );
          
          }

          通過(guò)快速檢查 to_do_views_factory,我們可以看到我們擁有管理待辦事項(xiàng)所需的所有視圖。 如果我們將該模塊從應(yīng)用程序中彈出并將其插入另一個(gè)應(yīng)用程序中,我們將立即看到我們正在刪除和添加的內(nèi)容。

          將刪除視圖完全集成到應(yīng)用程序中后,我們可以繼續(xù)第二步,即構(gòu)建 JavaScript 加載功能。

          添加 JavaScript 加載功能

          現(xiàn)在我們的所有端點(diǎn)都已準(zhǔn)備就緒,我們必須重新訪問(wèn)我們的主應(yīng)用程序視圖。 在上一節(jié)中,我們確定 <script> 部分中的 JavaScript 可以正常工作,即使它只是一個(gè)大字符串的一部分。 為了使我們能夠?qū)?JavaScript 放入單獨(dú)的文件中,我們的視圖會(huì)將 HTML 文件作為字符串加載,該字符串在 HTML 文件的 <script> 部分中具有 {{JAVASCRIPT}} 標(biāo)記。 然后,我們將 JavaScript 文件作為字符串加載,并將 {{JAVASCRIPT}} 標(biāo)記替換為 JavaScript 文件中的字符串。 最后,我們將在views/app/items.rs文件中返回正文中的完整字符串:

          pub async fn items() -> HttpResponse {
              let mut html_data=read_file(
                  "./templates/main.html");
              let javascript_data=read_file(
                  "./javascript/main.js");
              html_data=html_data.replace("{{JAVASCRIPT}}", 
                  &javascript_data);
              HttpResponse::Ok()
                  .content_type("text/html; charset=utf-8")
                  .body(html_data)
          }

          在 HTML 中添加 JavaScript 標(biāo)簽

          從上一步中的 items 函數(shù)中,我們可以看到我們需要在根目錄中構(gòu)建一個(gè)名為 JavaScript 的新目錄。 我們還必須在其中創(chuàng)建一個(gè)名為 main.js 的文件。 通過(guò)對(duì)應(yīng)用程序視圖的更改,我們還必須通過(guò)添加以下代碼來(lái)更改 templates/main.html 文件:

          <body>
              <h1>Done Items</h1>
              <div id="doneItems"></div>
              <h1>To Do Items</h1>
              <div id="pendingItems"></div>
              <input type="text" id="name" placeholder="create to do
               item">
              <button id="create-button" value="Send">Create</button>
          </body>
          <script>
              {{JAVASCRIPT}}
          </script>

          回想一下,我們的端點(diǎn)返回待處理項(xiàng)目和已完成項(xiàng)目。 因此,我們用自己的標(biāo)題定義了這兩個(gè)列表。 ID 為“doneItems”的 div 是我們將通過(guò) API 調(diào)用插入已完成的待辦事項(xiàng)的位置。

          然后,我們將從 API 調(diào)用中插入 ID 為“pendingItems”的待處理項(xiàng)目。 之后,我們必須定義一個(gè)帶有文本和按鈕的輸入。 這將供我們的用戶(hù)創(chuàng)建一個(gè)新項(xiàng)目。

          構(gòu)建渲染 JavaScript 函數(shù)

          現(xiàn)在我們的 HTML 已經(jīng)定義好了,我們將在 javascript/main.js 文件中定義邏輯:

          我們要構(gòu)建的第一個(gè)函數(shù)將在主頁(yè)面上呈現(xiàn)所有待辦事項(xiàng)。 必須注意的是,這是 javascript/main.js 文件中代碼中最復(fù)雜的部分。 我們本質(zhì)上是在編寫(xiě) JavaScript 代碼來(lái)編寫(xiě) HTML 代碼。 稍后,在創(chuàng)建 React 應(yīng)用程序部分中,我們將使用 React 框架來(lái)代替執(zhí)行此操作的需要。 現(xiàn)在,我們將構(gòu)建一個(gè)渲染函數(shù)來(lái)創(chuàng)建一個(gè)項(xiàng)目列表。 每個(gè)項(xiàng)目都采用以下 HTML 形式:

          <div>
          
              <div>
          
                  <p>learn to code rust</p>
          
                  <button id="edit-learn-to-code-rust">
          
                      edit
          
                  </button>
          
              </div>
          
          </div>

          我們可以看到待辦事項(xiàng)的標(biāo)題嵌套在段落 HTML 標(biāo)記中。 然后,我們有一個(gè)按鈕。 回想一下,HTML 標(biāo)記的 id 屬性必須是唯一的。 因此,我們根據(jù)按鈕將要執(zhí)行的操作以及待辦事項(xiàng)的標(biāo)題來(lái)構(gòu)造此 ID。 這將使我們能夠使用事件偵聽(tīng)器將執(zhí)行 API 調(diào)用的函數(shù)綁定到這些 id 屬性。

          為了構(gòu)建我們的渲染函數(shù),我們必須傳入要渲染的項(xiàng)目、我們要執(zhí)行的處理類(lèi)型(即編輯或刪除)、我們所在的 HTML 部分的元素 ID 將渲染這些項(xiàng)目,以及我們將綁定到每個(gè)待辦事項(xiàng)按鈕的功能。 該函數(shù)的概要定義如下:

          function renderItems(items, processType, 
          
              elementId, processFunction) {
          
           . . .
          
          }

          在 renderItems 函數(shù)中,我們可以首先構(gòu)建 HTML 并使用以下代碼循環(huán)遍歷我們的待辦事項(xiàng):

          let itemsMeta=[];
          
          let placeholder="<div>"
          
          for (let i=0; i < items.length; i++) {
          
              . . .
          
          }
          
          placeholder +="</div>"
          
          document.getElementById(elementId).innerHTML=placeholder;

          在這里,我們定義了一個(gè)數(shù)組,用于收集有關(guān)我們?yōu)槊總€(gè)待辦事項(xiàng)生成的待辦事項(xiàng) HTML 的元數(shù)據(jù)。 它位于 itemsMeta 變量下,稍后將在 renderItems 函數(shù)中使用,以使用事件偵聽(tīng)器將 processFunction 綁定到每個(gè)待辦事項(xiàng)按鈕。 然后,我們?cè)谡嘉环兞肯露x包含流程所有待辦事項(xiàng)的 HTML。 在這里,我們從 div 標(biāo)簽開(kāi)始。 然后,我們循環(huán)遍歷這些項(xiàng)目,將每個(gè)項(xiàng)目的數(shù)據(jù)轉(zhuǎn)換為 HTML,然后用結(jié)束 div 標(biāo)簽結(jié)束 HTML。 之后,我們將構(gòu)建的 HTML 字符串(稱(chēng)為占位符)插入到 innerHTML 中。 頁(yè)面上的 innerHTML 位置是我們希望看到構(gòu)建的待辦事項(xiàng)的位置。

          在循環(huán)內(nèi),我們必須使用以下代碼構(gòu)建單個(gè)待辦事項(xiàng) HTML:

          let title=items[i]["title"];
          
          let placeholderId=processType +
          
          "-" + title.replaceAll(" ", "-");
          
          placeholder +="<div>" + title +
          
          "<button " + 'id="' + placeholderId + '">'
          
          + processType +
          
          '</button>' + "</div>";
          
          itemsMeta.push({"id": placeholderId, "title": title});

          在這里,我們從正在循環(huán)的項(xiàng)目中提取項(xiàng)目的標(biāo)題。 然后,我們?yōu)閷⒂糜诮壎ǖ绞录陕?tīng)器的項(xiàng)目定義 ID。 請(qǐng)注意,我們將所有空格替換為 -。 現(xiàn)在我們已經(jīng)定義了標(biāo)題和 ID,我們將一個(gè)帶有標(biāo)題的 div 添加到占位符 HTML 字符串中。 我們還添加一個(gè)帶有 placeholderId 的按鈕,然后用一個(gè) div 來(lái)完成它。 我們可以看到,我們對(duì) HTML 字符串的添加是以 ; 結(jié)束的。 然后,我們將 placeholderId 和 title 添加到 itemsMeta 數(shù)組中以供稍后使用。

          接下來(lái),我們循環(huán) itemsMeta,使用以下代碼創(chuàng)建事件偵聽(tīng)器:

              . . .
          
              placeholder +="</div>"
          
              document.getElementById(elementId).innerHTML=placeholder;
          
              for (let i=0; i < itemsMeta.length; i++) {
          
                  document.getElementById(
          
                      itemsMeta[i]["id"]).addEventListener(
          
                      "click", processFunction);
          
              }
          
          }

          現(xiàn)在,如果單擊我們?cè)诖k事項(xiàng)旁邊創(chuàng)建的按鈕,則 processFunction 將觸發(fā)。 我們的函數(shù)現(xiàn)在呈現(xiàn)這些項(xiàng)目,但我們需要使用 API 調(diào)用函數(shù)從后端獲取它們。 我們現(xiàn)在來(lái)看看這個(gè)。

          構(gòu)建 API 調(diào)用 JavaScript 函數(shù)

          現(xiàn)在我們有了渲染函數(shù),我們可以看看我們的 API 調(diào)用函數(shù):

          首先,我們必須在 javascript/main.js 文件中定義 API 調(diào)用函數(shù)。 該函數(shù)接受一個(gè) URL,它是 API 調(diào)用的端點(diǎn)。 它還采用一個(gè)方法,該方法是 POST、GET 或 PUT 字符串。 然后,我們必須定義我們的請(qǐng)求對(duì)象:

          function apiCall(url, method) {
          
              let xhr=new XMLHttpRequest();
          
              xhr.withCredentials=true;

          然后,我們必須在 apiCall 函數(shù)內(nèi)定義事件監(jiān)聽(tīng)器,該函數(shù)在調(diào)用完成后使用返回的 JSON 呈現(xiàn)待辦事項(xiàng):

          xhr.addEventListener('readystatechange', function() {
          
              if (this.readyState===this.DONE) {
          
                  renderItems(JSON.parse(
          
                  this.responseText)["pending_items"], 
          
                  "edit", "pendingItems", editItem);
          
                  renderItems(JSON.parse(this.responseText)
          
                      ["done_items"],
          
                  "delete", "doneItems", deleteItem);
          
              }
          
          });

          在這里,我們可以看到我們正在傳遞在 templates/main.html 文件中定義的 ID。 我們還傳遞 API 調(diào)用的響應(yīng)。 我們還可以看到,我們傳入了 editItem 函數(shù),這意味著當(dāng)單擊待處理項(xiàng)目旁邊的按鈕時(shí),我們將觸發(fā)編輯函數(shù),將該項(xiàng)目轉(zhuǎn)換為已完成項(xiàng)目。 考慮到這一點(diǎn),如果單擊屬于已完成項(xiàng)目的按鈕,則會(huì)觸發(fā) deleteItem 函數(shù)。 現(xiàn)在,我們將繼續(xù)構(gòu)建 apiCall 函數(shù)。

          之后,我們必須構(gòu)建 editItem 和 deleteItem 函數(shù)。 我們還知道,每次調(diào)用 apiCall 函數(shù)時(shí),都會(huì)渲染項(xiàng)目。

          現(xiàn)在我們已經(jīng)定義了事件監(jiān)聽(tīng)器,我們必須使用方法和 URL 準(zhǔn)備 API 調(diào)用對(duì)象,定義標(biāo)頭,然后返回請(qǐng)求對(duì)象以便我們?cè)谛枰獣r(shí)發(fā)送:

              xhr.open(method, url);
              xhr.setRequestHeader('content-type', 
                  'application/json');
              xhr.setRequestHeader('user-token', 'token');
              return xhr
          }

          現(xiàn)在,我們可以使用 apiCall 函數(shù)對(duì)應(yīng)用程序的后端執(zhí)行調(diào)用,并在 API 調(diào)用后使用項(xiàng)目的新?tīng)顟B(tài)重新渲染前端。 這樣,我們就可以進(jìn)入最后一步,在這里我們將定義對(duì)待辦事項(xiàng)執(zhí)行創(chuàng)建、獲取、刪除和編輯功能的函數(shù)。

          為按鈕構(gòu)建 JavaScript 函數(shù)

          請(qǐng)注意,標(biāo)頭只是對(duì)后端中硬編碼的接受令牌進(jìn)行硬編碼。 我們將在第 7 章“管理用戶(hù)會(huì)話”中介紹如何正確定義 auth 標(biāo)頭。 現(xiàn)在我們的 API 調(diào)用函數(shù)已經(jīng)定義好了,我們可以繼續(xù)處理 editItem 函數(shù):

          function editItem() {
              let title=this.id.replaceAll("-", " ")
                  .replace("edit ", "");
              let call=apiCall("/v1/item/edit", "POST");
              let json={
                  "title": title,
                  "status": "DONE"
              };
              call.send(JSON.stringify(json));
          }

          在這里,我們可以看到事件監(jiān)聽(tīng)器所屬的 HTML 部分可以通過(guò) this 訪問(wèn)。 我們知道,如果我們刪除編輯詞,并用空格切換 - ,它會(huì)將待辦事項(xiàng)的 ID 轉(zhuǎn)換為待辦事項(xiàng)的標(biāo)題。 然后,我們利用 apiCall 函數(shù)來(lái)定義我們的端點(diǎn)和方法。 請(qǐng)注意,替換函數(shù)中的“edit”字符串中有一個(gè)空格。 我們有這個(gè)空格是因?yàn)槲覀冞€必須刪除編輯字符串后面的空格。 如果我們不刪除該空格,它將被發(fā)送到后端,從而導(dǎo)致錯(cuò)誤,因?yàn)槲覀兊膽?yīng)用程序后端在 JSON 文件中項(xiàng)目標(biāo)題旁邊沒(méi)有空格。 定義端點(diǎn)和 API 調(diào)用方法后,我們將標(biāo)題傳遞到狀態(tài)為已完成的字典中。 這是因?yàn)槲覀冎牢覀冋趯⒋幚淼捻?xiàng)目切換為完成。 完成此操作后,我們將使用 JSON 正文發(fā)送 API 調(diào)用。

          現(xiàn)在,我們可以對(duì) deleteItem 函數(shù)使用相同的方法:

          function deleteItem() {
              let title=this.id.replaceAll("-", " ")
                  .replace("delete ", "");
              let call=apiCall("/v1/item/delete", "POST");
              let json={
                  "title": title,
                  "status": "DONE"
              };
              call.send(JSON.stringify(json));
          }

          同樣,替換函數(shù)中的“delete”字符串中有一個(gè)空格。 至此,我們的渲染過(guò)程就完成了。 我們定義了編輯和刪除函數(shù)以及渲染函數(shù)。 現(xiàn)在,我們必須在頁(yè)面首次加載時(shí)加載項(xiàng)目,而無(wú)需單擊任何按鈕。 這可以通過(guò)簡(jiǎn)單的 API 調(diào)用來(lái)完成:

          function getItems() {
              let call=apiCall("/v1/item/get", 'GET');
              call.send()
          }
          getItems();

          在這里,我們可以看到我們只是使用 GET 方法進(jìn)行 API 調(diào)用并發(fā)送它。 另請(qǐng)注意,我們的 getItems 函數(shù)是在函數(shù)外部調(diào)用的。 當(dāng)視圖加載時(shí),這將被觸發(fā)一次。

          這是一段很長(zhǎng)的編碼時(shí)間; 然而,我們已經(jīng)快到了。 我們只需要定義創(chuàng)建文本輸入和按鈕的功能。 我們可以通過(guò)一個(gè)簡(jiǎn)單的事件監(jiān)聽(tīng)器和創(chuàng)建端點(diǎn)的 API 調(diào)用來(lái)管理它:

          document.getElementById("create-button")
                  .addEventListener("click", createItem);
          function createItem() {
              let title=document.getElementById("name");
              let call=apiCall("/v1/item/create/" + 
                  title.value, "POST");
              call.send();
              document.getElementById("name").value=null;
          }

          我們還添加了將文本輸入值設(shè)置為 null 的詳細(xì)信息。 我們將 input 設(shè)置為 null,以便用戶(hù)可以輸入要?jiǎng)?chuàng)建的另一個(gè)項(xiàng)目,而不必刪除剛剛創(chuàng)建的舊項(xiàng)目標(biāo)題。 點(diǎn)擊應(yīng)用程序的主視圖會(huì)得到以下輸出:


          圖 5.5 – 帶有渲染的待辦事項(xiàng)的主頁(yè)


          現(xiàn)在,要查看我們的前端是否按我們希望的方式工作,我們可以執(zhí)行以下步驟:

          按已清洗項(xiàng)目旁邊的刪除按鈕。

          輸入早餐吃麥片,然后單擊創(chuàng)建。

          輸入早餐吃拉面,然后單擊創(chuàng)建。

          單擊早餐吃拉面項(xiàng)目的編輯。

          這些步驟應(yīng)產(chǎn)生以下結(jié)果:


          圖 5.6 – 完成上述步驟后的主頁(yè)

          這樣,我們就有了一個(gè)功能齊全的網(wǎng)絡(luò)應(yīng)用程序。 所有按鈕都可以使用,并且列表會(huì)立即更新。 然而,它看起來(lái)不太漂亮。 沒(méi)有間距,一切都是黑白的。 為了修改這一點(diǎn),我們需要將 CSS 集成到 HTML 文件中,我們將在下一節(jié)中執(zhí)行此操作。

          將 CSS 注入 HTML

          注入 CSS 采用與注入 JavaScript 相同的方法。 我們將在 HTML 文件中添加一個(gè) CSS 標(biāo)簽,該標(biāo)簽將被文件中的 CSS 替換。 為了實(shí)現(xiàn)這一目標(biāo),我們必須執(zhí)行以下步驟:

          將 CSS 標(biāo)簽添加到我們的 HTML 文件中。

          為整個(gè)應(yīng)用程序創(chuàng)建一個(gè)基本 CSS 文件。

          為我們的主視圖創(chuàng)建一個(gè) CSS 文件。

          更新我們的 Rust 箱以服務(wù) CSS 和 JavaScript。

          讓我們仔細(xì)看看這個(gè)過(guò)程。

          將 CSS 標(biāo)簽添加到 HTML

          首先,讓我們對(duì) templates/main.html 文件進(jìn)行一些更改:

           <style>
              {{BASE_CSS}}
              {{CSS}}
          </style>
          <body>
              <div class="mainContainer">
                  <h1>Done Items</h1>
                  <div id="doneItems"></div>
                  <h1>To Do Items</h1>
                  <div id="pendingItems"></div>
                  <div class="inputContainer">
                      <input type="text" id="name"
                             placeholder="create to do item">
                      <div class="actionButton" 
                           id="create-button" 
                           value="Send">Create</div>
                  </div>
              </div>
          </body>
          <script>
              {{JAVASCRIPT}}
          </script>

          在這里,我們可以看到有兩個(gè) CSS 標(biāo)簽。 {{BASE_CSS}}標(biāo)簽用于基礎(chǔ)CSS,它在多個(gè)不同視圖中將保持一致,例如背景顏色和列比例,具體取決于屏幕尺寸。 {{BASE_CSS}} 標(biāo)簽用于管理此視圖的 CSS 類(lèi)。 恕我直言,css/base.css 和 css/main.css 文件是為我們的視圖而制作的。 另外,請(qǐng)注意,我們已將所有項(xiàng)目放入一個(gè)名為 mainContainer 的類(lèi)的 div 中。 這將使我們能夠?qū)⑺许?xiàng)目在屏幕上居中。 我們還添加了更多的類(lèi),以便 CSS 可以引用它們,并將創(chuàng)建項(xiàng)目的按鈕從按鈕 HTML 標(biāo)記更改為 div HTML 標(biāo)記。 完成此操作后,javascript/main.js 文件中的 renderItems 函數(shù)將對(duì)項(xiàng)目循環(huán)進(jìn)行以下更改:

          function renderItems(items, processType, 
              elementId, processFunction) {
              . . . 
              for (i=0; i < items.length; i++) {
                  . . .
                  placeholder +='<div class="itemContainer">' +
                      '<p>' + title + '</p>' +
                      '<div class="actionButton" ' + 
                            'id="' + placeholderId + '">'
                      + processType + '</div>' + "</div>";
                  itemsMeta.push({"id": placeholderId, "title":        title});
              }
              . . .
          }

          考慮到這一點(diǎn),我們現(xiàn)在可以在 css/base.css 文件中定義基本 CSS。

          創(chuàng)建基礎(chǔ) CSS

          現(xiàn)在,我們必須定義頁(yè)面及其組件的樣式。 一個(gè)好的起點(diǎn)是在 css/base.css 文件中定義頁(yè)面主體。 我們可以使用以下代碼對(duì)主體進(jìn)行基本配置:

          body {
              background-color: #92a8d1;
              font-family: Arial, Helvetica, sans-serif;
              height: 100vh;
          } 

          背景顏色是對(duì)一種顏色的引用。 僅看此參考可能看起來(lái)沒(méi)有意義,但有在線顏色選擇器,您可以在其中查看和選擇顏色,并提供參考代碼。 一些代碼編輯器支持此功能,但為了快速參考,只需使用 Google HTML 顏色選擇器,您就會(huì)因可用的免費(fèi)在線交互工具的數(shù)量而不知所措。 通過(guò)上述配置,整個(gè)頁(yè)面的背景將具有代碼#92a8d1,即海軍藍(lán)色。 如果我們只是這樣,頁(yè)面的大部分都會(huì)有白色背景。 海軍藍(lán)色背景只會(huì)出現(xiàn)在有內(nèi)容的地方。

          我們將高度設(shè)置為 100vh。 vh 相對(duì)于視口高度的 1%。 由此,我們可以推斷出 100vh 意味著我們?cè)?body 中定義的樣式占據(jù)了 100% 的視口。 然后,我們定義所有文本的字體,除非覆蓋為 Arial、Helvetica 或 sans-serif。 我們可以看到我們?cè)趂ont-family中定義了多種字體。 這并不意味著所有這些都已實(shí)現(xiàn),也不意味著不同級(jí)別的標(biāo)頭或 HTML 標(biāo)記有不同的字體。 相反,這是一種后備機(jī)制。 首先,瀏覽器會(huì)嘗試渲染 Arial; 如果瀏覽器不支持,它將嘗試渲染 Helvetica,如果也失敗,它將嘗試渲染 sans-serif。

          至此,我們已經(jīng)定義了機(jī)身的總體風(fēng)格,但是不同的屏幕尺寸呢? 例如,如果我們要在手機(jī)上訪問(wèn)我們的應(yīng)用程序,它應(yīng)該具有不同的尺寸。 我們可以在下圖中看到這一點(diǎn):


          圖 5.7 – 手機(jī)和桌面顯示器之間的邊距差異


          圖 5.7 顯示了邊距與待辦事項(xiàng)列表更改所填充的空間的比率。 對(duì)于手機(jī)來(lái)說(shuō),屏幕空間不大,所以大部分屏幕都需要被待辦事項(xiàng)占據(jù); 否則,我們將無(wú)法閱讀它。 但是,如果我們使用寬屏桌面顯示器,我們就不再需要大部分屏幕來(lái)顯示待辦事項(xiàng)。 如果比例相同,待辦事項(xiàng)將在 X 軸上拉伸,難以閱讀,而且坦率地說(shuō),看起來(lái)也不好看。 這就是媒體查詢(xún)的用武之地。我們可以根據(jù)窗口的寬度和高度等屬性設(shè)置不同的樣式條件。 我們將從手機(jī)規(guī)格開(kāi)始。 因此,如果屏幕寬度最大為 500 像素,則在 css/base.css 文件中,我們必須為正文定義以下 CSS 配置:

          @media(max-width: 500px) {
              body {
                  padding: 1px;
                  display: grid;
                  grid-template-columns: 1fr;
              }
          }

          在這里,我們可以看到頁(yè)面邊緣和每個(gè)元素周?chē)奶畛渲挥幸粋€(gè)像素。 我們還有一個(gè)網(wǎng)格顯示。 這是我們可以定義列和行的地方。 然而,我們并沒(méi)有充分利用它。 我們只有一欄。 這意味著我們的待辦事項(xiàng)將占據(jù)大部分屏幕,如圖 5.7 中的手機(jī)描述所示。 盡管我們?cè)谶@種情況下沒(méi)有使用網(wǎng)格,但我保留了它,以便您可以看到它與大屏幕的其他配置之間的關(guān)系。 如果我們的屏幕變大一點(diǎn),我們可以將頁(yè)面分成三個(gè)不同的垂直列; 但中間柱的寬度與兩側(cè)柱的寬度之比為5:1。 這是因?yàn)槲覀兊钠聊蝗匀徊皇呛艽螅⑶椅覀兿M覀兊捻?xiàng)目仍然占據(jù)大部分屏幕。 我們可以通過(guò)添加另一個(gè)具有不同參數(shù)的媒體查詢(xún)來(lái)對(duì)此進(jìn)行調(diào)整:

          @media(min-width: 501px) and (max-width: 550px) {
              body {
                  padding: 1px;
                  display: grid;
                  grid-template-columns: 1fr 5fr 1fr;
              } 
              .mainContainer {
                  grid-column-start: 2;
              }
          }

          我們還可以看到,對(duì)于存放待辦事項(xiàng)的 mainContainer CSS 類(lèi),我們將覆蓋 grid-column-start 屬性。 如果我們不這樣做,那么 mainContainer 將被擠壓在 1fr 寬度的左邊距中。 相反,我們?cè)?5fr 的中間開(kāi)始和結(jié)束。 我們可以使用 grid-column-finish 屬性使 mainContainer 跨多個(gè)列。

          如果我們的屏幕變大,那么我們希望進(jìn)一步調(diào)整比率,因?yàn)槲覀儾幌M?xiàng)目寬度失控。 為了實(shí)現(xiàn)這一點(diǎn),我們必須為中間列與兩側(cè)列定義 3:1 的比例,然后當(dāng)屏幕寬度高于 1001px 時(shí)定義 1:1 的比例:

          @media(min-width: 551px) and (max-width: 1000px) {
              body {
                  padding: 1px;
                  display: grid;
                  grid-template-columns: 1fr 3fr 1fr;
              } 
              .mainContainer {
                  grid-column-start: 2;
              }
          } 
          @media(min-width: 1001px) {
              body {
                  padding: 1px;
                  display: grid;
                  grid-template-columns: 1fr 1fr 1fr;
              } 
              .mainContainer {
                  grid-column-start: 2;
              }
          }

          現(xiàn)在我們已經(jīng)為所有視圖定義了通用 CSS,我們可以繼續(xù)在 css/main.css 文件中處理特定于視圖的 CSS。

          為主頁(yè)創(chuàng)建 CSS

          現(xiàn)在,我們必須分解我們的應(yīng)用程序組件。 我們有一份待辦事項(xiàng)清單。 列表中的每個(gè)項(xiàng)目都是一個(gè)具有不同背景顏色的 div:

          .itemContainer {
              background: #034f84;
              margin: 0.3rem;
          }

          我們可以看到這個(gè)類(lèi)的邊距為 0.3。 我們使用 rem 是因?yàn)槲覀兿M吘嘞鄬?duì)于根元素的字體大小進(jìn)行縮放。 如果我們的光標(biāo)懸停在項(xiàng)目上,我們還希望項(xiàng)目稍微改變顏色:

          .itemContainer:hover {
              background: #034f99;
          }

          在項(xiàng)目容器內(nèi),項(xiàng)目的標(biāo)題用段落標(biāo)簽表示。 我們想要定義項(xiàng)目容器中所有段落的樣式,而不是其他地方。 我們可以使用以下代碼定義容器中段落的樣式:

          .itemContainer p {
              color: white;
              display: inline-block;
              margin: 0.5rem;
              margin-right: 0.4rem;
              margin-left: 0.4rem;
          }

          inline-block 允許標(biāo)題與 div 一起顯示,這將充當(dāng)項(xiàng)目的按鈕。 邊距定義只是阻止標(biāo)題緊靠項(xiàng)目容器的邊緣。 我們還確保段落顏色為白色。

          設(shè)置項(xiàng)目標(biāo)題樣式后,剩下的唯一項(xiàng)目樣式是操作按鈕,即編輯或刪除。 該操作按鈕將以不同的背景顏色向右浮動(dòng),以便我們知道在哪里單擊。 為此,我們必須使用類(lèi)定義按鈕樣式,如以下代碼所示:

          .actionButton {
              display: inline-block;
              float: right;
              background: #f7786b;
              border: none;
              padding: 0.5rem;
              padding-left: 2rem;
              padding-right: 2rem;
              color: white;
          }

          在這里,我們定義了顯示,使其向右浮動(dòng),并定義了背景顏色和填充。 這樣,我們可以通過(guò)運(yùn)行以下代碼來(lái)確保懸停時(shí)顏色發(fā)生變化:

          .actionButton:hover {
              background: #f7686b;
              color: black;
          }

          現(xiàn)在我們已經(jīng)涵蓋了所有概念,我們必須定義輸入容器的樣式。 這可以通過(guò)運(yùn)行以下代碼來(lái)完成:

          .inputContainer {
              background: #034f84;
              margin: 0.3rem;
              margin-top: 2rem;
          }
          .inputContainer input {
              display: inline-block;
              margin: 0.4rem;
          }

          我們做到了! 我們已經(jīng)定義了所有 CSS、JavaScript 和 HTML。 在運(yùn)行應(yīng)用程序之前,我們需要在主視圖中加載數(shù)據(jù)。

          從 Rust 提供 CSS 和 JavaScript

          我們?cè)趘iews/app/items.rs 文件中提供CSS。 我們通過(guò)閱讀 HTML、JavaScript、基本 CSS 和主 CSS 文件來(lái)完成此操作。 然后,我們用其他文件中的數(shù)據(jù)替換 HTML 數(shù)據(jù)中的標(biāo)簽:

          pub async fn items() -> HttpResponse {
              let mut html_data=read_file(
                  "./templates/main.html");
              let javascript_data: String=read_file(
                  "./javascript/main.js");
              let css_data: String=read_file(
                  "./css/main.css");
              let base_css_data: String=read_file(
                  "./css/base.css");
              html_data=html_data.replace("{{JAVASCRIPT}}", 
              &javascript_data);
              html_data=html_data.replace("{{CSS}}", 
              &css_data);
              html_data=html_data.replace("{{BASE_CSS}}", 
              &base_css_data);
              HttpResponse::Ok()
                  .content_type("text/html; charset=utf-8")
                  .body(html_data)
          }

          現(xiàn)在,當(dāng)我們啟動(dòng)服務(wù)器時(shí),我們將擁有一個(gè)完全運(yùn)行的應(yīng)用程序,具有直觀的前端,如下圖所示:


          圖 5.8 – CSS 之后的主頁(yè)


          盡管我們的應(yīng)用程序正在運(yùn)行,并且我們已經(jīng)配置了基本 CSS 和 HTML,但我們可能希望擁有可重用的獨(dú)立 HTML 結(jié)構(gòu),這些結(jié)構(gòu)具有自己的 CSS。 這些結(jié)構(gòu)可以在需要時(shí)注入到視圖中。 它的作用是讓我們能夠編寫(xiě)一次組件,然后將其導(dǎo)入到其他 HTML 文件中。 反過(guò)來(lái),這使得維護(hù)變得更容易,并確保組件在多個(gè)視圖中的一致性。 例如,如果我們?cè)谝晥D頂部創(chuàng)建一個(gè)信息欄,我們將希望它在其余視圖中具有相同的樣式。 因此,將信息欄作為組件創(chuàng)建一次并將其插入到其他視圖中是有意義的,如下一節(jié)所述。

          繼承組件

          有時(shí),我們想要構(gòu)建一個(gè)可以注入視圖的組件。 為此,我們必須加載 CSS 和 HTML,然后將它們插入 HTML 的正確部分。

          為此,我們可以創(chuàng)建一個(gè) add_component 函數(shù),該函數(shù)獲取組件的名稱(chēng),根據(jù)組件名稱(chēng)創(chuàng)建標(biāo)簽,并根據(jù)組件名稱(chēng)加載 HTML 和 CSS。 我們將在views/app/content_loader.rs文件中定義這個(gè)函數(shù):

          pub fn add_component(component_tag: String, 
              html_data: String) -> String {
              let css_tag: String=component_tag.to_uppercase() + 
                  "_CSS";
              let html_tag: String=component_tag.to_uppercase() + 
                  "_HTML";
              let css_path=String::from("./templates/components/") 
                  + &component_tag.to_lowercase() + ".css";
              let css_loaded=read_file(&css_path);
              let html_path=String::from("./templates/components/") 
                  + &component_tag.to_lowercase() + ".html";
              let html_loaded=read_file(&html_path);
              let html_data=html_data.replace(html_tag.as_str(), 
                  &html_loaded);
              let html_data=html_data.replace(css_tag.as_str(), 
                  &css_loaded);
              return html_data
          } 

          在這里,我們使用同一文件中定義的 read_file 函數(shù)。 然后,我們將組件 HTML 和 CSS 注入到視圖數(shù)據(jù)中。 請(qǐng)注意,我們將組件嵌套在 templates/components/ 目錄中。 對(duì)于本例,我們要插入一個(gè)標(biāo)頭組件,因此當(dāng)我們將標(biāo)頭傳遞給 add_component 函數(shù)時(shí),我們的 add_component 函數(shù)將嘗試加載 header.html 和 header.css 文件。 在我們的 templates/components/header.html 文件中,我們必須定義以下 HTML:

          <div class="header">
              <p>complete tasks: </p><p id="completeNum"></p>
              <p>pending tasks: </p><p id="pendingNum"></p>
          </div>

          在這里,我們僅顯示已完成和待辦事項(xiàng)的數(shù)量計(jì)數(shù)。 在我們的 templates/components/header.css 文件中,我們必須定義以下 CSS:

          .header {
              background: #034f84;
              margin-bottom: 0.3rem;
          }
          .header p {
              color: white;
              display: inline-block;
              margin: 0.5rem;
              margin-right: 0.4rem;
              margin-left: 0.4rem;
          }

          為了讓 add_component 函數(shù)將 CSS 和 HTML 插入到正確的位置,我們必須將 HEADER 標(biāo)簽插入 templates/main.html 文件的 <style> 部分:

          . . . 
              <style>
                  {{BASE_CSS}}
                  {{CSS}}
                  HEADER_CSS
              </style>
              <body>
                  <div class="mainContainer">
                      HEADER_HTML
                      <h1>Done Items</h1>
          . . .

          現(xiàn)在我們所有的 HTML 和 CSS 都已定義,我們需要在 view/app/items.rs 文件中導(dǎo)入 add_component 函數(shù):

          use super::content_loader::add_component;

          在同一個(gè)文件中,我們必須在項(xiàng)目視圖函數(shù)中添加標(biāo)題,如下所示:

          html_data=add_component(String::from("header"), 
              html_data);

          現(xiàn)在,我們必須更改injecting_header/javascript/main.js 文件中的 apiCall 函數(shù),以確保標(biāo)頭隨待辦事項(xiàng)計(jì)數(shù)進(jìn)行更新:

          document.getElementById("completeNum").innerHTML=JSON.parse(this.responseText)["done_item_count"];
          document.getElementById("pendingNum").innerHTML=JSON.parse(this.responseText)["pending_item_count"]; 

          現(xiàn)在我們已經(jīng)插入了組件,我們得到以下渲染視圖:


          圖 5.9 – 帶標(biāo)題的主頁(yè)

          正如我們所看到的,我們的標(biāo)題正確顯示了數(shù)據(jù)。 如果我們將標(biāo)頭標(biāo)簽添加到視圖 HTML 文件中,并在視圖中調(diào)用 add_component,我們將獲得該標(biāo)頭。

          現(xiàn)在,我們有一個(gè)完全運(yùn)行的單頁(yè)應(yīng)用程序。 然而,這并非沒(méi)有困難。 我們可以看到,如果我們開(kāi)始向前端添加更多功能,我們的前端將開(kāi)始失控。 這就是 React 等框架的用武之地。通過(guò) React,我們可以將代碼構(gòu)建為適當(dāng)?shù)慕M件,以便我們可以在需要時(shí)使用它們。 在下一節(jié)中,我們將創(chuàng)建一個(gè)基本的 React 應(yīng)用程序。

          創(chuàng)建一個(gè) React 應(yīng)用程序

          React 是一個(gè)獨(dú)立的應(yīng)用程序。 因此,我們通常會(huì)將 React 應(yīng)用程序放在自己的 GitHub 存儲(chǔ)庫(kù)中。 如果您想將 Rust 應(yīng)用程序和 React 應(yīng)用程序保留在同一個(gè) GitHub 存儲(chǔ)庫(kù)中,那沒(méi)問(wèn)題,但只需確保它們位于根目錄中的不同目錄即可。 一旦我們導(dǎo)航到 Rust Web 應(yīng)用程序之外,我們就可以運(yùn)行以下命令:

          npx create-react-app front_end

          這將在 front_end 目錄中創(chuàng)建一個(gè) React 應(yīng)用程序。 如果我們查看里面,我們會(huì)看到有很多文件。 請(qǐng)記住,本書(shū)是關(guān)于 Rust 中的 Web 編程的。 探索有關(guān) React 的一切超出了本書(shū)的范圍。 不過(guò),進(jìn)一步閱讀部分建議您閱讀一本專(zhuān)門(mén)介紹 React 開(kāi)發(fā)的書(shū)。 現(xiàn)在,我們將重點(diǎn)關(guān)注 front_end/package.json 文件。 我們的 package.json 文件就像我們的 Cargo.toml 文件,我們?cè)谄渲卸x我們正在構(gòu)建的應(yīng)用程序的依賴(lài)項(xiàng)、腳本和其他元數(shù)據(jù)。 在我們的 package.json 文件中,我們有以下腳本:

          . . .
          "scripts": {
              "start": "react-scripts start",
              "build": "react-scripts build",
              "test": "react-scripts test",
              "eject": "react-scripts eject"
          },
          . . .

          如果需要,我們可以編輯它,但就目前情況而言,如果我們?cè)?package.json 文件所在的目錄中運(yùn)行 npm start 命令,我們將運(yùn)行 react-scripts start 命令。 我們很快就會(huì)運(yùn)行 React 應(yīng)用程序,但在此之前,我們必須使用以下代碼編輯 front_end/src/App.js 文件:

          import React, { Component } from 'react';
          class App extends Component {
            state={
              "message": "To Do"
            }
            render() {
              return (
                  <div className="App">
                    <p>{this.state.message} application</p>
                  </div>
              )
            }
          }
          export default App;

          在分解這段代碼之前,我們必須澄清一些事情。 如果您上網(wǎng),您可能會(huì)看到一些文章指出 JavaScript 不是基于類(lèi)的面向?qū)ο笳Z(yǔ)言。 本書(shū)不會(huì)深入探討 JavaScript。 相反,本章旨在為您提供足夠的知識(shí)來(lái)啟動(dòng)和運(yùn)行前端。 如果您想向 Rust Web 應(yīng)用程序添加前端,希望本章足以促進(jìn)進(jìn)一步閱讀并啟動(dòng)您的旅程。 在本章中,我們將只討論可以支持繼承的類(lèi)和對(duì)象。

          在前面的代碼中,我們從react包中導(dǎo)入了組件對(duì)象。 然后,我們定義了一個(gè)繼承組件類(lèi)的App類(lèi)。 App 類(lèi)是我們應(yīng)用程序的主要部分,我們可以將 front_end/src/App.js 文件視為前端應(yīng)用程序的入口點(diǎn)。 如果需要的話,我們可以在 App 類(lèi)中定義其他路由。 我們還可以看到有一個(gè)屬于App類(lèi)的狀態(tài)。 這是應(yīng)用程序的總體內(nèi)存。 我們必須稱(chēng)其為國(guó)家; 每次更新?tīng)顟B(tài)時(shí),都會(huì)執(zhí)行渲染函數(shù),更新組件渲染到前端的內(nèi)容。 當(dāng)我們的狀態(tài)更新我們的自制渲染函數(shù)時(shí),這抽象了本章前面幾節(jié)中我們所做的很多事情。 我們可以看到,我們的狀態(tài)可以在返回時(shí)在渲染函數(shù)中引用。 這就是所謂的 JSX,它允許我們直接在 JavaScript 中編寫(xiě) HTML 元素,而不需要任何額外的方法。 現(xiàn)在已經(jīng)定義了基本應(yīng)用程序,我們可以將其導(dǎo)出以使其可用。

          讓我們導(dǎo)航到 package.json 文件所在的目錄并運(yùn)行以下命令:

          npm start

          React 服務(wù)器將啟動(dòng),我們將在瀏覽器中看到以下視圖:


          圖 5.10 – React 應(yīng)用程序的第一個(gè)主視圖

          在這里,我們可以看到狀態(tài)中的消息已傳遞到渲染函數(shù)中,然后顯示在瀏覽器中。 現(xiàn)在我們的 React 應(yīng)用程序正在運(yùn)行,我們可以開(kāi)始使用 API 調(diào)用將數(shù)據(jù)加載到 React 應(yīng)用程序中。

          在 React 中進(jìn)行 API 調(diào)用

          現(xiàn)在基本應(yīng)用程序正在運(yùn)行,我們可以開(kāi)始對(duì)后端執(zhí)行 API 調(diào)用。 為此,我們將主要關(guān)注 front_end/src/App.js 文件。 我們可以構(gòu)建我們的應(yīng)用程序,以便它可以使用 Rust 應(yīng)用程序中的項(xiàng)目填充前端。 首先,我們必須將以下內(nèi)容添加到 package.json 文件的依賴(lài)項(xiàng)中:

          "axios": "^0.26.1"

          然后,我們可以運(yùn)行以下命令:

          npm install

          這將安裝我們的額外依賴(lài)項(xiàng)。 現(xiàn)在,我們可以轉(zhuǎn)到 front_end/src/App.js 文件并使用以下代碼導(dǎo)入我們需要的內(nèi)容:

          import React, { Component } from 'react';
          import axios from 'axios';

          我們將使用 Component 來(lái)繼承 App 類(lèi),并使用 axios 對(duì)后端執(zhí)行 API 調(diào)用。 現(xiàn)在,我們可以定義我們的 App 類(lèi)并使用以下代碼更新我們的狀態(tài):

          class App extends Component {
            state={
                "pending_items": [],
                "done_items": [],
                "pending_items_count": 0,
                "done_items_count": 0
            }
          }
          export default App;

          在這里,我們的結(jié)構(gòu)與我們自制的前端相同。 這也是我們從 Rust 服務(wù)器中的獲取項(xiàng)目視圖返回的數(shù)據(jù)。 現(xiàn)在我們知道要使用哪些數(shù)據(jù),我們可以執(zhí)行以下步驟:

          在我們的 App 類(lèi)中創(chuàng)建一個(gè)函數(shù),從 Rust 服務(wù)器獲取函數(shù)。

          確保該函數(shù)在App類(lèi)掛載時(shí)執(zhí)行。

          在我們的 App 類(lèi)中創(chuàng)建一個(gè)函數(shù),用于將從 Rust 服務(wù)器返回的項(xiàng)目處理為 HTML。

          在我們的 App 類(lèi)中創(chuàng)建一個(gè)函數(shù),一旦我們完成,它會(huì)將所有上述組件渲染到前端。

          使我們的 Rust 服務(wù)器能夠接收來(lái)自其他來(lái)源的調(diào)用。

          在開(kāi)始這些步驟之前,我們應(yīng)該注意 App 類(lèi)的大綱將采用以下形式:

          class App extends Component {
           
            state={
                . . .
            }
            // makes the API call
            getItems() {
                . . .
            }
            // ensures the API call is updated when mounted
            componentDidMount() {
                . . .
            }
            // convert items from API to HTML 
            processItemValues(items) {
                . . .
            }
            // returns the HTML to be rendered
            render() {
              return (
                  . . .
              )
            }
          }

          這樣,我們就可以開(kāi)始調(diào)用 API 的函數(shù)了:

          在我們的 App 類(lèi)中,我們的 getItems 函數(shù)采用以下布局:

          axios.get("http://127.0.0.1:8000/v1/item/get",
          
            {headers: {"token": "some_token"}})
          
            .then(response=> {
          
                let pending_items=response.data["pending_items"]
          
                let done_items=response.data["done_items"]
          
                this.setState({
          
                      . . .
          
                  })
          
            });

          在這里,我們定義 URL。 然后,我們將令牌添加到標(biāo)頭中。 現(xiàn)在,我們將只硬編碼一個(gè)簡(jiǎn)單的字符串,因?yàn)槲覀冞€沒(méi)有在 Rust 服務(wù)器中設(shè)置用戶(hù)會(huì)話; 我們將在第 7 章“管理用戶(hù)會(huì)話”中更新這一點(diǎn)。 然后,我們關(guān)閉它。 因?yàn)?axios.get 是一個(gè) Promise,所以我們必須使用 .then。 返回?cái)?shù)據(jù)時(shí)執(zhí)行 .then 括號(hào)內(nèi)的代碼。 在這些括號(hào)內(nèi),我們提取所需的數(shù)據(jù),然后執(zhí)行 this.setState 函數(shù)。 this.setState 函數(shù)更新 App 類(lèi)的狀態(tài)。 但是,執(zhí)行 this.setState 也會(huì)執(zhí)行 App 類(lèi)的 render 函數(shù),這將更新瀏覽器。 在 this.setState 函數(shù)中,我們傳入以下代碼:

          "pending_items": this.processItemValues(pending_items),
          "done_items": this.processItemValues(done_items),
          "pending_items_count": response.data["pending_item_count"],
          "done_items_count": response.data["done_item_count"]

          至此,我們就完成了getItems,可以從后端獲取item了。 現(xiàn)在我們已經(jīng)定義了它,我們必須確保它被執(zhí)行,我們接下來(lái)要做的就是。

          確保 getItems 函數(shù)被觸發(fā),從而在加載 App 類(lèi)時(shí)更新?tīng)顟B(tài)可以使用以下代碼來(lái)實(shí)現(xiàn):

          componentDidMount() {
          
            this.getItems();
          
          }

          這很簡(jiǎn)單。 getItems 將在我們的 App 組件安裝后立即執(zhí)行。 我們本質(zhì)上是在 componentDidMount 函數(shù)中調(diào)用 this.setState 。 這會(huì)在瀏覽器更新屏幕之前觸發(fā)額外的渲染。 即使渲染被調(diào)用兩次,用戶(hù)也不會(huì)看到中間狀態(tài)。 這是我們從 React Component 類(lèi)繼承的眾多函數(shù)之一。 現(xiàn)在我們?cè)陧?yè)面加載后就加載了數(shù)據(jù),我們可以繼續(xù)下一步:處理加載的數(shù)據(jù)。

          對(duì)于 App 類(lèi)中的 processItemValues 函數(shù),我們必須接收表示項(xiàng)目的 JSON 對(duì)象數(shù)組并將其轉(zhuǎn)換為 HTML,這可以通過(guò)以下代碼實(shí)現(xiàn):

          processItemValues(items) {
          
            let itemList=[];
          
            items.forEach((item, index)=>{
          
                itemList.push(
          
                    <li key={index}>{item.title} {item.status}</li>
          
                )
          
            })
          
            return itemList
          
          }

          在這里,我們只是循環(huán)遍歷這些項(xiàng)目,將它們轉(zhuǎn)換為 li HTML 元素并將它們添加到一個(gè)空數(shù)組中,然后在填充后返回該空數(shù)組。 請(qǐng)記住,我們使用 processItemValue 函數(shù)在數(shù)據(jù)進(jìn)入 getItems 函數(shù)中的狀態(tài)之前處理數(shù)據(jù)。 現(xiàn)在我們已經(jīng)擁有狀態(tài)中的所有 HTML 組件,我們需要使用渲染函數(shù)將它們放置在頁(yè)面上。

          對(duì)于我們的 App 類(lèi),渲染函數(shù)僅返回 HTML 組件。 我們?cè)诖瞬皇褂萌魏晤~外的邏輯。 我們可以返回以下內(nèi)容:

          <div className="App">
          
          <h1>Done Items</h1>
          
          <p>done item count: {this.state.done_items_count}</p>
          
          {this.state.done_items}
          
          <h1>Pending Items</h1>
          
          <p>pending item count: 
          
              {this.state.pending_items_count}</p>
          
          {this.state.pending_items}
          
          </div>

          在這里,我們可以看到我們的狀態(tài)被直接引用。 與我們?cè)诒菊虑懊媸褂玫氖謩?dòng)字符串操作相比,這是一個(gè)可愛(ài)的變化。 使用 React 更加干凈,降低了錯(cuò)誤的風(fēng)險(xiǎn)。 在我們的前端,調(diào)用后端的渲染過(guò)程應(yīng)該可以工作。 但是,我們的 Rust 服務(wù)器將阻止來(lái)自 React 應(yīng)用程序的請(qǐng)求,因?yàn)樗鼇?lái)自不同的應(yīng)用程序。 為了解決這個(gè)問(wèn)題,我們需要繼續(xù)下一步。

          現(xiàn)在,我們的 Rust 服務(wù)器將阻止我們對(duì)服務(wù)器的請(qǐng)求。 這取決于跨源資源共享(CORS)。 我們之前沒(méi)有遇到過(guò)任何問(wèn)題,因?yàn)槟J(rèn)情況下,CORS 允許來(lái)自同一來(lái)源的請(qǐng)求。 當(dāng)我們編寫(xiě)原始 HTML 并從 Rust 服務(wù)器提供服務(wù)時(shí),請(qǐng)求來(lái)自同一來(lái)源。 然而,對(duì)于 React 應(yīng)用程序,請(qǐng)求來(lái)自不同的來(lái)源。 為了糾正這個(gè)問(wèn)題,我們需要使用以下代碼在 Cargo.toml 文件中安裝 CORS 作為依賴(lài)項(xiàng):

          actix-cors="0.6.1"

          在我們的 src/main.rs 文件中,我們必須使用以下代碼導(dǎo)入 CORS:

          use actix_cors::Cors;

          現(xiàn)在,我們必須在定義服務(wù)器之前定義 CORS 策略,并在視圖配置之后使用以下代碼包裝 CORS 策略:

          #[actix_web::main]
          async fn main() -> std::io::Result<()> {
              HttpServer::new(|| {
                  let cors=Cors::default().allow_any_origin()
                                            .allow_any_method()
                                            .allow_any_header();
                  let app=App::new()
                      .wrap_fn(|req, srv|{
                          println!("{}-{}", req.method(), 
                                    req.uri());
                          let future=srv.call(req);
                          async {
                              let result=future.await?;
                              Ok(result)
                          }
                  }).configure(views::views_factory).wrap(cors);
                  return app
              })
              .bind("127.0.0.1:8000")?
              .run()
              .await
          }

          這樣,我們的服務(wù)器就準(zhǔn)備好接受來(lái)自 React 應(yīng)用程序的請(qǐng)求了。

          筆記

          當(dāng)我們定義 CORS 策略時(shí),我們明確表示我們希望允許所有方法、標(biāo)頭和來(lái)源。 然而,我們可以通過(guò)以下 CORS 定義更簡(jiǎn)潔:

          let cors=Cors::permissive();

          現(xiàn)在,我們可以測(cè)試我們的應(yīng)用程序,看看它是否正常工作。 我們可以通過(guò)使用 Cargo 運(yùn)行 Rust 服務(wù)器并在不同的終端中運(yùn)行 React 應(yīng)用程序來(lái)做到這一點(diǎn)。 一旦啟動(dòng)并運(yùn)行,我們的 React 應(yīng)用程序加載時(shí)應(yīng)如下所示:

          圖 5.11 – React 應(yīng)用程序首次與 Rust 服務(wù)器對(duì)話時(shí)的視圖


          這樣,我們可以看到對(duì) Rust 應(yīng)用程序的調(diào)用現(xiàn)在可以按預(yù)期工作。 然而,我們所做的只是列出待辦事項(xiàng)的名稱(chēng)和狀態(tài)。 React 的亮點(diǎn)在于構(gòu)建自定義組件。 這意味著我們可以為每個(gè)待辦事項(xiàng)構(gòu)建具有自己的狀態(tài)和功能的單獨(dú)類(lèi)。 我們將在下一節(jié)中看到這一點(diǎn)。

          在 React 中創(chuàng)建自定義組件

          當(dāng)我們查看 App 類(lèi)時(shí),我們可以看到,擁有一個(gè)具有狀態(tài)和函數(shù)的類(lèi)非常有用,這些狀態(tài)和函數(shù)可用于管理 HTML 呈現(xiàn)到瀏覽器的方式和時(shí)間。 當(dāng)涉及到單個(gè)待辦事項(xiàng)時(shí),我們可以使用狀態(tài)和函數(shù)。 這是因?yàn)槲覀冇幸粋€(gè)按鈕可以從待辦事項(xiàng)中獲取屬性并調(diào)用 Rust 服務(wù)器來(lái)編輯或刪除它。 在本節(jié)中,我們將構(gòu)建兩個(gè)組件:src/components/ToDoItem.js 文件中的 ToDoItem 組件和 src/components/CreateToDoItem.js 文件中的 CreateToDoItem 組件。 一旦我們構(gòu)建了這些,我們就可以將它們插入到我們的 App 組件中,因?yàn)槲覀兊?App 組件將獲取項(xiàng)目的數(shù)據(jù)并循環(huán)這些項(xiàng)目,創(chuàng)建多個(gè) ToDoItem 組件。 為了實(shí)現(xiàn)這一目標(biāo),我們需要處理幾個(gè)步驟,因此本節(jié)將分為以下小節(jié):

          創(chuàng)建我們的 ToDoItem 組件

          創(chuàng)建 CreateToDoItem 組件

          在我們的應(yīng)用程序組件中構(gòu)建和管理自定義組件

          讓我們開(kāi)始吧。

          創(chuàng)建我們的 ToDoItem 組件

          我們將從 src/components/ToDoItem.js 文件中更簡(jiǎn)單的 ToDoItem 組件開(kāi)始。 首先,我們必須導(dǎo)入以下內(nèi)容:

          import React, { Component } from 'react';
          import axios from "axios";

          這不是什么新鮮事。 現(xiàn)在我們已經(jīng)導(dǎo)入了我們需要的內(nèi)容,我們可以關(guān)注如何使用以下代碼定義 ToDoItem:

          class ToDoItem extends Component {
              state={
                  "title": this.props.title,
                  "status": this.props.status,
                  "button": this.processStatus(this.props.status)
              }
              processStatus(status) {
                  . . .
              }
              inverseStatus(status) {
                  . . .
              }
              sendRequest=()=> {
                  . . .
              }
              render() {
                  return(
                      . . .
                  )
              }
          }
          export default ToDoItem;

          在這里,我們使用 this.props 填充狀態(tài),這是構(gòu)造組件時(shí)傳遞到組件中的參數(shù)。 然后,我們的 ToDoItem 組件具有以下函數(shù):

          processStatus:此函數(shù)將待辦事項(xiàng)的狀態(tài)(例如 PENDING)轉(zhuǎn)換為按鈕上的消息(例如編輯)。

          inverseStatus:當(dāng)我們有一個(gè)狀態(tài)為 PENDING 的待辦事項(xiàng)并對(duì)其進(jìn)行編輯時(shí),我們希望將其轉(zhuǎn)換為 DONE 狀態(tài),以便可以將其發(fā)送到 Rust 服務(wù)器上的編輯端點(diǎn),這是相反的。 因此,該函數(shù)創(chuàng)建傳入狀態(tài)的反轉(zhuǎn)。

          sendRequest:此函數(shù)將請(qǐng)求發(fā)送到 Rust 服務(wù)器以編輯或刪除待辦事項(xiàng)。 我們還可以看到我們的 sendRequest 函數(shù)是一個(gè)箭頭函數(shù)。 箭頭語(yǔ)法本質(zhì)上將函數(shù)綁定到組件,以便我們可以在渲染返回語(yǔ)句中引用它,從而允許在單擊綁定到它的按鈕時(shí)執(zhí)行 sendRequest 函數(shù)。

          現(xiàn)在我們知道我們的函數(shù)應(yīng)該做什么,我們可以使用以下代碼定義我們的狀態(tài)函數(shù):

          processStatus(status) {
              if (status==="PENDING") {
                  return "edit"
              } else {
                  return "delete"
              }
          }
          inverseStatus(status) {
              if (status==="PENDING") {
                  return "DONE"
              } else {
                  return "PENDING"
              }
          }

          這很簡(jiǎn)單,不需要太多解釋。 現(xiàn)在我們的狀態(tài)處理函數(shù)已經(jīng)完成,我們可以使用以下代碼定義我們的 sendRequest 函數(shù):

          sendRequest=()=> {
              axios.post("http://127.0.0.1:8000/v1/item/" + 
                          this.state.button,
                  {
                      "title": this.state.title,
                      "status": this.inverseStatus(this.state.status)
                  },
              {headers: {"token": "some_token"}})
                  .then(response=> {
                      this.props.passBackResponse(response);
                  });
          }

          在這里,我們使用 this.state.button 定義端點(diǎn)更改時(shí) URL 的一部分,具體取決于我們按下的按鈕。 我們還可以看到我們執(zhí)行了 this.props.passBackResponse 函數(shù)。 這是我們傳遞到 ToDoItem 組件中的函數(shù)。 這是因?yàn)樵诰庉嫽騽h除請(qǐng)求后,我們從 Rust 服務(wù)器獲取了待辦事項(xiàng)的完整狀態(tài)。 我們需要啟用我們的應(yīng)用程序組件來(lái)處理已傳回的數(shù)據(jù)。 在這里,我們將在“應(yīng)用程序組件”小節(jié)中的“構(gòu)建和管理自定義組件”中先睹為快。 我們的 App 組件將在 passBackResponse 參數(shù)下有一個(gè)未執(zhí)行的函數(shù),它將傳遞給我們的 ToDoItem 組件。 該函數(shù)在 passBackResponse 參數(shù)下,將處理新的待辦事項(xiàng)的狀態(tài)并將其呈現(xiàn)在 App 組件中。

          至此,我們已經(jīng)配置了所有功能。 剩下的就是定義渲染函數(shù)的返回,它采用以下形式:

          <div>
              <p>{this.state.title}</p>
              <button onClick={this.sendRequest}>
                              {this.state.button}</button>
          </div>

          在這里,我們可以看到待辦事項(xiàng)的標(biāo)題呈現(xiàn)在段落標(biāo)記中,并且我們的按鈕在單擊時(shí)執(zhí)行 sendRequest 函數(shù)。 現(xiàn)在我們已經(jīng)完成了這個(gè)組件,并且可以在我們的應(yīng)用程序中顯示它了。 但是,在執(zhí)行此操作之前,我們需要構(gòu)建用于在下一節(jié)中創(chuàng)建待辦事項(xiàng)的組件。

          在 React 中創(chuàng)建自定義組件

          我們的 React 應(yīng)用程序可以列出、編輯和刪除待辦事項(xiàng)。 但是,我們無(wú)法創(chuàng)建任何待辦事項(xiàng)。 它由一個(gè)輸入和一個(gè)創(chuàng)建按鈕組成,以便我們可以放入一個(gè)待辦事項(xiàng),然后通過(guò)單擊該按鈕來(lái)創(chuàng)建該待辦事項(xiàng)。 在我們的 src/components/CreateToDoItem.js 文件中,我們需要導(dǎo)入以下內(nèi)容:

          import React, { Component } from 'react';
          import axios from "axios";

          這些是構(gòu)建我們組件的標(biāo)準(zhǔn)導(dǎo)入。 定義導(dǎo)入后,我們的 CreateToDoItem 組件將采用以下形式:

          class CreateToDoItem extends Component {
              state={
                  title: ""
              }
              createItem=()=> {
                  . . .
              }
              handleTitleChange=(e)=> {
                  . . .
              }
              render() {
                  return (
                      . . .
                  )
              }
          }
          export default CreateToDoItem;

          在上面的代碼中,我們可以看到我們的CreateToDoItem組件有以下功能:

          createItem:該函數(shù)向 Rust 服務(wù)器發(fā)送請(qǐng)求,以創(chuàng)建標(biāo)題為 state 的待辦事項(xiàng)

          handleTitleChange:每次更新輸入時(shí)該函數(shù)都會(huì)更新?tīng)顟B(tài)

          在探索這兩個(gè)函數(shù)之前,我們將翻轉(zhuǎn)這些函數(shù)的編碼順序,并使用以下代碼定義渲染函數(shù)的返回:

          <div className="inputContainer">
              <input type="text" id="name"
                     placeholder="create to do item"
                     value={this.state.title}
                     onChange={this.handleTitleChange}/>
              <div className="actionButton"
                   id="create-button"
                   onClick={this.createItem}>Create</div>
          </div>

          在這里,我們可以看到輸入的值為this.state.title。 另外,當(dāng)輸入更改時(shí),我們執(zhí)行 this.handleTitleChange 函數(shù)。 現(xiàn)在我們已經(jīng)介紹了渲染函數(shù),沒(méi)有什么新內(nèi)容要介紹了。 這是您再次查看 CreateToDoItem 組件的概要并嘗試自己定義 createItem 和 handleTitleChange 函數(shù)的好機(jī)會(huì)。 它們采用與 ToDoItem 組件中的函數(shù)類(lèi)似的形式。

          您嘗試定義 createItem 和 handleTitleChange 函數(shù)應(yīng)類(lèi)似于以下內(nèi)容:

          createItem=()=> {
              axios.post("http://127.0.0.1:8000/v1/item/create/" +
                  this.state.title,
                  {},
                  {headers: {"token": "some_token"}})
                  .then(response=> {
                      this.setState({"title": ""});
                      this.props.passBackResponse(response);
                  });
          }
          handleTitleChange=(e)=> {
              this.setState({"title": e.target.value});
          }    

          這樣,我們就定義了兩個(gè)自定義組件。 我們現(xiàn)在準(zhǔn)備好進(jìn)入下一小節(jié),我們將在其中管理我們的自定義組件。

          在我們的應(yīng)用程序組件中構(gòu)建和管理自定義組件

          雖然創(chuàng)建自定義組件很有趣,但如果我們不在應(yīng)用程序中使用它們,它們就沒(méi)有多大用處。 在本小節(jié)中,我們將向 src/App.js 文件添加一些額外的代碼,以啟用我們的自定義組件。 首先,我們必須使用以下代碼導(dǎo)入我們的組件:

          import ToDoItem from "./components/ToDoItem";
          import CreateToDoItem from "./components/CreateToDoItem";

          現(xiàn)在我們已經(jīng)有了組件,我們可以繼續(xù)進(jìn)行第一次更改。 我們的 App 組件的 processItemValues 函數(shù)可以使用以下代碼定義:

          processItemValues(items) {
            let itemList=[];
            items.forEach((item, _)=>{
                itemList.push(
                    <ToDoItem key={item.title + item.status}
                              title={item.title}
                              status={item.status.status}
                              passBackResponse={
                              this.handleReturnedState}/>
                )
            })
            return itemList
          }

          在這里,我們可以看到我們循環(huán)遍歷從 Rust 服務(wù)器獲取的數(shù)據(jù),但我們沒(méi)有將數(shù)據(jù)傳遞到通用 HTML 標(biāo)簽中,而是將待辦事項(xiàng)數(shù)據(jù)的參數(shù)傳遞到我們自己的自定義組件中,該組件將被處理 就像 HTML 標(biāo)簽一樣。 當(dāng)涉及到處理我們自己的返回狀態(tài)響應(yīng)時(shí),我們可以看到它是一個(gè)箭頭函數(shù),用于處理數(shù)據(jù)并使用以下代碼設(shè)置狀態(tài):

          handleReturnedState=(response)=> {
            let pending_items=response.data["pending_items"]
            let done_items=response.data["done_items"]
            this.setState({
                "pending_items": 
                 this.processItemValues(pending_items),
                "done_items": this.processItemValues(done_items),
                "pending_items_count": 
                 response.data["pending_item_count"],
                "done_items_count": response.data["done_item_count"]
            })
          }

          這與我們的 getItems 函數(shù)非常相似。 如果您想減少重復(fù)代碼的數(shù)量,可以在這里進(jìn)行一些重構(gòu)。 但是,為了使其工作,我們必須使用以下代碼定義渲染函數(shù)的 return 語(yǔ)句:

          <div className="App">
              <h1>Pending Items</h1>
              <p>done item count: 
              {this.state.pending_items_count}</p>
              {this.state.pending_items}
              <h1>Done Items</h1>
              <p>done item count: {this.state.done_items_count}</p>
              {this.state.done_items}
              <CreateToDoItem 
               passBackResponse={this.handleReturnedState} />
          </div>

          在這里,我們可以看到除了添加 createItem 組件之外沒(méi)有太多變化。 運(yùn)行 Rust 服務(wù)器和 React 應(yīng)用程序?qū)槲覀兲峁┮韵乱晥D:


          圖 5.12 – 帶有自定義組件的 React 應(yīng)用程序的視圖


          圖 5.12 顯示我們的自定義組件正在呈現(xiàn)。 我們可以單擊按鈕,結(jié)果是,我們將看到所有 API 調(diào)用都正常工作,并且我們的自定義組件也正常工作。 現(xiàn)在,阻礙我們的只是讓我們的前端看起來(lái)更美觀,我們可以通過(guò)將 CSS 提升到 React 應(yīng)用程序中來(lái)做到這一點(diǎn)。

          將 CSS 放到 React 中

          我們現(xiàn)在正處于使 React 應(yīng)用程序可用的最后階段。 我們可以將 CSS 分成多個(gè)不同的文件。 然而,我們即將結(jié)束本章,再次瀏覽所有 CSS 會(huì)不必要地讓本章充滿大量重復(fù)代碼。 雖然我們的 HTML 和 JavaScript 不同,但 CSS 是相同的。 為了讓它運(yùn)行,我們可以從以下文件中復(fù)制所有 CSS:

          templates/components/header.css

          css/base.css

          css/main.css

          將此處列出的 CSS 文件復(fù)制到 front_end/src/App.css 文件中。 CSS 有一項(xiàng)更改,所有 .body 引用都應(yīng)替換為 .App,如以下代碼片段所示:

          .App {
            background-color: #92a8d1;
            font-family: Arial, Helvetica, sans-serif;
            height: 100vh;
          }
          @media(min-width: 501px) and (max-width: 550px) {
            .App {
              padding: 1px;
              display: grid;
              grid-template-columns: 1fr 5fr 1fr;
            }
            .mainContainer {
              grid-column-start: 2;
            }
          }
          . . .

          現(xiàn)在,我們可以導(dǎo)入 CSS 并在我們的應(yīng)用程序和組件中使用它。 我們還必須更改渲染函數(shù)中的返回 HTML。 我們可以處理所有三個(gè)文件。 對(duì)于 src/App.js 文件,我們必須使用以下代碼導(dǎo)入 CSS:

          import "./App.css";

          然后,我們必須添加一個(gè)標(biāo)頭并使用正確的類(lèi)定義 div 標(biāo)簽,并使用以下代碼作為渲染函數(shù)的返回語(yǔ)句:

          <div className="App">
              <div className="mainContainer">
                  <div className="header">
                      <p>complete tasks: 
                      {this.state.done_items_count}</p>
                      <p>pending tasks: 
                      {this.state.pending_items_count}</p>
                  </div>
                  <h1>Pending Items</h1>
                  {this.state.pending_items}
                  <h1>Done Items</h1>
                  {this.state.done_items}
                  <CreateToDoItem passBackResponse={this.handleReturnedState}/>
              </div>
          </div>

          在我們的 src/components/ToDoItem.js 文件中,我們必須使用以下代碼導(dǎo)入 CSS:

          import "../App.css";

          然后,我們必須將按鈕更改為 div 并使用以下代碼定義渲染函數(shù)的 return 語(yǔ)句:

          <div className="itemContainer">
              <p>{this.state.title}</p>
              <div className="actionButton" onClick={this.sendRequest}>
              {this.state.button}</div>
          </div>

          在我們的 src/components/CreateToDoItem.js 文件中,我們必須使用以下代碼導(dǎo)入 CSS:

          import "../App.css";

          然后,我們必須將按鈕更改為 div 并使用以下代碼定義渲染函數(shù)的 return 語(yǔ)句:

          <div className="inputContainer">
              <input type="text" id="name"
                     placeholder="create to do item"
                     value={this.state.title}
                     onChange={this.handleTitleChange}/>
              <div className="actionButton"
                   id="create-button"
                   onClick={this.createItem}>Create</div>
          </div>

          這樣,我們就將 CSS 從 Rust Web 服務(wù)器提升到了 React 應(yīng)用程序中。 如果我們運(yùn)行 Rust 服務(wù)器和 React 應(yīng)用程序,我們將得到下圖所示的輸出:


          圖 5.13 – 添加了 CSS 的 React 應(yīng)用程序的視圖


          我們終于得到它了! 我們的 React 應(yīng)用程序正在運(yùn)行。 啟動(dòng)并運(yùn)行我們的 React 應(yīng)用程序需要更多時(shí)間,但我們可以看到 React 具有更大的靈活性。 我們還可以看到,我們的 React 應(yīng)用程序不太容易出錯(cuò),因?yàn)槲覀儾槐厥謩?dòng)操作字符串。 我們用 React 構(gòu)建還有一個(gè)優(yōu)勢(shì),那就是現(xiàn)有的基礎(chǔ)設(shè)施。 在下一部分也是最后一部分中,我們將通過(guò)將 React 應(yīng)用程序包裝在 Electron 中,將 React 應(yīng)用程序轉(zhuǎn)換為編譯后的桌面應(yīng)用程序,該應(yīng)用程序在計(jì)算機(jī)的應(yīng)用程序中運(yùn)行。

          將我們的 React 應(yīng)用程序轉(zhuǎn)換為桌面應(yīng)用程序

          將我們的 React 應(yīng)用程序轉(zhuǎn)換為桌面應(yīng)用程序并不復(fù)雜。 我們將使用 Electron 框架來(lái)做到這一點(diǎn)。 Electron 是一個(gè)功能強(qiáng)大的框架,可將 JavaScript、HTML 和 CSS 應(yīng)用程序轉(zhuǎn)換為跨 macOS、Linux 和 Windows 平臺(tái)編譯的桌面應(yīng)用程序。 Electron 框架還可以讓我們通過(guò) API 訪問(wèn)計(jì)算機(jī)的組件,例如加密存儲(chǔ)、通知、電源監(jiān)視器、消息端口、進(jìn)程、shell、系統(tǒng)首選項(xiàng)等等。 Electron 中內(nèi)置了 Slack、Visual Studio Code、Twitch、Microsoft Teams 等桌面應(yīng)用程序。 要轉(zhuǎn)換我們的 React 應(yīng)用程序,我們必須首先更新 package.json 文件。 首先,我們必須使用以下代碼更新 package.json 文件頂部的元數(shù)據(jù):

          {
            "name": "front_end",
            "version": "0.1.0",
            "private": true,
            "homepage": "./",
            "main": "public/electron.js",
            "description": "GUI Desktop Application for a simple To 
                            Do App",
            "author": "Maxwell Flitton",
            "build": {
              "appId": "Packt"
            },
            "dependencies": {
              . . .

          其中大部分是通用元數(shù)據(jù)。 然而,主力場(chǎng)是必不可少的。 我們將在此處編寫(xiě)定義 Electron 應(yīng)用程序如何運(yùn)行的文件。 將主頁(yè)字段設(shè)置為“./”還可以確保資源路徑相對(duì)于index.html 文件。 現(xiàn)在我們的元數(shù)據(jù)已經(jīng)定義了,我們可以添加以下依賴(lài)項(xiàng):

          "webpack": "4.28.3",
          "cross-env": "^7.0.3",
          "electron-is-dev": "^2.0.0"

          這些依賴(lài)項(xiàng)有助于構(gòu)建 Electron 應(yīng)用程序。 添加它們后,我們可以使用以下代碼重新定義腳本:

              . . .
          "scripts": {
              "react-start": "react-scripts start",
              "react-build": "react-scripts build",
              "react-test": "react-scripts test",
              "react-eject": "react-scripts eject",
              "electron-build": "electron-builder",
              "build": "npm run react-build && npm run electron-
                        build",
              "start": "concurrently \"cross-env BROWSER=none npm run 
                        react-start\" \"wait-on http://localhost:3000 
                        && electron .\""
          },

          在這里,我們?yōu)樗?React 腳本添加了前綴“react”。 這是為了將 React 進(jìn)程與 Electron 進(jìn)程分開(kāi)。 如果我們現(xiàn)在只想在開(kāi)發(fā)模式下運(yùn)行 React 應(yīng)用程序,則必須運(yùn)行以下命令:

          npm run react-start

          我們還為 Electron 定義了構(gòu)建命令和開(kāi)發(fā)啟動(dòng)命令。 這些還不能工作,因?yàn)槲覀冞€沒(méi)有定義我們的 Electron 文件。 在 package.json 文件的底部,我們必須定義構(gòu)建 Electron 應(yīng)用程序的開(kāi)發(fā)人員依賴(lài)項(xiàng):

              . . .
              "development": [
                "last 1 chrome version",
                "last 1 firefox version",
                "last 1 safari version"
              ]
            },
            "devDependencies": {
              "concurrently": "^7.1.0",
              "electron": "^18.0.1",
              "electron-builder": "^22.14.13",
              "wait-on": "^6.0.1"
            }
          }

          這樣,我們就在 package.json 文件中定義了我們需要的所有內(nèi)容。 我們需要使用以下命令安裝新的依賴(lài)項(xiàng):

          npm install

          現(xiàn)在,我們可以開(kāi)始構(gòu)建 front_end/public/electron.js 文件,以便構(gòu)建我們的 Electron 文件。 這本質(zhì)上是樣板代碼,您可能會(huì)在其他教程中看到此文件,因?yàn)檫@是在 Electron 中運(yùn)行應(yīng)用程序的最低要求。 首先,我們必須使用以下代碼導(dǎo)入我們需要的內(nèi)容:

          const { app, BrowserWindow }=require("electron");
          const path=require("path");
          const isDev=require("electron-is-dev");

          然后,我們必須使用以下代碼定義創(chuàng)建桌面窗口的函數(shù):

          function createWindow() {
              const mainWindow=new BrowserWindow({
                  width: 800,
                  height: 600,
                  webPreferences: {
                      nodeIntegration: true,
                      enableRemoteModule: true,
                      contextIsolation: false,
                  },
              });
              mainWindow.loadURL(
                  isDev
                     ? "http://localhost:3000"
                     : `file://${path.join(__dirname, 
                                           "../build/index.html")}`
              );
              if (isDev) {
                  mainWindow.webContents.openDevTools();
              }
          }

          在這里,我們本質(zhì)上定義了窗口的寬度和高度。 另請(qǐng)注意,nodeIntegration 和enableRemoteModule 使渲染器遠(yuǎn)程進(jìn)程(瀏覽器窗口)能夠在主進(jìn)程上運(yùn)行代碼。 然后,我們開(kāi)始在主窗口中加載 URL。 如果在開(kāi)發(fā)人員模式下運(yùn)行,我們只需加載 http://localhost:3000,因?yàn)槲覀冊(cè)?localhost 上運(yùn)行了 React 應(yīng)用程序。 如果我們構(gòu)建應(yīng)用程序,那么我們編碼的資產(chǎn)和文件將被編譯并可以通過(guò) ../build/index.html 文件加載。 我們還聲明,如果我們?cè)陂_(kāi)發(fā)人員模式下運(yùn)行,我們將打開(kāi)開(kāi)發(fā)人員工具。 當(dāng)窗口準(zhǔn)備好時(shí),我們必須使用以下代碼執(zhí)行 createWindow 函數(shù):

          app.whenReady().then(()=> {
              createWindow();
              app.on("activate", function () {
                  if (BrowserWindow.getAllWindows().length===0){
                     createWindow(); 
                  }
              });
          });

          如果操作系統(tǒng)是macOS,我們必須保持程序運(yùn)行,即使我們關(guān)閉窗口:

          app.on("window-all-closed", function () {
              if (process.platform !=="darwin") app.quit();
          });

          現(xiàn)在,我們必須運(yùn)行以下命令:

          npm start

          這將運(yùn)行 Electron 應(yīng)用程序,為我們提供以下輸出:


          圖 5.14 – 我們?cè)?Electron 中運(yùn)行的 React 應(yīng)用程序

          在圖 5.13 中,我們可以看到我們的應(yīng)用程序正在桌面上的一個(gè)窗口中運(yùn)行。 我們還可以看到我們的應(yīng)用程序可以通過(guò)屏幕頂部的菜單欄訪問(wèn)。 該應(yīng)用程序的徽標(biāo)顯示在我的任務(wù)欄上:


          圖 5.15 – 我的任務(wù)欄上的 Electron

          以下命令將在 dist 文件夾中編譯我們的應(yīng)用程序,如果單擊該文件夾,則會(huì)將該應(yīng)用程序安裝到您的計(jì)算機(jī)上:

          npm build

          以下是我在 Mac 上的應(yīng)用程序區(qū)域中使用 Electron 測(cè)試我為 OasisLMF 構(gòu)建的名為 Camel 的開(kāi)源包的 GUI 時(shí)的示例:


          圖 5.16 – 應(yīng)用程序區(qū)域中的 Electron 應(yīng)用程序


          最終,我會(huì)想出一個(gè)標(biāo)志。 不過(guò),關(guān)于在瀏覽器中顯示內(nèi)容的本章就到此結(jié)束。

          概括

          在本章中,我們最終使臨時(shí)用戶(hù)可以使用我們的應(yīng)用程序,而不必依賴(lài)于 Postman 等第三方應(yīng)用程序。 我們定義了自己的應(yīng)用程序視圖模塊,其中包含讀取文件和插入功能。 這導(dǎo)致我們構(gòu)建了一個(gè)流程,加載 HTML 文件,將 JavaScript 和 CSS 文件中的數(shù)據(jù)插入到視圖數(shù)據(jù)中,然后提供該數(shù)據(jù)。

          這為我們提供了一個(gè)動(dòng)態(tài)視圖,當(dāng)我們編輯、刪除或創(chuàng)建待辦事項(xiàng)時(shí),該視圖會(huì)自動(dòng)更新。 我們還探索了一些有關(guān) CSS 和 JavaScript 的基礎(chǔ)知識(shí),以便從前端進(jìn)行 API 調(diào)用并動(dòng)態(tài)編輯視圖某些部分的 HTML。 我們還根據(jù)窗口的大小管理整個(gè)視圖的樣式。 請(qǐng)注意,我們不依賴(lài)外部板條箱。 這是因?yàn)槲覀兿M軌蛄私馊绾翁幚?HTML 數(shù)據(jù)。

          然后,我們?cè)?React 中重建了前端。 雖然這需要更長(zhǎng)的時(shí)間并且有更多的移動(dòng)部件,但代碼更具可擴(kuò)展性并且更安全,因?yàn)槲覀儾槐厥謩?dòng)操作字符串來(lái)編寫(xiě) HTML 組件。 我們還可以明白為什么我們傾向于 React,因?yàn)樗浅_m合 Electron,為我們提供了另一種向用戶(hù)交付應(yīng)用程序的方式。

          雖然我們的應(yīng)用程序現(xiàn)在按表面價(jià)值運(yùn)行,但它在數(shù)據(jù)存儲(chǔ)方面不可擴(kuò)展。 我們沒(méi)有數(shù)據(jù)過(guò)濾流程。 我們不會(huì)檢查我們存儲(chǔ)的數(shù)據(jù),也沒(méi)有多個(gè)表。

          在下一章中,我們將構(gòu)建與 Docker 本地運(yùn)行的 PostgreSQL 數(shù)據(jù)庫(kù)交互的數(shù)據(jù)模型。

          問(wèn)題

          將 HTML 數(shù)據(jù)返回到用戶(hù)瀏覽器的最簡(jiǎn)單方法是什么?

          將 HTML、CSS 和 JavaScript 數(shù)據(jù)返回到用戶(hù)瀏覽器的最簡(jiǎn)單(不可擴(kuò)展)的方法是什么?

          我們?nèi)绾未_保某些元素的背景顏色和樣式標(biāo)準(zhǔn)在應(yīng)用程序的所有視圖中保持一致?

          API 調(diào)用后我們?nèi)绾胃?HTML?

          我們?nèi)绾螁⒂冒粹o來(lái)連接到我們的后端 API?

          答案

          我們只需定義一個(gè) HTML 字符串并將其放入 HttpResponse 結(jié)構(gòu)體中,同時(shí)將內(nèi)容類(lèi)型定義為 HTML,即可提供 HTML 數(shù)據(jù)。 然后 HttpResponse 結(jié)構(gòu)體返回到用戶(hù)的瀏覽器。

          最簡(jiǎn)單的方法是硬編碼一個(gè)完整的 HTML 字符串,CSS 硬編碼在 <style> 部分,我們的 JavaScript 硬編碼在 <script> 部分。 然后將該字符串放入 HttpResponse 結(jié)構(gòu)體中并返回到用戶(hù)的瀏覽器。

          我們創(chuàng)建一個(gè) CSS 文件來(lái)定義我們希望在整個(gè)應(yīng)用程序中保持一致的組件。 然后,我們?cè)谒?HTML 文件的 <style> 部分放置一個(gè)標(biāo)簽。 然后,對(duì)于每個(gè)文件,我們加載基本 CSS 文件并用 CSS 數(shù)據(jù)替換標(biāo)簽。

          API調(diào)用后,我們必須等待狀態(tài)準(zhǔn)備好。 然后,我們使用 getElementById 獲取要更新的 HTML 部分,序列化響應(yīng)數(shù)據(jù),然后將元素的內(nèi)部 HTML 設(shè)置為響應(yīng)數(shù)據(jù)。

          我們給按鈕一個(gè)唯一的 ID。 然后,我們添加一個(gè)事件偵聽(tīng)器,該偵聽(tīng)器由唯一 ID 定義。 在此事件偵聽(tīng)器中,我們將其綁定到一個(gè)使用 this 獲取 ID 的函數(shù)。 在此函數(shù)中,我們對(duì)后端進(jìn)行 API 調(diào)用,然后使用響應(yīng)來(lái)更新顯示數(shù)據(jù)的視圖其他部分的 HTML。


          主站蜘蛛池模板: 国产在线aaa片一区二区99| 精品视频一区二区三三区四区| 中文字幕日韩精品一区二区三区| 91一区二区视频| 国产精品伦子一区二区三区| 免费看一区二区三区四区| 麻豆精品人妻一区二区三区蜜桃| 亚洲一区精品伊人久久伊人| 亚洲综合av一区二区三区不卡| 99久久精品国产高清一区二区| 亚洲国产老鸭窝一区二区三区| 精品一区二区三区高清免费观看| 人妻av无码一区二区三区| 无码人妻精品一区二区三区9厂 | 国产日韩AV免费无码一区二区| 四虎成人精品一区二区免费网站| 麻豆果冻传媒2021精品传媒一区下载| 日韩AV无码一区二区三区不卡毛片 | 国产伦理一区二区| av无码精品一区二区三区四区| 亚洲AV无码国产一区二区三区| 久久久精品人妻一区二区三区四 | 国产福利酱国产一区二区| 91精品乱码一区二区三区| 成人精品一区二区户外勾搭野战| 国产麻豆精品一区二区三区v视界 国产美女精品一区二区三区 | 久久毛片免费看一区二区三区 | 亚洲美女视频一区| 99久久精品国产一区二区成人| 久久久久人妻精品一区二区三区| 亚洲男女一区二区三区| 亚洲Av高清一区二区三区| 在线日产精品一区| 国内精自品线一区91| 午夜福利国产一区二区| 国产成人精品一区二区三区| 国产精品小黄鸭一区二区三区 | 国产成人av一区二区三区不卡 | 中文字幕无码一区二区免费| 一本AV高清一区二区三区| 无码少妇一区二区性色AV|