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
著JavaScript越來越流行,越來越多的團隊廣泛的把JavaScript應用到前端、后臺、hybrid 應用、嵌入式等等領域。
這篇文章旨在深入挖掘JavaScript,以及向大家解釋JavaScript是如何工作的。我們通過了解它的底層構建以及它是怎么發揮作用的,可以幫助我們寫出更好的代碼與應用。據 GitHut 統計顯示,JavaScript 長期占據GitHub中 Active Repositories 和 Total Pushes 的榜首,并且在其他的類別中也不會落后太多。
如果一個項目越來越依賴 JavaScript,這就意味著開發人員必須利用這些語言和生態系統提供更深層次的核心內容去構建一個令人振奮的應用。然而,事實證明,有很多的開發者每天都在使用 JavaScript,但是卻不知道在底層 JavaScript 是怎么運作的。
概述
幾乎每個人聽說過 V8 引擎的概念,而且,大多數人都知道 JavaScript 是單線程的,或者是它是使用回調隊列的。
在這篇文章中,我們將詳細的介紹這些概念,并解釋 JavaScript 是怎么工作的。通過了解這些細節,你就能利用這些提供的 API 來寫出更好的,非阻塞的應用來。如果你對 JavaScript 比較陌生,那么這篇文章將幫助您理解為什么 JavaScript 相較于其他語言顯得如此“怪異”。如果您是一位經驗豐富的 JavaScript 開發人員,希望它能給你帶來一些新的見解,說明 JavaScript 的運行時,盡管你可能每天都會用到它。
JavaScript 引擎
JavaScript 引擎說起來最流行的當然是谷歌的 V8 引擎了, V8 引擎使用在 Chrome 以及 Node 中,下面有個簡單的圖能說明他們的關系:
這個引擎主要由兩部分組成:
運行時
有些瀏覽器的 API 經常被使用到(比如說:setTimeout),但是,這些 API 卻不是引擎提供的。那么,他們是從哪兒來的呢?事實上這里面實際情況有點復雜。
所以說我們還有很多引擎之外的 API,我們把這些稱為瀏覽器提供的 Web API,比如說 DOM、AJAX、setTimeout等等。
然后我們還擁有如此流行的事件循環和回調隊列。
調用棧
JavaScript 是一門單線程的語言,這意味著它只有一個調用棧,因此,它同一時間只能做一件事。
調用棧是一種數據結構,它記錄了我們在程序中的位置。如果我們運行到一個函數,它就會將其放置到棧頂。當從這個函數返回的時候,就會將這個函數從棧頂彈出,這就是調用棧做的事情。
讓我們來看一看下面的例子:
function multiply(x, y) { return x * y; } function printSquare(x) { var s=multiply(x, x); console.log(s); } printSquare(5);
當程序開始執行的時候,調用棧是空的,然后,步驟如下:
每一個進入調用棧的都稱為__調用幀__。
這能清楚的知道當異常發生的時候堆棧追蹤是怎么被構造的,堆棧的狀態是如何的。讓我們看一下下面的代碼:
function foo() { throw new Error('SessionStack will help you resolve crashes :)'); } function bar() { foo(); } function start() { bar(); } start();
如果這發生在 Chrome 里(假設這段代碼實在一個名為 foo.js 的文件中),那么將會生成以下的堆棧追蹤:
"堆棧溢出",當你達到調用棧最大的大小的時候就會發生這種情況,而且這相當容易發生,特別是在你寫遞歸的時候卻沒有全方位的測試它。我們來看看下面的代碼:
function foo() { foo(); } foo();
當我們的引擎開始執行這段代碼的時候,它從 foo 函數開始。然后這是個遞歸的函數,并且在沒有任何的終止條件的情況下開始調用自己。因此,每執行一步,就會把這個相同的函數一次又一次地添加到調用堆棧中。然后它看起來就像是這樣的:
然后,在某一時刻,調用棧中的函數調用的數量超過了調用棧的實際大小,瀏覽器決定干掉它,拋出一個錯誤,它看起來就像是這樣:
在單個線程上運行代碼很容易,因為你不必處理在多線程環境中出現的復雜場景——例如死鎖。但是在一個線程上運行也非常有限制。由于 JavaScript 只有一個調用堆棧,當某段代碼運行變慢時會發生什么?
并發與事件循環
調用棧中的函數調用需要大量的時間來處理,那么這會發生什么情況呢?例如,假設你想在瀏覽器中使用 JavaScript 進行一些復雜的圖片轉碼。
你可能會問?這算什么問題?事實上,問題是當調用棧有函數要執行,瀏覽器就不能做任何事,它會被堵塞住。這意味著瀏覽器不能渲染,不能運行其他的代碼,它被卡住了。如果你想在應用里讓 UI 很流暢的話,這就會產生問題。
而且這不是唯一的問題,一旦你的瀏覽器開始處理調用棧中的眾多任務,它可能會停止響應相當長一段時間。大多數瀏覽器都會這么做,報一個錯誤,詢問你是否想終止 web 頁面。
這樣看來,這并不是最好的用戶體驗,不是嗎?
作者:小烜同學
鏈接:https://juejin.im/post/5a05b4576fb9a04519690d42
.HTML 介紹
是網頁的后綴,txt 后綴是文本 ,py 后綴是 python ,html 后綴就是網頁的意思。我們如果想創建一個網頁的話,可以直接將文本的后綴改為 html 。HTMLSHI 超文本標記語言,是一種標識性的語言。它包括一系列標記標簽,通過這些標記標簽可以將網絡上的文檔格式統一,使分散的Internet資源連接為一個邏輯整體。
1.html 的介紹
頁面整體分為兩部分:
一部分是head部分,主要是頁面的整體信息和配置,內容不會出現在瀏覽器內部。
一部分是body部分,這部分內容則會在瀏覽器中展示出來
我們使用 pycharm 創建一個 html ,打開后就是下圖模樣。
(1)文檔類型聲明(默認的可以不用設置)
<!DOCTYPE html>
(2)開始標簽和結束標簽
一般的標簽是成對出現的,一般稱第一個標簽是開始標簽,第二個是結束標簽。開始和結束標簽也稱為開放標簽和閉合標簽。
開始標簽:
<html lang="en">
其中的 html 為根元素,是所有元素的基礎。lang 表示語言,en 表示英文。
結束標簽:
</html>
(3)頭部標簽
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
其中 utf-8 表示字符編碼格式,如果沒有寫這個就會發生亂碼。Title 表示文檔的標題。
(4)身體標簽
<body>
</body>
身體標簽是文檔的主題,可視化區域,所有的音頻,視頻,圖片,文字都可在其中搭建,相當于我們打開網頁時所看到內容。
(5)標簽的特點
標簽是由一對尖括號包裹單詞構成的,標簽要使用小寫。 一般的標簽是成對出現的,一般稱第一個標簽是開始標簽,第二個是結束標簽。開始和結束標簽也稱為開放標簽和閉合標簽。
二.標簽
標簽分為塊級標簽和內聯標簽(運行時點擊右上角的谷歌模式的小圓圈就可以)
1.內容的書寫
(1)塊級標簽(p)
兩個 p 中間可隨意書寫內容
<p>故事和酒,淘寶都有</p>
(2)內聯標簽(span)
<span>故事和酒,淘寶都有</span>
完整代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- 塊級標簽-->
<p>故事和酒,淘寶都有</p>
<!--內聯標簽-->
<span>故事和酒,淘寶都有</span>
</body>
</html>
運行后:
運行后看不出塊級標簽和內聯標簽的區別,所有我們使用檢查。右擊后點擊檢查
在點擊下圖中左上角的方框箭頭,變成藍色說明正在運行,之后就可以查看有關的數據了
無需點擊,只要將箭頭放在文字上就會出現相關內容
上面兩圖可以明顯看出兩句話的寬度不相同。
塊級標簽:在不設置寬度的情況下,寬度始終和瀏覽器寬度保持一致。
內聯標簽:寬度和內容有關
2.設置高度寬度
<p style="width: 500px;height: 50px;">故事和酒,淘寶都有</p>
<span style="width: 500px;height: 50px;">故事和酒,淘寶都有</span>
如圖所示,只有塊級標簽寬高改變了,內聯標簽不改變。由此可得,塊級標簽設置寬高有效,內聯標簽設置寬高無效。
3.多個標簽同時存在
<body>
<!-- 塊級標簽-->
<p>故事和酒,淘寶都有</p>
<p>故事和酒,淘寶都有</p>
<!--內聯標簽-->
<span>故事和酒,淘寶都有22</span>
<span>故事和酒,淘寶都有22</span>
</body>
多個塊級標簽同時存在的情況下,排列方式從上往下
多個內聯標簽同時存在的情況下,排列方式從左往右
4.是否包含
<body>
<!-- 塊級標簽-->
<p>故事和酒,淘寶都有
<span>故事和酒,淘寶都有22</span>
</p>
<!--內聯標簽-->
<span>故事和酒,淘寶都有22
<p>故事和酒,淘寶都有</p>
</span>
</body>
由此可知,塊級標簽可以包含內聯標簽,但內聯標簽不可以包含塊級標簽,只可以包含內聯標簽。
5.塊級標簽與內聯標簽相互轉換
(1)塊級轉內聯
<body>
<!--將塊級標簽轉化成內聯標簽-->
<p style="display: inline">故事和酒,淘寶都有</p>
<p style="display: inline">故事和酒,淘寶都有</p>
</body>
(2)內聯轉塊級(display: block)
內聯轉為塊級之后,具有了塊級的性質。
<span style="display: block">故事和酒,淘寶都有222</span>
<span style="display: block">故事和酒,淘寶都有222</span>
(3)內聯塊元素(display: inline-block)
內聯塊元素包含了內聯標簽和塊級標簽的部分特性。
<span style="display: inline-block">故事和酒,淘寶都有333</span>
<span style="display: inline-block;height: 50px">故事和酒,淘寶都有333</span>
(4)段落標簽(p)
<!--段落標簽-->
<p></p>
(5)標題標簽(h)
們現在正處于可以構建一個 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小時內與您取得聯系。