章將包含以下幾方面內容:
HTTP 是一種 超文本傳輸協議(Hypertext Transfer Protocol),超文本傳輸協議可以進行文字分割:超文本(Hypertext)、傳輸(Transfer)、協議(Protocol) ,它們之間的關系如下:
分別對這三個名次做一個解釋:
兩臺電腦之間只能傳輸簡單文字,后面還想要傳輸圖片、音頻、視頻,甚至點擊文字或圖片能夠進行超鏈接的跳轉,那么文本的語義就被擴大了,這種語義擴大后的文本就被稱為超文本(Hypertext)。
兩臺計算機之間會形成互聯關系進行通信,我們存儲的超文本會被解析成為二進制數據包,由傳輸載體(例如同軸電纜,電話線,光纜)負責把二進制數據包由計算機終端傳輸到另一個終端的過程
網絡協議就是網絡中(包括互聯網)傳遞、管理信息的一些規范
網絡設計者以分層(layer)的方式組織協議,每個協議屬于層次模型之一。每一層都是向它的上一層提供服務(service),即所謂的服務模型(service model)。每個分層中所有的協議稱為 協議棧(protocol stack)。因特網的協議棧由五個部分組成:物理層、鏈路層、網絡層、傳輸層和應用層。我們采用自上而下的方法研究其原理,也就是應用層 -> 物理層的方式(了解)。
應用層是網絡應用程序和網絡協議存放的分層,因特網的應用層包括許多協議。比如HTTP,電子郵件傳送協議 SMTP、端系統文件上傳協議 FTP、還有為我們進行域名解析的 DNS 協議
輸層在應用程序斷點之間傳送應用程序報文,在這一層主要有兩種傳輸協議 TCP和 UDP。
TCP 是面向連接的,它能夠控制并確認報文是否到達,并提供了擁塞機制來控制網絡傳輸,因此當網絡擁塞時,會抑制其傳輸速率。
UDP 協議向它的應用程序提供了無連接服務。它是不具備可靠性的,沒有流量控制,也沒有擁塞控制。我們把運輸層的分組稱為 報文段(segment)
網絡層負責將稱為 數據報(datagram) 的網絡分層從一臺主機移動到另一臺主機。網絡層一個非常重要的協議是 IP 協議,所有具有網絡層的因特網組件都必須運行 IP 協議。
為了將分組從一個節點(主機或路由器)運輸到另一個節點,網絡層必須依靠鏈路層提供服務。鏈路層的例子包括以太網、WiFi 和電纜接入的 DOCSIS 協議,因為數據從源目的地傳送通常需要經過幾條鏈路,一個數據包可能被沿途不同的鏈路層協議處理,我們把鏈路層的分組稱為 幀(frame)。
雖然鏈路層的作用是將幀從一個端系統運輸到另一個端系統,而物理層的作用是將幀中的一個個 比特 從一個節點運輸到另一個節點,,物理層的協議仍然使用鏈路層協議,這些協議與實際的物理傳輸介質有關,例如,以太網有很多物理層協議:關于雙絞銅線、關于同軸電纜、關于光纖等等。
五層網絡協議的示意圖如下:
HTTP 屬于應用層的協議,需要其他層次協議的配合完成信息的交換,在完成一次 HTTP 請求和響應的過程中,需要以下協議的配合:
TCP/IP 我們一般稱之為協議簇,什么意思呢?就是 TCP/IP 協議簇中不僅僅只有 TCP 協議和 IP 協議,它是一系列網絡通信協議的統稱。而其中最核心的兩個協議就是 TCP / IP 協議,其他的還有 UDP、ICMP、ARP 等等,共同構成了一個復雜但有層次的協議棧。
TCP 協議的全稱是 Transmission Control Protocol 的縮寫,意思是傳輸控制協議,HTTP 使用 TCP 作為通信協議,這是因為 TCP 是一種可靠的協議,而可靠能保證數據不丟失。
IP 協議的全稱是 Internet Protocol 的縮寫,它主要解決的是通信雙方尋址的問題。IP 協議使用 IP 地址 來標識互聯網上的每一臺計算機。
DNS 的全稱是域名系統(Domain Name System,縮寫:DNS),它作為將域名和 IP 地址相互映射的一個分布式數據庫,能夠使人更方便地訪問互聯網。比如:www.google.com -> 193.XXX.XXX.XXX
可以通過輸入 www.google.com 地址來訪問谷歌的官網,輸入的地址格式必須要滿足 URI 的規范。
URI的全稱是(Uniform Resource Identifier),中文名稱是統一資源標識符,使用它就能夠唯一地標記互聯網上資源。
URL的全稱是(Uniform Resource Locator),中文名稱是統一資源定位符,也就是我們俗稱的網址,它實際上是 URI 的一個子集。
起始行和頭部字段并成為 請求頭 或者 響應頭,統稱為 Header;消息正文也叫做實體,稱為 body。HTTP 協議規定每次發送的報文必須要有 Header,但是可以沒有 body,在 header 和 body 之間必須要有一個空行(CRLF)。
舉個例子:http://www.someSchool.edu/someDepartment/home.index 請求的請求頭:
報文的起始行都是由三個字段組成:方法、URL 字段和 HTTP 版本字段。
一般最常用的方法也就是 GET 方法和 POST 方法,其他方法暫時了解即可。
完整的域名解析一下 URL:http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
?key1=value1&key2=value2 是提供給 Web 服務器的額外參數。如果是 GET 請求,一般帶有請求 URL 參數,如果是 POST 請求,則不會在路徑后面直接加參數。
比如http://www.someSchool.edu/someDepartment/home.index,來看一下它的請求頭部
Host: www.someschool.edu
Connection: close
User-agent: Mozilla/5.0
Accept-language: fr
復制代碼
HTTP 的請求標頭分為四種: 通用標頭、請求標頭、響應標頭 和 實體標頭
通用標頭主要有三個,分別是 Date、Cache-Control 和 Connection Date
Date 出現在請求標頭和響應標頭中,它的基本表示如下
Date: Wed, 21 Oct 2015 07:28:00 GMT
復制代碼
Cache-Control
Cache-Control 可以出現在請求標頭和響應標頭中,Cache-Control 的種類比較多,雖然說這是一個通用標頭,但是又一些特性是請求標頭具有的,有一些是響應標頭才有的。主要大類有 可緩存性、閾值性、 重新驗證并重新加載 和其他特性
Connection
Connection 決定當前事務(一次三次握手和四次揮手)完成后,是否會關閉網絡連接。Connection 有兩種,一種是持久性連接,即一次事務完成后不關閉網絡連接
Connection: keep-alive
復制代碼
復制代碼
另一種是非持久性連接,即一次事務完成后關閉網絡連接
Connection: close
復制代碼
實體標頭是描述消息正文內容的 HTTP 標頭。實體標頭用于 HTTP 請求和響應中。頭部Content-Length、 Content-Language、 Content-Encoding 是實體頭。
Content-Language: de-DE
Content-Language: en-US
Content-Language: de-DE, en-CA
復制代碼
復制代碼
Accept-Encoding: gzip, deflate //請求頭
Content-Encoding: gzip //響應頭
復制代碼
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
復制代碼
Host
Host 請求頭指明了服務器的域名,以及(可選的)服務器監聽的TCP端口號
Referer
HTTP Referer 屬性是請求標頭的一部分,告訴服務器該網頁是從哪個頁面鏈接過來的
If-Modified-Since
HTTP 的 If-Modified-Since 使其成為條件請求:
If-Modified-Since 通常會與 If-None-Match 搭配使用,If-Modified-Since 用于確認代理或客戶端擁有的本地資源的有效性。獲取資源的更新日期時間,可通過確認首部字段 Last-Modified 來確定。
大白話說就是如果在 Last-Modified 之后更新了服務器資源,那么服務器會響應200,如果在 Last-Modified 之后沒有更新過資源,則返回 304。
If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT
復制代碼
復制代碼
If-None-Match
If-None-Match HTTP請求標頭使請求成為條件請求。 對于 GET 和 HEAD 方法,僅當服務器沒有與給定資源匹配的 ETag 時,服務器才會以200狀態發送回請求的資源。 對于其他方法,僅當最終現有資源的ETag與列出的任何值都不匹配時,才會處理請求。
If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"
復制代碼
復制代碼
內容協商機制是指客戶端和服務器端就響應的資源內容進行交涉,然后提供給客戶端最為適合的資源。內容協商會以響應資源的語言、字符集、編碼方式等作為判斷的標準。
內容協商主要有以下3種類型:
這種協商方式是由服務器端進行內容協商。服務器端會根據請求首部字段進行自動處理
這種協商方式是由客戶端來進行內容協商。
是服務器驅動和客戶端驅動的結合體,是由服務器端和客戶端各自進行內容協商的一種方法。
內容協商的分類有很多種,主要的幾種類型是 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。
Accept
接受請求 HTTP 標頭會通告客戶端其能夠理解的 MIME 類型
MIME: MIME (Multipurpose Internet Mail Extensions) 是描述消息內容類型的因特網標準。MIME 消息能包含文本、圖像、音頻、視頻以及其他應用程序專用的數據。
復制代碼
文本文件: text/html、text/plain、text/css、application/xhtml+xml、application/xml
圖片文件: image/jpeg、image/gif、image/png
視頻文件: video/mpeg、video/quicktime
應用程序二進制文件: application/octet-stream、application/zip
比如,如果瀏覽器不支持 PNG 圖片的顯示,那 Accept 就不指定image/png,而指定可處理的 image/gif 和 image/jpeg 等圖片類型。
一般 MIME 類型也會和 q 這個屬性一起使用,q 是什么?q 表示的是權重,來看一個例子
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
復制代碼
若想要給顯示的媒體類型增加優先級,則使用 q=來額外表示權重值,沒有顯示權重的時候默認值是1.0。
q | MIME |
1.0 | text/html |
1.0 | application/xhtml+xml |
0.9 | application/xml |
0.8 | * / * |
Accept-Charset
accept-charset 屬性規定服務器處理表單數據所接受的字符集。
accept-charset 屬性允許您指定一系列字符集,服務器必須支持這些字符集,從而得以正確解釋表單中的數據。
Accept-Language
首部字段 Accept-Language 用來告知服務器用戶代理能夠處理的自然語言集(指中文或英文等),以及自然語言集的相對優先級。
Accept-Language: en-US,en;q=0.5
復制代碼
響應標頭是可以在 HTTP 響應中使用的 HTTP 標頭。并不是所有出現在響應中的標頭都是響應標頭。還有一些特殊的我們上面說過,有通用標頭和實體標頭也會出現在響應標頭中,比如 Content-Length 就是一個實體標頭,但是,在這種情況下,這些實體請求通常稱為響應頭。下面以一個例子為例和你探討一下響應頭
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
復制代碼
響應狀態碼
以 2xx 為開頭的都表示請求成功響應。
狀態碼 | 含義 |
200 | 成功響應 |
204 | 請求處理成功,但是沒有資源可以返回 |
206 | 對資源某一部分進行響應,由Content-Range 指定范圍的實體內容。 |
以 3xx 為開頭的都表示需要進行附加操作以完成請求
狀態碼 | 含義 |
301 | 永久性重定向,該狀態碼表示請求的資源已經重新分配 URI,以后應該使用資源現有的 URI |
302 | 臨時性重定向。該狀態碼表示請求的資源已被分配了新的 URI,希望用戶(本次)能使用新的 URI 訪問。 |
303 | 該狀態碼表示由于請求對應的資源存在著另一個 URI,應使用 GET 方法定向獲取請求的資源。 |
304 | 該狀態碼表示客戶端發送附帶條件的請求時,服務器端允許請求訪問資源,但未滿足條件的情況。 |
307 | 臨時重定向。該狀態碼與 302 Found 有著相同的含義。 |
以 4xx 的響應結果表明客戶端是發生錯誤的原因所在。
狀態碼 | 含義 |
400 | 該狀態碼表示請求報文中存在語法錯誤。當錯誤發生時,需修改請求的內容后再次發送請求。 |
401 | 該狀態碼表示發送的請求需要有通過 HTTP 認證(BASIC 認證、DIGEST 認證)的認證信息。 |
403 | 該狀態碼表明對請求資源的訪問被服務器拒絕了。 |
404 | 該狀態碼表明服務器上無法找到請求的資源。 |
以 5xx 為開頭的響應標頭都表示服務器本身發生錯誤
狀態碼 | 含義 |
500 | 該狀態碼表明服務器端在執行請求時發生了錯誤。 |
503 | 該狀態碼表明服務器暫時處于超負載或正在進行停機維護,現在無法處理請求。 |
HTTP 的協議比較簡單,它的主要組成就是 header + body,頭部信息也是簡單的文本格式
HTTP 協議又多了靈活 和 易擴展 的優點。
HTTP 協議里的請求方法、URI、狀態碼、原因短語、頭字段等每一個核心組成要素都沒有被制定死,允許開發者任意定制、擴充或解釋,給予了瀏覽器和服務器最大程度的信任和自由。
天然具有跨語言、跨平臺的優越性,而且,因為本身的簡單特性很容易實現,所以幾乎所有的編程語言都有 HTTP 調用庫和外圍的開發測試工具
既是優點又是缺點。因為服務器沒有記憶能力,所以就不需要額外的資源來記錄狀態信息,不僅實現上會簡單一些,而且還能減輕服務器的負擔,能夠把更多的 CPU 和內存用來對外提供服務。
服務器沒有記憶能力,它就無法支持需要連續多個步驟的事務操作。每次都得問一遍身份信息,需要增加了不必要的數據傳輸量。由此出現了 Cookie 技術。
明文傳輸,協議里的報文(準確地說是 header 部分)不使用二進制數據,而是用簡單可閱讀的文本形式。
對比 TCP、UDP 這樣的二進制協議,它的優點顯而易見,不需要借助任何外部工具,用瀏覽器、Wireshark 或者 tcpdump 抓包后,直接用肉眼就可以很容易地查看或者修改,為我們的開發調試工作帶來極大的便利。
當然缺點也是顯而易見的,就是不安全,可以被監聽和被窺探。因為無法判斷通信雙方的身份,不能判斷報文是否被更改過。
總結起來即:
作者:captain_p
鏈接:https://juejin.cn/post/7041744237905346568
如果本文對你有幫助,麻煩轉發關注支持一下
章涵蓋
在第1章中,當我們嘗試定義應用程序編程接口(API)時,我們將其稱為軟件平臺,它公開了不同計算機程序可以通過交換數據進行交互的工具和服務。從這個定義開始,我們可以說 API(包括 Web API)的目的是創建一個公共場所,讓獨立且通常不相關的系統可以使用普遍接受的標準進行會面、問候和通信。這些“參與者”大多是由其他開發人員實現的計算機程序,例如網站、移動應用程序和微服務。出于這個原因,無論誰承擔設計、創建和發布 Web API 的任務,都必須承認一種新型用戶的存在和需求:第三方開發人員,這將我們帶到本章的主題。
在現代軟件開發中,記錄接口、中間件、服務或任何旨在達到目的的手段的產品不再被視為一種選擇:只要我們想增加或加速其采用,它就是一項設計要求。這也是讓感興趣的第三方能夠充分理解我們工作價值的最快方法。近年來,這方面變得如此重要,以至于它有利于定義一個新的設計領域:開發人員體驗(DX),即從開發人員角度看的用戶體驗。通過考慮DX,我將在本章專門確定API文檔的最佳實踐,并展示我們如何借助 ASP.NET Core提供的許多工具將它們付諸實踐。
產品的技術文檔只有在滿足閱讀者的需求和期望時才有用。出于這個原因,首先要做的是確定我們的 Web API 潛在受眾:期望選擇和/或使用它的利益相關者。在提到它們時,我通常將它們分為三種主要類型,使用取自建筑俚語的名稱。
探礦者是充滿激情的開發人員和 IT 愛好者,他們愿意嘗試我們的 Web API,而除了個人興趣、知識獲取、測試/審查目的等之外,沒有迫切的需求。如果我們希望我們的 Web API 成為我們打算向公眾發布的通用產品(或其中的一部分),則此組很重要;他們的反饋可能會對開發人員社區產生直接影響,可能會引入承包商和建筑商(分別參見第 11.1.2 節和第 11.1.3 節)。
承包商是 IT 分析師、解決方案架構師和后端設計人員,他們負責創建產品、解決問題或解決我們的 Web API 可以幫助他們處理的潛在挑戰。雖然通常情況下,他們不會著手實施,但他們通常充當決策者,因為他們擁有權力、處理預算和/或擁有建議、選擇或規定使用哪些組件所需的專業知識(除非他們讓構建者選擇它們)。
構建者是選擇(或被指示)使用我們的 Web API 來解決特定問題的軟件開發人員。他們代表了我們受眾中技術性最強的部分,可能很難滿足,因為處理我們的 API 是他們工作任務的一部分,而且他們完成工作的時間通常有限。構建者必須學會實際使用我們的 Web API;他們是我之前提到的第三方開發人員。
在閱讀了這些描述之后,似乎很明顯地認為我們的文檔應該關注構建者,他們是我們的 Web API 的最終用戶。這個前提是有效的。我們將在本章中討論的大多數 API 文檔最佳實踐都將考慮這種方法。但我們不應該忘記其他兩種受眾類型,因為我們項目的成功也可能取決于他們。
開發人員是特殊類型的用戶。他們善于分析、精確且要求苛刻,特別是如果我們認為他們通常希望使用我們的 API 來實現主要目標:實現需求、解決問題等。每當他們發現自己由于文檔編寫不佳而無法實現目標時,他們很可能會認為 API 不夠好——盡管聽起來很殘酷,但他們是對的。歸根結底,API 的好壞取決于它們的文檔,這不可避免地會對采用和可維護性產生巨大影響。
我們所說的良好文檔是什么意思,我們如何實現它?沒有一個答案在所有情況下都有效。但是一些好的做法可以幫助我們找到一種可行的方法來實現我們想要的東西,例如:
以下部分將詳細介紹這些概念,并展示如何在我們的 MyBGList Web API 中實現它們。
如果我們想讓第三方開發人員滿意,我們必須確保我們的 API 文檔始終更新。沒有什么比處理缺少的規范、不存在的操作或端點、錯誤的參數等更令人沮喪的了。過時的文檔會讓我們的用戶認為我們的 API 壞了,即使它不是。
注意記錄不佳(或錯誤)的 Web API 在技術上已損壞,因為第三方沒有機會看到它按預期工作。這就是我們最初聲明API文檔是設計要求,而不是選項或附加組件的原因。即使對于預計僅由內部開發人員使用的內部 API 也是如此,因為缺乏適當的文檔最終會影響新員工、潛在合作伙伴、維護任務、移交流程、外包商等。
對于 RESTful API 來說,自動化文檔過程的需求尤其強烈,因為 REST 架構標準沒有為此目的提供標準化的機制、模式或參考。這是Open API(以前稱為Swagger)成功的主要原因,Open API(以前稱為Swagger)是SmartBear Software于2011年發布的自動化API文檔的開源規范,旨在解決這個問題。
我們從第2章開始就知道Swagger/OpenAPI,因為Visual Studio的 ASP.NET Core Web API模板(我們用來創建MyBGList項目)包括Swashbuckle的服務和中間件,這是一組用于在Core中實現OpenAPI的服務,中間件和工具 ASP.NET。我們還體驗了它的自動發現和描述功能,它為我們提供了一個代碼生成的 OpenAPI 3.0 描述文件 (swagger.json) 和一個基于 Web 的交互式 API 客戶端 (SwaggerUI),我們用它來測試我們的端點。因為我們已經在使用Swashbuckle,所以我們可以說我們已經準備好了。但是,在以下部分中,我們將擴展其功能以滿足我們的需求。
如果我們查看我們的 SwaggerUI 主儀表板,我們會發現我們當前的“文檔”僅包含端點及其輸入變量的列表,而沒有對每個方法的作用進行單一描述。我們的受眾必須從名稱中推斷端點的使用,以及每個請求標頭和/或輸入參數的用途,這不是展示、評估或推廣我們工作的最佳方式。
注意如果我們的 API 遵循使用 HTTP 謂詞來標識操作類型的 RESTful 良好實踐,它將提供有關每個端點使用的其他有用提示 - 至少對具有所需專業知識的用戶而言。
相反,我們應該采用標準化的方式來為每個端點及其輸入變量創建簡潔而相關的描述。這種做法不僅可以節省建筑商的時間,還可以讓探礦者和承包商更好地掌握API的工作方式及其功能。Swashbuckle 提供了兩種向端點和輸入參數添加自定義描述的方法:
每種技術都有優點和缺點。在下一節中,我們將了解如何實現這兩種技術。
如果我們已經使用 C# 提供的三斜杠語法在源代碼中添加了注釋,則 XML 文檔方法可能很有用、方便且快速實現。我說的是一個簡潔的 C# 功能,它允許開發人員通過編寫由三斜杠指示的特殊注釋字段來創建代碼級文檔。某些集成開發環境 (IDE)(如 Visual Studio)也使用此功能,這些環境自動生成 XML 元素來描述各種代碼部分,例如<摘要>(用于方法)、<param>(用于輸入參數)和<returns>(用于返回值)。
注意有關 C# XML 文檔注釋的其他信息,請查看 http://mng.bz/qdy6。有關支持的 XML 標記的完整參考,請參閱 http://mng.bz/7187。
學習如何使用此功能的最佳方法是將其付諸實踐。打開 /Controllers/AccountController.cs 文件,找到 Register 操作方法,將光標置于其上方(及其所有屬性),然后在其上方鍵入斜杠 (/) 字符三次。添加第三個斜杠后,Visual Studio 應生成以下 XML 注釋樣板:
/// <summary>
///
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
[ResponseCache(CacheProfileName="NoCache")]
public async Task<ActionResult> Register(RegisterDTO input)
請注意,自動生成的 XML 結構標識操作方法的 RegisterDTO 輸入參數的名稱。現在我們有了樣板,讓我們填充它。以下是我們如何記錄帳戶控制器的注冊終結點:
/// <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>
之后,向下滾動到登錄操作方法,然后執行相同的操作。以下是我們可以用來記錄它的合適描述:
/// <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>
保存并關閉帳戶控制器。接下來,告訴編譯器使用我們添加的 XML 注釋以及代碼中存在的任何其他此類注釋來生成 XML 文檔文件。
生成 XML 文檔文件
要啟用此功能,我們需要更新 MyBGList 項目的配置文件。在“解決方案資源管理器”窗口中右鍵單擊項目的根節點,然后從上下文菜單中選擇“編輯項目文件”選項以打開 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
現在,每當我們構建項目時,編譯器都會生成 XML 文檔文件。
克服 CS1591 警告
我們在前面的代碼中使用的 <NoWarn> 元素將禁止顯示 CS1591 警告,GenerateDocumentationFile 開關將為任何公共類型和成員引發該警告,而無需三斜杠注釋。我們選擇在我們的示例項目中全局關閉它們,因為我們不需要該建議,但是如果我們想確保我們注釋/記錄所有內容,那么保持它可能會很有用。
有關生成文檔文件開關的詳細信息,請參閱 http://mng.bz/mJdn。
我們需要做的最后一件事是配置 Swashbuckle 以獲取 XML 文檔文件的內容。
配置虛張聲勢扣
要讀取我們項目的XML文檔文件,Swashbuckle需要知道它的完整路徑和文件名,與我們項目的名稱(帶有.xml擴展名)相對應。我們可以使用 Reflection(一種 C# 技術,允許我們在運行時檢索類型的元數據),而不是手動編寫它,而是手動編寫它。這種編程方法通常更可取,因為它比使用文本字符串更能確保代碼可維護性,因此我們將選擇它。打開 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
? 必需的命名空間
? 構建 XML 文檔文件名
? 組裝 XML 文檔文件完整路徑
請注意,在此代碼中,我們使用反射生成與項目名稱匹配的 XML 文件名,然后使用它來構造 XML 文件的完整路徑。接下來,我們將測試我們所做的工作,看看它是否有效。
測試 XML 文檔
在調試模式下運行項目,并查看 SwaggerUI 主儀表板,我們應該在其中看到我們在三斜杠注釋中使用的相同描述性字符串(圖 11.1)。
圖 11.1 Swashbuckle 獲取并在 SwaggerUI 中使用的 XML 文檔
請注意,摘要緊跟在終結點定義之后。說明顯示在端點的可展開面板中。
評估 XML 文檔的優缺點
能夠自動將所有代碼級注釋轉換為 API 文檔,使我們能夠用一塊石頭殺死兩只鳥。如果我們習慣于編寫注釋來描述我們的類和方法(開發人員的良好做法),我們可以重用大量工作。此外,這種方法對內部開發人員特別有用,因為他們可以直接從源代碼中讀取我們的API文檔,甚至不必查看swagger.json文件和/或SwaggerUI。
但這種顯著的好處很容易成為不利的一面。例如,如果我們想將內部源代碼文檔(針對內部開發人員)與公共 API 文檔(針對第三方開發人員/最終用戶)分開,我們可能會發現這種方法是有限的,更不用說涉及非自愿數據泄露的潛在風險。代碼級注釋通常被大多數開發人員視為機密,他們可能會使用它們來跟蹤內部注釋、警告、已知問題/錯誤、漏洞和其他不應向公眾發布的嚴格保留的數據。為了克服這樣的問題,我們可以考慮使用 [SwaggerOperation] 數據屬性替代,它可以更好地分離內部注釋和 API 文檔之間的關注點,以及我們可能想要使用的一些簡潔的附加功能。
除了 XML 文檔之外,Swashbuckle 還提供了另一種基于屬性的功能,用于向我們的 Web API 端點添加自定義描述。此功能由一個名為 Swashbuckle.AspNetCore.Annotations 的可選模塊處理,該模塊隨專用 NuGet 包一起提供。在以下部分中,我們將學習如何安裝和使用它。
安裝 NuGet 包
與往常一樣,若要安裝 Swashbuckle 注釋的 NuGet 包,我們可以使用 Visual Studio 的 NuGet 圖形用戶界面 (GUI)、包管理器控制臺窗口或 .NET 命令行界面 (CLI)。下面是在 .NET CLI 中安裝它們的命令:
dotnet add package Swashbuckle.AspNetCore.Annotations --version 6.4.0
安裝后,我們將能夠使用一些數據注釋屬性來增強我們的 API 文檔。我們將從 [SwaggerOperation] 開始,它允許我們為控制器的操作方法以及最小 API 方法設置自定義摘要、描述和/或標簽。
使用 [SwaggerOperation] 屬性
由于我們的帳戶控制器的操作方法已經通過XML記錄,因此這次我們將使用BoardGamesController。打開 /Controllers/ BoardGamesController.cs 文件,并將屬性添加到四個現有的操作方法中,如清單 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
? 必需的命名空間
? [招搖操作]屬性
? 添加端點摘要
? 添加端點描述
現在我們知道如何使用 Swashbuckle 注釋來描述我們的操作,讓我們對它們的輸入參數做同樣的事情。
使用 [SwaggerParameter] 屬性
要設置輸入參數的描述,我們可以使用 [SwaggerParameter] 屬性,該屬性對應于 XML 文檔的 <param> 標記的 Swashbuckle 注釋。但是,盡管 XML 標記必須在方法級別定義,然后通過 name 屬性綁定到其相應的參數,但 [SwaggerParameter] 注釋必須在它要描述的參數之上定義。
要了解它是如何工作的,讓我們實現它。在保持 BoardGamesController.cs 文件打開的同時,找到 Get() 方法,并通過以下方式將 [SwaggerParameter] 添加到現有輸入參數(新行以粗體顯示):
public async Task<RestDTO<BoardGame[]>> Get(
[FromQuery]
[SwaggerParameter("A DTO object that can be used " + ?
"to customize the data-retrieval parameters.")]
RequestDTO<BoardGameDTO> input)
? 添加 [招搖參數]
現在,描述性屬性已經設置好,我們需要通過更新我們的 Swagger 配置來全局啟用虛張聲勢注釋功能。
啟用批注
若要啟用虛張聲勢注釋,請打開 Program.cs 文件,并將以下配置設置添加到現有的 AddSwaggerGen() 方法中:
builder.Services.AddSwaggerGen(options=>
{
options.EnableAnnotations(); ?
// ... existing code
? 啟用虛張聲勢注釋功能
添加最低 API 支持
[SwaggerOperation] 屬性以及整個 Swashbuckle 注釋功能甚至可以使用最小 API 方法。讓我們將其中一些方法添加到循環中。保持 Program.cs 文件打開狀態,向下滾動到我們在第 9 章中實現的三個最小 API 方法,以測試 ASP.NET Core 授權功能。然后將 [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
? 必需的命名空間
? [招搖操作]屬性
? 添加端點摘要
? 添加端點描述
現在,我們已準備好測試我們所做的工作。
測試注釋
要測試我們的新注釋,請在調試模式下運行我們的項目,并查看 SwaggerUI 主儀表板,我們應該能夠在其中看到它們(圖 11.2)。
圖 11.2 通過 [SwaggerOperation] 屬性添加的 OpenAPI 注釋
如我們所見,總體結果與使用 XML 文檔方法獲得的結果非常相似。但是,我們可以記錄每種技術的內容之間存在一些顯著差異。例如,XML文檔允許我們描述示例(使用<example>元素),Swashbuckle注釋目前不支持這些示例。同時,Swashbuckle 注釋功能可以使用自定義模式過濾器進行擴展,以支持 Swagger/OpenAPI 規范中提到的幾乎任何文檔選項。在以下各節中,我們將以互補的方式使用這兩種方法,以充分利用它們。
用于終結點和輸入參數的相同描述性方法也應應用于我們的 Web API 響應。此方法不僅適用于返回的 JavaScript 對象表示法 (JSON) 數據,還適用于 HTTP 狀態代碼(應始終根據其含義使用)和相關響應標頭(如果有)。
同樣,為了描述我們的響應,我們可以使用<響應> XML 文檔標簽或專用的 [SwaggerResponse] Swashbuckle 注釋屬性。在以下部分中,我們將采用這兩種方法。
使用 XML 文檔
正如我們之前對 <param> 標簽所做的那樣,它可以多次用于描述每個輸入參數,我們可以為該方法返回的任何 HTTP 狀態代碼創建一個 <response> 標簽。每個 XML <響應>標記都需要一個代碼屬性(以確定它所描述的響應的 HTTP 狀態代碼)和一個包含實際說明的基于文本的值。若要對其進行測試,請再次打開 /Controllers/AccountController.cs 文件,并將以下 <response> 標記追加到 Register 方法的現有 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 狀態代碼 201 說明
? HTTP 狀態代碼 400 說明
? HTTP 狀態代碼 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 狀態代碼 200 說明
? HTTP 狀態代碼 400 說明
? HTTP 狀態代碼 401 說明
若要測試我們所做的工作,請在調試模式下啟動項目,訪問 SwaggerUI 主儀表板,然后展開“帳戶/注冊”和“帳戶/登錄”終結點。如果我們正確執行了所有操作,我們應該看到我們的響應描述,如圖 11.3 所示。
圖 11.3 /帳戶/登錄端點的響應說明
現在我們知道了如何使用 XML 文檔注釋來獲取此結果,讓我們看看如何使用 [SwaggerResponse] 數據注釋屬性實現相同的操作。
使用虛張聲勢扣批注
[SwaggerResponse] 屬性與其對應的 <response> XML 標記對應項一樣,可以多次添加到同一方法,以描述受影響的方法可能發送回客戶端的所有結果、HTTP 狀態代碼和響應類型。此外,它還需要兩個主要參數:
學習如何使用它的最好方法是看到它的實際效果。打開 Program.cs 文件,向下滾動到 /auth/test/1 最小 API 端點,然后添加新的 [SwaggerResponse] 屬性以按以下方式描述其唯一響應:
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 狀態代碼 201 說明
? HTTP 狀態代碼 401 說明
請注意,我們使用了 Microsoft.AspNetCore 提供的 StatusCodes 枚舉。Http 命名空間,它允許我們使用強類型方法指定 HTTP 狀態代碼。
注意使用基于屬性的方法的一個優點是,它為我們提供了 C# 和 ASP.NET Core 功能提供的所有好處,包括但不限于強類型成員。例如,我們可以通過使用 Core 的內置本地化支持(由于篇幅原因,本書中沒有介紹 ASP.NET 為不同的語言和/或文化指定不同的描述。
若要測試該屬性,請在調試模式下啟動項目,訪問 SwaggerUI 儀表板,并在 /auth/test/1 終結點的 SwaggerUI 面板的“響應”部分中檢查是否存在上述說明(圖 11.4)。
圖 11.4 /auth/test/1 端點的響應說明
不錯。但是,我們的大多數端點不僅發出 HTTP 狀態代碼;如果請求成功,它們還會返回具有明確定義的預定結構的 JSON 對象。向我們的 API 用戶描述這些返回類型,讓他們知道會發生什么,這不是很好嗎?為了實現這樣的目標,我們需要在這些描述中添加一些樣本。在下一節中,我們將看到如何操作。
理想情況下,每個 API 操作都應包含一個請求和響應示例,以便用戶了解每個操作的預期工作方式。正如我們已經知道的,我們心愛的 SwaggerUI 負責請求部分的任務;每當我們使用它時,它都會顯示一個示例值選項卡,其中包含 JSON 格式的示例輸入數據傳輸對象 (DTO),如圖 11.5 所示。
圖 11.5 /帳戶/注冊終結點的響應示例
“示例值”選項卡的右側是一個簡潔的“架構”選項卡,其中顯示了對象的架構和許多有用的信息,例如每個字段的最大大小、可為空性和基礎類型。遺憾的是,此自動功能并不總是適用于 JSON 響應類型,需要一些手動干預。
注意有時,SwaggerUI 會設法自動檢測(并顯示示例)響應類型。例如,如果我們展開 GET /BoardGames 端點的 SwaggerUI 面板,則 RestDTO<BoardGame> 對象將正確顯示在響應部分中。遺憾的是,當該方法具有多種返回類型時,此方便的功能通常無法自動檢測其中的大多數返回類型。下一節中介紹的方法將處理這些方案。
讓我們看看如何告訴 SwaggerUI 隨時顯示響應示例。[ProducesResponseType] 屬性附帶 Microsoft.AspNetCore.Mvc 命名空間,不是 Swashbuckle 的一部分。但是,由于我們將組件配置為考慮注釋,因此 SwaggerUI 將使用它來確定每個方法的響應類型并采取相應的行動。
與 [ProducesResponseType] 屬性一起使用的主要參數是響應類型和方法返回的狀態代碼。同樣,由于終結點可以返回不同的響應類型和狀態代碼,因此可以多次將其添加到每個方法中。我們已經知道 SwaggerUI 無法自動檢測 /Account/Register 和 /Account/Login 端點的返回類型,這使得它們成為此屬性的完美候選者。
打開 /控制器/帳戶控制器.cs 文件,并找到注冊操作方法。然后在現有屬性下方,在方法聲明之前添加以下屬性(粗體新行):
[HttpPost]
[ResponseCache(CacheProfileName="NoCache")]
[ProducesResponseType(typeof(string), 201)] ?
[ProducesResponseType(typeof(BadRequestObjectResult), 400)] ?
[ProducesResponseType(typeof(ProblemDetails), 500)] ?
? HTTP 狀態代碼 201 說明
? HTTP 狀態代碼 400 說明
? HTTP 狀態代碼 500 說明
使用以下屬性對 Login 操作方法執行相同的操作:
[HttpPost]
[ResponseCache(CacheProfileName="NoCache")]
[ProducesResponseType(typeof(string), 200)] ?
[ProducesResponseType(typeof(BadRequestObjectResult), 400)] ?
[ProducesResponseType(typeof(ProblemDetails), 401)] ?
? HTTP 狀態代碼 200 說明
? HTTP 狀態代碼 400 說明
? HTTP 狀態代碼 401 說明
若要測試我們所做的工作,請在調試模式下啟動項目,并查看 SwaggerUI 中“/帳戶/注冊”和“/帳戶/登錄終結點”面板的“響應”部分,以確保它們看起來像圖 11.6 中的那些。
圖 11.6 /帳戶/注冊返回類型的 JSON 示例
圖 11.6 中描述的屏幕截圖已被裁剪,因為在 HTTP 狀態代碼 400 的情況下返回的 BadRequestObjectResult 的 JSON 表示形式很長。但是這個數字應該讓我們了解我們做了什么。現在我們已經知道如何強制 SwaggerUI 提供響應類型的示例,我們已準備好掌握另一種良好做法:終結點分組。
如果 Web API 具有大量終結點,則將它們分組到與其角色/用途對應的部分中可能很有用。在我們的方案中,明智的做法是將身份驗證終結點、在棋盤游戲實體上運行的終結點等分組。我們可以說我們已經這樣做了,因為我們為每個組都使用了一個控制器,遵循 ASP.NET Core默認行為。正如我們從第 1 章開始就知道的那樣,ASP.NET Core 控制器允許我們對一組具有共同主題、含義或記錄類型的操作方法進行分組。我們在 MyBGList 方案中采用了此約定,對與棋盤游戲相關的終結點使用 BoardGamesController,對基于域的終結點使用 DomainsController 等。
這種方法由我們當前的 Open API 實現自動實施。如果我們查看 SwaggerUI 儀表板,我們會看到由與同一控制器相關的操作方法處理的 API 端點被分組,如圖 11.7 所示。
圖 11.7 控制器處理的終結點的 SwaggerUI 組名稱
我們可以猜到,這些組是由Swashbuckle自動生成的。此技巧是通過 tags 屬性將控制器的名稱添加到 swagger.json 文件中來執行的,該屬性旨在處理分組任務。
提示有關 Swagger 的標簽屬性的其他信息,請查看 http://mng.bz/5178。
要進行檢查,請單擊 SwaggerUI 主標題下方的超鏈接打開 swagger.json 文件,或導航到 https://localhost:40443/swagger/v1/swagger.json。/Account/Register 終結點的 tags 屬性位于文件開頭附近:
{
"openapi": "3.0.1",
"info": {
"title": "MyBGList",
"version": "1.0"
},
"paths": {
"/Account/Register": { ?
"post": {
"tags": [ ?
"Account"
],
? /帳戶/注冊端點說明
? “帳戶”標簽取自控制者的名稱
遺憾的是,此自動行為不適用于最小 API 方法,因為它們不屬于控制器。Swashbuckle唯一能做的就是將它們全部列出在一個通用組中,并帶有應用程序的名稱(在我們的場景中是MyBGList),如圖11.8所示。
圖 11.8 最小 API 處理的終結點的通用組
這種回退行為的結果還不錯。但是,由于我們當前的最小 API 端點處理不同的相關任務集,因此我們可能希望找到一種更好的方法來對它們進行分組。
如果我們想改進 Swashbuckle 的默認標記行為,我們可以使用 [SwaggerOperation] 屬性提供的 Tags 屬性來覆蓋它。讓我們測試一下。假設我們要將三個端點分組,從 /auth/ 段開始,在一個名為“Auth”的新 SwaggerUI 部分中。打開程序.cs文件;找到這些方法;并對其現有的 [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 方法執行相同的操作,然后在調試模式下運行項目以查看新的身份驗證組(圖 11.9)。
圖 11.9 與授權相關的終結點的新身份驗證組
我們可以使用相同的技術來覆蓋屬于控制器的操作方法的 Swashbuckle 默認行為。每當 Tags 參數與自定義值一起存在時,Swashbuckle 將始終使用它來填充 swagger.json 文件,而不是回退到控制器或操作方法的名稱。
注意如果我們想自定義端點的組名稱而不是使用控制器名稱,則此覆蓋功能會很方便。但是,請務必記住,這種級別的自定義違反了 ASP.NET Core 強制執行的最重要的開發最佳實踐之一:配置設計范例約定,該范例旨在限制開發人員需要做出的決策數量以及源代碼量,而不會失去靈活性。出于這個原因,我強烈建議遵守控制器的 ASP.NET 核心分組和標記約定,將 Tags 屬性自定義做法保留為最小 API 方法和有限數量的異常。
ApiExplorer 服務 Swashbuckle 用于在我們項目的源代碼中自動查找所有控制器的操作方法和最小 API 方法,并在 swagger.json 文件中描述它們,在大多數情況下是一個很棒的功能。但我們可能想要隱藏一些我們不想向觀眾展示的方法(或整個控制器)。
在我們當前的情況下,這種情況可能適用于 SeedController,其中包含幾個旨在由管理員調用和知道的方法。從 swagger.json 文件中排除這些操作可能是明智的,這也將把它們從 SwaggerUI 中刪除。
為了實現這個結果,我們可以使用 [ApiExplorerSettings] 屬性,其中包含一個有用的 IgnoreApi 屬性。此屬性可應用于任何控制器、操作方法或最小 API 方法。讓我們用它來從 swagger.json 文件中排除我們的 SeedController。打開 /Controllers/SeedController.cs 文件,并按以下方式將該屬性應用于類聲明:
[Authorize(Roles=RoleNames.Administrator)]
[ApiExplorerSettings(IgnoreApi=true)] ?
[Route("[controller]/[action]")]
[ApiController]
public class SeedController : ControllerBase
? 從 swagger.json 文件中排除控制器
要測試我們所做的工作,請在調試模式下運行項目;導航到 招搖UI主儀表板;并確認我們之前訪問該頁面時存在的整個種子部分不再可見。
警告請務必了解,IgnoreApi=true 設置只會阻止控制器及其操作方法包含在 swagger.json 文件中;它不會阻止用戶調用(并可能執行)它。這就是為什么我們還通過使用第 9 章中的 [Authorize] 屬性將其限制為管理員。
到目前為止,我們已經學習了如何使用 XML 文檔或數據注釋屬性處理各個方法來配置 swagger.json 文件的內容和生成的 SwaggerUI 布局。在下一節中,我們將了解如何基于Swashbuckle過濾器的使用,使用更加結構化和集中的方法執行這些類型的更改。
正如我們從第6章中知道的,Swashbuckle公開了一個方便的過濾器管道,它與swagger.json文件生成過程掛鉤,允許我們創建和添加自己的過濾器,以根據需要自定義文件的內容。要實現過濾器,我們需要做的就是擴展 Swashbuckle 提供的內置接口之一,每個接口都提供了一個方便的 Apply 方法來自定義自動生成的文件。以下是Swashbuckle提供的過濾器接口的完整列表:
我們在第6章中使用了這個功能,當時我們添加了SortColumnFilter和SortOrderFilter(擴展IParameterFilter接口),為SwaggerUI提供了一些基于正則表達式的模式來驗證一些輸入參數。Swashbuckle 使用這些過濾器,我們在 /Swagger/ 文件夾中實現了這些過濾器,然后將其添加到 Program.cs 文件中的 Swashbuckle 過濾器管道中,有選擇地將模式 JSON 鍵添加到使用 [SortColumnValidator] 和 [SortOrderValidator] 自定義屬性修飾的所有參數。我們所做的是一個簡單而完美的過濾器管道如何工作的例子。
在本節中,我們將學習如何使用 Swashbuckle 提供的其他過濾器接口來進一步配置自動生成的 swagger.json 文件,從而相應地更新 SwaggerUI。與往常一樣,我們假設我們被要求實現一些可信的新功能請求。
在第 9 章中,當我們學習如何使用 [Authorize] 屬性時,我們在現有的 Swagger 配置設置中添加了安全定義和安全要求。我們這樣做是為了使“授權”按鈕顯示在 SwaggerUI 中,現在允許我們設置持有者令牌并測試受授權限制的終結點。但是這個添加有一個我們當時故意忽略的次要效果:它還在我們的所有端點旁邊添加了一個奇怪的掛鎖圖標,如圖 11.10 所示。
圖11.10 招搖UI中的掛鎖圖標
單擊這些圖標時,將顯示“授權”彈出窗口,就像我們在第 9 章中多次使用的頁面右上角附近的“授權”按鈕一樣。但是,無論終結點的授權要求如何,掛鎖圖標始終顯示為打開狀態,這不是我們預期的行為。理想情況下,我們希望掛鎖圖標僅出現在需要某種授權的端點旁邊。下一節將介紹如何實現此結果。
在深入研究源代碼之前,讓我們看看掛鎖圖標功能在后臺是如何工作的。如果端點具有某種安全要求(換句話說,如果需要某種級別的授權),則 SwaggerUI 會自動呈現這些圖標。此信息取自 swagger.json 文件,該文件為這些端點分配安全屬性:
"security": [{
"Bearer": [ ]
}]
在第 9 章中,當我們配置 Swagger 以支持基于令牌的授權機制時,我們使用項目程序.cs文件中的專用配置選項向 swagger.json 文件生成器服務添加了全局安全要求:
// ...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
由于這一全局安全要求,我們在所有端點上都設置了安全屬性,因為它們被認為受到基于令牌的授權方案的保護,即使它們不是。為了修補此行為,我們需要將該全局要求替換為特定規則,該規則將僅針對受此類方案限制的方法觸發。
最有效的方法是使用 IOperationFilter 接口創建自定義篩選器,該接口可以擴展 swagger.json 生成器服務,以便為受影響的操作提供其他信息(或修改現有/默認信息)。在我們的方案中,我們需要一個篩選器,該篩選器可以設置當前分配給所有操作的相同安全要求,但僅適用于應用了 [Authorize] 屬性的操作。若要實現此要求,請在 /Swagger/ 根級文件夾中創建一個新的 AuthRequirementsFilter.cs 類文件,并使用以下列表中的源代碼填充其內容。
清單 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[]{}
}
}
};
}
}
}
? 檢查 [授權] 屬性
? 如果不存在,則不執行任何操作
? 如果存在,則確保操作安全
如我們所見,我們的新操作過濾器在內部執行當前在 Program.cs 文件中完成的相同任務。唯一的區別是它跳過了沒有 [Authorize] 屬性的操作,因為我們不希望它們在 swagger.json 文件(或掛鎖圖標)中記錄任何安全要求。
現在我們有了 AuthRequirementsFilter,我們需要更新 Swagger 生成器配置選項以使用它,而不是我們當前擁有的全局縮放要求。打開程序.cs文件;向下滾動到 AddSwaggerGen 方法;并將現有的 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 存儲庫中,我注釋掉了以前的代碼行,而不是刪除它們。
為了測試我們所做的工作,我們可以在調試模式下啟動項目,并再次查看以前具有掛鎖圖標的相同端點(圖 11.11)。正如我們所看到的,掛鎖圖標對于可公開訪問的端點已經消失,但對于那些需要某種授權的端點來說,掛鎖圖標仍然存在。我們的自定義IOperationFilter允許我們做我們想做的事情。
圖11.11 SwaggerUI中掛鎖圖標的新行為
假設我們要在 SwaggerUI 中更改應用程序的標題,該標題當前設置為 MyBGList,根據 Swashbuckle 的默認行為,與 ASP.NET Core 項目同名。如果我們查看 swagger.json 文件,我們可以看到托管該值的 JSON 屬性稱為 title,它是在文檔級別設置的父信息屬性的一部分:
{
"openapi": "3.0.1",
"info": {
"title": "MyBGList Web API",
"version": "1.0"
},
這意味著,如果我們想覆蓋它,我們需要創建一個自定義過濾器,允許我們自定義 swagger.json 文件的文檔級參數。實現目標的最有效方法是創建自定義 DocumentFilter(通過擴展 IDocumentFilter 接口)并將其添加到篩選器管道中。在 /Swagger/ 根級文件夾中創建一個新的 CustomDocumentFilter.cs 文件,并使用以下清單的內容填充該文件。
清單 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"; ?
}
}
}
? 設置自定義標題
然后,通過按以下方式更新 Program.cs 文件,將文件掛接到 Swashbuckle 的過濾器管道(新行以粗體顯示):
options.OperationFilter<AuthRequirementFilter>(); ?
options.DocumentFilter<CustomDocumentFilter>(); ?
? 現有過濾器
? 新過濾器
要測試我們所做的工作,請在調試模式下啟動項目,并查看 SwaggerUI 儀表板的新標題(圖 11.12)。
圖 11.12 SwaggerUI 標題隨自定義文檔篩選器更改
不錯。讓我們看看我們可以用IRequestBodyFilter接口做什么。
假設我們希望在用戶需要向我們的 Web API 發送密碼時為其設置自定義警告文本。通過查看我們當前的端點,我們可以很容易地確定,目前,這樣的警告只會影響賬戶控制器的注冊和登錄方法。通過考慮這一事實,我們可以使用 XML 文檔注釋(第 11.2.3 節)或 [SwaggerOperation] 屬性(第 11.2.4 節)將此消息插入到操作的“摘要”或“說明”屬性中,正如我們之前所學習的那樣。或者,我們可以通過使用 <param> XML 標記或 [SwaggerParameter] 屬性在參數級別工作。
這兩種方法都有一個不平凡的缺點。如果我們將來添加接受密碼的端點,我們還必須在那里重復 XML 標記或數據注釋屬性,這意味著復制大量代碼 — 除非我們忘記這樣做,因為這樣的方法很容易出錯。
為了克服這些問題,最好找到一種方法來集中這種行為,方法是創建一個新的過濾器并將其添加到Swashbuckle的管道中。我們需要確定在可用過濾器接口中擴展哪個過濾器接口。理想情況下,IRequestBodyFilter 接口將是一個不錯的選擇,考慮到我們希望定位名稱等于“密碼”的特定參數,該參數目前(并且可能總是)隨 POST 請求一起出現。讓我們繼續這種方法。在 /Swagger/ 根文件夾中創建一個新的 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!";
}
}
}
}
? 輸入參數名稱
? 名稱檢查(基元類型)
? 屬性檢查(復雜型)
通過查看此代碼,我們檢查輸入參數名稱是否等于“password”(對于基元類型)或包含具有該名稱的屬性(對于復雜類型,例如 DTO)。現在我們有了過濾器,我們需要通過以下方式在 Swashbuckle 的過濾器管道中注冊它,在我們之前添加的 AuthRequirementsFilter 和 CustomDocumentFilter 下面:
options.OperationFilter<AuthRequirementFilter>(); ?
options.DocumentFilter<CustomDocumentFilter>(); ?
options.RequestBodyFilter<PasswordRequestFilter>(); ?
? 現有過濾器
? 新過濾器
與往常一樣,我們可以通過在調試模式下執行項目并檢查 SwaggerUI 中的預期更改來測試我們所做的工作(圖 11.13)。
圖 11.13 密碼請求篩選器添加的新描述
這些變化似乎奏效了。由于這種方法,我們的密碼警告消息將涵蓋我們的兩個現有終結點以及在其請求正文中接受密碼參數的任何未來終結點。
注意如果我們想將覆蓋范圍擴展到查詢字符串參數,我們需要添加另一個擴展 IParameterFilter 接口并執行相同工作的篩選器,然后使用 ParameterFilter 幫助程序方法將其注冊到 Program.cs 文件中。
現在要完成我們的過濾器概述,剩下要做的就是 ISchemaFilter 接口。
讓我們再看一下我們在第 6 章中實現的 SortColumnFilter 和 SortOrderFilter 類。擴展 IParameterFilter 接口是個好主意,因為我們只需要處理來自查詢字符串的一些特定輸入參數。換句話說,我們希望將模式鍵添加到 swagger.json 文件中這些參數的 JSON 模式中,從用于標識它們的相同數據注釋屬性 [SortColumnAttribute] 或 [SortOrderAttribute] 中獲取值。
假設我們要擴展該方法以實現一個新的過濾器,該過濾器能夠將任意 JSON 鍵(和值)添加到任何屬性,無論是請求參數、響應參數還是其他任何內容。在本節中,我們將通過實現以下內容來實現這一目標:
ISchemaFilter 接口是處理此任務的完美選擇,因為它專門設計用于對 Swashbuckle 的 SwaggerGen 服務生成的 JSON 模式進行后修改,用于控制器操作和最小 API 方法公開的每個輸入和輸出參數以及復雜類型。現在我們已經選擇了我們的路線,讓我們把它付諸實踐。
實現自定義鍵值屬性
在 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; }
}
}
請注意,我們已經使用 [AttributeUsage] 屬性裝飾了我們的新類,它允許我們指定屬性的使用。我們這樣做有兩個重要原因:
現在我們有了屬性,我們準備實現將處理它的篩選器。
實現自定義鍵值篩選器
在 /Swagger/ 文件夾中添加新的 CustomKeyValueFilter.cs 類文件。新類必須實現 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)
);
}
}
}
}
}
? 確定我們是在處理屬性還是參數
? 檢查參數是否具有屬性
? 如果存在一個或多個屬性,則相應地采取行動
此代碼應易于理解。我們將使用語言集成查詢 (LINQ) 檢查 ISchemaFilter 接口提供的上下文,以確定我們的屬性或參數是否應用了一個或多個 [CustomKeyValue] 屬性并采取相應的操作。我們現在需要做的就是將新的過濾器添加到 Swashbuckle 的過濾器管道中。與往常一樣,我們可以通過以下方式更新程序.cs文件:
options.OperationFilter<AuthRequirementFilter>(); ?
options.DocumentFilter<CustomDocumentFilter>(); ?
options.RequestBodyFilter<PasswordRequestFilter>(); ?
options.SchemaFilter<CustomKeyValueFilter>(); ?
? 現有過濾器
? 新過濾器
現在我們的兩個類已經準備就緒,并且過濾器已經注冊,我們可以通過將 [CustomKeyValue] 屬性應用于現有 DTO 之一的屬性來測試 [CustomKeyValue] 屬性。讓我們選擇帳戶控制器的登錄操作方法使用的登錄DTO。打開 /DTO/LoginDTO.cs 文件,并按以下方式將其中幾個屬性應用于現有 UserName 屬性:
[Required]
[MaxLength(255)]
[CustomKeyValue("x-test-1", "value 1")] ?
[CustomKeyValue("x-test-2", "value 2")] ?
public string? UserName { get; set; }
? 第一個自定義鍵值屬性
接下來,在調試模式下運行項目,訪問 SwaggerUI 儀表板,然后單擊主標題下方的 swagger.json 文件鏈接(圖 11.14),在新選項卡中打開它。
圖 11.14 swagger.json 文件 URL
使用瀏覽器的搜索功能在 swagger.json 文件中查找“x-test-”字符串。如果我們正確執行了所有操作,我們應該在 LoginDTO 的用戶名屬性的 JSON 架構中看到此字符串的兩個條目,如以下列表所示。
清單 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"
}
}
? 自定義鍵/值對
目前為止,一切都好。讓我們執行另一個測試,以確保相同的邏輯適用于基元類型的標準 GET 參數。打開 /Controllers/BoardGamesController.cs 文件,向下滾動到 Get 操作方法,接受 int 類型的單個 id 參數,然后按以下方式向該參數添加 [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
)
? 添加新的 [自定義鍵值] 屬性
接下來,在調試模式下運行項目,像我們之前一樣訪問 swagger.json 文件內容,并再次檢查其中是否存在“x-test-”字符串。這一次,我們應該找到三個條目,最后一個是我們添加的條目(請參閱下面的列表)。
清單 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" ?
}
}
],
? 自定義鍵/值對
我們的自定義鍵/值功能似乎運行良好。最后一個任務結束了我們了解 Swashbuckle 的過濾器管道和 API 文檔概述的旅程。現在唯一要做的就是學習將我們的 Web API 項目部署到生產環境中,這是第 12 章的主題。
是時候用我們的產品所有者給出的一系列新的假設任務分配來挑戰自己了。與往常一樣,處理這些任務將極大地幫助我們記住和記住本章中涵蓋的概念和學到的技術。
注意練習的解決方案可在 GitHub 的 /Chapter_11/Exercises/ 文件夾中找到。若要測試它們,請將 MyBGList 項目中的相關文件替換為該文件夾中的文件,然后運行應用。
使用 XML 文檔方法,按以下方式描述 GET /Domains 終結點:
提示可以使用 <備注> XML 元素添加說明。
使用 Swashbuckle 注釋方法,按以下方式描述 GET /Mechanics 終結點:
使用 [ApiExplorerSettings] 屬性從 swagger.json 文件中隱藏以下端點:
然后,確保這些終結點也從 SwaggerUI 儀表板中排除。
擴展 IRequestBodyFilter 接口以實現新的 UsernameRequestFilter,該篩選器將向名稱等于“用戶名”的任何輸入參數添加以下說明。然后在 Swashbuckle 的過濾器管道中注冊新過濾器,并通過檢查 POST 帳戶/登錄和 POST 帳戶/注冊端點使用的 sername 參數在 SwaggerUI 儀表板中對其進行測試。
警告請務必記住您的用戶名,因為您需要它來執行登錄!
使用 [自定義鍵值] 屬性將以下鍵/值對添加到 DELETE 機制端點的現有 id 參數:
然后檢查 swagger.json 文件中終結點的 JSON 架構中是否存在新屬性。
們現在正處于可以構建一個 Web 應用程序的階段,該應用程序可以使用不同的方法和數據管理一系列 HTTP 請求。 這很有用,特別是當我們為微服務構建服務器時。 然而,我們也希望非程序員能夠與我們的應用程序交互來使用它。 為了使非程序員能夠使用我們的應用程序,我們必須創建一個圖形用戶界面。 不過,必須注意的是,本章包含的 Rust 內容并不多。 這是因為存在其他語言來呈現圖形用戶界面。 我們將主要使用 HTML、JavaScript 和 CSS。 這些工具已經成熟并廣泛用于前端 Web 開發。 雖然我個人很喜歡 Rust(否則我不會寫一本關于它的書),但我們必須使用正確的工具來完成正確的工作。 在撰寫本書時,我們可以使用 Yew 框架在 Rust 中構建前端應用程序。 然而,能夠將更成熟的工具融合到我們的 Rust 技術堆棧中是一項更有價值的技能。
本章將涵蓋以下主題:
使用 Rust 提供 HTML、CSS 和 JavaScript 服務
構建連接到 Rust 服務器的 React 應用程序
將我們的 React 應用程序轉換為要安裝在計算機上的桌面應用程序
在上一版本(Rust Web 編程:使用 Rust 編程語言開發快速、安全的 Web 應用程序的實踐指南)中,我們只是直接從 Rust 提供前端資產。 然而,由于反饋和修訂,這不能很好地擴展,導致大量重復。 由于使用這種方法的非結構化性質,由 Rust 直接提供的原始 HTML、CSS 和 JavaScript 也容易出錯,這就是為什么在第二版中,我們將介紹 React 并簡要介紹如何提供前端資產 直接使用 Rust。 到本章結束時,您將能夠在沒有任何依賴的情況下編寫基本的前端圖形界面,并了解低依賴前端解決方案和完整前端框架(例如 React)之間的權衡。 您不僅會了解何時使用它們,而且還能夠在項目需要時實施這兩種方法。 因此,您將能夠為正確的工作選擇正確的工具,并在后端使用 Rust 并在前端使用 JavaScript 構建端到端產品。
在上一章中,我們以 JSON 的形式返回了所有數據。 在本節中,我們將返回 HTML 數據供用戶查看。 在此 HTML 數據中,我們將具有按鈕和表單,使用戶能夠與我們在上一章中定義的 API 端點進行交互,以創建、編輯和刪除待辦事項。 為此,我們需要構建自己的應用程序視圖模塊,該模塊采用以下結構:
views
├── app
│ ├── items.rs
│ └── mod.rs
在我們的 items.rs 文件中,我們將定義顯示待辦事項的主視圖。 但是,在此之前,我們應該探索在 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 結構,該結構具有 HTML 內容類型和 <h1>Items</h1> 主體。 要將 HttpResponse 傳遞到應用程序中,我們必須在 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));
}
在這里,我們可以看到,我們只是為應用程序定義了一條路由,而不是構建服務。 這是因為這是登陸頁面。 如果我們要定義服務而不是路由,我們將無法在沒有前綴的情況下定義服務的視圖。
一旦我們定義了app_views_factory,我們就可以在views/mod.rs 文件中調用它。 然而,首先,我們必須在views/mod.rs文件的頂部定義app模塊:
mod app;
一旦我們定義了應用程序模塊,我們就可以在同一文件中的views_factory函數中調用應用程序工廠:
app::app_views_factory(app);
現在我們的 HTML 服務視圖是我們應用程序的一部分,我們可以運行它并在瀏覽器中調用主 URL,給出以下輸出:
圖 5.1 – 第一個呈現的 HTML 視圖
我們可以看到我們的 HTML 已渲染! 根據圖 5.1 中的內容,我們可以推斷出我們可以在響應正文中返回一個字符串,其中包含以下內容:
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body("<h1>Items</h1>")
如果字符串是 HTML 格式,則會呈現 HTML。 根據這個啟示,您認為我們如何從 Rust 服務器提供的 HTML 文件中渲染 HTML? 在繼續之前,想一想——這將鍛煉你解決問題的能力。
如果我們有一個 HTML 文件,我們只需將該 HTML 文件準備為一個字符串并將該字符串插入到 HttpResponse 的正文中即可呈現它。 是的,就是這么簡單。 為了實現這一目標,我們將構建一個內容加載器。
要構建基本的內容加載器,首先在views/app/content_loader.rs文件中構建HTML文件讀取函數:
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
}
我們在這里要做的就是返回一個字符串,因為這就是我們響應正文所需的全部內容。 然后,我們必須在views/app/mod.rs文件中使用mod content_loader定義加載器; 文件頂部的行。
現在我們有了加載功能,我們需要一個 HTML 目錄。 這可以與稱為 templates 的 src 目錄一起定義。 在 templates 目錄中,我們可以添加一個名為 templates/main.html 的 HTML 文件,其中包含以下內容:
<!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 標簽具有與我們之前呈現的內容相同的內容 - 即 <h1>To Do Items</h1>。 然后,我們有一個 head 標簽,它定義了一系列元標簽。 我們可以看到我們定義了視口。 這告訴瀏覽器如何處理頁面內容的尺寸和縮放。 縮放很重要,因為我們的應用程序可以通過一系列不同的設備和屏幕尺寸來訪問。 通過這個視口,我們可以將頁面的寬度設置為與設備屏幕相同的寬度。 然后,我們可以將訪問的頁面的初始比例設置為1.0。 轉到 httpEquiv 標簽,我們將其設置為 X-UA-Compatible,這意味著我們支持舊版瀏覽器。 最終標簽只是搜索引擎可以使用的頁面的描述。 我們的標題標簽確保待辦事項應用程序顯示在瀏覽器標簽上。 這樣,我們的正文中就有了標準的標題標題。
現在我們已經定義了 HTML 文件,我們必須加載并提供它。 回到我們的 src/views/app/items.rs 文件,我們必須加載 HTML 文件并使用以下代碼提供服務:
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)
}
如果我們運行我們的應用程序,我們將得到以下輸出:
圖 5.2 – 加載 HTML 頁面的視圖
在圖 5.2 中,我們可以看到輸出與之前相同。 這并不奇怪; 但是,我們必須注意到,圖 5.2 中的選項卡現在顯示了“To Do App”,這意味著 HTML 文件中的元數據正在加載到視圖中。 沒有什么可以阻止我們充分利用 HTML 文件。 現在我們的 HTML 文件已經提供,我們可以繼續我們的下一個目標,即向我們的頁面添加功能。
如果前端用戶無法對我們的待辦事項狀態執行任何操作,那么這對前端用戶來說就沒有用。 在修改之前,我們需要通過查看下圖來了解 HTML 文件的布局:
圖 5.3 – HTML 文件的一般布局
在圖 5.3 中,我們可以看到我們可以在標頭中定義元標記。 然而,我們也可以看到我們可以在標題中定義樣式標簽。 在標題下方的樣式標簽中,我們可以將 CSS 插入到樣式中。 在主體下方,還有一個腳本部分,我們可以在其中注入 JavaScript。 該 JavaScript 在瀏覽器中運行并與正文中的元素交互。 由此,我們可以看到,提供加載了 CSS 和 JavaScript 的 HTML 文件提供了一個功能齊全的前端單頁應用程序。 至此,我們可以反思一下本章的介紹。 雖然我喜歡 Rust,并且強烈希望告訴你用它來編寫所有內容,但這對于軟件工程中的任何語言來說都不是一個好主意。 現在,我們可以輕松地使用 JavaScript 提供功能性前端視圖,使其成為滿足您前端需求的最佳選擇。
現在我們知道了將 JavaScript 插入到 HTML 文件中的位置,我們可以測試我們的方向了。 在本節的其余部分中,我們將在 HTML 正文中創建一個按鈕,將其融合到 JavaScript 函數,然后讓瀏覽器在按下該按鈕時打印出帶有輸入消息的警報。 這對我們的后端應用程序沒有任何作用,但它將證明我們對 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>
在我們的正文部分,我們可以看到我們定義了一個輸入和一個按鈕。 我們為輸入和按鈕屬性提供唯一的 ID 名稱。 然后,我們使用按鈕的 ID 添加事件監聽器。 之后,我們將 postAlert 函數綁定到該事件偵聽器,以便在單擊按鈕時觸發。 當我們觸發 postAlert 函數時,我們使用其 ID 獲取輸入并打印出警報中的輸入值。 然后,我們將input的值設置為null,以便用戶可以填寫另一個要處理的值。 提供新的 main.html 文件,在輸入中進行測試,然后單擊按鈕將產生以下輸出:
圖 5.4 – 連接到 JavaScript 中的警報時單擊按鈕的效果
我們的 JavaScript 不必停止讓元素在主體中交互。 我們還可以使用 JavaScript 對后端 Rust 應用程序執行 API 調用。 然而,在我們匆忙將整個應用程序寫入 main.html 文件之前,我們必須停下來思考一下。 如果我們這樣做,main.html 文件就會膨脹成一個巨大的文件。 調試起來會很困難。 此外,這可能會導致代碼重復。 如果我們想在其他視圖中使用相同的 JavaScript 怎么辦? 我們必須將其復制并粘貼到另一個 HTML 文件中。 這無法很好地擴展,如果我們需要更新某個函數,我們可能會面臨忘記更新某些重復函數的風險。 這就是 React 等 JavaScript 框架派上用場的地方。 我們將在本章后面探討 React,但現在,我們將通過提出一種將 JavaScript 與 HTML 文件分離的方法來完成我們的低依賴前端。
必須警告的是,我們實際上是使用此 JavaScript 手動動態重寫 HTML。 人們可以將其描述為“hacky”解決方案。 然而,在探索 React 之前,重要的是要先掌握我們的方法,才能真正體會到不同方法的好處。 在繼續下一部分之前,我們必須在 src/views/to_do/create.rs 文件中重構我們的創建視圖。 這是一個很好的機會來回顧我們在前幾章中開發的內容。 您必須本質上轉換創建視圖,以便它返回待辦事項的當前狀態而不是字符串。 嘗試此操作后,解決方案應如下所示:
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())
}
現在,我們所有的待辦事項均已更新并正常運行。 現在我們可以進入下一部分,我們將讓前端調用后端。
完成本節后,我們將擁有一個不太漂亮但功能齊全的主視圖,我們可以在其中使用 JavaScript 調用 Rust 服務器來添加、編輯和刪除待辦事項。 但是,您可能還記得,我們沒有添加刪除 API 端點。 要將 JavaScript 注入到 HTML 中,我們必須執行以下步驟:
創建刪除項目 API 端點。
添加 JavaScript 加載功能,并將 HTML 數據中的 JavaScript 標簽替換為主項 Rust 視圖中加載的 JavaScript 數據。
在 HTML 文件中添加 JavaScript 標簽,并在 HTML 組件中添加 ID,以便我們可以在 JavaScript 中引用組件。
在 JavaScript 中為我們的待辦事項構建一個渲染函數,并通過 ID 將其綁定到我們的 HTML。
在 JavaScript 中構建一個 API 調用函數來與后端對話。
在 JavaScript 中構建獲取、刪除、編輯和創建函數,供我們的按鈕使用。
讓我們詳細看看這一點。
現在添加刪除 API 端點應該很簡單。 如果您愿意,建議您自己嘗試并實現此視圖,因為您現在應該已經熟悉此過程了:
如果您遇到困難,我們可以通過將以下第三方依賴項導入到views/to_do/delete.rs 文件中來實現此目的:
use actix_web::{web, HttpResponse};
use serde_json::value::Value;
use serde_json::Map;
這些并不新鮮,您應該熟悉它們并知道我們需要在哪里使用它們。
然后,我們必須使用以下代碼導入結構和函數:
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 模塊來構建我們的待辦事項。 通過我們的 json_serialization 模塊,我們可以看到我們正在接受 ToDoItem 并返回 ToDoItems。 然后,我們使用 process_input 函數執行項目的刪除。 我們也不希望任何可以訪問我們頁面的人刪除我們的項目。 因此,我們需要 JwToken 結構。 最后,我們使用 read_file 函數讀取項目的狀態。
現在我們已經擁有了所需的一切,我們可以使用以下代碼定義刪除視圖:
pub async fn delete(to_do_item: web::Json<ToDoItem>,
token: JwToken) -> HttpResponse {
. . .
}
在這里,我們可以看到我們已經接受了 JSON 形式的 ToDoItem,并且我們已經為視圖附加了 JwToken,以便用戶必須有權訪問它。 此時,我們只有 JwToken 附加一條消息; 我們將在第 7 章“管理用戶會話”中管理 JwToken 的身份驗證邏輯。
在刪除視圖中,我們可以通過使用以下代碼讀取 JSON 文件來獲取待辦事項的狀態:
let state: Map<String, Value>=read_file("./state.json");
然后,我們可以檢查具有該標題的項目是否處于該狀態。 如果不是,那么我們返回一個未找到的 HTTP 響應。 如果是,我們就會傳遞狀態,因為我們需要標題和狀態來構建項目。 我們可以使用以下代碼來實現這種檢查和狀態提取:
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))
}
}
現在我們有了待辦事項的狀態和標題,我們可以構建我們的項目并使用刪除命令將其傳遞到 process_input 函數。 這將從 JSON 文件中刪除我們的項目:
let existing_item=to_do_factory(to_do_item.title.as_ str(),
status.clone());
process_input(existing_item, "delete". to_owned(),
&state);
請記住,我們為 ToDoItems 結構實現了 Responder 特征,并且 ToDoItems::get_state() 函數返回一個 ToDoItems 結構,其中填充了 JSON 文件中的項目。 因此,我們可以從刪除視圖中得到以下返回語句:
return HttpResponse::Ok().json(ToDoItems::get_state())
現在我們的刪除視圖已經定義了,我們可以將其添加到我們的 src/views/to_do/mod.rs 文件中,導致我們的視圖工廠如下所示:
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,我們可以看到我們擁有管理待辦事項所需的所有視圖。 如果我們將該模塊從應用程序中彈出并將其插入另一個應用程序中,我們將立即看到我們正在刪除和添加的內容。
將刪除視圖完全集成到應用程序中后,我們可以繼續第二步,即構建 JavaScript 加載功能。
現在我們的所有端點都已準備就緒,我們必須重新訪問我們的主應用程序視圖。 在上一節中,我們確定 <script> 部分中的 JavaScript 可以正常工作,即使它只是一個大字符串的一部分。 為了使我們能夠將 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)
}
從上一步中的 items 函數中,我們可以看到我們需要在根目錄中構建一個名為 JavaScript 的新目錄。 我們還必須在其中創建一個名為 main.js 的文件。 通過對應用程序視圖的更改,我們還必須通過添加以下代碼來更改 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 調用插入已完成的待辦事項的位置。
然后,我們將從 API 調用中插入 ID 為“pendingItems”的待處理項目。 之后,我們必須定義一個帶有文本和按鈕的輸入。 這將供我們的用戶創建一個新項目。
構建渲染 JavaScript 函數
現在我們的 HTML 已經定義好了,我們將在 javascript/main.js 文件中定義邏輯:
我們要構建的第一個函數將在主頁面上呈現所有待辦事項。 必須注意的是,這是 javascript/main.js 文件中代碼中最復雜的部分。 我們本質上是在編寫 JavaScript 代碼來編寫 HTML 代碼。 稍后,在創建 React 應用程序部分中,我們將使用 React 框架來代替執行此操作的需要。 現在,我們將構建一個渲染函數來創建一個項目列表。 每個項目都采用以下 HTML 形式:
<div>
<div>
<p>learn to code rust</p>
<button id="edit-learn-to-code-rust">
edit
</button>
</div>
</div>
我們可以看到待辦事項的標題嵌套在段落 HTML 標記中。 然后,我們有一個按鈕。 回想一下,HTML 標記的 id 屬性必須是唯一的。 因此,我們根據按鈕將要執行的操作以及待辦事項的標題來構造此 ID。 這將使我們能夠使用事件偵聽器將執行 API 調用的函數綁定到這些 id 屬性。
為了構建我們的渲染函數,我們必須傳入要渲染的項目、我們要執行的處理類型(即編輯或刪除)、我們所在的 HTML 部分的元素 ID 將渲染這些項目,以及我們將綁定到每個待辦事項按鈕的功能。 該函數的概要定義如下:
function renderItems(items, processType,
elementId, processFunction) {
. . .
}
在 renderItems 函數中,我們可以首先構建 HTML 并使用以下代碼循環遍歷我們的待辦事項:
let itemsMeta=[];
let placeholder="<div>"
for (let i=0; i < items.length; i++) {
. . .
}
placeholder +="</div>"
document.getElementById(elementId).innerHTML=placeholder;
在這里,我們定義了一個數組,用于收集有關我們為每個待辦事項生成的待辦事項 HTML 的元數據。 它位于 itemsMeta 變量下,稍后將在 renderItems 函數中使用,以使用事件偵聽器將 processFunction 綁定到每個待辦事項按鈕。 然后,我們在占位符變量下定義包含流程所有待辦事項的 HTML。 在這里,我們從 div 標簽開始。 然后,我們循環遍歷這些項目,將每個項目的數據轉換為 HTML,然后用結束 div 標簽結束 HTML。 之后,我們將構建的 HTML 字符串(稱為占位符)插入到 innerHTML 中。 頁面上的 innerHTML 位置是我們希望看到構建的待辦事項的位置。
在循環內,我們必須使用以下代碼構建單個待辦事項 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});
在這里,我們從正在循環的項目中提取項目的標題。 然后,我們為將用于綁定到事件偵聽器的項目定義 ID。 請注意,我們將所有空格替換為 -。 現在我們已經定義了標題和 ID,我們將一個帶有標題的 div 添加到占位符 HTML 字符串中。 我們還添加一個帶有 placeholderId 的按鈕,然后用一個 div 來完成它。 我們可以看到,我們對 HTML 字符串的添加是以 ; 結束的。 然后,我們將 placeholderId 和 title 添加到 itemsMeta 數組中以供稍后使用。
接下來,我們循環 itemsMeta,使用以下代碼創建事件偵聽器:
. . .
placeholder +="</div>"
document.getElementById(elementId).innerHTML=placeholder;
for (let i=0; i < itemsMeta.length; i++) {
document.getElementById(
itemsMeta[i]["id"]).addEventListener(
"click", processFunction);
}
}
現在,如果單擊我們在待辦事項旁邊創建的按鈕,則 processFunction 將觸發。 我們的函數現在呈現這些項目,但我們需要使用 API 調用函數從后端獲取它們。 我們現在來看看這個。
現在我們有了渲染函數,我們可以看看我們的 API 調用函數:
首先,我們必須在 javascript/main.js 文件中定義 API 調用函數。 該函數接受一個 URL,它是 API 調用的端點。 它還采用一個方法,該方法是 POST、GET 或 PUT 字符串。 然后,我們必須定義我們的請求對象:
function apiCall(url, method) {
let xhr=new XMLHttpRequest();
xhr.withCredentials=true;
然后,我們必須在 apiCall 函數內定義事件監聽器,該函數在調用完成后使用返回的 JSON 呈現待辦事項:
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 調用的響應。 我們還可以看到,我們傳入了 editItem 函數,這意味著當單擊待處理項目旁邊的按鈕時,我們將觸發編輯函數,將該項目轉換為已完成項目。 考慮到這一點,如果單擊屬于已完成項目的按鈕,則會觸發 deleteItem 函數。 現在,我們將繼續構建 apiCall 函數。
之后,我們必須構建 editItem 和 deleteItem 函數。 我們還知道,每次調用 apiCall 函數時,都會渲染項目。
現在我們已經定義了事件監聽器,我們必須使用方法和 URL 準備 API 調用對象,定義標頭,然后返回請求對象以便我們在需要時發送:
xhr.open(method, url);
xhr.setRequestHeader('content-type',
'application/json');
xhr.setRequestHeader('user-token', 'token');
return xhr
}
現在,我們可以使用 apiCall 函數對應用程序的后端執行調用,并在 API 調用后使用項目的新狀態重新渲染前端。 這樣,我們就可以進入最后一步,在這里我們將定義對待辦事項執行創建、獲取、刪除和編輯功能的函數。
請注意,標頭只是對后端中硬編碼的接受令牌進行硬編碼。 我們將在第 7 章“管理用戶會話”中介紹如何正確定義 auth 標頭。 現在我們的 API 調用函數已經定義好了,我們可以繼續處理 editItem 函數:
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));
}
在這里,我們可以看到事件監聽器所屬的 HTML 部分可以通過 this 訪問。 我們知道,如果我們刪除編輯詞,并用空格切換 - ,它會將待辦事項的 ID 轉換為待辦事項的標題。 然后,我們利用 apiCall 函數來定義我們的端點和方法。 請注意,替換函數中的“edit”字符串中有一個空格。 我們有這個空格是因為我們還必須刪除編輯字符串后面的空格。 如果我們不刪除該空格,它將被發送到后端,從而導致錯誤,因為我們的應用程序后端在 JSON 文件中項目標題旁邊沒有空格。 定義端點和 API 調用方法后,我們將標題傳遞到狀態為已完成的字典中。 這是因為我們知道我們正在將待處理的項目切換為完成。 完成此操作后,我們將使用 JSON 正文發送 API 調用。
現在,我們可以對 deleteItem 函數使用相同的方法:
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));
}
同樣,替換函數中的“delete”字符串中有一個空格。 至此,我們的渲染過程就完成了。 我們定義了編輯和刪除函數以及渲染函數。 現在,我們必須在頁面首次加載時加載項目,而無需單擊任何按鈕。 這可以通過簡單的 API 調用來完成:
function getItems() {
let call=apiCall("/v1/item/get", 'GET');
call.send()
}
getItems();
在這里,我們可以看到我們只是使用 GET 方法進行 API 調用并發送它。 另請注意,我們的 getItems 函數是在函數外部調用的。 當視圖加載時,這將被觸發一次。
這是一段很長的編碼時間; 然而,我們已經快到了。 我們只需要定義創建文本輸入和按鈕的功能。 我們可以通過一個簡單的事件監聽器和創建端點的 API 調用來管理它:
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;
}
我們還添加了將文本輸入值設置為 null 的詳細信息。 我們將 input 設置為 null,以便用戶可以輸入要創建的另一個項目,而不必刪除剛剛創建的舊項目標題。 點擊應用程序的主視圖會得到以下輸出:
圖 5.5 – 帶有渲染的待辦事項的主頁
現在,要查看我們的前端是否按我們希望的方式工作,我們可以執行以下步驟:
按已清洗項目旁邊的刪除按鈕。
輸入早餐吃麥片,然后單擊創建。
輸入早餐吃拉面,然后單擊創建。
單擊早餐吃拉面項目的編輯。
這些步驟應產生以下結果:
圖 5.6 – 完成上述步驟后的主頁
這樣,我們就有了一個功能齊全的網絡應用程序。 所有按鈕都可以使用,并且列表會立即更新。 然而,它看起來不太漂亮。 沒有間距,一切都是黑白的。 為了修改這一點,我們需要將 CSS 集成到 HTML 文件中,我們將在下一節中執行此操作。
注入 CSS 采用與注入 JavaScript 相同的方法。 我們將在 HTML 文件中添加一個 CSS 標簽,該標簽將被文件中的 CSS 替換。 為了實現這一目標,我們必須執行以下步驟:
將 CSS 標簽添加到我們的 HTML 文件中。
為整個應用程序創建一個基本 CSS 文件。
為我們的主視圖創建一個 CSS 文件。
更新我們的 Rust 箱以服務 CSS 和 JavaScript。
讓我們仔細看看這個過程。
首先,讓我們對 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}}標簽用于基礎CSS,它在多個不同視圖中將保持一致,例如背景顏色和列比例,具體取決于屏幕尺寸。 {{BASE_CSS}} 標簽用于管理此視圖的 CSS 類。 恕我直言,css/base.css 和 css/main.css 文件是為我們的視圖而制作的。 另外,請注意,我們已將所有項目放入一個名為 mainContainer 的類的 div 中。 這將使我們能夠將所有項目在屏幕上居中。 我們還添加了更多的類,以便 CSS 可以引用它們,并將創建項目的按鈕從按鈕 HTML 標記更改為 div HTML 標記。 完成此操作后,javascript/main.js 文件中的 renderItems 函數將對項目循環進行以下更改:
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});
}
. . .
}
考慮到這一點,我們現在可以在 css/base.css 文件中定義基本 CSS。
現在,我們必須定義頁面及其組件的樣式。 一個好的起點是在 css/base.css 文件中定義頁面主體。 我們可以使用以下代碼對主體進行基本配置:
body {
background-color: #92a8d1;
font-family: Arial, Helvetica, sans-serif;
height: 100vh;
}
背景顏色是對一種顏色的引用。 僅看此參考可能看起來沒有意義,但有在線顏色選擇器,您可以在其中查看和選擇顏色,并提供參考代碼。 一些代碼編輯器支持此功能,但為了快速參考,只需使用 Google HTML 顏色選擇器,您就會因可用的免費在線交互工具的數量而不知所措。 通過上述配置,整個頁面的背景將具有代碼#92a8d1,即海軍藍色。 如果我們只是這樣,頁面的大部分都會有白色背景。 海軍藍色背景只會出現在有內容的地方。
我們將高度設置為 100vh。 vh 相對于視口高度的 1%。 由此,我們可以推斷出 100vh 意味著我們在 body 中定義的樣式占據了 100% 的視口。 然后,我們定義所有文本的字體,除非覆蓋為 Arial、Helvetica 或 sans-serif。 我們可以看到我們在font-family中定義了多種字體。 這并不意味著所有這些都已實現,也不意味著不同級別的標頭或 HTML 標記有不同的字體。 相反,這是一種后備機制。 首先,瀏覽器會嘗試渲染 Arial; 如果瀏覽器不支持,它將嘗試渲染 Helvetica,如果也失敗,它將嘗試渲染 sans-serif。
至此,我們已經定義了機身的總體風格,但是不同的屏幕尺寸呢? 例如,如果我們要在手機上訪問我們的應用程序,它應該具有不同的尺寸。 我們可以在下圖中看到這一點:
圖 5.7 – 手機和桌面顯示器之間的邊距差異
圖 5.7 顯示了邊距與待辦事項列表更改所填充的空間的比率。 對于手機來說,屏幕空間不大,所以大部分屏幕都需要被待辦事項占據; 否則,我們將無法閱讀它。 但是,如果我們使用寬屏桌面顯示器,我們就不再需要大部分屏幕來顯示待辦事項。 如果比例相同,待辦事項將在 X 軸上拉伸,難以閱讀,而且坦率地說,看起來也不好看。 這就是媒體查詢的用武之地。我們可以根據窗口的寬度和高度等屬性設置不同的樣式條件。 我們將從手機規格開始。 因此,如果屏幕寬度最大為 500 像素,則在 css/base.css 文件中,我們必須為正文定義以下 CSS 配置:
@media(max-width: 500px) {
body {
padding: 1px;
display: grid;
grid-template-columns: 1fr;
}
}
在這里,我們可以看到頁面邊緣和每個元素周圍的填充只有一個像素。 我們還有一個網格顯示。 這是我們可以定義列和行的地方。 然而,我們并沒有充分利用它。 我們只有一欄。 這意味著我們的待辦事項將占據大部分屏幕,如圖 5.7 中的手機描述所示。 盡管我們在這種情況下沒有使用網格,但我保留了它,以便您可以看到它與大屏幕的其他配置之間的關系。 如果我們的屏幕變大一點,我們可以將頁面分成三個不同的垂直列; 但中間柱的寬度與兩側柱的寬度之比為5:1。 這是因為我們的屏幕仍然不是很大,并且我們希望我們的項目仍然占據大部分屏幕。 我們可以通過添加另一個具有不同參數的媒體查詢來對此進行調整:
@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 的中間開始和結束。 我們可以使用 grid-column-finish 屬性使 mainContainer 跨多個列。
如果我們的屏幕變大,那么我們希望進一步調整比率,因為我們不希望項目寬度失控。 為了實現這一點,我們必須為中間列與兩側列定義 3:1 的比例,然后當屏幕寬度高于 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;
}
}
現在我們已經為所有視圖定義了通用 CSS,我們可以繼續在 css/main.css 文件中處理特定于視圖的 CSS。
現在,我們必須分解我們的應用程序組件。 我們有一份待辦事項清單。 列表中的每個項目都是一個具有不同背景顏色的 div:
.itemContainer {
background: #034f84;
margin: 0.3rem;
}
我們可以看到這個類的邊距為 0.3。 我們使用 rem 是因為我們希望邊距相對于根元素的字體大小進行縮放。 如果我們的光標懸停在項目上,我們還希望項目稍微改變顏色:
.itemContainer:hover {
background: #034f99;
}
在項目容器內,項目的標題用段落標簽表示。 我們想要定義項目容器中所有段落的樣式,而不是其他地方。 我們可以使用以下代碼定義容器中段落的樣式:
.itemContainer p {
color: white;
display: inline-block;
margin: 0.5rem;
margin-right: 0.4rem;
margin-left: 0.4rem;
}
inline-block 允許標題與 div 一起顯示,這將充當項目的按鈕。 邊距定義只是阻止標題緊靠項目容器的邊緣。 我們還確保段落顏色為白色。
設置項目標題樣式后,剩下的唯一項目樣式是操作按鈕,即編輯或刪除。 該操作按鈕將以不同的背景顏色向右浮動,以便我們知道在哪里單擊。 為此,我們必須使用類定義按鈕樣式,如以下代碼所示:
.actionButton {
display: inline-block;
float: right;
background: #f7786b;
border: none;
padding: 0.5rem;
padding-left: 2rem;
padding-right: 2rem;
color: white;
}
在這里,我們定義了顯示,使其向右浮動,并定義了背景顏色和填充。 這樣,我們可以通過運行以下代碼來確保懸停時顏色發生變化:
.actionButton:hover {
background: #f7686b;
color: black;
}
現在我們已經涵蓋了所有概念,我們必須定義輸入容器的樣式。 這可以通過運行以下代碼來完成:
.inputContainer {
background: #034f84;
margin: 0.3rem;
margin-top: 2rem;
}
.inputContainer input {
display: inline-block;
margin: 0.4rem;
}
我們做到了! 我們已經定義了所有 CSS、JavaScript 和 HTML。 在運行應用程序之前,我們需要在主視圖中加載數據。
我們在views/app/items.rs 文件中提供CSS。 我們通過閱讀 HTML、JavaScript、基本 CSS 和主 CSS 文件來完成此操作。 然后,我們用其他文件中的數據替換 HTML 數據中的標簽:
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)
}
現在,當我們啟動服務器時,我們將擁有一個完全運行的應用程序,具有直觀的前端,如下圖所示:
圖 5.8 – CSS 之后的主頁
盡管我們的應用程序正在運行,并且我們已經配置了基本 CSS 和 HTML,但我們可能希望擁有可重用的獨立 HTML 結構,這些結構具有自己的 CSS。 這些結構可以在需要時注入到視圖中。 它的作用是讓我們能夠編寫一次組件,然后將其導入到其他 HTML 文件中。 反過來,這使得維護變得更容易,并確保組件在多個視圖中的一致性。 例如,如果我們在視圖頂部創建一個信息欄,我們將希望它在其余視圖中具有相同的樣式。 因此,將信息欄作為組件創建一次并將其插入到其他視圖中是有意義的,如下一節所述。
有時,我們想要構建一個可以注入視圖的組件。 為此,我們必須加載 CSS 和 HTML,然后將它們插入 HTML 的正確部分。
為此,我們可以創建一個 add_component 函數,該函數獲取組件的名稱,根據組件名稱創建標簽,并根據組件名稱加載 HTML 和 CSS。 我們將在views/app/content_loader.rs文件中定義這個函數:
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 函數。 然后,我們將組件 HTML 和 CSS 注入到視圖數據中。 請注意,我們將組件嵌套在 templates/components/ 目錄中。 對于本例,我們要插入一個標頭組件,因此當我們將標頭傳遞給 add_component 函數時,我們的 add_component 函數將嘗試加載 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>
在這里,我們僅顯示已完成和待辦事項的數量計數。 在我們的 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 函數將 CSS 和 HTML 插入到正確的位置,我們必須將 HEADER 標簽插入 templates/main.html 文件的 <style> 部分:
. . .
<style>
{{BASE_CSS}}
{{CSS}}
HEADER_CSS
</style>
<body>
<div class="mainContainer">
HEADER_HTML
<h1>Done Items</h1>
. . .
現在我們所有的 HTML 和 CSS 都已定義,我們需要在 view/app/items.rs 文件中導入 add_component 函數:
use super::content_loader::add_component;
在同一個文件中,我們必須在項目視圖函數中添加標題,如下所示:
html_data=add_component(String::from("header"),
html_data);
現在,我們必須更改injecting_header/javascript/main.js 文件中的 apiCall 函數,以確保標頭隨待辦事項計數進行更新:
document.getElementById("completeNum").innerHTML=JSON.parse(this.responseText)["done_item_count"];
document.getElementById("pendingNum").innerHTML=JSON.parse(this.responseText)["pending_item_count"];
現在我們已經插入了組件,我們得到以下渲染視圖:
圖 5.9 – 帶標題的主頁
正如我們所看到的,我們的標題正確顯示了數據。 如果我們將標頭標簽添加到視圖 HTML 文件中,并在視圖中調用 add_component,我們將獲得該標頭。
現在,我們有一個完全運行的單頁應用程序。 然而,這并非沒有困難。 我們可以看到,如果我們開始向前端添加更多功能,我們的前端將開始失控。 這就是 React 等框架的用武之地。通過 React,我們可以將代碼構建為適當的組件,以便我們可以在需要時使用它們。 在下一節中,我們將創建一個基本的 React 應用程序。
React 是一個獨立的應用程序。 因此,我們通常會將 React 應用程序放在自己的 GitHub 存儲庫中。 如果您想將 Rust 應用程序和 React 應用程序保留在同一個 GitHub 存儲庫中,那沒問題,但只需確保它們位于根目錄中的不同目錄即可。 一旦我們導航到 Rust Web 應用程序之外,我們就可以運行以下命令:
npx create-react-app front_end
這將在 front_end 目錄中創建一個 React 應用程序。 如果我們查看里面,我們會看到有很多文件。 請記住,本書是關于 Rust 中的 Web 編程的。 探索有關 React 的一切超出了本書的范圍。 不過,進一步閱讀部分建議您閱讀一本專門介紹 React 開發的書。 現在,我們將重點關注 front_end/package.json 文件。 我們的 package.json 文件就像我們的 Cargo.toml 文件,我們在其中定義我們正在構建的應用程序的依賴項、腳本和其他元數據。 在我們的 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 應用程序,但在此之前,我們必須使用以下代碼編輯 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;
在分解這段代碼之前,我們必須澄清一些事情。 如果您上網,您可能會看到一些文章指出 JavaScript 不是基于類的面向對象語言。 本書不會深入探討 JavaScript。 相反,本章旨在為您提供足夠的知識來啟動和運行前端。 如果您想向 Rust Web 應用程序添加前端,希望本章足以促進進一步閱讀并啟動您的旅程。 在本章中,我們將只討論可以支持繼承的類和對象。
在前面的代碼中,我們從react包中導入了組件對象。 然后,我們定義了一個繼承組件類的App類。 App 類是我們應用程序的主要部分,我們可以將 front_end/src/App.js 文件視為前端應用程序的入口點。 如果需要的話,我們可以在 App 類中定義其他路由。 我們還可以看到有一個屬于App類的狀態。 這是應用程序的總體內存。 我們必須稱其為國家; 每次更新狀態時,都會執行渲染函數,更新組件渲染到前端的內容。 當我們的狀態更新我們的自制渲染函數時,這抽象了本章前面幾節中我們所做的很多事情。 我們可以看到,我們的狀態可以在返回時在渲染函數中引用。 這就是所謂的 JSX,它允許我們直接在 JavaScript 中編寫 HTML 元素,而不需要任何額外的方法。 現在已經定義了基本應用程序,我們可以將其導出以使其可用。
讓我們導航到 package.json 文件所在的目錄并運行以下命令:
npm start
React 服務器將啟動,我們將在瀏覽器中看到以下視圖:
圖 5.10 – React 應用程序的第一個主視圖
在這里,我們可以看到狀態中的消息已傳遞到渲染函數中,然后顯示在瀏覽器中。 現在我們的 React 應用程序正在運行,我們可以開始使用 API 調用將數據加載到 React 應用程序中。
現在基本應用程序正在運行,我們可以開始對后端執行 API 調用。 為此,我們將主要關注 front_end/src/App.js 文件。 我們可以構建我們的應用程序,以便它可以使用 Rust 應用程序中的項目填充前端。 首先,我們必須將以下內容添加到 package.json 文件的依賴項中:
"axios": "^0.26.1"
然后,我們可以運行以下命令:
npm install
這將安裝我們的額外依賴項。 現在,我們可以轉到 front_end/src/App.js 文件并使用以下代碼導入我們需要的內容:
import React, { Component } from 'react';
import axios from 'axios';
我們將使用 Component 來繼承 App 類,并使用 axios 對后端執行 API 調用。 現在,我們可以定義我們的 App 類并使用以下代碼更新我們的狀態:
class App extends Component {
state={
"pending_items": [],
"done_items": [],
"pending_items_count": 0,
"done_items_count": 0
}
}
export default App;
在這里,我們的結構與我們自制的前端相同。 這也是我們從 Rust 服務器中的獲取項目視圖返回的數據。 現在我們知道要使用哪些數據,我們可以執行以下步驟:
在我們的 App 類中創建一個函數,從 Rust 服務器獲取函數。
確保該函數在App類掛載時執行。
在我們的 App 類中創建一個函數,用于將從 Rust 服務器返回的項目處理為 HTML。
在我們的 App 類中創建一個函數,一旦我們完成,它會將所有上述組件渲染到前端。
使我們的 Rust 服務器能夠接收來自其他來源的調用。
在開始這些步驟之前,我們應該注意 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 (
. . .
)
}
}
這樣,我們就可以開始調用 API 的函數了:
在我們的 App 類中,我們的 getItems 函數采用以下布局:
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。 然后,我們將令牌添加到標頭中。 現在,我們將只硬編碼一個簡單的字符串,因為我們還沒有在 Rust 服務器中設置用戶會話; 我們將在第 7 章“管理用戶會話”中更新這一點。 然后,我們關閉它。 因為 axios.get 是一個 Promise,所以我們必須使用 .then。 返回數據時執行 .then 括號內的代碼。 在這些括號內,我們提取所需的數據,然后執行 this.setState 函數。 this.setState 函數更新 App 類的狀態。 但是,執行 this.setState 也會執行 App 類的 render 函數,這將更新瀏覽器。 在 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,可以從后端獲取item了。 現在我們已經定義了它,我們必須確保它被執行,我們接下來要做的就是。
確保 getItems 函數被觸發,從而在加載 App 類時更新狀態可以使用以下代碼來實現:
componentDidMount() {
this.getItems();
}
這很簡單。 getItems 將在我們的 App 組件安裝后立即執行。 我們本質上是在 componentDidMount 函數中調用 this.setState 。 這會在瀏覽器更新屏幕之前觸發額外的渲染。 即使渲染被調用兩次,用戶也不會看到中間狀態。 這是我們從 React Component 類繼承的眾多函數之一。 現在我們在頁面加載后就加載了數據,我們可以繼續下一步:處理加載的數據。
對于 App 類中的 processItemValues 函數,我們必須接收表示項目的 JSON 對象數組并將其轉換為 HTML,這可以通過以下代碼實現:
processItemValues(items) {
let itemList=[];
items.forEach((item, index)=>{
itemList.push(
<li key={index}>{item.title} {item.status}</li>
)
})
return itemList
}
在這里,我們只是循環遍歷這些項目,將它們轉換為 li HTML 元素并將它們添加到一個空數組中,然后在填充后返回該空數組。 請記住,我們使用 processItemValue 函數在數據進入 getItems 函數中的狀態之前處理數據。 現在我們已經擁有狀態中的所有 HTML 組件,我們需要使用渲染函數將它們放置在頁面上。
對于我們的 App 類,渲染函數僅返回 HTML 組件。 我們在此不使用任何額外的邏輯。 我們可以返回以下內容:
<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>
在這里,我們可以看到我們的狀態被直接引用。 與我們在本章前面使用的手動字符串操作相比,這是一個可愛的變化。 使用 React 更加干凈,降低了錯誤的風險。 在我們的前端,調用后端的渲染過程應該可以工作。 但是,我們的 Rust 服務器將阻止來自 React 應用程序的請求,因為它來自不同的應用程序。 為了解決這個問題,我們需要繼續下一步。
現在,我們的 Rust 服務器將阻止我們對服務器的請求。 這取決于跨源資源共享(CORS)。 我們之前沒有遇到過任何問題,因為默認情況下,CORS 允許來自同一來源的請求。 當我們編寫原始 HTML 并從 Rust 服務器提供服務時,請求來自同一來源。 然而,對于 React 應用程序,請求來自不同的來源。 為了糾正這個問題,我們需要使用以下代碼在 Cargo.toml 文件中安裝 CORS 作為依賴項:
actix-cors="0.6.1"
在我們的 src/main.rs 文件中,我們必須使用以下代碼導入 CORS:
use actix_cors::Cors;
現在,我們必須在定義服務器之前定義 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
}
這樣,我們的服務器就準備好接受來自 React 應用程序的請求了。
筆記
當我們定義 CORS 策略時,我們明確表示我們希望允許所有方法、標頭和來源。 然而,我們可以通過以下 CORS 定義更簡潔:
let cors=Cors::permissive();
現在,我們可以測試我們的應用程序,看看它是否正常工作。 我們可以通過使用 Cargo 運行 Rust 服務器并在不同的終端中運行 React 應用程序來做到這一點。 一旦啟動并運行,我們的 React 應用程序加載時應如下所示:
圖 5.11 – React 應用程序首次與 Rust 服務器對話時的視圖
這樣,我們可以看到對 Rust 應用程序的調用現在可以按預期工作。 然而,我們所做的只是列出待辦事項的名稱和狀態。 React 的亮點在于構建自定義組件。 這意味著我們可以為每個待辦事項構建具有自己的狀態和功能的單獨類。 我們將在下一節中看到這一點。
當我們查看 App 類時,我們可以看到,擁有一個具有狀態和函數的類非常有用,這些狀態和函數可用于管理 HTML 呈現到瀏覽器的方式和時間。 當涉及到單個待辦事項時,我們可以使用狀態和函數。 這是因為我們有一個按鈕可以從待辦事項中獲取屬性并調用 Rust 服務器來編輯或刪除它。 在本節中,我們將構建兩個組件:src/components/ToDoItem.js 文件中的 ToDoItem 組件和 src/components/CreateToDoItem.js 文件中的 CreateToDoItem 組件。 一旦我們構建了這些,我們就可以將它們插入到我們的 App 組件中,因為我們的 App 組件將獲取項目的數據并循環這些項目,創建多個 ToDoItem 組件。 為了實現這一目標,我們需要處理幾個步驟,因此本節將分為以下小節:
創建我們的 ToDoItem 組件
創建 CreateToDoItem 組件
在我們的應用程序組件中構建和管理自定義組件
讓我們開始吧。
我們將從 src/components/ToDoItem.js 文件中更簡單的 ToDoItem 組件開始。 首先,我們必須導入以下內容:
import React, { Component } from 'react';
import axios from "axios";
這不是什么新鮮事。 現在我們已經導入了我們需要的內容,我們可以關注如何使用以下代碼定義 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 填充狀態,這是構造組件時傳遞到組件中的參數。 然后,我們的 ToDoItem 組件具有以下函數:
processStatus:此函數將待辦事項的狀態(例如 PENDING)轉換為按鈕上的消息(例如編輯)。
inverseStatus:當我們有一個狀態為 PENDING 的待辦事項并對其進行編輯時,我們希望將其轉換為 DONE 狀態,以便可以將其發送到 Rust 服務器上的編輯端點,這是相反的。 因此,該函數創建傳入狀態的反轉。
sendRequest:此函數將請求發送到 Rust 服務器以編輯或刪除待辦事項。 我們還可以看到我們的 sendRequest 函數是一個箭頭函數。 箭頭語法本質上將函數綁定到組件,以便我們可以在渲染返回語句中引用它,從而允許在單擊綁定到它的按鈕時執行 sendRequest 函數。
現在我們知道我們的函數應該做什么,我們可以使用以下代碼定義我們的狀態函數:
processStatus(status) {
if (status==="PENDING") {
return "edit"
} else {
return "delete"
}
}
inverseStatus(status) {
if (status==="PENDING") {
return "DONE"
} else {
return "PENDING"
}
}
這很簡單,不需要太多解釋。 現在我們的狀態處理函數已經完成,我們可以使用以下代碼定義我們的 sendRequest 函數:
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 的一部分,具體取決于我們按下的按鈕。 我們還可以看到我們執行了 this.props.passBackResponse 函數。 這是我們傳遞到 ToDoItem 組件中的函數。 這是因為在編輯或刪除請求后,我們從 Rust 服務器獲取了待辦事項的完整狀態。 我們需要啟用我們的應用程序組件來處理已傳回的數據。 在這里,我們將在“應用程序組件”小節中的“構建和管理自定義組件”中先睹為快。 我們的 App 組件將在 passBackResponse 參數下有一個未執行的函數,它將傳遞給我們的 ToDoItem 組件。 該函數在 passBackResponse 參數下,將處理新的待辦事項的狀態并將其呈現在 App 組件中。
至此,我們已經配置了所有功能。 剩下的就是定義渲染函數的返回,它采用以下形式:
<div>
<p>{this.state.title}</p>
<button onClick={this.sendRequest}>
{this.state.button}</button>
</div>
在這里,我們可以看到待辦事項的標題呈現在段落標記中,并且我們的按鈕在單擊時執行 sendRequest 函數。 現在我們已經完成了這個組件,并且可以在我們的應用程序中顯示它了。 但是,在執行此操作之前,我們需要構建用于在下一節中創建待辦事項的組件。
我們的 React 應用程序可以列出、編輯和刪除待辦事項。 但是,我們無法創建任何待辦事項。 它由一個輸入和一個創建按鈕組成,以便我們可以放入一個待辦事項,然后通過單擊該按鈕來創建該待辦事項。 在我們的 src/components/CreateToDoItem.js 文件中,我們需要導入以下內容:
import React, { Component } from 'react';
import axios from "axios";
這些是構建我們組件的標準導入。 定義導入后,我們的 CreateToDoItem 組件將采用以下形式:
class CreateToDoItem extends Component {
state={
title: ""
}
createItem=()=> {
. . .
}
handleTitleChange=(e)=> {
. . .
}
render() {
return (
. . .
)
}
}
export default CreateToDoItem;
在上面的代碼中,我們可以看到我們的CreateToDoItem組件有以下功能:
createItem:該函數向 Rust 服務器發送請求,以創建標題為 state 的待辦事項
handleTitleChange:每次更新輸入時該函數都會更新狀態
在探索這兩個函數之前,我們將翻轉這些函數的編碼順序,并使用以下代碼定義渲染函數的返回:
<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。 另外,當輸入更改時,我們執行 this.handleTitleChange 函數。 現在我們已經介紹了渲染函數,沒有什么新內容要介紹了。 這是您再次查看 CreateToDoItem 組件的概要并嘗試自己定義 createItem 和 handleTitleChange 函數的好機會。 它們采用與 ToDoItem 組件中的函數類似的形式。
您嘗試定義 createItem 和 handleTitleChange 函數應類似于以下內容:
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});
}
這樣,我們就定義了兩個自定義組件。 我們現在準備好進入下一小節,我們將在其中管理我們的自定義組件。
雖然創建自定義組件很有趣,但如果我們不在應用程序中使用它們,它們就沒有多大用處。 在本小節中,我們將向 src/App.js 文件添加一些額外的代碼,以啟用我們的自定義組件。 首先,我們必須使用以下代碼導入我們的組件:
import ToDoItem from "./components/ToDoItem";
import CreateToDoItem from "./components/CreateToDoItem";
現在我們已經有了組件,我們可以繼續進行第一次更改。 我們的 App 組件的 processItemValues 函數可以使用以下代碼定義:
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
}
在這里,我們可以看到我們循環遍歷從 Rust 服務器獲取的數據,但我們沒有將數據傳遞到通用 HTML 標簽中,而是將待辦事項數據的參數傳遞到我們自己的自定義組件中,該組件將被處理 就像 HTML 標簽一樣。 當涉及到處理我們自己的返回狀態響應時,我們可以看到它是一個箭頭函數,用于處理數據并使用以下代碼設置狀態:
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 函數非常相似。 如果您想減少重復代碼的數量,可以在這里進行一些重構。 但是,為了使其工作,我們必須使用以下代碼定義渲染函數的 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 服務器和 React 應用程序將為我們提供以下視圖:
圖 5.12 – 帶有自定義組件的 React 應用程序的視圖
圖 5.12 顯示我們的自定義組件正在呈現。 我們可以單擊按鈕,結果是,我們將看到所有 API 調用都正常工作,并且我們的自定義組件也正常工作。 現在,阻礙我們的只是讓我們的前端看起來更美觀,我們可以通過將 CSS 提升到 React 應用程序中來做到這一點。
我們現在正處于使 React 應用程序可用的最后階段。 我們可以將 CSS 分成多個不同的文件。 然而,我們即將結束本章,再次瀏覽所有 CSS 會不必要地讓本章充滿大量重復代碼。 雖然我們的 HTML 和 JavaScript 不同,但 CSS 是相同的。 為了讓它運行,我們可以從以下文件中復制所有 CSS:
templates/components/header.css
css/base.css
css/main.css
將此處列出的 CSS 文件復制到 front_end/src/App.css 文件中。 CSS 有一項更改,所有 .body 引用都應替換為 .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;
}
}
. . .
現在,我們可以導入 CSS 并在我們的應用程序和組件中使用它。 我們還必須更改渲染函數中的返回 HTML。 我們可以處理所有三個文件。 對于 src/App.js 文件,我們必須使用以下代碼導入 CSS:
import "./App.css";
然后,我們必須添加一個標頭并使用正確的類定義 div 標簽,并使用以下代碼作為渲染函數的返回語句:
<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 文件中,我們必須使用以下代碼導入 CSS:
import "../App.css";
然后,我們必須將按鈕更改為 div 并使用以下代碼定義渲染函數的 return 語句:
<div className="itemContainer">
<p>{this.state.title}</p>
<div className="actionButton" onClick={this.sendRequest}>
{this.state.button}</div>
</div>
在我們的 src/components/CreateToDoItem.js 文件中,我們必須使用以下代碼導入 CSS:
import "../App.css";
然后,我們必須將按鈕更改為 div 并使用以下代碼定義渲染函數的 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 服務器提升到了 React 應用程序中。 如果我們運行 Rust 服務器和 React 應用程序,我們將得到下圖所示的輸出:
圖 5.13 – 添加了 CSS 的 React 應用程序的視圖
我們終于得到它了! 我們的 React 應用程序正在運行。 啟動并運行我們的 React 應用程序需要更多時間,但我們可以看到 React 具有更大的靈活性。 我們還可以看到,我們的 React 應用程序不太容易出錯,因為我們不必手動操作字符串。 我們用 React 構建還有一個優勢,那就是現有的基礎設施。 在下一部分也是最后一部分中,我們將通過將 React 應用程序包裝在 Electron 中,將 React 應用程序轉換為編譯后的桌面應用程序,該應用程序在計算機的應用程序中運行。
將我們的 React 應用程序轉換為桌面應用程序并不復雜。 我們將使用 Electron 框架來做到這一點。 Electron 是一個功能強大的框架,可將 JavaScript、HTML 和 CSS 應用程序轉換為跨 macOS、Linux 和 Windows 平臺編譯的桌面應用程序。 Electron 框架還可以讓我們通過 API 訪問計算機的組件,例如加密存儲、通知、電源監視器、消息端口、進程、shell、系統首選項等等。 Electron 中內置了 Slack、Visual Studio Code、Twitch、Microsoft Teams 等桌面應用程序。 要轉換我們的 React 應用程序,我們必須首先更新 package.json 文件。 首先,我們必須使用以下代碼更新 package.json 文件頂部的元數據:
{
"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": {
. . .
其中大部分是通用元數據。 然而,主力場是必不可少的。 我們將在此處編寫定義 Electron 應用程序如何運行的文件。 將主頁字段設置為“./”還可以確保資源路徑相對于index.html 文件。 現在我們的元數據已經定義了,我們可以添加以下依賴項:
"webpack": "4.28.3",
"cross-env": "^7.0.3",
"electron-is-dev": "^2.0.0"
這些依賴項有助于構建 Electron 應用程序。 添加它們后,我們可以使用以下代碼重新定義腳本:
. . .
"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 .\""
},
在這里,我們為所有 React 腳本添加了前綴“react”。 這是為了將 React 進程與 Electron 進程分開。 如果我們現在只想在開發模式下運行 React 應用程序,則必須運行以下命令:
npm run react-start
我們還為 Electron 定義了構建命令和開發啟動命令。 這些還不能工作,因為我們還沒有定義我們的 Electron 文件。 在 package.json 文件的底部,我們必須定義構建 Electron 應用程序的開發人員依賴項:
. . .
"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 文件中定義了我們需要的所有內容。 我們需要使用以下命令安裝新的依賴項:
npm install
現在,我們可以開始構建 front_end/public/electron.js 文件,以便構建我們的 Electron 文件。 這本質上是樣板代碼,您可能會在其他教程中看到此文件,因為這是在 Electron 中運行應用程序的最低要求。 首先,我們必須使用以下代碼導入我們需要的內容:
const { app, BrowserWindow }=require("electron");
const path=require("path");
const isDev=require("electron-is-dev");
然后,我們必須使用以下代碼定義創建桌面窗口的函數:
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();
}
}
在這里,我們本質上定義了窗口的寬度和高度。 另請注意,nodeIntegration 和enableRemoteModule 使渲染器遠程進程(瀏覽器窗口)能夠在主進程上運行代碼。 然后,我們開始在主窗口中加載 URL。 如果在開發人員模式下運行,我們只需加載 http://localhost:3000,因為我們在 localhost 上運行了 React 應用程序。 如果我們構建應用程序,那么我們編碼的資產和文件將被編譯并可以通過 ../build/index.html 文件加載。 我們還聲明,如果我們在開發人員模式下運行,我們將打開開發人員工具。 當窗口準備好時,我們必須使用以下代碼執行 createWindow 函數:
app.whenReady().then(()=> {
createWindow();
app.on("activate", function () {
if (BrowserWindow.getAllWindows().length===0){
createWindow();
}
});
});
如果操作系統是macOS,我們必須保持程序運行,即使我們關閉窗口:
app.on("window-all-closed", function () {
if (process.platform !=="darwin") app.quit();
});
現在,我們必須運行以下命令:
npm start
這將運行 Electron 應用程序,為我們提供以下輸出:
圖 5.14 – 我們在 Electron 中運行的 React 應用程序
在圖 5.13 中,我們可以看到我們的應用程序正在桌面上的一個窗口中運行。 我們還可以看到我們的應用程序可以通過屏幕頂部的菜單欄訪問。 該應用程序的徽標顯示在我的任務欄上:
圖 5.15 – 我的任務欄上的 Electron
以下命令將在 dist 文件夾中編譯我們的應用程序,如果單擊該文件夾,則會將該應用程序安裝到您的計算機上:
npm build
以下是我在 Mac 上的應用程序區域中使用 Electron 測試我為 OasisLMF 構建的名為 Camel 的開源包的 GUI 時的示例:
圖 5.16 – 應用程序區域中的 Electron 應用程序
最終,我會想出一個標志。 不過,關于在瀏覽器中顯示內容的本章就到此結束。
在本章中,我們最終使臨時用戶可以使用我們的應用程序,而不必依賴于 Postman 等第三方應用程序。 我們定義了自己的應用程序視圖模塊,其中包含讀取文件和插入功能。 這導致我們構建了一個流程,加載 HTML 文件,將 JavaScript 和 CSS 文件中的數據插入到視圖數據中,然后提供該數據。
這為我們提供了一個動態視圖,當我們編輯、刪除或創建待辦事項時,該視圖會自動更新。 我們還探索了一些有關 CSS 和 JavaScript 的基礎知識,以便從前端進行 API 調用并動態編輯視圖某些部分的 HTML。 我們還根據窗口的大小管理整個視圖的樣式。 請注意,我們不依賴外部板條箱。 這是因為我們希望能夠了解如何處理 HTML 數據。
然后,我們在 React 中重建了前端。 雖然這需要更長的時間并且有更多的移動部件,但代碼更具可擴展性并且更安全,因為我們不必手動操作字符串來編寫 HTML 組件。 我們還可以明白為什么我們傾向于 React,因為它非常適合 Electron,為我們提供了另一種向用戶交付應用程序的方式。
雖然我們的應用程序現在按表面價值運行,但它在數據存儲方面不可擴展。 我們沒有數據過濾流程。 我們不會檢查我們存儲的數據,也沒有多個表。
在下一章中,我們將構建與 Docker 本地運行的 PostgreSQL 數據庫交互的數據模型。
將 HTML 數據返回到用戶瀏覽器的最簡單方法是什么?
將 HTML、CSS 和 JavaScript 數據返回到用戶瀏覽器的最簡單(不可擴展)的方法是什么?
我們如何確保某些元素的背景顏色和樣式標準在應用程序的所有視圖中保持一致?
API 調用后我們如何更新 HTML?
我們如何啟用按鈕來連接到我們的后端 API?
我們只需定義一個 HTML 字符串并將其放入 HttpResponse 結構體中,同時將內容類型定義為 HTML,即可提供 HTML 數據。 然后 HttpResponse 結構體返回到用戶的瀏覽器。
最簡單的方法是硬編碼一個完整的 HTML 字符串,CSS 硬編碼在 <style> 部分,我們的 JavaScript 硬編碼在 <script> 部分。 然后將該字符串放入 HttpResponse 結構體中并返回到用戶的瀏覽器。
我們創建一個 CSS 文件來定義我們希望在整個應用程序中保持一致的組件。 然后,我們在所有 HTML 文件的 <style> 部分放置一個標簽。 然后,對于每個文件,我們加載基本 CSS 文件并用 CSS 數據替換標簽。
API調用后,我們必須等待狀態準備好。 然后,我們使用 getElementById 獲取要更新的 HTML 部分,序列化響應數據,然后將元素的內部 HTML 設置為響應數據。
我們給按鈕一個唯一的 ID。 然后,我們添加一個事件偵聽器,該偵聽器由唯一 ID 定義。 在此事件偵聽器中,我們將其綁定到一個使用 this 獲取 ID 的函數。 在此函數中,我們對后端進行 API 調用,然后使用響應來更新顯示數據的視圖其他部分的 HTML。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。