龙空技术网

Rust Web编程:第五章 在浏览器上显示内容

启辰8 118

前言:

现时大家对“手机怎么查看html源代码”大约比较关注,各位老铁们都想要剖析一些“手机怎么查看html源代码”的相关知识。那么小编在网上汇集了一些有关“手机怎么查看html源代码””的相关资讯,希望同学们能喜欢,朋友们一起来学习一下吧!

我们现在正处于可以构建一个 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 构建端到端产品。

使用 Rust 提供 HTML、CSS 和 JavaScript 服务

在上一章中,我们以 JSON 的形式返回了所有数据。 在本节中,我们将返回 HTML 数据供用户查看。 在此 HTML 数据中,我们将具有按钮和表单,使用户能够与我们在上一章中定义的 API 端点进行交互,以创建、编辑和删除待办事项。 为此,我们需要构建自己的应用程序视图模块,该模块采用以下结构:

views├── app│   ├── items.rs│   └── mod.rs
提供基本的 HTML

在我们的 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 文件,我们只需将该 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

现在我们已经定义了 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 文件已经提供,我们可以继续我们的下一个目标,即向我们的页面添加功能。

将 JavaScript 添加到 HTML 文件

如果前端用户无法对我们的待办事项状态执行任何操作,那么这对前端用户来说就没有用。 在修改之前,我们需要通过查看下图来了解 HTML 文件的布局:

图 5.3 – HTML 文件的一般布局

在图 5.3 中,我们可以看到我们可以在标头中定义元标记。 然而,我们也可以看到我们可以在标题中定义样式标签。 在标题下方的样式标签中,我们可以将 CSS 插入到样式中。 在主体下方,还有一个脚本部分,我们可以在其中注入 JavaScript。 该 JavaScript 在浏览器中运行并与正文中的元素交互。 由此,我们可以看到,提供加载了 CSS 和 JavaScript 的 HTML 文件提供了一个功能齐全的前端单页应用程序。 至此,我们可以反思一下本章的介绍。 虽然我喜欢 Rust,并且强烈希望告诉你用它来编写所有内容,但这对于软件工程中的任何语言来说都不是一个好主意。 现在,我们可以轻松地使用 JavaScript 提供功能性前端视图,使其成为满足您前端需求的最佳选择。

使用 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 注入 HTML

完成本节后,我们将拥有一个不太漂亮但功能齐全的主视图,我们可以在其中使用 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 加载功能。

添加 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)}
在 HTML 中添加 JavaScript 标签

从上一步中的 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 函数

现在我们有了渲染函数,我们可以看看我们的 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 调用后使用项目的新状态重新渲染前端。 这样,我们就可以进入最后一步,在这里我们将定义对待办事项执行创建、获取、删除和编辑功能的函数。

为按钮构建 JavaScript 函数

请注意,标头只是对后端中硬编码的接受令牌进行硬编码。 我们将在第 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 注入 HTML

注入 CSS 采用与注入 JavaScript 相同的方法。 我们将在 HTML 文件中添加一个 CSS 标签,该标签将被文件中的 CSS 替换。 为了实现这一目标,我们必须执行以下步骤:

将 CSS 标签添加到我们的 HTML 文件中。

为整个应用程序创建一个基本 CSS 文件。

为我们的主视图创建一个 CSS 文件。

更新我们的 Rust 箱以服务 CSS 和 JavaScript。

让我们仔细看看这个过程。

将 CSS 标签添加到 HTML

首先,让我们对 templates/main.html 文件进行一些更改:

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

在这里,我们可以看到有两个 CSS 标签。 {{BASE_CSS}}标签用于基础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

现在,我们必须定义页面及其组件的样式。 一个好的起点是在 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。

为主页创建 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。 在运行应用程序之前,我们需要在主视图中加载数据。

从 Rust 提供 CSS 和 JavaScript

我们在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 是一个独立的应用程序。 因此,我们通常会将 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 应用程序中。

在 React 中进行 API 调用

现在基本应用程序正在运行,我们可以开始对后端执行 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(";,  {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 的亮点在于构建自定义组件。 这意味着我们可以为每个待办事项构建具有自己的状态和功能的单独类。 我们将在下一节中看到这一点。

在 React 中创建自定义组件

当我们查看 App 类时,我们可以看到,拥有一个具有状态和函数的类非常有用,这些状态和函数可用于管理 HTML 呈现到浏览器的方式和时间。 当涉及到单个待办事项时,我们可以使用状态和函数。 这是因为我们有一个按钮可以从待办事项中获取属性并调用 Rust 服务器来编辑或删除它。 在本节中,我们将构建两个组件:src/components/ToDoItem.js 文件中的 ToDoItem 组件和 src/components/CreateToDoItem.js 文件中的 CreateToDoItem 组件。 一旦我们构建了这些,我们就可以将它们插入到我们的 App 组件中,因为我们的 App 组件将获取项目的数据并循环这些项目,创建多个 ToDoItem 组件。 为了实现这一目标,我们需要处理几个步骤,因此本节将分为以下小节:

创建我们的 ToDoItem 组件

创建 CreateToDoItem 组件

在我们的应用程序组件中构建和管理自定义组件

让我们开始吧。

创建我们的 ToDoItem 组件

我们将从 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("; +                 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 中创建自定义组件

我们的 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("; +        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 应用程序中来做到这一点。

将 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 应用程序转换为桌面应用程序

将我们的 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                && 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           ? ";           : `{path.join(__dirname,                                  "../build/index.html")}`    );    if (isDev) {        mainWindow.webContents.openDevTools();    }}

在这里,我们本质上定义了窗口的宽度和高度。 另请注意,nodeIntegration 和enableRemoteModule 使渲染器远程进程(浏览器窗口)能够在主进程上运行代码。 然后,我们开始在主窗口中加载 URL。 如果在开发人员模式下运行,我们只需加载 ,因为我们在 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。

标签: #手机怎么查看html源代码 #js注入网页文本 #web前端图片路径 #web中白色表示 #css行内显示代码