Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 免费一级毛片清高播放,免费一看一级毛片全播放,99国产精品免费视频

          整合營銷服務(wù)商

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

          免費咨詢熱線:

          HTTP協(xié)議是什么?詳細解讀HTTP看完還不懂你來找

          HTTP協(xié)議是什么?詳細解讀HTTP看完還不懂你來找我

          章將包含以下幾方面內(nèi)容:

          • HTTP協(xié)議解讀
          • 與HTTP相關(guān)組件
          • 與HTTP相關(guān)協(xié)議
          • HTTP組成
          • HTTP協(xié)議優(yōu)缺點

          HTTP協(xié)議解讀

          HTTP 是一種 超文本傳輸協(xié)議(Hypertext Transfer Protocol),超文本傳輸協(xié)議可以進行文字分割:超文本(Hypertext)、傳輸(Transfer)、協(xié)議(Protocol) ,它們之間的關(guān)系如下:

          分別對這三個名次做一個解釋:

          超文本

          兩臺電腦之間只能傳輸簡單文字,后面還想要傳輸圖片、音頻、視頻,甚至點擊文字或圖片能夠進行超鏈接的跳轉(zhuǎn),那么文本的語義就被擴大了,這種語義擴大后的文本就被稱為超文本(Hypertext)。

          傳輸

          兩臺計算機之間會形成互聯(lián)關(guān)系進行通信,我們存儲的超文本會被解析成為二進制數(shù)據(jù)包,由傳輸載體(例如同軸電纜,電話線,光纜)負責(zé)把二進制數(shù)據(jù)包由計算機終端傳輸?shù)搅硪粋€終端的過程

          協(xié)議

          網(wǎng)絡(luò)協(xié)議就是網(wǎng)絡(luò)中(包括互聯(lián)網(wǎng))傳遞、管理信息的一些規(guī)范

          與HTTP相關(guān)組件

          網(wǎng)絡(luò)設(shè)計者以分層(layer)的方式組織協(xié)議,每個協(xié)議屬于層次模型之一。每一層都是向它的上一層提供服務(wù)(service),即所謂的服務(wù)模型(service model)。每個分層中所有的協(xié)議稱為 協(xié)議棧(protocol stack)。因特網(wǎng)的協(xié)議棧由五個部分組成:物理層、鏈路層、網(wǎng)絡(luò)層、傳輸層和應(yīng)用層。我們采用自上而下的方法研究其原理,也就是應(yīng)用層 -> 物理層的方式(了解)。

          應(yīng)用層

          應(yīng)用層是網(wǎng)絡(luò)應(yīng)用程序和網(wǎng)絡(luò)協(xié)議存放的分層,因特網(wǎng)的應(yīng)用層包括許多協(xié)議。比如HTTP,電子郵件傳送協(xié)議 SMTP、端系統(tǒng)文件上傳協(xié)議 FTP、還有為我們進行域名解析的 DNS 協(xié)議

          傳輸層

          輸層在應(yīng)用程序斷點之間傳送應(yīng)用程序報文,在這一層主要有兩種傳輸協(xié)議 TCP和 UDP。

          TCP 是面向連接的,它能夠控制并確認報文是否到達,并提供了擁塞機制來控制網(wǎng)絡(luò)傳輸,因此當(dāng)網(wǎng)絡(luò)擁塞時,會抑制其傳輸速率。

          UDP 協(xié)議向它的應(yīng)用程序提供了無連接服務(wù)。它是不具備可靠性的,沒有流量控制,也沒有擁塞控制。我們把運輸層的分組稱為 報文段(segment)

          網(wǎng)絡(luò)層

          網(wǎng)絡(luò)層負責(zé)將稱為 數(shù)據(jù)報(datagram) 的網(wǎng)絡(luò)分層從一臺主機移動到另一臺主機。網(wǎng)絡(luò)層一個非常重要的協(xié)議是 IP 協(xié)議,所有具有網(wǎng)絡(luò)層的因特網(wǎng)組件都必須運行 IP 協(xié)議。

          鏈路層

          為了將分組從一個節(jié)點(主機或路由器)運輸?shù)搅硪粋€節(jié)點,網(wǎng)絡(luò)層必須依靠鏈路層提供服務(wù)。鏈路層的例子包括以太網(wǎng)、WiFi 和電纜接入的 DOCSIS 協(xié)議,因為數(shù)據(jù)從源目的地傳送通常需要經(jīng)過幾條鏈路,一個數(shù)據(jù)包可能被沿途不同的鏈路層協(xié)議處理,我們把鏈路層的分組稱為 幀(frame)。

          物理層

          雖然鏈路層的作用是將幀從一個端系統(tǒng)運輸?shù)搅硪粋€端系統(tǒng),而物理層的作用是將幀中的一個個 比特 從一個節(jié)點運輸?shù)搅硪粋€節(jié)點,,物理層的協(xié)議仍然使用鏈路層協(xié)議,這些協(xié)議與實際的物理傳輸介質(zhì)有關(guān),例如,以太網(wǎng)有很多物理層協(xié)議:關(guān)于雙絞銅線、關(guān)于同軸電纜、關(guān)于光纖等等。

          五層網(wǎng)絡(luò)協(xié)議的示意圖如下:

          與HTTP相關(guān)協(xié)議

          HTTP 屬于應(yīng)用層的協(xié)議,需要其他層次協(xié)議的配合完成信息的交換,在完成一次 HTTP 請求和響應(yīng)的過程中,需要以下協(xié)議的配合:

          TCP/IP

          TCP/IP 我們一般稱之為協(xié)議簇,什么意思呢?就是 TCP/IP 協(xié)議簇中不僅僅只有 TCP 協(xié)議和 IP 協(xié)議,它是一系列網(wǎng)絡(luò)通信協(xié)議的統(tǒng)稱。而其中最核心的兩個協(xié)議就是 TCP / IP 協(xié)議,其他的還有 UDP、ICMP、ARP 等等,共同構(gòu)成了一個復(fù)雜但有層次的協(xié)議棧。

          TCP 協(xié)議的全稱是 Transmission Control Protocol 的縮寫,意思是傳輸控制協(xié)議,HTTP 使用 TCP 作為通信協(xié)議,這是因為 TCP 是一種可靠的協(xié)議,而可靠能保證數(shù)據(jù)不丟失。

          IP 協(xié)議的全稱是 Internet Protocol 的縮寫,它主要解決的是通信雙方尋址的問題。IP 協(xié)議使用 IP 地址 來標識互聯(lián)網(wǎng)上的每一臺計算機。

          DNS

          DNS 的全稱是域名系統(tǒng)(Domain Name System,縮寫:DNS),它作為將域名和 IP 地址相互映射的一個分布式數(shù)據(jù)庫,能夠使人更方便地訪問互聯(lián)網(wǎng)。比如:www.google.com -> 193.XXX.XXX.XXX

          URI / URL

          可以通過輸入 www.google.com 地址來訪問谷歌的官網(wǎng),輸入的地址格式必須要滿足 URI 的規(guī)范。

          URI的全稱是(Uniform Resource Identifier),中文名稱是統(tǒng)一資源標識符,使用它就能夠唯一地標記互聯(lián)網(wǎng)上資源。

          URL的全稱是(Uniform Resource Locator),中文名稱是統(tǒng)一資源定位符,也就是我們俗稱的網(wǎng)址,它實際上是 URI 的一個子集。

          HTTP報文

          • 起始行(start line):描述請求或響應(yīng)的基本信息;
          • 頭部字段(header):使用 key-value 形式更詳細地說明報文;
          • 消息正文(entity):實際傳輸?shù)臄?shù)據(jù),它不一定是純文本,可以是圖片、視頻等二進制數(shù)據(jù)。

          起始行和頭部字段并成為 請求頭 或者 響應(yīng)頭,統(tǒng)稱為 Header;消息正文也叫做實體,稱為 body。HTTP 協(xié)議規(guī)定每次發(fā)送的報文必須要有 Header,但是可以沒有 body,在 header 和 body 之間必須要有一個空行(CRLF)。

          舉個例子:http://www.someSchool.edu/someDepartment/home.index 請求的請求頭:

          報文的起始行都是由三個字段組成:方法、URL 字段和 HTTP 版本字段

          HTTP 請求方法

          • GET 獲取資源,GET 方法用來請求訪問已被 URI 識別的資源。指定的資源經(jīng)服務(wù)器端解析后返回響應(yīng)內(nèi)容。
          • POST 傳輸實體,使用 POST 傳輸實體信息,提交表格內(nèi)容。
          • PUT 傳輸文件,PUT 方法用來傳輸文件。就像 FTP 協(xié)議的文件上傳一樣,要求在請求報文的主體中包含文件內(nèi)容,然后保存到請求 URI 指定的位置。 但是,鑒于 HTTP 的 PUT 方法自身不帶驗證機制,任何人都可以上傳文件 , 存在安全性問題,因此一般的 W eb 網(wǎng)站不使用該方法。若配合 W eb 應(yīng)用程序的驗證機制,或架構(gòu)設(shè)計采用REST(REpresentational State Transfer,表征狀態(tài)轉(zhuǎn)移)標準的同類 Web 網(wǎng)站,就可能會開放使用 PUT 方法。
          • HEAD 獲得響應(yīng)首部,HEAD 方法和 GET 方法一樣,只是不返回報文主體部分。用于確認 URI 的有效性及資源更新的日期時間等。
          • DELETE 刪除文件,DELETE 方法用來刪除文件,是與 PUT 相反的方法。DELETE 方法按請求 URI 刪除指定的資源。
          • OPTIONS 詢問支持的方法,OPTIONS 方法用來查詢針對請求 URI 指定的資源支持的方法。
          • TRACE 追蹤路徑,TRACE 方法是讓 Web 服務(wù)器端將之前的請求通信環(huán)回給客戶端的方法。
          • CONNECT 要求用隧道協(xié)議連接代理,CONNECT 方法要求在與代理服務(wù)器通信時建立隧道,實現(xiàn)用隧道協(xié)議進行 TCP 通信。主要使用 SSL(Secure Sockets Layer,安全套接層)和 TLS(Transport Layer Security,傳輸層安全)協(xié)議把通信內(nèi)容加 密后經(jīng)網(wǎng)絡(luò)隧道傳輸。

          一般最常用的方法也就是 GET 方法和 POST 方法,其他方法暫時了解即可。

          HTTP 請求 URL

          完整的域名解析一下 URL:http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument

          • http://告訴瀏覽器使用何種協(xié)議。
          • www.example.com 是域名、主機,指示了需要向網(wǎng)絡(luò)上的哪一臺主機發(fā)起請求。也可以直接向主機的ip發(fā)起請求。
          • 端口 兩個主機之間要發(fā)起 TCP 連接需要兩個條件,主機 + 端口,表示用于訪問 Web 服務(wù)器上資源的入口,如果訪問的該 Web 服務(wù)器使用HTTP協(xié)議的標準端口(HTTP為80,HTTPS為443)授予對其資源的訪問權(quán)限,則通常省略此部分。否則端口就是 URI 必須的部分。
          • 路徑 /path/to/myfile.html 是 Web 服務(wù)器上資源的路徑。以端口后面的第一個 / 開始,到 ? 號之前結(jié)束,中間的 每一個/ 都代表了層級(上下級)關(guān)系。
          • 查詢參數(shù)

          ?key1=value1&key2=value2 是提供給 Web 服務(wù)器的額外參數(shù)。如果是 GET 請求,一般帶有請求 URL 參數(shù),如果是 POST 請求,則不會在路徑后面直接加參數(shù)。

          • 錨點 #SomewhereInTheDocument 是資源本身的某一部分的一個錨點。錨點代表資源內(nèi)的一種“書簽”。

          請求頭部

          比如http://www.someSchool.edu/someDepartment/home.index,來看一下它的請求頭部

          Host: www.someschool.edu
          Connection: close
          User-agent: Mozilla/5.0
          Accept-language: fr
          復(fù)制代碼
          • Host :表示的是對象所在的主機
          • Connection: close 表示的是瀏覽器需要告訴服務(wù)器使用的是非持久連接。它要求服務(wù)器在發(fā)送完響應(yīng)的對象后就關(guān)閉連接。
          • User-agent: 這是請求頭用來告訴 Web 服務(wù)器,瀏覽器使用的類型是 Mozilla/5.0,即 Firefox 瀏覽器。
          • Accept-language 告訴 Web 服務(wù)器,瀏覽器想要得到對象的法語版本。

          HTTP 的請求標頭分為四種: 通用標頭、請求標頭、響應(yīng)標頭 和 實體標頭

          通用標頭

          通用標頭主要有三個,分別是 Date、Cache-Control 和 Connection Date

          Date 出現(xiàn)在請求標頭和響應(yīng)標頭中,它的基本表示如下

          Date: Wed, 21 Oct 2015 07:28:00 GMT 
          復(fù)制代碼

          Cache-Control

          Cache-Control 可以出現(xiàn)在請求標頭和響應(yīng)標頭中,Cache-Control 的種類比較多,雖然說這是一個通用標頭,但是又一些特性是請求標頭具有的,有一些是響應(yīng)標頭才有的。主要大類有 可緩存性、閾值性、 重新驗證并重新加載 和其他特性

          Connection

          Connection 決定當(dāng)前事務(wù)(一次三次握手和四次揮手)完成后,是否會關(guān)閉網(wǎng)絡(luò)連接。Connection 有兩種,一種是持久性連接,即一次事務(wù)完成后不關(guān)閉網(wǎng)絡(luò)連接

          Connection: keep-alive
          復(fù)制代碼
          復(fù)制代碼

          另一種是非持久性連接,即一次事務(wù)完成后關(guān)閉網(wǎng)絡(luò)連接

          Connection: close
          復(fù)制代碼

          實體標頭

          實體標頭是描述消息正文內(nèi)容的 HTTP 標頭。實體標頭用于 HTTP 請求和響應(yīng)中。頭部Content-Length、 Content-Language、 Content-Encoding 是實體頭。

          • Content-Length 實體報頭指示實體主體的大小,以字節(jié)為單位,發(fā)送到接收方。
          • Content-Language 實體報頭描述了客戶端或者服務(wù)端能夠接受的語言,例如
          Content-Language: de-DE
          Content-Language: en-US
          Content-Language: de-DE, en-CA
          復(fù)制代碼
          復(fù)制代碼
          • Content-Encoding 這又是一個比較麻煩的屬性,這個實體報頭用來壓縮媒體類型。Content-Encoding 指示對實體應(yīng)用了何種編碼。 常見的內(nèi)容編碼有這幾種: gzip、compress、deflate、identity ,這個屬性可以應(yīng)用在請求報文和響應(yīng)報文中
          Accept-Encoding: gzip, deflate //請求頭
          Content-Encoding: gzip  //響應(yīng)頭
          復(fù)制代碼

          請求標頭

          GET /home.html HTTP/1.1
          Host: developer.mozilla.org
          User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
          Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
          Accept-Language: en-US,en;q=0.5
          Accept-Encoding: gzip, deflate, br
          Referer: https://developer.mozilla.org/testpage.html
          Connection: keep-alive
          Upgrade-Insecure-Requests: 1
          If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT
          If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"
          Cache-Control: max-age=0 
          
          復(fù)制代碼

          Host

          Host 請求頭指明了服務(wù)器的域名,以及(可選的)服務(wù)器監(jiān)聽的TCP端口號

          Referer

          HTTP Referer 屬性是請求標頭的一部分,告訴服務(wù)器該網(wǎng)頁是從哪個頁面鏈接過來的

          If-Modified-Since

          HTTP 的 If-Modified-Since 使其成為條件請求:

          • 返回200,只有在給定日期的最后一次修改資源后,服務(wù)器才會以200狀態(tài)發(fā)送回請求的資源。
          • 如果請求從開始以來沒有被修改過,響應(yīng)會返回304并且沒有任何響應(yīng)體

          If-Modified-Since 通常會與 If-None-Match 搭配使用,If-Modified-Since 用于確認代理或客戶端擁有的本地資源的有效性。獲取資源的更新日期時間,可通過確認首部字段 Last-Modified 來確定。

          大白話說就是如果在 Last-Modified 之后更新了服務(wù)器資源,那么服務(wù)器會響應(yīng)200,如果在 Last-Modified 之后沒有更新過資源,則返回 304。

          If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT
          復(fù)制代碼
          復(fù)制代碼

          If-None-Match

          If-None-Match HTTP請求標頭使請求成為條件請求。 對于 GET 和 HEAD 方法,僅當(dāng)服務(wù)器沒有與給定資源匹配的 ETag 時,服務(wù)器才會以200狀態(tài)發(fā)送回請求的資源。 對于其他方法,僅當(dāng)最終現(xiàn)有資源的ETag與列出的任何值都不匹配時,才會處理請求。

          If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"
          復(fù)制代碼
          復(fù)制代碼

          內(nèi)容協(xié)商

          內(nèi)容協(xié)商機制是指客戶端和服務(wù)器端就響應(yīng)的資源內(nèi)容進行交涉,然后提供給客戶端最為適合的資源。內(nèi)容協(xié)商會以響應(yīng)資源的語言、字符集、編碼方式等作為判斷的標準。

          內(nèi)容協(xié)商主要有以下3種類型:

          • 服務(wù)器驅(qū)動協(xié)商(Server-driven Negotiation)

          這種協(xié)商方式是由服務(wù)器端進行內(nèi)容協(xié)商。服務(wù)器端會根據(jù)請求首部字段進行自動處理

          • 客戶端驅(qū)動協(xié)商(Agent-driven Negotiation)

          這種協(xié)商方式是由客戶端來進行內(nèi)容協(xié)商。

          • 透明協(xié)商(Transparent Negotiation)

          是服務(wù)器驅(qū)動和客戶端驅(qū)動的結(jié)合體,是由服務(wù)器端和客戶端各自進行內(nèi)容協(xié)商的一種方法。

          內(nèi)容協(xié)商的分類有很多種,主要的幾種類型是 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language

          Accept

          接受請求 HTTP 標頭會通告客戶端其能夠理解的 MIME 類型

          MIME: MIME (Multipurpose Internet Mail Extensions) 是描述消息內(nèi)容類型的因特網(wǎng)標準。MIME 消息能包含文本、圖像、音頻、視頻以及其他應(yīng)用程序?qū)S玫臄?shù)據(jù)。
          復(fù)制代碼

          文本文件: text/html、text/plain、text/css、application/xhtml+xml、application/xml

          圖片文件: image/jpeg、image/gif、image/png

          視頻文件: video/mpeg、video/quicktime

          應(yīng)用程序二進制文件: application/octet-stream、application/zip

          比如,如果瀏覽器不支持 PNG 圖片的顯示,那 Accept 就不指定image/png,而指定可處理的 image/gif 和 image/jpeg 等圖片類型。

          一般 MIME 類型也會和 q 這個屬性一起使用,q 是什么?q 表示的是權(quán)重,來看一個例子

          Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
          復(fù)制代碼

          若想要給顯示的媒體類型增加優(yōu)先級,則使用 q=來額外表示權(quán)重值,沒有顯示權(quán)重的時候默認值是1.0。

          q

          MIME

          1.0

          text/html

          1.0

          application/xhtml+xml

          0.9

          application/xml

          0.8

          * / *

          Accept-Charset

          accept-charset 屬性規(guī)定服務(wù)器處理表單數(shù)據(jù)所接受的字符集。

          accept-charset 屬性允許您指定一系列字符集,服務(wù)器必須支持這些字符集,從而得以正確解釋表單中的數(shù)據(jù)。

          Accept-Language

          首部字段 Accept-Language 用來告知服務(wù)器用戶代理能夠處理的自然語言集(指中文或英文等),以及自然語言集的相對優(yōu)先級。

          Accept-Language: en-US,en;q=0.5
          復(fù)制代碼

          響應(yīng)標頭

          響應(yīng)標頭是可以在 HTTP 響應(yīng)中使用的 HTTP 標頭。并不是所有出現(xiàn)在響應(yīng)中的標頭都是響應(yīng)標頭。還有一些特殊的我們上面說過,有通用標頭和實體標頭也會出現(xiàn)在響應(yīng)標頭中,比如 Content-Length 就是一個實體標頭,但是,在這種情況下,這些實體請求通常稱為響應(yīng)頭。下面以一個例子為例和你探討一下響應(yīng)頭

          200 OK
          Access-Control-Allow-Origin: *
          Connection: Keep-Alive
          Content-Encoding: gzip
          Content-Type: text/html; charset=utf-8
          Date: Mon, 18 Jul 2016 16:06:00 GMT
          Etag: "c561c68d0ba92bbeb8b0f612a9199f722e3a621a"
          Keep-Alive: timeout=5, max=997
          Last-Modified: Mon, 18 Jul 2016 02:36:04 GMT
          Server: Apache
          Set-Cookie: mykey=myvalue; expires=Mon, 17-Jul-2017 16:06:00 GMT; Max-Age=31449600; Path=/; secure
          Transfer-Encoding: chunked
          Vary: Cookie, Accept-Encoding
          x-frame-options: DENY
          
          復(fù)制代碼

          響應(yīng)狀態(tài)碼

          以 2xx 為開頭的都表示請求成功響應(yīng)。

          狀態(tài)碼

          含義

          200

          成功響應(yīng)

          204

          請求處理成功,但是沒有資源可以返回

          206

          對資源某一部分進行響應(yīng),由Content-Range 指定范圍的實體內(nèi)容。

          以 3xx 為開頭的都表示需要進行附加操作以完成請求

          狀態(tài)碼

          含義

          301

          永久性重定向,該狀態(tài)碼表示請求的資源已經(jīng)重新分配 URI,以后應(yīng)該使用資源現(xiàn)有的 URI

          302

          臨時性重定向。該狀態(tài)碼表示請求的資源已被分配了新的 URI,希望用戶(本次)能使用新的 URI 訪問。

          303

          該狀態(tài)碼表示由于請求對應(yīng)的資源存在著另一個 URI,應(yīng)使用 GET 方法定向獲取請求的資源。

          304

          該狀態(tài)碼表示客戶端發(fā)送附帶條件的請求時,服務(wù)器端允許請求訪問資源,但未滿足條件的情況。

          307

          臨時重定向。該狀態(tài)碼與 302 Found 有著相同的含義。

          以 4xx 的響應(yīng)結(jié)果表明客戶端是發(fā)生錯誤的原因所在。

          狀態(tài)碼

          含義

          400

          該狀態(tài)碼表示請求報文中存在語法錯誤。當(dāng)錯誤發(fā)生時,需修改請求的內(nèi)容后再次發(fā)送請求。

          401

          該狀態(tài)碼表示發(fā)送的請求需要有通過 HTTP 認證(BASIC 認證、DIGEST 認證)的認證信息。

          403

          該狀態(tài)碼表明對請求資源的訪問被服務(wù)器拒絕了。

          404

          該狀態(tài)碼表明服務(wù)器上無法找到請求的資源。

          以 5xx 為開頭的響應(yīng)標頭都表示服務(wù)器本身發(fā)生錯誤

          狀態(tài)碼

          含義

          500

          該狀態(tài)碼表明服務(wù)器端在執(zhí)行請求時發(fā)生了錯誤。

          503

          該狀態(tài)碼表明服務(wù)器暫時處于超負載或正在進行停機維護,現(xiàn)在無法處理請求。

          HTTP協(xié)議優(yōu)缺點

          HTTP 的優(yōu)點

          • 簡單靈活易擴展

          HTTP 的協(xié)議比較簡單,它的主要組成就是 header + body,頭部信息也是簡單的文本格式

          HTTP 協(xié)議又多了靈活 和 易擴展 的優(yōu)點。

          HTTP 協(xié)議里的請求方法、URI、狀態(tài)碼、原因短語、頭字段等每一個核心組成要素都沒有被制定死,允許開發(fā)者任意定制、擴充或解釋,給予了瀏覽器和服務(wù)器最大程度的信任和自由。

          • 應(yīng)用廣泛、環(huán)境成熟

          天然具有跨語言、跨平臺的優(yōu)越性,而且,因為本身的簡單特性很容易實現(xiàn),所以幾乎所有的編程語言都有 HTTP 調(diào)用庫和外圍的開發(fā)測試工具

          • 無狀態(tài)

          既是優(yōu)點又是缺點。因為服務(wù)器沒有記憶能力,所以就不需要額外的資源來記錄狀態(tài)信息,不僅實現(xiàn)上會簡單一些,而且還能減輕服務(wù)器的負擔(dān),能夠把更多的 CPU 和內(nèi)存用來對外提供服務(wù)。

          HTTP 的缺點

          • 無狀態(tài)

          服務(wù)器沒有記憶能力,它就無法支持需要連續(xù)多個步驟的事務(wù)操作。每次都得問一遍身份信息,需要增加了不必要的數(shù)據(jù)傳輸量。由此出現(xiàn)了 Cookie 技術(shù)。

          • 安全性

          明文傳輸,協(xié)議里的報文(準確地說是 header 部分)不使用二進制數(shù)據(jù),而是用簡單可閱讀的文本形式。

          對比 TCP、UDP 這樣的二進制協(xié)議,它的優(yōu)點顯而易見,不需要借助任何外部工具,用瀏覽器、Wireshark 或者 tcpdump 抓包后,直接用肉眼就可以很容易地查看或者修改,為我們的開發(fā)調(diào)試工作帶來極大的便利。

          當(dāng)然缺點也是顯而易見的,就是不安全,可以被監(jiān)聽和被窺探。因為無法判斷通信雙方的身份,不能判斷報文是否被更改過。

          總結(jié)起來即:

          1. 明文,請求報文未加密;
          2. 未hash,即使報文被修改過也不知道;
          3. 未驗證身份,容易導(dǎo)致中間人攻擊;


          作者:captain_p
          鏈接:https://juejin.cn/post/7041744237905346568

          如果本文對你有幫助,麻煩轉(zhuǎn)發(fā)關(guān)注支持一下

          章涵蓋

          • 識別 Web API 的潛在用戶
          • 在 Swagger 和 Swashbuckle 中應(yīng)用 API 文檔最佳實踐
          • 使用可擴展標記語言 (XML) 文檔和虛張聲勢注釋
          • 使用 Swashbuckle 的篩選器管道自定義 swagger.json 文件

          在第1章中,當(dāng)我們嘗試定義應(yīng)用程序編程接口(API)時,我們將其稱為軟件平臺,它公開了不同計算機程序可以通過交換數(shù)據(jù)進行交互的工具和服務(wù)。從這個定義開始,我們可以說 API(包括 Web API)的目的是創(chuàng)建一個公共場所,讓獨立且通常不相關(guān)的系統(tǒng)可以使用普遍接受的標準進行會面、問候和通信。這些“參與者”大多是由其他開發(fā)人員實現(xiàn)的計算機程序,例如網(wǎng)站、移動應(yīng)用程序和微服務(wù)。出于這個原因,無論誰承擔(dān)設(shè)計、創(chuàng)建和發(fā)布 Web API 的任務(wù),都必須承認一種新型用戶的存在和需求:第三方開發(fā)人員,這將我們帶到本章的主題。

          在現(xiàn)代軟件開發(fā)中,記錄接口、中間件、服務(wù)或任何旨在達到目的的手段的產(chǎn)品不再被視為一種選擇:只要我們想增加或加速其采用,它就是一項設(shè)計要求。這也是讓感興趣的第三方能夠充分理解我們工作價值的最快方法。近年來,這方面變得如此重要,以至于它有利于定義一個新的設(shè)計領(lǐng)域:開發(fā)人員體驗(DX),即從開發(fā)人員角度看的用戶體驗。通過考慮DX,我將在本章專門確定API文檔的最佳實踐,并展示我們?nèi)绾谓柚?ASP.NET Core提供的許多工具將它們付諸實踐。

          11.1 網(wǎng)頁應(yīng)用接口潛在受眾

          產(chǎn)品的技術(shù)文檔只有在滿足閱讀者的需求和期望時才有用。出于這個原因,首先要做的是確定我們的 Web API 潛在受眾:期望選擇和/或使用它的利益相關(guān)者。在提到它們時,我通常將它們分為三種主要類型,使用取自建筑俚語的名稱。

          11.1.1 探礦者

          探礦者是充滿激情的開發(fā)人員和 IT 愛好者,他們愿意嘗試我們的 Web API,而除了個人興趣、知識獲取、測試/審查目的等之外,沒有迫切的需求。如果我們希望我們的 Web API 成為我們打算向公眾發(fā)布的通用產(chǎn)品(或其中的一部分),則此組很重要;他們的反饋可能會對開發(fā)人員社區(qū)產(chǎn)生直接影響,可能會引入承包商和建筑商(分別參見第 11.1.2 節(jié)和第 11.1.3 節(jié))。

          11.1.2 承包商

          承包商是 IT 分析師、解決方案架構(gòu)師和后端設(shè)計人員,他們負責(zé)創(chuàng)建產(chǎn)品、解決問題或解決我們的 Web API 可以幫助他們處理的潛在挑戰(zhàn)。雖然通常情況下,他們不會著手實施,但他們通常充當(dāng)決策者,因為他們擁有權(quán)力、處理預(yù)算和/或擁有建議、選擇或規(guī)定使用哪些組件所需的專業(yè)知識(除非他們讓構(gòu)建者選擇它們)。

          11.1.3 構(gòu)建器

          構(gòu)建者是選擇(或被指示)使用我們的 Web API 來解決特定問題的軟件開發(fā)人員。他們代表了我們受眾中技術(shù)性最強的部分,可能很難滿足,因為處理我們的 API 是他們工作任務(wù)的一部分,而且他們完成工作的時間通常有限。構(gòu)建者必須學(xué)會實際使用我們的 Web API;他們是我之前提到的第三方開發(fā)人員。

          在閱讀了這些描述之后,似乎很明顯地認為我們的文檔應(yīng)該關(guān)注構(gòu)建者,他們是我們的 Web API 的最終用戶。這個前提是有效的。我們將在本章中討論的大多數(shù) API 文檔最佳實踐都將考慮這種方法。但我們不應(yīng)該忘記其他兩種受眾類型,因為我們項目的成功也可能取決于他們。

          11.2 API 文檔最佳實踐

          開發(fā)人員是特殊類型的用戶。他們善于分析、精確且要求苛刻,特別是如果我們認為他們通常希望使用我們的 API 來實現(xiàn)主要目標:實現(xiàn)需求、解決問題等。每當(dāng)他們發(fā)現(xiàn)自己由于文檔編寫不佳而無法實現(xiàn)目標時,他們很可能會認為 API 不夠好——盡管聽起來很殘酷,但他們是對的。歸根結(jié)底,API 的好壞取決于它們的文檔,這不可避免地會對采用和可維護性產(chǎn)生巨大影響。

          我們所說的良好文檔是什么意思,我們?nèi)绾螌崿F(xiàn)它?沒有一個答案在所有情況下都有效。但是一些好的做法可以幫助我們找到一種可行的方法來實現(xiàn)我們想要的東西,例如:

          • 采用自動描述工具 - 這樣,如果我們忘記更新文檔以及 Web API 的源代碼,我們的文檔就不會過時或過時
          • 描述端點和輸入?yún)?shù) - 以便我們的受眾不僅承認它們的存在,而且了解它們的用途以及如何使用它們
          • 描述響應(yīng) - 以便我們的聽眾知道調(diào)用每個端點時會發(fā)生什么以及如何處理結(jié)果
          • 添加請求和響應(yīng)示例 - 為我們的受眾節(jié)省大量開發(fā)時間
          • 將端點分組到多個部分 - 更好地區(qū)分用戶的不同作用域、用途和角色
          • 排除保留端點 - 防止用戶知道它們的存在和/或嘗試調(diào)用它們
          • 強調(diào)授權(quán)要求 - 讓我們的受眾區(qū)分可公開訪問的操作和僅限于授權(quán)用戶的操作
          • 自定義文檔上下文 - 例如選擇適當(dāng)?shù)拿Q、圖標和元數(shù)據(jù)以幫助用戶查找所需信息

          以下部分將詳細介紹這些概念,并展示如何在我們的 MyBGList Web API 中實現(xiàn)它們。

          11.2.1 采用自動描述工具

          如果我們想讓第三方開發(fā)人員滿意,我們必須確保我們的 API 文檔始終更新。沒有什么比處理缺少的規(guī)范、不存在的操作或端點、錯誤的參數(shù)等更令人沮喪的了。過時的文檔會讓我們的用戶認為我們的 API 壞了,即使它不是。

          注意記錄不佳(或錯誤)的 Web API 在技術(shù)上已損壞,因為第三方?jīng)]有機會看到它按預(yù)期工作。這就是我們最初聲明API文檔是設(shè)計要求,而不是選項或附加組件的原因。即使對于預(yù)計僅由內(nèi)部開發(fā)人員使用的內(nèi)部 API 也是如此,因為缺乏適當(dāng)?shù)奈臋n最終會影響新員工、潛在合作伙伴、維護任務(wù)、移交流程、外包商等。

          對于 RESTful API 來說,自動化文檔過程的需求尤其強烈,因為 REST 架構(gòu)標準沒有為此目的提供標準化的機制、模式或參考。這是Open API(以前稱為Swagger)成功的主要原因,Open API(以前稱為Swagger)是SmartBear Software于2011年發(fā)布的自動化API文檔的開源規(guī)范,旨在解決這個問題。

          我們從第2章開始就知道Swagger/OpenAPI,因為Visual Studio的 ASP.NET Core Web API模板(我們用來創(chuàng)建MyBGList項目)包括Swashbuckle的服務(wù)和中間件,這是一組用于在Core中實現(xiàn)OpenAPI的服務(wù),中間件和工具 ASP.NET。我們還體驗了它的自動發(fā)現(xiàn)和描述功能,它為我們提供了一個代碼生成的 OpenAPI 3.0 描述文件 (swagger.json) 和一個基于 Web 的交互式 API 客戶端 (SwaggerUI),我們用它來測試我們的端點。因為我們已經(jīng)在使用Swashbuckle,所以我們可以說我們已經(jīng)準備好了。但是,在以下部分中,我們將擴展其功能以滿足我們的需求。

          11.2.2 描述端點和輸入?yún)?shù)

          如果我們查看我們的 SwaggerUI 主儀表板,我們會發(fā)現(xiàn)我們當(dāng)前的“文檔”僅包含端點及其輸入變量的列表,而沒有對每個方法的作用進行單一描述。我們的受眾必須從名稱中推斷端點的使用,以及每個請求標頭和/或輸入?yún)?shù)的用途,這不是展示、評估或推廣我們工作的最佳方式。

          注意如果我們的 API 遵循使用 HTTP 謂詞來標識操作類型的 RESTful 良好實踐,它將提供有關(guān)每個端點使用的其他有用提示 - 至少對具有所需專業(yè)知識的用戶而言。

          相反,我們應(yīng)該采用標準化的方式來為每個端點及其輸入變量創(chuàng)建簡潔而相關(guān)的描述。這種做法不僅可以節(jié)省建筑商的時間,還可以讓探礦者和承包商更好地掌握API的工作方式及其功能。Swashbuckle 提供了兩種向端點和輸入?yún)?shù)添加自定義描述的方法:

          • 使用由 .NET 編譯器從標準三斜杠、XML 格式注釋自動生成的可擴展標記語言 (XML) 文檔文件,我們可以將其添加到 C# 類中。
          • 使用 [SwaggerOperation] 數(shù)據(jù)屬性,該屬性由可選的 Swashbuckle 提供。AspNetCore.Annotations NuGet 包。

          每種技術(shù)都有優(yōu)點和缺點。在下一節(jié)中,我們將了解如何實現(xiàn)這兩種技術(shù)。

          11.2.3 添加 XML 文檔支持

          如果我們已經(jīng)使用 C# 提供的三斜杠語法在源代碼中添加了注釋,則 XML 文檔方法可能很有用、方便且快速實現(xiàn)。我說的是一個簡潔的 C# 功能,它允許開發(fā)人員通過編寫由三斜杠指示的特殊注釋字段來創(chuàng)建代碼級文檔。某些集成開發(fā)環(huán)境 (IDE)(如 Visual Studio)也使用此功能,這些環(huán)境自動生成 XML 元素來描述各種代碼部分,例如<摘要>(用于方法)、<param>(用于輸入?yún)?shù))和<returns>(用于返回值)。

          注意有關(guān) C# XML 文檔注釋的其他信息,請查看 http://mng.bz/qdy6。有關(guān)支持的 XML 標記的完整參考,請參閱 http://mng.bz/7187

          學(xué)習(xí)如何使用此功能的最佳方法是將其付諸實踐。打開 /Controllers/AccountController.cs 文件,找到 Register 操作方法,將光標置于其上方(及其所有屬性),然后在其上方鍵入斜杠 (/) 字符三次。添加第三個斜杠后,Visual Studio 應(yīng)生成以下 XML 注釋樣板:

          /// <summary>
          ///
          /// </summary>
          /// <param name="input"></param>
          /// <returns></returns>
          [HttpPost]
          [ResponseCache(CacheProfileName="NoCache")]
          public async Task<ActionResult> Register(RegisterDTO input)

          請注意,自動生成的 XML 結(jié)構(gòu)標識操作方法的 RegisterDTO 輸入?yún)?shù)的名稱。現(xiàn)在我們有了樣板,讓我們填充它。以下是我們?nèi)绾斡涗泿艨刂破鞯淖越K結(jié)點:

          /// <summary>
          /// Registers a new user.
          /// </summary>
          /// <param name="input">A DTO containing the user data.</param>
          /// <returns>A 201 - Created Status Code in case of success.</returns>

          之后,向下滾動到登錄操作方法,然后執(zhí)行相同的操作。以下是我們可以用來記錄它的合適描述:

          /// <summary>
          /// Performs a user login.
          /// </summary>
          /// <param name="input">A DTO containing the user's credentials.</param>
          /// <returns>The Bearer Token (in JWT format).</returns>

          保存并關(guān)閉帳戶控制器。接下來,告訴編譯器使用我們添加的 XML 注釋以及代碼中存在的任何其他此類注釋來生成 XML 文檔文件。

          生成 XML 文檔文件

          要啟用此功能,我們需要更新 MyBGList 項目的配置文件。在“解決方案資源管理器”窗口中右鍵單擊項目的根節(jié)點,然后從上下文菜單中選擇“編輯項目文件”選項以打開 MyBGList.csproj 文件。接下來,在文件底部的 <ItemGroup> 塊下方添加以下代碼,我們在第 10 章中添加了包含 protobuf 文件:

          // ... existing code
           
          <ItemGroup>
            <Protobuf Include="gRPC/grpc.proto" />
          </ItemGroup>
           
          <PropertyGroup>
            <GenerateDocumentationFile>true</GenerateDocumentationFile>
            <NoWarn>$(NoWarn);1591</NoWarn>
          </PropertyGroup>
           
          // ... existing code

          現(xiàn)在,每當(dāng)我們構(gòu)建項目時,編譯器都會生成 XML 文檔文件。

          克服 CS1591 警告

          我們在前面的代碼中使用的 <NoWarn> 元素將禁止顯示 CS1591 警告,GenerateDocumentationFile 開關(guān)將為任何公共類型和成員引發(fā)該警告,而無需三斜杠注釋。我們選擇在我們的示例項目中全局關(guān)閉它們,因為我們不需要該建議,但是如果我們想確保我們注釋/記錄所有內(nèi)容,那么保持它可能會很有用。

          有關(guān)生成文檔文件開關(guān)的詳細信息,請參閱 http://mng.bz/mJdn。

          我們需要做的最后一件事是配置 Swashbuckle 以獲取 XML 文檔文件的內(nèi)容。

          配置虛張聲勢扣

          要讀取我們項目的XML文檔文件,Swashbuckle需要知道它的完整路徑和文件名,與我們項目的名稱(帶有.xml擴展名)相對應(yīng)。我們可以使用 Reflection(一種 C# 技術(shù),允許我們在運行時檢索類型的元數(shù)據(jù)),而不是手動編寫它,而是手動編寫它。這種編程方法通常更可取,因為它比使用文本字符串更能確保代碼可維護性,因此我們將選擇它。打開 Program.cs 文件,找到 AddSwaggerGen() 方法,并在其配置塊中添加以下代碼(粗體新行):

          using System.Reflection;                                           ?
           
          // ... existing code
           
          builder.Services.AddSwaggerGen(options=>
          {
              var xmlFilename=                                    ?
                  $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
              options.IncludeXmlComments(System.IO.Path.Combine(             ?
                  AppContext.BaseDirectory, xmlFilename));
           
              // ... existing code

          ? 必需的命名空間

          ? 構(gòu)建 XML 文檔文件名

          ? 組裝 XML 文檔文件完整路徑

          請注意,在此代碼中,我們使用反射生成與項目名稱匹配的 XML 文件名,然后使用它來構(gòu)造 XML 文件的完整路徑。接下來,我們將測試我們所做的工作,看看它是否有效。

          測試 XML 文檔

          在調(diào)試模式下運行項目,并查看 SwaggerUI 主儀表板,我們應(yīng)該在其中看到我們在三斜杠注釋中使用的相同描述性字符串(圖 11.1)。


          圖 11.1 Swashbuckle 獲取并在 SwaggerUI 中使用的 XML 文檔

          請注意,摘要緊跟在終結(jié)點定義之后。說明顯示在端點的可展開面板中。

          評估 XML 文檔的優(yōu)缺點

          能夠自動將所有代碼級注釋轉(zhuǎn)換為 API 文檔,使我們能夠用一塊石頭殺死兩只鳥。如果我們習(xí)慣于編寫注釋來描述我們的類和方法(開發(fā)人員的良好做法),我們可以重用大量工作。此外,這種方法對內(nèi)部開發(fā)人員特別有用,因為他們可以直接從源代碼中讀取我們的API文檔,甚至不必查看swagger.json文件和/或SwaggerUI。

          但這種顯著的好處很容易成為不利的一面。例如,如果我們想將內(nèi)部源代碼文檔(針對內(nèi)部開發(fā)人員)與公共 API 文檔(針對第三方開發(fā)人員/最終用戶)分開,我們可能會發(fā)現(xiàn)這種方法是有限的,更不用說涉及非自愿數(shù)據(jù)泄露的潛在風(fēng)險。代碼級注釋通常被大多數(shù)開發(fā)人員視為機密,他們可能會使用它們來跟蹤內(nèi)部注釋、警告、已知問題/錯誤、漏洞和其他不應(yīng)向公眾發(fā)布的嚴格保留的數(shù)據(jù)。為了克服這樣的問題,我們可以考慮使用 [SwaggerOperation] 數(shù)據(jù)屬性替代,它可以更好地分離內(nèi)部注釋和 API 文檔之間的關(guān)注點,以及我們可能想要使用的一些簡潔的附加功能。

          11.2.4 使用虛張聲勢的注釋

          除了 XML 文檔之外,Swashbuckle 還提供了另一種基于屬性的功能,用于向我們的 Web API 端點添加自定義描述。此功能由一個名為 Swashbuckle.AspNetCore.Annotations 的可選模塊處理,該模塊隨專用 NuGet 包一起提供。在以下部分中,我們將學(xué)習(xí)如何安裝和使用它。

          安裝 NuGet 包

          與往常一樣,若要安裝 Swashbuckle 注釋的 NuGet 包,我們可以使用 Visual Studio 的 NuGet 圖形用戶界面 (GUI)、包管理器控制臺窗口或 .NET 命令行界面 (CLI)。下面是在 .NET CLI 中安裝它們的命令:

          dotnet add package Swashbuckle.AspNetCore.Annotations --version 6.4.0

          安裝后,我們將能夠使用一些數(shù)據(jù)注釋屬性來增強我們的 API 文檔。我們將從 [SwaggerOperation] 開始,它允許我們?yōu)榭刂破鞯牟僮鞣椒ㄒ约白钚?API 方法設(shè)置自定義摘要、描述和/或標簽。

          使用 [SwaggerOperation] 屬性

          由于我們的帳戶控制器的操作方法已經(jīng)通過XML記錄,因此這次我們將使用BoardGamesController。打開 /Controllers/ BoardGamesController.cs 文件,并將屬性添加到四個現(xiàn)有的操作方法中,如清單 11.1 所示(新行以粗體顯示)。

          清單 11.1 /控制器/棋盤游戲控制器.cs 文件: 添加注釋

          using Swashbuckle.AspNetCore.Annotations;                                ?
           
          // ... existing code
           
          [HttpGet(Name="GetBoardGames")]
          [ResponseCache(CacheProfileName="Any-60")]
          [SwaggerOperation(                                                       ?
              Summary="Get a list of board games.",                              ?
              Description="Retrieves a list of board games " +                   ?
              "with custom paging, sorting, and filtering rules.")]
          public async Task<RestDTO<BoardGame[]>> Get(
           
          // ... existing code
           
          [HttpGet("{id}")]
          [ResponseCache(CacheProfileName="Any-60")]
          [SwaggerOperation(                                                       ?
              Summary="Get a single board game.",                                ?
              Description="Retrieves a single board game with the given Id.")]   ?
          public async Task<RestDTO<BoardGame?>> Get(int id)
           
          // ... existing code
           
          [Authorize(Roles=RoleNames.Moderator)]
          [HttpPost(Name="UpdateBoardGame")]
          [ResponseCache(CacheProfileName="NoCache")]
          [SwaggerOperation(                                                       ?
              Summary="Updates a board game.",                                   ?
              Description="Updates the board game's data.")]                     ?
          public async Task<RestDTO<BoardGame?>> Post(BoardGameDTO model)
           
          // ... existing code
           
          [Authorize(Roles=RoleNames.Administrator)]
          [HttpDelete(Name="DeleteBoardGame")]
          [ResponseCache(CacheProfileName="NoCache")]
          [SwaggerOperation(                                                       ?
              Summary="Deletes a board game.",                                   ?
              Description="Deletes a board game from the database.")]            ?
          public async Task<RestDTO<BoardGame?>> Delete(int id)
           
          // ... existing code

          ? 必需的命名空間

          ? [招搖操作]屬性

          ? 添加端點摘要

          ? 添加端點描述

          現(xiàn)在我們知道如何使用 Swashbuckle 注釋來描述我們的操作,讓我們對它們的輸入?yún)?shù)做同樣的事情。

          使用 [SwaggerParameter] 屬性

          要設(shè)置輸入?yún)?shù)的描述,我們可以使用 [SwaggerParameter] 屬性,該屬性對應(yīng)于 XML 文檔的 <param> 標記的 Swashbuckle 注釋。但是,盡管 XML 標記必須在方法級別定義,然后通過 name 屬性綁定到其相應(yīng)的參數(shù),但 [SwaggerParameter] 注釋必須在它要描述的參數(shù)之上定義。

          要了解它是如何工作的,讓我們實現(xiàn)它。在保持 BoardGamesController.cs 文件打開的同時,找到 Get() 方法,并通過以下方式將 [SwaggerParameter] 添加到現(xiàn)有輸入?yún)?shù)(新行以粗體顯示):

          public async Task<RestDTO<BoardGame[]>> Get(
              [FromQuery]
              [SwaggerParameter("A DTO object that can be used " +     ?
                  "to customize the data-retrieval parameters.")]
              RequestDTO<BoardGameDTO> input)

          ? 添加 [招搖參數(shù)]

          現(xiàn)在,描述性屬性已經(jīng)設(shè)置好,我們需要通過更新我們的 Swagger 配置來全局啟用虛張聲勢注釋功能。

          啟用批注

          若要啟用虛張聲勢注釋,請打開 Program.cs 文件,并將以下配置設(shè)置添加到現(xiàn)有的 AddSwaggerGen() 方法中:

          builder.Services.AddSwaggerGen(options=>
          {
              options.EnableAnnotations();     ?
           
              // ... existing code

          ? 啟用虛張聲勢注釋功能

          添加最低 API 支持

          [SwaggerOperation] 屬性以及整個 Swashbuckle 注釋功能甚至可以使用最小 API 方法。讓我們將其中一些方法添加到循環(huán)中。保持 Program.cs 文件打開狀態(tài),向下滾動到我們在第 9 章中實現(xiàn)的三個最小 API 方法,以測試 ASP.NET Core 授權(quán)功能。然后將 [SwaggerOperation] 屬性添加到它們,如以下列表所示(粗體新行)。

          清單 11.2 程序.cs文件:向最小 API 方法添加注釋

          using Swashbuckle.AspNetCore.Annotations;                           ?
           
          // ... existing code
           
          app.MapGet("/auth/test/1",
          [Authorize]
          [EnableCors("AnyOrigin")]
          [SwaggerOperation(                                                  ?
              Summary="Auth test #1 (authenticated users).",                ?
              Description="Returns 200 - OK if called by " +                ?
              "an authenticated user regardless of its role(s).")]
          [ResponseCache(NoStore=true)] ()=>
           
          // ... existing code
           
          app.MapGet("/auth/test/2",
          [Authorize(Roles=RoleNames.Moderator)]
          [EnableCors("AnyOrigin")]
          [SwaggerOperation(                                                  ?
              Summary="Auth test #2 (Moderator role).",                     ?
              Description="Returns 200 - OK status code if called by " +    ?
              "an authenticated user assigned to the Moderator role.")]
          [ResponseCache(NoStore=true)] ()=>
           
          // ... existing code
           
          app.MapGet("/auth/test/3",
          [Authorize(Roles=RoleNames.Administrator)]
          [EnableCors("AnyOrigin")]
          [SwaggerOperation(                                                  ?
              Summary="Auth test #3 (Administrator role).",                 ?
              Description="Returns 200 - OK if called by " +                ?
              "an authenticated user assigned to the Administrator role.")]
          [ResponseCache(NoStore=true)] ()=>
           
          // ... existing code

          ? 必需的命名空間

          ? [招搖操作]屬性

          ? 添加端點摘要

          ? 添加端點描述

          現(xiàn)在,我們已準備好測試我們所做的工作。

          測試注釋

          要測試我們的新注釋,請在調(diào)試模式下運行我們的項目,并查看 SwaggerUI 主儀表板,我們應(yīng)該能夠在其中看到它們(圖 11.2)。

          圖 11.2 通過 [SwaggerOperation] 屬性添加的 OpenAPI 注釋

          如我們所見,總體結(jié)果與使用 XML 文檔方法獲得的結(jié)果非常相似。但是,我們可以記錄每種技術(shù)的內(nèi)容之間存在一些顯著差異。例如,XML文檔允許我們描述示例(使用<example>元素),Swashbuckle注釋目前不支持這些示例。同時,Swashbuckle 注釋功能可以使用自定義模式過濾器進行擴展,以支持 Swagger/OpenAPI 規(guī)范中提到的幾乎任何文檔選項。在以下各節(jié)中,我們將以互補的方式使用這兩種方法,以充分利用它們。

          11.2.5 描述響應(yīng)

          用于終結(jié)點和輸入?yún)?shù)的相同描述性方法也應(yīng)應(yīng)用于我們的 Web API 響應(yīng)。此方法不僅適用于返回的 JavaScript 對象表示法 (JSON) 數(shù)據(jù),還適用于 HTTP 狀態(tài)代碼(應(yīng)始終根據(jù)其含義使用)和相關(guān)響應(yīng)標頭(如果有)。

          同樣,為了描述我們的響應(yīng),我們可以使用<響應(yīng)> XML 文檔標簽或?qū)S玫?[SwaggerResponse] Swashbuckle 注釋屬性。在以下部分中,我們將采用這兩種方法。

          使用 XML 文檔

          正如我們之前對 <param> 標簽所做的那樣,它可以多次用于描述每個輸入?yún)?shù),我們可以為該方法返回的任何 HTTP 狀態(tài)代碼創(chuàng)建一個 <response> 標簽。每個 XML <響應(yīng)>標記都需要一個代碼屬性(以確定它所描述的響應(yīng)的 HTTP 狀態(tài)代碼)和一個包含實際說明的基于文本的值。若要對其進行測試,請再次打開 /Controllers/AccountController.cs 文件,并將以下 <response> 標記追加到 Register 方法的現(xiàn)有 XML 文檔注釋塊(粗體新行):

          /// <summary>
          /// Registers a new user.
          /// </summary>
          /// <param name="input">A DTO containing the user data.</param>
          /// <returns>A 201 - Created Status Code in case of success.</returns>
          /// <response code="201">User has been registered</response>    ?
          /// <response code="400">Invalid data</response>                ?
          /// <response code="500">An error occurred</response>           ?

          ? HTTP 狀態(tài)代碼 201 說明

          ? HTTP 狀態(tài)代碼 400 說明

          ? HTTP 狀態(tài)代碼 500 說明

          接下來,向下滾動到 Login 方法,并在其中附加以下 <response> 標記:

          /// <summary>
          /// Performs a user login.
          /// </summary>
          /// <param name="input">A DTO containing the user's credentials.</param>
          /// <returns>The Bearer Token (in JWT format).</returns>
          /// <response code="200">User has been logged in</response>       ?
          /// <response code="400">Login failed (bad request)</response>    ?
          /// <response code="401">Login failed (unauthorized)</response>   ?

          ? HTTP 狀態(tài)代碼 200 說明

          ? HTTP 狀態(tài)代碼 400 說明

          ? HTTP 狀態(tài)代碼 401 說明

          若要測試我們所做的工作,請在調(diào)試模式下啟動項目,訪問 SwaggerUI 主儀表板,然后展開“帳戶/注冊”和“帳戶/登錄”終結(jié)點。如果我們正確執(zhí)行了所有操作,我們應(yīng)該看到我們的響應(yīng)描述,如圖 11.3 所示。


          圖 11.3 /帳戶/登錄端點的響應(yīng)說明

          現(xiàn)在我們知道了如何使用 XML 文檔注釋來獲取此結(jié)果,讓我們看看如何使用 [SwaggerResponse] 數(shù)據(jù)注釋屬性實現(xiàn)相同的操作。

          使用虛張聲勢扣批注

          [SwaggerResponse] 屬性與其對應(yīng)的 <response> XML 標記對應(yīng)項一樣,可以多次添加到同一方法,以描述受影響的方法可能發(fā)送回客戶端的所有結(jié)果、HTTP 狀態(tài)代碼和響應(yīng)類型。此外,它還需要兩個主要參數(shù):

          • 要描述的響應(yīng)的 HTTP 狀態(tài)代碼
          • 我們要顯示的描述

          學(xué)習(xí)如何使用它的最好方法是看到它的實際效果。打開 Program.cs 文件,向下滾動到 /auth/test/1 最小 API 端點,然后添加新的 [SwaggerResponse] 屬性以按以下方式描述其唯一響應(yīng):

          app.MapGet("/auth/test/1",
              [Authorize]
              [EnableCors("AnyOrigin")]
              [SwaggerOperation(
                  Summary="Auth test #1 (authenticated users).",
                  Description="Returns 200 - OK if called by " +
                  "an authenticated user regardless of its role(s).")]
              [SwaggerResponse(StatusCodes.Status200OK,
                  "Authorized")]                                  ?
              [SwaggerResponse(StatusCodes.Status401Unauthorized,
                  "Not authorized")]                              ?

          ? HTTP 狀態(tài)代碼 201 說明

          ? HTTP 狀態(tài)代碼 401 說明

          請注意,我們使用了 Microsoft.AspNetCore 提供的 StatusCodes 枚舉。Http 命名空間,它允許我們使用強類型方法指定 HTTP 狀態(tài)代碼。

          注意使用基于屬性的方法的一個優(yōu)點是,它為我們提供了 C# 和 ASP.NET Core 功能提供的所有好處,包括但不限于強類型成員。例如,我們可以通過使用 Core 的內(nèi)置本地化支持(由于篇幅原因,本書中沒有介紹 ASP.NET 為不同的語言和/或文化指定不同的描述。

          若要測試該屬性,請在調(diào)試模式下啟動項目,訪問 SwaggerUI 儀表板,并在 /auth/test/1 終結(jié)點的 SwaggerUI 面板的“響應(yīng)”部分中檢查是否存在上述說明(圖 11.4)。


          圖 11.4 /auth/test/1 端點的響應(yīng)說明

          不錯。但是,我們的大多數(shù)端點不僅發(fā)出 HTTP 狀態(tài)代碼;如果請求成功,它們還會返回具有明確定義的預(yù)定結(jié)構(gòu)的 JSON 對象。向我們的 API 用戶描述這些返回類型,讓他們知道會發(fā)生什么,這不是很好嗎?為了實現(xiàn)這樣的目標,我們需要在這些描述中添加一些樣本。在下一節(jié)中,我們將看到如何操作。

          11.2.6 添加請求和響應(yīng)示例

          理想情況下,每個 API 操作都應(yīng)包含一個請求和響應(yīng)示例,以便用戶了解每個操作的預(yù)期工作方式。正如我們已經(jīng)知道的,我們心愛的 SwaggerUI 負責(zé)請求部分的任務(wù);每當(dāng)我們使用它時,它都會顯示一個示例值選項卡,其中包含 JSON 格式的示例輸入數(shù)據(jù)傳輸對象 (DTO),如圖 11.5 所示。


          圖 11.5 /帳戶/注冊終結(jié)點的響應(yīng)示例

          “示例值”選項卡的右側(cè)是一個簡潔的“架構(gòu)”選項卡,其中顯示了對象的架構(gòu)和許多有用的信息,例如每個字段的最大大小、可為空性和基礎(chǔ)類型。遺憾的是,此自動功能并不總是適用于 JSON 響應(yīng)類型,需要一些手動干預(yù)。

          注意有時,SwaggerUI 會設(shè)法自動檢測(并顯示示例)響應(yīng)類型。例如,如果我們展開 GET /BoardGames 端點的 SwaggerUI 面板,則 RestDTO<BoardGame> 對象將正確顯示在響應(yīng)部分中。遺憾的是,當(dāng)該方法具有多種返回類型時,此方便的功能通常無法自動檢測其中的大多數(shù)返回類型。下一節(jié)中介紹的方法將處理這些方案。

          讓我們看看如何告訴 SwaggerUI 隨時顯示響應(yīng)示例。[ProducesResponseType] 屬性附帶 Microsoft.AspNetCore.Mvc 命名空間,不是 Swashbuckle 的一部分。但是,由于我們將組件配置為考慮注釋,因此 SwaggerUI 將使用它來確定每個方法的響應(yīng)類型并采取相應(yīng)的行動。

          與 [ProducesResponseType] 屬性一起使用的主要參數(shù)是響應(yīng)類型和方法返回的狀態(tài)代碼。同樣,由于終結(jié)點可以返回不同的響應(yīng)類型和狀態(tài)代碼,因此可以多次將其添加到每個方法中。我們已經(jīng)知道 SwaggerUI 無法自動檢測 /Account/Register 和 /Account/Login 端點的返回類型,這使得它們成為此屬性的完美候選者。

          打開 /控制器/帳戶控制器.cs 文件,并找到注冊操作方法。然后在現(xiàn)有屬性下方,在方法聲明之前添加以下屬性(粗體新行):

          [HttpPost]
          [ResponseCache(CacheProfileName="NoCache")]
          [ProducesResponseType(typeof(string), 201)]                      ?
          [ProducesResponseType(typeof(BadRequestObjectResult), 400)]      ?
          [ProducesResponseType(typeof(ProblemDetails), 500)]              ?

          ? HTTP 狀態(tài)代碼 201 說明

          ? HTTP 狀態(tài)代碼 400 說明

          ? HTTP 狀態(tài)代碼 500 說明

          使用以下屬性對 Login 操作方法執(zhí)行相同的操作:

          [HttpPost]
          [ResponseCache(CacheProfileName="NoCache")]
          [ProducesResponseType(typeof(string), 200)]                   ?
          [ProducesResponseType(typeof(BadRequestObjectResult), 400)]   ?
          [ProducesResponseType(typeof(ProblemDetails), 401)]           ?

          ? HTTP 狀態(tài)代碼 200 說明

          ? HTTP 狀態(tài)代碼 400 說明

          ? HTTP 狀態(tài)代碼 401 說明

          若要測試我們所做的工作,請在調(diào)試模式下啟動項目,并查看 SwaggerUI 中“/帳戶/注冊”和“/帳戶/登錄終結(jié)點”面板的“響應(yīng)”部分,以確保它們看起來像圖 11.6 中的那些。


          圖 11.6 /帳戶/注冊返回類型的 JSON 示例

          圖 11.6 中描述的屏幕截圖已被裁剪,因為在 HTTP 狀態(tài)代碼 400 的情況下返回的 BadRequestObjectResult 的 JSON 表示形式很長。但是這個數(shù)字應(yīng)該讓我們了解我們做了什么。現(xiàn)在我們已經(jīng)知道如何強制 SwaggerUI 提供響應(yīng)類型的示例,我們已準備好掌握另一種良好做法:終結(jié)點分組。

          11.2.7 將端點分組為多個部分

          如果 Web API 具有大量終結(jié)點,則將它們分組到與其角色/用途對應(yīng)的部分中可能很有用。在我們的方案中,明智的做法是將身份驗證終結(jié)點、在棋盤游戲?qū)嶓w上運行的終結(jié)點等分組。我們可以說我們已經(jīng)這樣做了,因為我們?yōu)槊總€組都使用了一個控制器,遵循 ASP.NET Core默認行為。正如我們從第 1 章開始就知道的那樣,ASP.NET Core 控制器允許我們對一組具有共同主題、含義或記錄類型的操作方法進行分組。我們在 MyBGList 方案中采用了此約定,對與棋盤游戲相關(guān)的終結(jié)點使用 BoardGamesController,對基于域的終結(jié)點使用 DomainsController 等。

          這種方法由我們當(dāng)前的 Open API 實現(xiàn)自動實施。如果我們查看 SwaggerUI 儀表板,我們會看到由與同一控制器相關(guān)的操作方法處理的 API 端點被分組,如圖 11.7 所示。


          圖 11.7 控制器處理的終結(jié)點的 SwaggerUI 組名稱

          我們可以猜到,這些組是由Swashbuckle自動生成的。此技巧是通過 tags 屬性將控制器的名稱添加到 swagger.json 文件中來執(zhí)行的,該屬性旨在處理分組任務(wù)。

          提示有關(guān) Swagger 的標簽屬性的其他信息,請查看 http://mng.bz/5178

          要進行檢查,請單擊 SwaggerUI 主標題下方的超鏈接打開 swagger.json 文件,或?qū)Ш降?https://localhost:40443/swagger/v1/swagger.json。/Account/Register 終結(jié)點的 tags 屬性位于文件開頭附近:

          {
            "openapi": "3.0.1",
            "info": {
              "title": "MyBGList",
              "version": "1.0"
            },
            "paths": {
              "/Account/Register": {   ?
                "post": {
                  "tags": [            ?
                    "Account"
                  ],

          ? /帳戶/注冊端點說明

          ? “帳戶”標簽取自控制者的名稱

          遺憾的是,此自動行為不適用于最小 API 方法,因為它們不屬于控制器。Swashbuckle唯一能做的就是將它們?nèi)苛谐鲈谝粋€通用組中,并帶有應(yīng)用程序的名稱(在我們的場景中是MyBGList),如圖11.8所示。


          圖 11.8 最小 API 處理的終結(jié)點的通用組

          這種回退行為的結(jié)果還不錯。但是,由于我們當(dāng)前的最小 API 端點處理不同的相關(guān)任務(wù)集,因此我們可能希望找到一種更好的方法來對它們進行分組。

          如果我們想改進 Swashbuckle 的默認標記行為,我們可以使用 [SwaggerOperation] 屬性提供的 Tags 屬性來覆蓋它。讓我們測試一下。假設(shè)我們要將三個端點分組,從 /auth/ 段開始,在一個名為“Auth”的新 SwaggerUI 部分中。打開程序.cs文件;找到這些方法;并對其現(xiàn)有的 [SwaggerOperation] 屬性進行以下更改,從 /auth/test/1 端點開始(新行以粗體顯示):

          app.MapGet("/auth/test/1",
              [Authorize]
              [EnableCors("AnyOrigin")]
              [SwaggerOperation(
                  Tags=new[] { "Auth" },    ?
                  Summary="Auth test #1 (authenticated users).",
                  Description="Returns 200 - OK if called by " +
                  "an authenticated user regardless of its role(s).")]

          ? 添加標簽屬性

          對 /auth/test/2 和 /auth/test/3 方法執(zhí)行相同的操作,然后在調(diào)試模式下運行項目以查看新的身份驗證組(圖 11.9)。


          圖 11.9 與授權(quán)相關(guān)的終結(jié)點的新身份驗證組

          我們可以使用相同的技術(shù)來覆蓋屬于控制器的操作方法的 Swashbuckle 默認行為。每當(dāng) Tags 參數(shù)與自定義值一起存在時,Swashbuckle 將始終使用它來填充 swagger.json 文件,而不是回退到控制器或操作方法的名稱。

          注意如果我們想自定義端點的組名稱而不是使用控制器名稱,則此覆蓋功能會很方便。但是,請務(wù)必記住,這種級別的自定義違反了 ASP.NET Core 強制執(zhí)行的最重要的開發(fā)最佳實踐之一:配置設(shè)計范例約定,該范例旨在限制開發(fā)人員需要做出的決策數(shù)量以及源代碼量,而不會失去靈活性。出于這個原因,我強烈建議遵守控制器的 ASP.NET 核心分組和標記約定,將 Tags 屬性自定義做法保留為最小 API 方法和有限數(shù)量的異常。

          11.2.8 排除保留端點

          ApiExplorer 服務(wù) Swashbuckle 用于在我們項目的源代碼中自動查找所有控制器的操作方法和最小 API 方法,并在 swagger.json 文件中描述它們,在大多數(shù)情況下是一個很棒的功能。但我們可能想要隱藏一些我們不想向觀眾展示的方法(或整個控制器)。

          在我們當(dāng)前的情況下,這種情況可能適用于 SeedController,其中包含幾個旨在由管理員調(diào)用和知道的方法。從 swagger.json 文件中排除這些操作可能是明智的,這也將把它們從 SwaggerUI 中刪除。

          為了實現(xiàn)這個結(jié)果,我們可以使用 [ApiExplorerSettings] 屬性,其中包含一個有用的 IgnoreApi 屬性。此屬性可應(yīng)用于任何控制器、操作方法或最小 API 方法。讓我們用它來從 swagger.json 文件中排除我們的 SeedController。打開 /Controllers/SeedController.cs 文件,并按以下方式將該屬性應(yīng)用于類聲明:

          [Authorize(Roles=RoleNames.Administrator)]
          [ApiExplorerSettings(IgnoreApi=true)]       ?
          [Route("[controller]/[action]")]
          [ApiController]
          public class SeedController : ControllerBase

          ? 從 swagger.json 文件中排除控制器

          要測試我們所做的工作,請在調(diào)試模式下運行項目;導(dǎo)航到 招搖UI主儀表板;并確認我們之前訪問該頁面時存在的整個種子部分不再可見。

          警告請務(wù)必了解,IgnoreApi=true 設(shè)置只會阻止控制器及其操作方法包含在 swagger.json 文件中;它不會阻止用戶調(diào)用(并可能執(zhí)行)它。這就是為什么我們還通過使用第 9 章中的 [Authorize] 屬性將其限制為管理員。

          到目前為止,我們已經(jīng)學(xué)習(xí)了如何使用 XML 文檔或數(shù)據(jù)注釋屬性處理各個方法來配置 swagger.json 文件的內(nèi)容和生成的 SwaggerUI 布局。在下一節(jié)中,我們將了解如何基于Swashbuckle過濾器的使用,使用更加結(jié)構(gòu)化和集中的方法執(zhí)行這些類型的更改。

          11.3 基于過濾器的招搖自定義

          正如我們從第6章中知道的,Swashbuckle公開了一個方便的過濾器管道,它與swagger.json文件生成過程掛鉤,允許我們創(chuàng)建和添加自己的過濾器,以根據(jù)需要自定義文件的內(nèi)容。要實現(xiàn)過濾器,我們需要做的就是擴展 Swashbuckle 提供的內(nèi)置接口之一,每個接口都提供了一個方便的 Apply 方法來自定義自動生成的文件。以下是Swashbuckle提供的過濾器接口的完整列表:

          • IDocumentFilter - 自定義整個 swagger.json 文件
          • IOperationFilter - 自定義操作/端點
          • IParameterFilter - 自定義操作的查詢字符串輸入?yún)?shù)
          • IRequestBodyFilter - 自定義操作的請求正文輸入?yún)?shù)
          • ISchemaFilter - 自定義輸入?yún)?shù)的默認方案

          我們在第6章中使用了這個功能,當(dāng)時我們添加了SortColumnFilter和SortOrderFilter(擴展IParameterFilter接口),為SwaggerUI提供了一些基于正則表達式的模式來驗證一些輸入?yún)?shù)。Swashbuckle 使用這些過濾器,我們在 /Swagger/ 文件夾中實現(xiàn)了這些過濾器,然后將其添加到 Program.cs 文件中的 Swashbuckle 過濾器管道中,有選擇地將模式 JSON 鍵添加到使用 [SortColumnValidator] 和 [SortOrderValidator] 自定義屬性修飾的所有參數(shù)。我們所做的是一個簡單而完美的過濾器管道如何工作的例子。

          在本節(jié)中,我們將學(xué)習(xí)如何使用 Swashbuckle 提供的其他過濾器接口來進一步配置自動生成的 swagger.json 文件,從而相應(yīng)地更新 SwaggerUI。與往常一樣,我們假設(shè)我們被要求實現(xiàn)一些可信的新功能請求。

          11.3.1 強調(diào)授權(quán)要求

          在第 9 章中,當(dāng)我們學(xué)習(xí)如何使用 [Authorize] 屬性時,我們在現(xiàn)有的 Swagger 配置設(shè)置中添加了安全定義和安全要求。我們這樣做是為了使“授權(quán)”按鈕顯示在 SwaggerUI 中,現(xiàn)在允許我們設(shè)置持有者令牌并測試受授權(quán)限制的終結(jié)點。但是這個添加有一個我們當(dāng)時故意忽略的次要效果:它還在我們的所有端點旁邊添加了一個奇怪的掛鎖圖標,如圖 11.10 所示。


          圖11.10 招搖UI中的掛鎖圖標

          單擊這些圖標時,將顯示“授權(quán)”彈出窗口,就像我們在第 9 章中多次使用的頁面右上角附近的“授權(quán)”按鈕一樣。但是,無論終結(jié)點的授權(quán)要求如何,掛鎖圖標始終顯示為打開狀態(tài),這不是我們預(yù)期的行為。理想情況下,我們希望掛鎖圖標僅出現(xiàn)在需要某種授權(quán)的端點旁邊。下一節(jié)將介紹如何實現(xiàn)此結(jié)果。

          在深入研究源代碼之前,讓我們看看掛鎖圖標功能在后臺是如何工作的。如果端點具有某種安全要求(換句話說,如果需要某種級別的授權(quán)),則 SwaggerUI 會自動呈現(xiàn)這些圖標。此信息取自 swagger.json 文件,該文件為這些端點分配安全屬性:

          "security": [{
                  "Bearer": [ ]
              }]

          在第 9 章中,當(dāng)我們配置 Swagger 以支持基于令牌的授權(quán)機制時,我們使用項目程序.cs文件中的專用配置選項向 swagger.json 文件生成器服務(wù)添加了全局安全要求:

          // ...existing code
           
          options.AddSecurityRequirement(new OpenApiSecurityRequirement
          {
              {
                  new OpenApiSecurityScheme
                  {
                      Name="Bearer",
                      In=ParameterLocation.Header,
                      Reference=new OpenApiReference
                      {
                          Type=ReferenceType.SecurityScheme,
                          Id="Bearer"
                      }
                  },
                  new string[]{}
              }
          });
           
          // ...existing code

          由于這一全局安全要求,我們在所有端點上都設(shè)置了安全屬性,因為它們被認為受到基于令牌的授權(quán)方案的保護,即使它們不是。為了修補此行為,我們需要將該全局要求替換為特定規(guī)則,該規(guī)則將僅針對受此類方案限制的方法觸發(fā)。

          最有效的方法是使用 IOperationFilter 接口創(chuàng)建自定義篩選器,該接口可以擴展 swagger.json 生成器服務(wù),以便為受影響的操作提供其他信息(或修改現(xiàn)有/默認信息)。在我們的方案中,我們需要一個篩選器,該篩選器可以設(shè)置當(dāng)前分配給所有操作的相同安全要求,但僅適用于應(yīng)用了 [Authorize] 屬性的操作。若要實現(xiàn)此要求,請在 /Swagger/ 根級文件夾中創(chuàng)建一個新的 AuthRequirementsFilter.cs 類文件,并使用以下列表中的源代碼填充其內(nèi)容。

          清單 11.3 /Swagger/AuthRequirementsFilter.cs 文件

          using Microsoft.AspNetCore.Authorization;
          using Microsoft.OpenApi.Models;
          using Swashbuckle.AspNetCore.SwaggerGen;
           
          namespace MyBGList.Swagger
          {
              internal class AuthRequirementFilter : IOperationFilter
              {
                  public void Apply(
                      OpenApiOperation operation,
                      OperationFilterContext context)
                  {
                      if (!context.ApiDescription                                 ?
                          .ActionDescriptor
                          .EndpointMetadata
                          .OfType<AuthorizeAttribute>()
                          .Any())
                          return;                                                 ?
           
                      operation.Security=new List<OpenApiSecurityRequirement>   ?
                      {
                          new OpenApiSecurityRequirement
                          {
                              {
                                  new OpenApiSecurityScheme
                                  {
                                      Name="Bearer",
                                      In=ParameterLocation.Header,
                                      Reference=new OpenApiReference
                                      {
                                          Type=ReferenceType.SecurityScheme,
                                          Id="Bearer"
                                      }
                                  },
                                  new string[]{}
                              }
                          }
                      };
                  }
              }
          }

          ? 檢查 [授權(quán)] 屬性

          ? 如果不存在,則不執(zhí)行任何操作

          ? 如果存在,則確保操作安全

          如我們所見,我們的新操作過濾器在內(nèi)部執(zhí)行當(dāng)前在 Program.cs 文件中完成的相同任務(wù)。唯一的區(qū)別是它跳過了沒有 [Authorize] 屬性的操作,因為我們不希望它們在 swagger.json 文件(或掛鎖圖標)中記錄任何安全要求。

          現(xiàn)在我們有了 AuthRequirementsFilter,我們需要更新 Swagger 生成器配置選項以使用它,而不是我們當(dāng)前擁有的全局縮放要求。打開程序.cs文件;向下滾動到 AddSwaggerGen 方法;并將現(xiàn)有的 AddSecurityRequirement 語句替換為新的 AddOperationFilter 語句,如下面的代碼清單所示。(前面的代碼行被注釋掉;新的代碼行以粗體顯示。

          清單 11.4 程序.cs文件: AddSwaggerGen 配置更新

          using MyBGList.Swagger;                                           ?
           
          // ... existing code...
           
          //options.AddSecurityRequirement(new OpenApiSecurityRequirement   ?
          //{
          //    {
          //        new OpenApiSecurityScheme
          //        {
          //            Name="Bearer",
          //            In=ParameterLocation.Header,
          //            Reference=new OpenApiReference
          //            {
          //                Type=ReferenceType.SecurityScheme,
          //                Id="Bearer"
          //            }
          //        },
          //        new string[]{}
          //    }
          //});
          options.OperationFilter<AuthRequirementFilter>();                 ?
           
          // ... existing code...

          ? 必需的命名空間

          ? 要刪除的先前代碼

          ? 要添加的新代碼

          提示在本章的 GitHub 存儲庫中,我注釋掉了以前的代碼行,而不是刪除它們。

          為了測試我們所做的工作,我們可以在調(diào)試模式下啟動項目,并再次查看以前具有掛鎖圖標的相同端點(圖 11.11)。正如我們所看到的,掛鎖圖標對于可公開訪問的端點已經(jīng)消失,但對于那些需要某種授權(quán)的端點來說,掛鎖圖標仍然存在。我們的自定義IOperationFilter允許我們做我們想做的事情。


          圖11.11 SwaggerUI中掛鎖圖標的新行為

          11.3.2 更改應(yīng)用程序標題

          假設(shè)我們要在 SwaggerUI 中更改應(yīng)用程序的標題,該標題當(dāng)前設(shè)置為 MyBGList,根據(jù) Swashbuckle 的默認行為,與 ASP.NET Core 項目同名。如果我們查看 swagger.json 文件,我們可以看到托管該值的 JSON 屬性稱為 title,它是在文檔級別設(shè)置的父信息屬性的一部分:

          {
            "openapi": "3.0.1",
            "info": {
              "title": "MyBGList Web API",
              "version": "1.0"
            },

          這意味著,如果我們想覆蓋它,我們需要創(chuàng)建一個自定義過濾器,允許我們自定義 swagger.json 文件的文檔級參數(shù)。實現(xiàn)目標的最有效方法是創(chuàng)建自定義 DocumentFilter(通過擴展 IDocumentFilter 接口)并將其添加到篩選器管道中。在 /Swagger/ 根級文件夾中創(chuàng)建一個新的 CustomDocumentFilter.cs 文件,并使用以下清單的內(nèi)容填充該文件。

          清單 11.5 /Swagger/CustomDocumentFilter.cs 文件

          using Microsoft.AspNetCore.Authorization;
          using Microsoft.OpenApi.Models;
          using Swashbuckle.AspNetCore.SwaggerGen;
           
          namespace MyBGList.Swagger
          {
              internal class CustomDocumentFilter : IDocumentFilter
              {
                  public void Apply(
                      OpenApiDocument swaggerDoc,
                      DocumentFilterContext context)
                  {
                      swaggerDoc.Info.Title="MyBGList Web API";   ?
                  }
              }
          }

          ? 設(shè)置自定義標題

          然后,通過按以下方式更新 Program.cs 文件,將文件掛接到 Swashbuckle 的過濾器管道(新行以粗體顯示):

          options.OperationFilter<AuthRequirementFilter>();   ?
          options.DocumentFilter<CustomDocumentFilter>();     ?

          ? 現(xiàn)有過濾器

          ? 新過濾器

          要測試我們所做的工作,請在調(diào)試模式下啟動項目,并查看 SwaggerUI 儀表板的新標題(圖 11.12)。

          圖 11.12 SwaggerUI 標題隨自定義文檔篩選器更改

          不錯。讓我們看看我們可以用IRequestBodyFilter接口做什么。

          11.3.3 為密碼添加警告文本

          假設(shè)我們希望在用戶需要向我們的 Web API 發(fā)送密碼時為其設(shè)置自定義警告文本。通過查看我們當(dāng)前的端點,我們可以很容易地確定,目前,這樣的警告只會影響賬戶控制器的注冊和登錄方法。通過考慮這一事實,我們可以使用 XML 文檔注釋(第 11.2.3 節(jié))或 [SwaggerOperation] 屬性(第 11.2.4 節(jié))將此消息插入到操作的“摘要”或“說明”屬性中,正如我們之前所學(xué)習(xí)的那樣。或者,我們可以通過使用 <param> XML 標記或 [SwaggerParameter] 屬性在參數(shù)級別工作。

          這兩種方法都有一個不平凡的缺點。如果我們將來添加接受密碼的端點,我們還必須在那里重復(fù) XML 標記或數(shù)據(jù)注釋屬性,這意味著復(fù)制大量代碼 — 除非我們忘記這樣做,因為這樣的方法很容易出錯。

          為了克服這些問題,最好找到一種方法來集中這種行為,方法是創(chuàng)建一個新的過濾器并將其添加到Swashbuckle的管道中。我們需要確定在可用過濾器接口中擴展哪個過濾器接口。理想情況下,IRequestBodyFilter 接口將是一個不錯的選擇,考慮到我們希望定位名稱等于“密碼”的特定參數(shù),該參數(shù)目前(并且可能總是)隨 POST 請求一起出現(xiàn)。讓我們繼續(xù)這種方法。在 /Swagger/ 根文件夾中創(chuàng)建一個新的 PasswordRequestFilter.cs 文件,并使用以下清單中的代碼填充該文件。

          清單 11.6 /Swagger/PasswordRequestFilter.cs 文件

          using Microsoft.OpenApi.Models;
          using Swashbuckle.AspNetCore.SwaggerGen;
           
          namespace MyBGList.Swagger
          {
              internal class PasswordRequestFilter : IRequestBodyFilter
              {
                  public void Apply(
                      OpenApiRequestBody requestBody,
                      RequestBodyFilterContext context)
                  {
                      var fieldName="password";                         ?
           
                      if (context.BodyParameterDescription.Name
                          .Equals(fieldName,
                              StringComparison.OrdinalIgnoreCase)         ?
                          || context.BodyParameterDescription.Type
                          .GetProperties().Any(p=> p.Name
                              .Equals(fieldName,
                                  StringComparison.OrdinalIgnoreCase)))   ?
                      {
                          requestBody.Description=            "IMPORTANT: be sure to always use a strong password " +
                              "and store it in a secure location!";
                      }
                  }
              }
          }

          ? 輸入?yún)?shù)名稱

          ? 名稱檢查(基元類型)

          ? 屬性檢查(復(fù)雜型)

          通過查看此代碼,我們檢查輸入?yún)?shù)名稱是否等于“password”(對于基元類型)或包含具有該名稱的屬性(對于復(fù)雜類型,例如 DTO)。現(xiàn)在我們有了過濾器,我們需要通過以下方式在 Swashbuckle 的過濾器管道中注冊它,在我們之前添加的 AuthRequirementsFilter 和 CustomDocumentFilter 下面:

          options.OperationFilter<AuthRequirementFilter>();     ?
          options.DocumentFilter<CustomDocumentFilter>();       ?
          options.RequestBodyFilter<PasswordRequestFilter>();   ?

          ? 現(xiàn)有過濾器

          ? 新過濾器

          與往常一樣,我們可以通過在調(diào)試模式下執(zhí)行項目并檢查 SwaggerUI 中的預(yù)期更改來測試我們所做的工作(圖 11.13)。


          圖 11.13 密碼請求篩選器添加的新描述

          這些變化似乎奏效了。由于這種方法,我們的密碼警告消息將涵蓋我們的兩個現(xiàn)有終結(jié)點以及在其請求正文中接受密碼參數(shù)的任何未來終結(jié)點。

          注意如果我們想將覆蓋范圍擴展到查詢字符串參數(shù),我們需要添加另一個擴展 IParameterFilter 接口并執(zhí)行相同工作的篩選器,然后使用 ParameterFilter 幫助程序方法將其注冊到 Program.cs 文件中。

          現(xiàn)在要完成我們的過濾器概述,剩下要做的就是 ISchemaFilter 接口。

          11.3.4 添加自定義鍵/值對

          讓我們再看一下我們在第 6 章中實現(xiàn)的 SortColumnFilter 和 SortOrderFilter 類。擴展 IParameterFilter 接口是個好主意,因為我們只需要處理來自查詢字符串的一些特定輸入?yún)?shù)。換句話說,我們希望將模式鍵添加到 swagger.json 文件中這些參數(shù)的 JSON 模式中,從用于標識它們的相同數(shù)據(jù)注釋屬性 [SortColumnAttribute] 或 [SortOrderAttribute] 中獲取值。

          假設(shè)我們要擴展該方法以實現(xiàn)一個新的過濾器,該過濾器能夠?qū)⑷我?JSON 鍵(和值)添加到任何屬性,無論是請求參數(shù)、響應(yīng)參數(shù)還是其他任何內(nèi)容。在本節(jié)中,我們將通過實現(xiàn)以下內(nèi)容來實現(xiàn)這一目標:

          • 自定義數(shù)據(jù)注釋屬性,這將允許我們?yōu)槿魏螌傩栽O(shè)置一個或多個自定義 JSON 鍵和值對
          • 一個自定義 SchemaFilter,它擴展了 ISchemaFilter 接口,將這些鍵和值對添加到應(yīng)用了這些數(shù)據(jù)注釋屬性的所有參數(shù)、響應(yīng)和屬性中

          ISchemaFilter 接口是處理此任務(wù)的完美選擇,因為它專門設(shè)計用于對 Swashbuckle 的 SwaggerGen 服務(wù)生成的 JSON 模式進行后修改,用于控制器操作和最小 API 方法公開的每個輸入和輸出參數(shù)以及復(fù)雜類型。現(xiàn)在我們已經(jīng)選擇了我們的路線,讓我們把它付諸實踐。

          實現(xiàn)自定義鍵值屬性

          在 Visual Studio 的“解決方案資源管理器”面板中,右鍵單擊 MyBGList 項目根目錄中的 /Attributes/ 文件夾,并添加新的 CustomKeyValueAttribute.cs 類文件,其中包含兩個字符串屬性:Key 和 Value。下面的清單提供了新類的源代碼。

          11.7 自定義鍵值屬性

          namespace MyBGList.Attributes
          {
              [AttributeUsage(
                  AttributeTargets.Property | AttributeTargets.Parameter,
                  AllowMultiple=true)]
              public class CustomKeyValueAttribute : Attribute
              {
                  public CustomKeyValueAttribute(string? key, string? value)
                  {
                      Key=key;
                      Value=value;
                  }
           
                  public string? Key { get; set; }
           
                  public string? Value { get; set; }
              }
          }

          請注意,我們已經(jīng)使用 [AttributeUsage] 屬性裝飾了我們的新類,它允許我們指定屬性的使用。我們這樣做有兩個重要原因:

          • 若要允許將屬性應(yīng)用于屬性和參數(shù),請使用 AttributeTargets 枚舉。
          • 允許多次應(yīng)用該屬性,因為 AllowMultiple 屬性設(shè)置為 true。此設(shè)置是必需的,因為我們希望有機會將多個 [SwaggerSchema] 屬性(從而設(shè)置多個自定義鍵/值對)應(yīng)用于單個屬性或參數(shù)。

          現(xiàn)在我們有了屬性,我們準備實現(xiàn)將處理它的篩選器。

          實現(xiàn)自定義鍵值篩選器

          在 /Swagger/ 文件夾中添加新的 CustomKeyValueFilter.cs 類文件。新類必須實現(xiàn) ISchemaFilter 接口及其 Apply 方法,我們將在其中處理 [CustomKeyValue] 屬性查找和 JSON 鍵/值對插入過程。以下清單顯示了如何操作。

          清單 11.8 自定義鍵值過濾器

          using Microsoft.OpenApi.Any;
          using Microsoft.OpenApi.Models;
          using Swashbuckle.AspNetCore.SwaggerGen;
           
          namespace MyBGList.Attributes
          {
              public class CustomKeyValueFilter : ISchemaFilter
              {
                  public void Apply(
                      OpenApiSchema schema,
                      SchemaFilterContext context)
                  {
                      var caProvider=context.MemberInfo
                          ?? context.ParameterInfo
                          as IcustomAttributeProvider;            ?
           
                      var attributes=caProvider?
                          .GetCustomAttributes(true)
                          .OfType<CustomKeyValueAttribute>();     ?
           
                      if (attributes !=null)                     ?
                      {
                          foreach (var attribute in attributes)
                          {
                              schema.Extensions.Add(
                                  attribute.Key,
                                  new OpenApiString(attribute.Value)
                                  );
                          }
                      }
                  }
              }
          }

          ? 確定我們是在處理屬性還是參數(shù)

          ? 檢查參數(shù)是否具有屬性

          ? 如果存在一個或多個屬性,則相應(yīng)地采取行動

          此代碼應(yīng)易于理解。我們將使用語言集成查詢 (LINQ) 檢查 ISchemaFilter 接口提供的上下文,以確定我們的屬性或參數(shù)是否應(yīng)用了一個或多個 [CustomKeyValue] 屬性并采取相應(yīng)的操作。我們現(xiàn)在需要做的就是將新的過濾器添加到 Swashbuckle 的過濾器管道中。與往常一樣,我們可以通過以下方式更新程序.cs文件:

          options.OperationFilter<AuthRequirementFilter>();    ?
          options.DocumentFilter<CustomDocumentFilter>();      ?
          options.RequestBodyFilter<PasswordRequestFilter>();  ?
          options.SchemaFilter<CustomKeyValueFilter>();        ?

          ? 現(xiàn)有過濾器

          ? 新過濾器

          現(xiàn)在我們的兩個類已經(jīng)準備就緒,并且過濾器已經(jīng)注冊,我們可以通過將 [CustomKeyValue] 屬性應(yīng)用于現(xiàn)有 DTO 之一的屬性來測試 [CustomKeyValue] 屬性。讓我們選擇帳戶控制器的登錄操作方法使用的登錄DTO。打開 /DTO/LoginDTO.cs 文件,并按以下方式將其中幾個屬性應(yīng)用于現(xiàn)有 UserName 屬性:

          [Required]
          [MaxLength(255)]
          [CustomKeyValue("x-test-1", "value 1")]   ?
          [CustomKeyValue("x-test-2", "value 2")]   ?
          public string? UserName { get; set; }

          ? 第一個自定義鍵值屬性

          接下來,在調(diào)試模式下運行項目,訪問 SwaggerUI 儀表板,然后單擊主標題下方的 swagger.json 文件鏈接(圖 11.14),在新選項卡中打開它。


          圖 11.14 swagger.json 文件 URL

          使用瀏覽器的搜索功能在 swagger.json 文件中查找“x-test-”字符串。如果我們正確執(zhí)行了所有操作,我們應(yīng)該在 LoginDTO 的用戶名屬性的 JSON 架構(gòu)中看到此字符串的兩個條目,如以下列表所示。

          清單 11.9 swagger.json 文件 (登錄DTO 模式)

                "LoginDTO": {
                  "required": [
                    "password",
                    "userName"
                  ],
                  "type": "object",
                  "properties": {
                    "userName": {
                      "maxLength": 255,
                      "minLength": 1,
                      "type": "string",
                      "x-test-1": "value 1",   ?
                      "x-test-2": "value 2"    ?
                    },
                    "password": {
                      "minLength": 1,
                      "type": "string"
                    }
                  }

          ? 自定義鍵/值對

          目前為止,一切都好。讓我們執(zhí)行另一個測試,以確保相同的邏輯適用于基元類型的標準 GET 參數(shù)。打開 /Controllers/BoardGamesController.cs 文件,向下滾動到 Get 操作方法,接受 int 類型的單個 id 參數(shù),然后按以下方式向該參數(shù)添加 [CustomKeyValue] 屬性:

          [HttpGet("{id}")]
          [ResponseCache(CacheProfileName="Any-60")]
          [SwaggerOperation(
              Summary="Get a single board game.",
              Description="Retrieves a single board game with the given Id.")]
          public async Task<RestDTO<BoardGame?>> Get(
              [CustomKeyValue("x-test-3", "value 3")]    ?
              int id
              )

          ? 添加新的 [自定義鍵值] 屬性

          接下來,在調(diào)試模式下運行項目,像我們之前一樣訪問 swagger.json 文件內(nèi)容,并再次檢查其中是否存在“x-test-”字符串。這一次,我們應(yīng)該找到三個條目,最后一個是我們添加的條目(請參閱下面的列表)。

          清單 11.10 swagger.json 文件 (/BoardGames/{id} 端點模式)

              "/BoardGames/{id}": {
                "get": {
                  "tags": [
                    "BoardGames"
                  ],
                  "summary": "Get a single board game.",
                  "description": "Retrieves a single board game with the given Id.",
                  "parameters": [
                    {
                      "name": "id",
                      "in": "path",
                      "required": true,
                      "schema": {
                        "type": "integer",
                        "format": "int32",
                        "x-test-3": "value 3"    ?
                      }
                    }
                  ],

          ? 自定義鍵/值對

          我們的自定義鍵/值功能似乎運行良好。最后一個任務(wù)結(jié)束了我們了解 Swashbuckle 的過濾器管道和 API 文檔概述的旅程。現(xiàn)在唯一要做的就是學(xué)習(xí)將我們的 Web API 項目部署到生產(chǎn)環(huán)境中,這是第 12 章的主題。

          11.4 練習(xí)

          是時候用我們的產(chǎn)品所有者給出的一系列新的假設(shè)任務(wù)分配來挑戰(zhàn)自己了。與往常一樣,處理這些任務(wù)將極大地幫助我們記住和記住本章中涵蓋的概念和學(xué)到的技術(shù)。

          注意練習(xí)的解決方案可在 GitHub 的 /Chapter_11/Exercises/ 文件夾中找到。若要測試它們,請將 MyBGList 項目中的相關(guān)文件替換為該文件夾中的文件,然后運行應(yīng)用。

          11.4.1 使用 XML 文檔

          使用 XML 文檔方法,按以下方式描述 GET /Domains 終結(jié)點:

          • 摘要 - 獲取域列表
          • 描述 - 檢索具有自定義分頁、排序和過濾規(guī)則的域列表
          • 參數(shù) - 可用于自定義某些檢索參數(shù)的 DTO 對象
          • 返回 - 包含域列表的 RestDTO 對象

          提示可以使用 <備注> XML 元素添加說明。

          11.4.2 使用虛張聲勢的注釋

          使用 Swashbuckle 注釋方法,按以下方式描述 GET /Mechanics 終結(jié)點:

          • 摘要 - 獲取機制列表
          • 描述 - 檢索具有自定義分頁、排序和過濾規(guī)則的機制列表
          • 參數(shù) - 可用于自定義某些檢索參數(shù)的 DTO 對象
          • 返回 - 包含機制列表的 RestDTO 對象

          11.4.3 排除某些端點

          使用 [ApiExplorerSettings] 屬性從 swagger.json 文件中隱藏以下端點:

          • 帖子/域
          • 刪除/域

          然后,確保這些終結(jié)點也從 SwaggerUI 儀表板中排除。

          11.4.4 添加自定義過濾器

          擴展 IRequestBodyFilter 接口以實現(xiàn)新的 UsernameRequestFilter,該篩選器將向名稱等于“用戶名”的任何輸入?yún)?shù)添加以下說明。然后在 Swashbuckle 的過濾器管道中注冊新過濾器,并通過檢查 POST 帳戶/登錄和 POST 帳戶/注冊端點使用的 sername 參數(shù)在 SwaggerUI 儀表板中對其進行測試。

          警告請務(wù)必記住您的用戶名,因為您需要它來執(zhí)行登錄!

          11.4.5 添加自定義鍵/值對

          使用 [自定義鍵值] 屬性將以下鍵/值對添加到 DELETE 機制端點的現(xiàn)有 id 參數(shù):

          • 鍵:x-測試-4,值:值 4
          • 鍵:x-測試-5,值:值 5

          然后檢查 swagger.json 文件中終結(jié)點的 JSON 架構(gòu)中是否存在新屬性。

          總結(jié)

          • 編寫良好的文檔可以大大增加或加速 Web API 的采用。
            • 因此,請務(wù)必確定 API 文檔最佳實踐并學(xué)習(xí)使用 ASP.NET Core 內(nèi)置和第三方工具遵循這些最佳實踐。
          • 確定我們 Web API 的潛在受眾(期望選擇和/或使用它的利益相關(guān)者)可以幫助我們編寫引人注目的文檔。理想情況下,我們需要滿足
            • 渴望嘗試我們所做的事情的早期采用者(探礦者)。
            • 旨在評估我們工作的 IT 解決方案架構(gòu)師(承包商)。
            • 將被要求實現(xiàn)我們 Web API 端點的軟件開發(fā)人員(構(gòu)建器)。
          • 在不忘記其他兩個受眾群體(探礦者和承包商)的情況下關(guān)注建筑商的需求幾乎總是要走的路。
          • 開發(fā)人員善于分析、精確且要求苛刻。為了滿足他們的期望,請務(wù)必遵守 IT 行業(yè)廣泛采用的一些眾所周知的文檔最佳實踐,包括
            • 采用自動描述工具。
            • 描述端點、輸入?yún)?shù)和響應(yīng)。
            • 提供請求和響應(yīng)示例。
            • 將終結(jié)點分組為多個部分。
            • 強調(diào)授權(quán)要求。
            • 自定義文檔上下文。
          • Swagger/OpenAPI 框架提供了一種標準化的方法來記錄和描述 API,使用每個人都能理解的通用語言。
            • 借助Swashbuckle,我們可以使用Swagger為我們的Web API創(chuàng)建文檔:一組服務(wù),中間件和工具,允許我們在遵循之前確定的最佳實踐的同時 ASP.NET Core中實現(xiàn)OpenAPI規(guī)范。
          • Swashbuckle 公開了一組方便的數(shù)據(jù)屬性,以及一個強大的過濾器管道,可用于對自動生成的 swagger.json 文件進行后修改,從而自定義 API 文檔以滿足我們的需求。
            • Swashbuckle 的功能使我們能夠改進操作、輸入?yún)?shù)和輸出參數(shù)的描述,以及向現(xiàn)有 JSON 模式添加自定義鍵/值對。


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

          本章將涵蓋以下主題:

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

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

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

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

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

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

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

          提供基本的 HTML

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

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

          在這里,我們簡單地返回一個 HttpResponse 結(jié)構(gòu),該結(jié)構(gòu)具有 HTML 內(nè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ù)。 這是因為這是登陸頁面。 如果我們要定義服務(wù)而不是路由,我們將無法在沒有前綴的情況下定義服務(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)用程序的一部分,我們可以運行它并在瀏覽器中調(diào)用主 URL,給出以下輸出:


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

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

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

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

          從文件中讀取基本 HTML

          如果我們有一個 HTML 文件,我們只需將該 HTML 文件準備為一個字符串并將該字符串插入到 HttpResponse 的正文中即可呈現(xiàn)它。 是的,就是這么簡單。 為了實現(xiàn)這一目標,我們將構(gòu)建一個內(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
          }

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

          現(xiàn)在我們有了加載功能,我們需要一個 HTML 目錄。 這可以與稱為 templates 的 src 目錄一起定義。 在 templates 目錄中,我們可以添加一個名為 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 標簽具有與我們之前呈現(xiàn)的內(nèi)容相同的內(nèi)容 - 即 <h1>To Do Items</h1>。 然后,我們有一個 head 標簽,它定義了一系列元標簽。 我們可以看到我們定義了視口。 這告訴瀏覽器如何處理頁面內(nèi)容的尺寸和縮放。 縮放很重要,因為我們的應(yīng)用程序可以通過一系列不同的設(shè)備和屏幕尺寸來訪問。 通過這個視口,我們可以將頁面的寬度設(shè)置為與設(shè)備屏幕相同的寬度。 然后,我們可以將訪問的頁面的初始比例設(shè)置為1.0。 轉(zhuǎn)到 httpEquiv 標簽,我們將其設(shè)置為 X-UA-Compatible,這意味著我們支持舊版瀏覽器。 最終標簽只是搜索引擎可以使用的頁面的描述。 我們的標題標簽確保待辦事項應(yīng)用程序顯示在瀏覽器標簽上。 這樣,我們的正文中就有了標準的標題標題。

          提供從文件加載的基本 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īng)用程序,我們將得到以下輸出:


          圖 5.2 – 加載 HTML 頁面的視圖

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

          將 JavaScript 添加到 HTML 文件

          如果前端用戶無法對我們的待辦事項狀態(tài)執(zhí)行任何操作,那么這對前端用戶來說就沒有用。 在修改之前,我們需要通過查看下圖來了解 HTML 文件的布局:


          圖 5.3 – HTML 文件的一般布局

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

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

          現(xiàn)在我們知道了將 JavaScript 插入到 HTML 文件中的位置,我們可以測試我們的方向了。 在本節(jié)的其余部分中,我們將在 HTML 正文中創(chuàng)建一個按鈕,將其融合到 JavaScript 函數(shù),然后讓瀏覽器在按下該按鈕時打印出帶有輸入消息的警報。 這對我們的后端應(yīng)用程序沒有任何作用,但它將證明我們對 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>

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


          圖 5.4 – 連接到 JavaScript 中的警報時單擊按鈕的效果


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

          必須警告的是,我們實際上是使用此 JavaScript 手動動態(tài)重寫 HTML。 人們可以將其描述為“hacky”解決方案。 然而,在探索 React 之前,重要的是要先掌握我們的方法,才能真正體會到不同方法的好處。 在繼續(xù)下一部分之前,我們必須在 src/views/to_do/create.rs 文件中重構(gòu)我們的創(chuàng)建視圖。 這是一個很好的機會來回顧我們在前幾章中開發(fā)的內(nèi)容。 您必須本質(zhì)上轉(zhuǎn)換創(chuà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àn)在我們可以進入下一部分,我們將讓前端調(diào)用后端。

          將 JavaScript 注入 HTML

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

          創(chuàng)建刪除項目 API 端點。

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

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

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

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

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

          讓我們詳細看看這一點。

          添加刪除端點

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

          如果您遇到困難,我們可以通過將以下第三方依賴項導(dǎo)入到views/to_do/delete.rs 文件中來實現(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 模塊來構(gòu)建我們的待辦事項。 通過我們的 json_serialization 模塊,我們可以看到我們正在接受 ToDoItem 并返回 ToDoItems。 然后,我們使用 process_input 函數(shù)執(zhí)行項目的刪除。 我們也不希望任何可以訪問我們頁面的人刪除我們的項目。 因此,我們需要 JwToken 結(jié)構(gòu)。 最后,我們使用 read_file 函數(shù)讀取項目的狀態(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,以便用戶必須有權(quán)訪問它。 此時,我們只有 JwToken 附加一條消息; 我們將在第 7 章“管理用戶會話”中管理 JwToken 的身份驗證邏輯。

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

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

          然后,我們可以檢查具有該標題的項目是否處于該狀態(tài)。 如果不是,那么我們返回一個未找到的 HTTP 響應(yīng)。 如果是,我們就會傳遞狀態(tài),因為我們需要標題和狀態(tài)來構(gòu)建項目。 我們可以使用以下代碼來實現(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)在我們有了待辦事項的狀態(tài)和標題,我們可以構(gòu)建我們的項目并使用刪除命令將其傳遞到 process_input 函數(shù)。 這將從 JSON 文件中刪除我們的項目:

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

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

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

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

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

          添加 JavaScript 加載功能

          現(xiàn)在我們的所有端點都已準備就緒,我們必須重新訪問我們的主應(yīng)用程序視圖。 在上一節(jié)中,我們確定 <script> 部分中的 JavaScript 可以正常工作,即使它只是一個大字符串的一部分。 為了使我們能夠?qū)?JavaScript 放入單獨的文件中,我們的視圖會將 HTML 文件作為字符串加載,該字符串在 HTML 文件的 <script> 部分中具有 {{JAVASCRIPT}} 標記。 然后,我們將 JavaScript 文件作為字符串加載,并將 {{JAVASCRIPT}} 標記替換為 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 標簽

          從上一步中的 items 函數(shù)中,我們可以看到我們需要在根目錄中構(gòu)建一個名為 JavaScript 的新目錄。 我們還必須在其中創(chuàng)建一個名為 main.js 的文件。 通過對應(yīng)用程序視圖的更改,我們還必須通過添加以下代碼來更改 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>

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

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

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

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

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

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

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

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

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

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

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

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

          在循環(huán)內(nèi),我們必須使用以下代碼構(gòu)建單個待辦事項 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)的項目中提取項目的標題。 然后,我們?yōu)閷⒂糜诮壎ǖ绞录陕犉鞯捻椖慷x ID。 請注意,我們將所有空格替換為 -。 現(xiàn)在我們已經(jīng)定義了標題和 ID,我們將一個帶有標題的 div 添加到占位符 HTML 字符串中。 我們還添加一個帶有 placeholderId 的按鈕,然后用一個 div 來完成它。 我們可以看到,我們對 HTML 字符串的添加是以 ; 結(jié)束的。 然后,我們將 placeholderId 和 title 添加到 itemsMeta 數(shù)組中以供稍后使用。

          接下來,我們循環(huán) itemsMeta,使用以下代碼創(chuà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)在,如果單擊我們在待辦事項旁邊創(chuàng)建的按鈕,則 processFunction 將觸發(fā)。 我們的函數(shù)現(xiàn)在呈現(xiàn)這些項目,但我們需要使用 API 調(diào)用函數(shù)從后端獲取它們。 我們現(xiàn)在來看看這個。

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

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

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

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

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

          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)單擊待處理項目旁邊的按鈕時,我們將觸發(fā)編輯函數(shù),將該項目轉(zhuǎn)換為已完成項目。 考慮到這一點,如果單擊屬于已完成項目的按鈕,則會觸發(fā) deleteItem 函數(shù)。 現(xiàn)在,我們將繼續(xù)構(gòu)建 apiCall 函數(shù)。

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

          現(xiàn)在我們已經(jīng)定義了事件監(jiān)聽器,我們必須使用方法和 URL 準備 API 調(diào)用對象,定義標頭,然后返回請求對象以便我們在需要時發(fā)送:

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

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

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

          請注意,標頭只是對后端中硬編碼的接受令牌進行硬編碼。 我們將在第 7 章“管理用戶會話”中介紹如何正確定義 auth 標頭。 現(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)聽器所屬的 HTML 部分可以通過 this 訪問。 我們知道,如果我們刪除編輯詞,并用空格切換 - ,它會將待辦事項的 ID 轉(zhuǎn)換為待辦事項的標題。 然后,我們利用 apiCall 函數(shù)來定義我們的端點和方法。 請注意,替換函數(shù)中的“edit”字符串中有一個空格。 我們有這個空格是因為我們還必須刪除編輯字符串后面的空格。 如果我們不刪除該空格,它將被發(fā)送到后端,從而導(dǎo)致錯誤,因為我們的應(yīng)用程序后端在 JSON 文件中項目標題旁邊沒有空格。 定義端點和 API 調(diào)用方法后,我們將標題傳遞到狀態(tài)為已完成的字典中。 這是因為我們知道我們正在將待處理的項目切換為完成。 完成此操作后,我們將使用 JSON 正文發(fā)送 API 調(diào)用。

          現(xiàn)在,我們可以對 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”字符串中有一個空格。 至此,我們的渲染過程就完成了。 我們定義了編輯和刪除函數(shù)以及渲染函數(shù)。 現(xiàn)在,我們必須在頁面首次加載時加載項目,而無需單擊任何按鈕。 這可以通過簡單的 API 調(diào)用來完成:

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

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

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

          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 的詳細信息。 我們將 input 設(shè)置為 null,以便用戶可以輸入要創(chuàng)建的另一個項目,而不必刪除剛剛創(chuàng)建的舊項目標題。 點擊應(yīng)用程序的主視圖會得到以下輸出:


          圖 5.5 – 帶有渲染的待辦事項的主頁


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

          按已清洗項目旁邊的刪除按鈕。

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

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

          單擊早餐吃拉面項目的編輯。

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


          圖 5.6 – 完成上述步驟后的主頁

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

          將 CSS 注入 HTML

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

          將 CSS 標簽添加到我們的 HTML 文件中。

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

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

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

          讓我們仔細看看這個過程。

          將 CSS 標簽添加到 HTML

          首先,讓我們對 templates/main.html 文件進行一些更改:

           <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>

          在這里,我們可以看到有兩個 CSS 標簽。 {{BASE_CSS}}標簽用于基礎(chǔ)CSS,它在多個不同視圖中將保持一致,例如背景顏色和列比例,具體取決于屏幕尺寸。 {{BASE_CSS}} 標簽用于管理此視圖的 CSS 類。 恕我直言,css/base.css 和 css/main.css 文件是為我們的視圖而制作的。 另外,請注意,我們已將所有項目放入一個名為 mainContainer 的類的 div 中。 這將使我們能夠?qū)⑺许椖吭谄聊簧暇又小?我們還添加了更多的類,以便 CSS 可以引用它們,并將創(chuàng)建項目的按鈕從按鈕 HTML 標記更改為 div HTML 標記。 完成此操作后,javascript/main.js 文件中的 renderItems 函數(shù)將對項目循環(huá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});
              }
              . . .
          }

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

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

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

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

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

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

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


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


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

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

          在這里,我們可以看到頁面邊緣和每個元素周圍的填充只有一個像素。 我們還有一個網(wǎng)格顯示。 這是我們可以定義列和行的地方。 然而,我們并沒有充分利用它。 我們只有一欄。 這意味著我們的待辦事項將占據(jù)大部分屏幕,如圖 5.7 中的手機描述所示。 盡管我們在這種情況下沒有使用網(wǎng)格,但我保留了它,以便您可以看到它與大屏幕的其他配置之間的關(guān)系。 如果我們的屏幕變大一點,我們可以將頁面分成三個不同的垂直列; 但中間柱的寬度與兩側(cè)柱的寬度之比為5:1。 這是因為我們的屏幕仍然不是很大,并且我們希望我們的項目仍然占據(jù)大部分屏幕。 我們可以通過添加另一個具有不同參數(shù)的媒體查詢來對此進行調(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;
              }
          }

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

          如果我們的屏幕變大,那么我們希望進一步調(diào)整比率,因為我們不希望項目寬度失控。 為了實現(xiàn)這一點,我們必須為中間列與兩側(cè)列定義 3:1 的比例,然后當(dāng)屏幕寬度高于 1001px 時定義 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。

          為主頁創(chuàng)建 CSS

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

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

          我們可以看到這個類的邊距為 0.3。 我們使用 rem 是因為我們希望邊距相對于根元素的字體大小進行縮放。 如果我們的光標懸停在項目上,我們還希望項目稍微改變顏色:

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

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

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

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

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

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

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

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

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

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

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

          從 Rust 提供 CSS 和 JavaScript

          我們在views/app/items.rs 文件中提供CSS。 我們通過閱讀 HTML、JavaScript、基本 CSS 和主 CSS 文件來完成此操作。 然后,我們用其他文件中的數(shù)據(jù)替換 HTML 數(shù)據(jù)中的標簽:

          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)我們啟動服務(wù)器時,我們將擁有一個完全運行的應(yīng)用程序,具有直觀的前端,如下圖所示:


          圖 5.8 – CSS 之后的主頁


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

          繼承組件

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

          為此,我們可以創(chuàng)建一個 add_component 函數(shù),該函數(shù)獲取組件的名稱,根據(jù)組件名稱創(chuàng)建標簽,并根據(jù)組件名稱加載 HTML 和 CSS。 我們將在views/app/content_loader.rs文件中定義這個函數(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ù)中。 請注意,我們將組件嵌套在 templates/components/ 目錄中。 對于本例,我們要插入一個標頭組件,因此當(dāng)我們將標頭傳遞給 add_component 函數(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>

          在這里,我們僅顯示已完成和待辦事項的數(shù)量計數(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 標簽插入 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;

          在同一個文件中,我們必須在項目視圖函數(shù)中添加標題,如下所示:

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

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

          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 – 帶標題的主頁

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

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

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

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

          npx create-react-app front_end

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

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

          如果需要,我們可以編輯它,但就目前情況而言,如果我們在 package.json 文件所在的目錄中運行 npm start 命令,我們將運行 react-scripts start 命令。 我們很快就會運行 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),您可能會看到一些文章指出 JavaScript 不是基于類的面向?qū)ο笳Z言。 本書不會深入探討 JavaScript。 相反,本章旨在為您提供足夠的知識來啟動和運行前端。 如果您想向 Rust Web 應(yīng)用程序添加前端,希望本章足以促進進一步閱讀并啟動您的旅程。 在本章中,我們將只討論可以支持繼承的類和對象。

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

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

          npm start

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


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

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

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

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

          "axios": "^0.26.1"

          然后,我們可以運行以下命令:

          npm install

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

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

          我們將使用 Component 來繼承 App 類,并使用 axios 對后端執(zhí)行 API 調(diào)用。 現(xiàn)在,我們可以定義我們的 App 類并使用以下代碼更新我們的狀態(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ù)器中的獲取項目視圖返回的數(shù)據(jù)。 現(xiàn)在我們知道要使用哪些數(shù)據(jù),我們可以執(zhí)行以下步驟:

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

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

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

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

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

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

          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 (
                  . . .
              )
            }
          }

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

          在我們的 App 類中,我們的 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。 然后,我們將令牌添加到標頭中。 現(xiàn)在,我們將只硬編碼一個簡單的字符串,因為我們還沒有在 Rust 服務(wù)器中設(shè)置用戶會話; 我們將在第 7 章“管理用戶會話”中更新這一點。 然后,我們關(guān)閉它。 因為 axios.get 是一個 Promise,所以我們必須使用 .then。 返回數(shù)據(jù)時執(zhí)行 .then 括號內(nèi)的代碼。 在這些括號內(nèi),我們提取所需的數(shù)據(jù),然后執(zhí)行 this.setState 函數(shù)。 this.setState 函數(shù)更新 App 類的狀態(tài)。 但是,執(zhí)行 this.setState 也會執(zhí)行 App 類的 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í)行,我們接下來要做的就是。

          確保 getItems 函數(shù)被觸發(fā),從而在加載 App 類時更新狀態(tài)可以使用以下代碼來實現(xiàn):

          componentDidMount() {
          
            this.getItems();
          
          }

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

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

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

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

          對于我們的 App 類,渲染函數(shù)僅返回 HTML 組件。 我們在此不使用任何額外的邏輯。 我們可以返回以下內(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)被直接引用。 與我們在本章前面使用的手動字符串操作相比,這是一個可愛的變化。 使用 React 更加干凈,降低了錯誤的風(fēng)險。 在我們的前端,調(diào)用后端的渲染過程應(yīng)該可以工作。 但是,我們的 Rust 服務(wù)器將阻止來自 React 應(yīng)用程序的請求,因為它來自不同的應(yīng)用程序。 為了解決這個問題,我們需要繼續(xù)下一步。

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

          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ù)器就準備好接受來自 React 應(yīng)用程序的請求了。

          筆記

          當(dāng)我們定義 CORS 策略時,我們明確表示我們希望允許所有方法、標頭和來源。 然而,我們可以通過以下 CORS 定義更簡潔:

          let cors=Cors::permissive();

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

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


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

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

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

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

          創(chuàng)建 CreateToDoItem 組件

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

          讓我們開始吧。

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

          我們將從 src/components/ToDoItem.js 文件中更簡單的 ToDoItem 組件開始。 首先,我們必須導(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ù)。 然后,我們的 ToDoItem 組件具有以下函數(shù):

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

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

          sendRequest:此函數(shù)將請求發(fā)送到 Rust 服務(wù)器以編輯或刪除待辦事項。 我們還可以看到我們的 sendRequest 函數(shù)是一個箭頭函數(shù)。 箭頭語法本質(zhì)上將函數(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"
              }
          }

          這很簡單,不需要太多解釋。 現(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 定義端點更改時 URL 的一部分,具體取決于我們按下的按鈕。 我們還可以看到我們執(zhí)行了 this.props.passBackResponse 函數(shù)。 這是我們傳遞到 ToDoItem 組件中的函數(shù)。 這是因為在編輯或刪除請求后,我們從 Rust 服務(wù)器獲取了待辦事項的完整狀態(tài)。 我們需要啟用我們的應(yīng)用程序組件來處理已傳回的數(shù)據(jù)。 在這里,我們將在“應(yīng)用程序組件”小節(jié)中的“構(gòu)建和管理自定義組件”中先睹為快。 我們的 App 組件將在 passBackResponse 參數(shù)下有一個未執(zhí)行的函數(shù),它將傳遞給我們的 ToDoItem 組件。 該函數(shù)在 passBackResponse 參數(shù)下,將處理新的待辦事項的狀態(tài)并將其呈現(xiàn)在 App 組件中。

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

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

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

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

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

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

          這些是構(gòu)建我們組件的標準導(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ā)送請求,以創(chuàng)建標題為 state 的待辦事項

          handleTitleChange:每次更新輸入時該函數(shù)都會更新狀態(tài)

          在探索這兩個函數(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)輸入更改時,我們執(zhí)行 this.handleTitleChange 函數(shù)。 現(xiàn)在我們已經(jīng)介紹了渲染函數(shù),沒有什么新內(nèi)容要介紹了。 這是您再次查看 CreateToDoItem 組件的概要并嘗試自己定義 createItem 和 handleTitleChange 函數(shù)的好機會。 它們采用與 ToDoItem 組件中的函數(shù)類似的形式。

          您嘗試定義 createItem 和 handleTitleChange 函數(shù)應(yīng)類似于以下內(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});
          }    

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

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

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

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

          現(xiàn)在我們已經(jīng)有了組件,我們可以繼續(xù)進行第一次更改。 我們的 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ù),但我們沒有將數(shù)據(jù)傳遞到通用 HTML 標簽中,而是將待辦事項數(shù)據(jù)的參數(shù)傳遞到我們自己的自定義組件中,該組件將被處理 就像 HTML 標簽一樣。 當(dāng)涉及到處理我們自己的返回狀態(tài)響應(yīng)時,我們可以看到它是一個箭頭函數(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ù)量,可以在這里進行一些重構(gòu)。 但是,為了使其工作,我們必須使用以下代碼定義渲染函數(shù)的 return 語句:

          <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 組件之外沒有太多變化。 運行 Rust 服務(wù)器和 React 應(yīng)用程序?qū)槲覀兲峁┮韵乱晥D:


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


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

          將 CSS 放到 React 中

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

          templates/components/header.css

          css/base.css

          css/main.css

          將此處列出的 CSS 文件復(fù)制到 front_end/src/App.css 文件中。 CSS 有一項更改,所有 .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。 我們可以處理所有三個文件。 對于 src/App.js 文件,我們必須使用以下代碼導(dǎo)入 CSS:

          import "./App.css";

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

          <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 語句:

          <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 語句:

          <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)用程序中。 如果我們運行 Rust 服務(wù)器和 React 應(yīng)用程序,我們將得到下圖所示的輸出:


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


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

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

          將我們的 React 應(yīng)用程序轉(zhuǎn)換為桌面應(yīng)用程序并不復(fù)雜。 我們將使用 Electron 框架來做到這一點。 Electron 是一個功能強大的框架,可將 JavaScript、HTML 和 CSS 應(yīng)用程序轉(zhuǎn)換為跨 macOS、Linux 和 Windows 平臺編譯的桌面應(yīng)用程序。 Electron 框架還可以讓我們通過 API 訪問計算機的組件,例如加密存儲、通知、電源監(jiān)視器、消息端口、進程、shell、系統(tǒ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ù)。 然而,主力場是必不可少的。 我們將在此處編寫定義 Electron 應(yīng)用程序如何運行的文件。 將主頁字段設(shè)置為“./”還可以確保資源路徑相對于index.html 文件。 現(xiàn)在我們的元數(shù)據(jù)已經(jīng)定義了,我們可以添加以下依賴項:

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

          這些依賴項有助于構(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 進程與 Electron 進程分開。 如果我們現(xiàn)在只想在開發(fā)模式下運行 React 應(yīng)用程序,則必須運行以下命令:

          npm run react-start

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

              . . .
              "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)容。 我們需要使用以下命令安裝新的依賴項:

          npm install

          現(xiàn)在,我們可以開始構(gòu)建 front_end/public/electron.js 文件,以便構(gòu)建我們的 Electron 文件。 這本質(zhì)上是樣板代碼,您可能會在其他教程中看到此文件,因為這是在 Electron 中運行應(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ì)上定義了窗口的寬度和高度。 另請注意,nodeIntegration 和enableRemoteModule 使渲染器遠程進程(瀏覽器窗口)能夠在主進程上運行代碼。 然后,我們開始在主窗口中加載 URL。 如果在開發(fā)人員模式下運行,我們只需加載 http://localhost:3000,因為我們在 localhost 上運行了 React 應(yīng)用程序。 如果我們構(gòu)建應(yīng)用程序,那么我們編碼的資產(chǎn)和文件將被編譯并可以通過 ../build/index.html 文件加載。 我們還聲明,如果我們在開發(fā)人員模式下運行,我們將打開開發(fā)人員工具。 當(dāng)窗口準備好時,我們必須使用以下代碼執(zhí)行 createWindow 函數(shù):

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

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

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

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

          npm start

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


          圖 5.14 – 我們在 Electron 中運行的 React 應(yīng)用程序

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


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

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

          npm build

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


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


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

          概括

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

          這為我們提供了一個動態(tài)視圖,當(dāng)我們編輯、刪除或創(chuàng)建待辦事項時,該視圖會自動更新。 我們還探索了一些有關(guān) CSS 和 JavaScript 的基礎(chǔ)知識,以便從前端進行 API 調(diào)用并動態(tài)編輯視圖某些部分的 HTML。 我們還根據(jù)窗口的大小管理整個視圖的樣式。 請注意,我們不依賴外部板條箱。 這是因為我們希望能夠了解如何處理 HTML 數(shù)據(jù)。

          然后,我們在 React 中重建了前端。 雖然這需要更長的時間并且有更多的移動部件,但代碼更具可擴展性并且更安全,因為我們不必手動操作字符串來編寫 HTML 組件。 我們還可以明白為什么我們傾向于 React,因為它非常適合 Electron,為我們提供了另一種向用戶交付應(yīng)用程序的方式。

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

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

          問題

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

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

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

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

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

          答案

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

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

          我們創(chuàng)建一個 CSS 文件來定義我們希望在整個應(yīng)用程序中保持一致的組件。 然后,我們在所有 HTML 文件的 <style> 部分放置一個標簽。 然后,對于每個文件,我們加載基本 CSS 文件并用 CSS 數(shù)據(jù)替換標簽。

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

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


          主站蜘蛛池模板: 91香蕉福利一区二区三区| 视频在线一区二区| 狠狠综合久久av一区二区| 国产精品盗摄一区二区在线| 亚洲国产成人一区二区精品区 | 国产成人精品亚洲一区| 久久se精品一区二区国产| 日韩动漫av在线播放一区| 日本一区二区三区爆乳| 亚洲午夜精品一区二区公牛电影院 | 中文字幕AV无码一区二区三区| 国模精品一区二区三区| 波多野结衣中文一区| 日本香蕉一区二区三区| 国精产品999一区二区三区有限| 制服丝袜一区在线| 久久久精品人妻一区二区三区四 | 久久久久人妻精品一区| 久久亚洲一区二区| 日韩电影一区二区| 久久久久人妻精品一区蜜桃| 精品国产福利第一区二区三区| 精品乱子伦一区二区三区| а天堂中文最新一区二区三区| 国产在线一区二区三区av| 视频精品一区二区三区| 中文字幕日本一区| 国产精华液一区二区区别大吗| 亚洲Aⅴ无码一区二区二三区软件| 香蕉久久ac一区二区三区| 精品国产免费观看一区| 老鸭窝毛片一区二区三区| 亚洲人成网站18禁止一区| 久久综合一区二区无码| 麻豆精品久久久一区二区| 亚洲成在人天堂一区二区| 国产日韩一区二区三区在线播放| 亚洲精品国产suv一区88| 国产一区二区三区在线观看免费| 老鸭窝毛片一区二区三区| 丝袜人妻一区二区三区|