用Rust搭建React Server Components 的Web服务器

2023-10-23 11:54:37 浏览数 (4)

前言

在前面的文章中,我们介绍过ReactRSCRust

  • React Server Components手把手教学
  • Rust学习笔记

而如果想使用RSC,就需要使用Next.js的最新版本。而今天,我们做一次技术尝试。

「用Rust搭建适配RSC的Web服务器」

我们在致所有渴望学习Rust的人的信中也介绍过,RustWeb开发中也能大放异彩。

最近,在评论区,有些人说Rust在国内的工作岗位少,其实如果大家细心去找的话,还是有些区块链或者类似的公司都有招聘计划。

从我个人角度来看的话,Rust其实不是啥屠龙术,更多的是给自己的另外一种机会。

好了,天不早了,干点正事哇。

前置知识点

「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」 同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。 ❞

Tokio.rs

你没看错,这个单词和「小日子」有关。

Tokio.rs(通常称为Tokio)是一个用于异步编程的Rust编程语言的库和运行时系统。它提供了一个基于futuresasync/await的高性能异步编程框架,使开发者能够编写高效、非阻塞的网络应用程序和系统服务。

  1. 「异步编程」
    • Tokio.rs的核心特性是异步编程。它利用Rustasync/await语法来编写异步代码,使得编写非阻塞的程序更加直观和容易。
    • 异步编程可以在单个线程上处理大量的并发连接和I/O操作,从而提高了应用程序的性能和资源利用率。
  2. 「事件驱动」
    • Tokio基于事件驱动的模型,应用程序被构建成响应事件的方式,例如,网络套接字上的数据到达、定时器触发等。
    • 这种模型使得应用程序可以有效地管理多个并发任务,而不必为每个任务分配一个独立的线程,从而减少了资源开销。
  3. 「核心组件」
    • 「tokio-core」:提供了异步基础设施,包括异步任务的调度和基本的I/O操作。
    • 「tokio-io」:提供了对网络和文件I/O的高级异步支持。
    • 「tokio-tcp」「tokio-udp」:用于构建异步TCP和UDP网络应用程序的库。
    • 「tokio-timer」:用于创建和管理定时器的库。
    • 「tokio-sync」:提供了各种异步的同步原语,如通道(channels)和锁(locks)。
    • 「tokio-async-await」:用于支持Rust的async/await语法的库。
  4. 「生态系统」
    • Tokio.rs有一个丰富的生态系统,包括许多第三方库和插件,用于构建各种类型的应用程序,从Web服务器到分布式系统。
    • 一些流行的Web框架,如Actixwarp,都建立在Tokio.rs之上。
  5. 「性能」
    • Tokio.rs被设计成高性能的异步运行时系统,通过有效地管理事件循环和任务调度,使得应用程序能够处理大量并发连接而不会出现性能瓶颈。
  6. 「安全性」
    • Rust的内存安全性和线程安全性特性也适用于Tokio.rs应用程序,帮助开发者避免了许多常见的并发和内存错误。
  7. 「使用场景」
    • Tokio.rs广泛用于构建高性能的网络服务器、代理、数据库连接池、消息队列等异步应用程序。
    • 它也适用于需要大规模并发处理的任务,如网络爬虫和实时数据处理。

下图是用简单的命令,构建了一个服务器。

更详细的介绍,可以参考tokio.rs 官网[1]

Axum.rs

Axum.rs(通常简称为Axum)是一个用于构建异步Rust应用程序的Web框架。它专注于提供简单而强大的工具,使得开发者能够构建高性能、可维护的异步Web服务。

  1. 「异步和性能」
    • Axum.rs是一个异步的Web框架,它利用Rustasync/await语法来编写「非阻塞的Web应用程序」
    • 异步编程模型允许应用程序有效地处理大量并发请求,提高性能和资源利用率。
  2. 「基于Actix和Hyper」
    • Axum.rs构建在ActixHyper之上,这两个项目都是Rust生态系统中非常受欢迎的Web框架和HTTP库。
    • 它继承了Actix的优秀特性,如Actor模型和中间件支持,以及Hyper的高性能HTTP请求处理能力。
  3. 「简洁和模块化」
    • Axum的设计目标之一是提供清晰、模块化的API,使得开发者能够轻松构建和维护Web应用程序。
    • 它鼓励将应用程序划分为各种小模块,每个模块负责处理特定的请求和响应。
  4. 「中间件支持」
    • Axum支持中间件,这些中间件可以用于处理请求和响应,例如身份验证、日志记录、跨域资源共享(CORS)等。
    • 这使得开发者可以轻松地扩展和自定义应用程序的行为。
  5. 「路由」
    • Axum提供了强大的路由功能,可以根据不同的HTTP请求路径和HTTP方法来映射到处理程序(handlers)。
    • 开发者可以定义多个路由,并且可以通过宏来声明路由和处理程序。
  6. 「Type-Safe」
    • Axum.rs是类型安全的,它利用Rust的类型系统来防止常见的编程错误,如空指针和数据竞争。
  7. 「生态系统」
    • 虽然Axum.rs是相对较新的项目,但它已经开始吸引了一些关注,并且正在建立一个生态系统,包括与数据库连接、模板引擎和其他常见Web开发工具的集成。
  8. 「使用场景」
    • Axum.rs适用于构建高性能的Web服务,特别是需要处理大量并发请求的应用程序,如实时通信服务、游戏服务器、物联网后端等。

下面是利用axum简单的构建了一个web应用。访问localhost:3000/就会返回Hello Rust

代码语言:javascript复制
use axum::{
    routing::{get, post},
    Router,
};

use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // 初始化tracing
    tracing_subscriber::fmt::init();

    // 构建我们的应用程序,包括一个路由
    let app = Router::new()
        .route("/", get(root));

    // 使用hyper运行我们的应用程序,在全局监听端口3000
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

// 基本的处理函数,响应一个静态字符串
async fn root() -> &'static str {
    "Hello, Rust!"
}

更详细的内容,可以参考axum官网[2]

rscx 的介绍

下面的代码是用类似JSX语法构建页面结构的例子。

代码语言:javascript复制
use rscx::{component, html, props, CollectFragment};

#[tokio::main]
async fn main() {
    let app = app().await;
    println!("{}", app);
}


async fn app() -> String {
    let s = "ul { color: red; }";
    html! {
        <!DOCTYPE html>
        <html>
            <head>
                <style>{s}</style>
            </head>
            <body>
                <Section />
                <Section title="Hello">
                    <Items />
                </Section>
            </body>
        </html>
    }
}

#[component]

fn Section(
    
    #[builder(default = "Default title".into(), setter(into))] 
    title: String,
    #[builder(default)] 
    children: String,
) -> String {
    html! {
        <div>
            <h1>{ title }</h1>
            { children }
        </div>
    }
}

#[component]
async fn Items() -> String {
    let data = load_data_async().await;
    html! {
        <ul>
            {
                data
                    .into_iter()
                    .map(|item| html! { <li>{ item }</li> })
                    .collect_fragment() // helper method to collect a list of components into a String
            }
        </ul>
    }
}

async fn load_data_async() -> Vec<String> {
    vec!["a".to_string(), "b".to_string(), "c".to_string()]
}

更多内容,可以参考rscx 官网[3]。

CURL

curl(全称:Client for URLs)是一个用于传输数据的命令行工具和库,广泛用于各种操作系统中,包括LinuxUnixmacOSWindows。它支持多种协议,包括HTTPHTTPSFTPFTPS等,因此可以用来与各种远程服务器进行数据交互。

  1. 「功能特点」
    • 「数据传输」curl可以用于上传和下载文件,发送和接收数据。
    • 「多协议支持」:支持多种协议,使其适用于各种网络通信需求。
    • 「命令行工具」curl是一个命令行工具,可以通过终端或脚本来使用。
    • 「跨平台」:可在多种操作系统上运行,包括LinuxUnixmacOSWindows
    • 「开源」curl是开源软件,可以自由使用和修改。
  2. 「基本用法」
    • 下载文件:curl -O URL,例如:curl -O https://www.example.com/file.txt,将会下载文件到当前目录。
    • 发送HTTP请求:curl -X GET/POST/PUT/DELETE URL,可以发送不同类型的HTTP请求。
    • 基本认证:curl -u username:password URL,用于进行基本的HTTP认证。
    • 保存响应到文件:curl -o output.txt URL,将服务器响应保存到指定文件中。
  3. 「高级用法」
    • 「自定义请求头」:通过-H选项可以添加自定义的HTTP请求头。
    • 「表单提交」:可以使用-d选项发送表单数据,例如-d "key1=value1&key2=value2"
    • 「文件上传」:使用-F选项可以上传文件,例如-F "file=@path/to/file"
    • 「代理服务器」:通过--proxy选项可以设置代理服务器。
    • 「SSL选项」:支持HTTPS,可以使用--insecure选项来跳过SSL证书验证。
    • 「断点续传」:使用-C -选项可以实现断点续传,继续之前的下载。
    • 「Cookie管理」:通过-b-c选项可以发送和保存Cookie信息。
    • 「并发下载」:使用-O URL1 -O URL2可以并发下载多个文件。

总之,curl是一个功能强大且灵活的工具,可用于各种网络通信需求。它的命令行界面和丰富的选项使其成为开发人员和系统管理员的重要工具之一。


2. 服务端渲染

说一个事实,其实见到的网页都是通过网络传输的HTML构建而成(毕竟HTML构成了网页的骨架)。 只不过,有些网页是「一股脑」的所有HTML都返回了,而有的网页是通过SPA/SSR等资源分离技术通过Node/JS/React等各种眼花缭乱的技术来构建一个网站。

构建服务器

让我们使用Axum作为应用框架构建一个最简单的Web服务器

首先,我们先在Cargo.toml中引入axumtokio。这两具体干啥的,我们在前面介绍了,这里就不过赘述了。

代码语言:javascript复制
[dependencies]
axum = { version = "0.6.20" }
tokio = { version = "1", features = ["full"] }

main.rs

下面这段代码是构建一个监听指定端口的服务器。并对指定路径的做出反应。

代码语言:javascript复制
use axum::{  
  response::Html, 
  response::IntoResponse, 
  routing::get, Router
  };
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
   // 创建一个 Axum 应用程序
   let app = Router::new()
       .route("/page1", get(page1))
       .route("/page2", get(page2));

   // 指定服务器地址(这里监听本地的 127.0.0.1:3000)
   let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

   // 使用 Axum 的服务器绑定和服务方法启动服务器
   axum::Server::bind(&addr)
       .serve(app.into_make_service())
       .await
       .unwrap();
}

async fn page1() -> impl IntoResponse {
    Html("<h1>Page 1</h1>")
}

async fn page2() -> impl IntoResponse {
    Html("<h1>Page 2!!!</h1>")
}

我们对其中核心代码做简单的分析和解释:

  1. main 函数:
    • #[tokio::main] 注解标识 main 函数是「异步」的,这意味着它可以在一个异步运行时环境中执行。
    • main 函数中,首先通过 Router::new() 创建了一个 Axum 应用程序 app,然后使用 .route() 方法定义了两个路由规则:一个是 /page1,另一个是 /page2,分别映射到 page1page2 函数。
    • 接着,通过 SocketAddr 创建了服务器地址对象,指定服务器监听的地址和端口。
    • 最后,使用 AxumServer::bind() 方法绑定服务器地址,并使用 .serve() 方法启动服务器。服务器将处理传入的请求,并根据路由规则调用相应的处理函数。
  2. page1page2 函数:
    • 这两个函数是「路由处理函数」,它们接收请求并返回响应。
    • 这里的 page1 函数返回一个 Html 响应,其中包含一个简单的标题标签 <h1>Page 1</h1>
    • 同样,page2 函数返回一个 Html 响应,其中包含 <h1>Page 2!!!</h1>

使用cargo run后,我们可以在浏览器中,通过访问对应的页面地址进行页面展示。


页面共有逻辑抽离

在我们页面开发中,总是会有「多个页面拥有共同的布局和样式」,我们可以对其进行抽离。也就是消除重复的HTML,此时我们可以使用format!()[4]来操作。

代码语言:javascript复制
// 代码省略

async fn layout(content: &'static str) -> String {
    format!(
        "
        <h1>柒八九陪你一起学Rust</h1>
        {content}
        "
    )
}

async fn page1() -> impl IntoResponse {
    Html(layout("<h1>Page 1</h1>").await)
}

async fn page2() -> impl IntoResponse {
    Html(layout("<h1>Page 2!!!</h1>").await)
}

上面代码中最重要的是 content 参数的类型是 &'static str,这意味着它是一个「静态字符串切片」,其生命周期被标记为 'static,表示这个字符串切片在「整个程序运行期间都有效,不会被销毁」。这样做是为了确保在异步函数中使用字符串切片时,不会出现生命周期问题,因为异步函数可能会在较长的时间内挂起。

同时函数签名定义为async fn 表示这是一个异步函数,它可以在执行期间挂起而不会阻塞整个线程。

我们还是熟悉的配方,在浏览器中访问对应的页面地址。


在Rust中定义组件

熟悉前端开发的同学,感觉到这种逻辑或者页面结构抽离很熟悉,这不就是我们经常挂在嘴边的「组件封装」吗。既然页面结构可以进行抽离,那如果我们站在页面全链路角度来看,那是不是我们可以在服务器中定义我们页面中需要展示的组件,然后再输出到浏览器中。

咦,这个观点和概念是不是又感觉似曾相识。这就是我们之前讲过的RSC

例如,现在有一个React组件。

代码语言:javascript复制
// 一个JSX组件
function Count({ count }) {
    if (count < 0) {
        return <p>数字为负数</p>;
    }
    return <h1>{count}</h1>;
}

等价于Rust服务器组件

代码语言:javascript复制
fn count(count: i64) -> String {
    if count < 0 {
        "<p>数字为负数</p>".into()
    } else {
        format!("<h1>{count}</h1>")
    }
}

❝可能大家没注意到,这里做一下拓展,在React中我们定义组件的时候,我们有一个不成文的规定,组件「首字母大写」,例如上面的例子中Count。 而到了Rust中定义组件时候,组件名称变成了小写了(count)。其实这也是Rust的不成文规定。这是因为Rust代码使用蛇形命名法Snake Case 来作为规范函数和变量名称的风格。而蛇形命名法「只使用小写的字母进行命名,并以下画线分隔单词」。 ❞

代码语言:javascript复制
use axum::{
  response::Html, 
  response::IntoResponse, 
  routing::get, Router
};
// 省略部分代码

async fn layout(content: String) -> String {
    format!(
        "
        <h1>柒八九陪你一起学Rust</h1>
        {content}
        "
    )
}

fn count(count: i64) -> String {
    if count < 0 {
        "<p>数字为负数</p>".into()
    } else {
        format!("<h1>{count}</h1>")
    }
}

async fn page1() -> impl IntoResponse {
    Html(layout(count(789)).await)
}

async fn page2() -> impl IntoResponse {
    Html(layout(count(-1)).await)
}

我们可以将count引入到我们的页面中。

page1

page2

注意:上面例子中,layout(content: String)的函数签名变成接受String了。(不过这里不是很重要)


优化组件定义方式

我们上面的代码示例中,是利用fromat!()对字符串进行页面结构的拼装,在一些简单的页面这种方式还是很有作用的,但是如果阁下遇到页面逻辑复杂,层级众多的时候,使用format!()就有点「捉襟见肘」了。

我们可以使用Maud[5] crate,来重新处理上面的组件。

下面,我们做一次简单的改造。

首先,我们引入了一个新的依赖项。

代码语言:javascript复制
[dependencies]
maud = { varsion = "0.25.0", features = ["axum"] } 

这里的features是适配各种Rust框架的,具体情况可以参考Web framework integration[6]

使用MaudRSC

代码语言:javascript复制
fn count(count: i64) -> Markup {
    // 我们仍然可以在这里放一些其他逻辑。
    // 但是,Maud模板方便地支持if、let、match和循环。
    html! {
        @if (count < 0) {
            p { "数字为负数" }
        } else {
            h1 { (count) }
        }
    }
}

我们现在将format!()maud替换。代码如下:

代码语言:javascript复制
// ...省略
use maud::{html, Markup};


// 省略main 函数

async fn layout(content: Markup) -> Markup {
    html! {
        h1 {"柒八九陪你一起学Rust"}
        {(content)}
    }
}

fn count(count: i64) -> Markup {
    html! {
        @if count < 0 {
            p {"数字为负数"}
        }@ else {
            h1 {(count)}
        }
    }
}

async fn page1() -> impl IntoResponse {
    html! {
        {(layout(count(789)).await)}
    }
}

async fn page2() -> impl IntoResponse {
    html! {
        {(layout(count(-1)).await)}
    }
}

然后,我们继续访问对应的网址,可以查看一下效果。(亲测有效,这里就不贴图了)

细心的小伙伴,可能使用Maud时,函数签名中使用了Markup而不是String。这里简单的解释一下。

Markup是一个字符串,但它也是一种表示包含HTML的字符串的方式。默认情况下,Maud会转义字符串内容。「直接返回Markup更容易嵌套Maud组件」

Last but not least,最浓墨重彩的一笔是,MaudRender特性

默认情况下,Maud会使用标准的Display特性将组件呈现为HTML。类型可以通过实现Render来自定义其输出。

这对于创建自己的组件非常有用:

我们可以在各自页面或者共有页面中引入对应的样式信息。对应的代码如下。

代码语言:javascript复制
struct Css(&'static str);
impl Render for Css {
    fn render(&self) -> Markup {
        html! {
            link rel="stylesheet" type="text/css" href=(self.0);
        }
    }
}

async fn layout(content: Markup) -> Markup {
    // 创建一个 Stylesheet 实例,传入样式表的链接地址
    let stylesheet = Stylesheet("./styles.css");

    // 使用 render 方法将 Stylesheet 实例转换为 Markup
    let stylesheet_markup: Markup = stylesheet.render();
    html! {
        head {
            title {"Rust 搭建RSC服务器"}
            // 插入样式表链接
            (stylesheet_markup)
        }
        body {
            h1 {"柒八九陪你一起学Rust"}
            {(content)}
        }
    }
}


3. Server components

使用了RSC,服务器最终将JSX组件呈现为HTML字符串,就像我们前面所做的那样。

然后,我们上面的代码示例中,大部分都是基于fromat!()或者它的改进版本Maud对字符串进行页面结构的拼装。上面的写法显然不够优雅。

对于熟悉了JSX的语法的前端开发者,我们还是希望能够有一种类JSX的语法方式,让我们能够产生比较小的心里负担来开发页面。

那么,我们可以使用rscx来构建我们的页面结构。(具体的使用情况可以参考「前置知识点」

首先我们在Cargo.toml中引入对应的项目

代码语言:javascript复制
[dependencies]
rscx = { version = "0.1.8", features = ["axum"] }

format!()相比,JSX提供了显著更好的开发体验。

让我们使用rscx来改造上面的例子。

改造Layout 和 Count

代码语言:javascript复制
#[props]
struct LayoutProps {
    #[builder(default)]
    children: String,
}
#[component]
async fn Layout(props: LayoutProps) -> String {
    let s = "h1 { color: red; }";
    html! {
       <!DOCTYPE html>
        <html>
            <head>
                <style>{s}</style>
            </head>
            <body>
                <h1>柒八九陪你一起学Rust</h1>
                {props.children}
            </body>
        </html>
    }
}

#[props]
struct CountProps {
    #[builder(default = 0)]
    count: i32,
}

#[component]
fn Count(props: CountProps) -> String {
    let count = props.count;
    html! {
        <p>
            {
                if count < 0 {
                    "数字为负数".to_string()
                } else {
                    format!("{}!", count)
                }
            }
        </p>
    }
}

改造PageN

代码语言:javascript复制

async fn page1() -> impl IntoResponse {
    render_with_meta(|| async {
        html! {
            <Layout>
                <Count count=-1/>
            </Layout>
        }
    })
    .await
}

async fn page2() -> impl IntoResponse {
    render_with_meta(|| async {
        html! {
            <Layout>
                <Count count=789/>
            </Layout>
        }
    })
    .await
}

工具函数 render_with_meta

代码语言:javascript复制
async fn render_with_meta<F>(
    render_fn: impl FnOnce() -> F   Send   'static,
) -> axum::response::Html<String>
where
    F: futures::Future<Output = String>   Send   'static,
{
    rscx::axum::render(async move { render_fn().await }).await
}

其中,render_with_meta我们需要额外的关注一下:

这段代码定义了一个名为 render_with_meta 的异步函数,该函数接受一个闭包 render_fn 作为参数。这个函数的主要目的是将一个异步渲染函数包装起来,以便在 Axum 框架中进行处理,并返回一个 HTML 响应。

以下是对这段代码的详细解释:

  1. async fn render_with_meta<F>(...) -> axum::response::Html<String>
    • 这是一个异步函数,它返回一个 HTML 响应,响应内容是一个 String
    • 函数接受一个名为 render_fn 的参数,该参数是一个闭包,闭包的返回值是一个实现了 Future trait 的类型(F)。
  2. where F: futures::Future<Output = String> Send 'static
    • 这是一个「泛型约束」,限定了闭包 render_fn 返回的类型 F 必须是一个实现了 Future trait 的类型,并且其 Output 类型是 String
    • Send 表示 F 必须是可跨线程发送的。
    • 'static 表示 F 必须具有静态生命周期,这意味着它可以在整个程序运行期间保持有效。
  3. 函数体:
    • 函数体开始时调用了 rscx::axum::render 函数,该函数似乎是用于渲染的工具函数,接受一个异步闭包作为参数。
    • 在这个异步闭包中,我们使用 async move { render_fn().await } 来调用传入的 render_fn,并等待它的结果。这部分代码负责实际的渲染工作。
  4. rscx::axum::render 返回的结果再次被 await,这表示整个异步函数 render_with_meta 将等待渲染完成,然后返回一个 HTML 响应对象,响应内容是渲染结果的 String

这个函数的主要目的是将渲染逻辑封装在一个异步函数中,并处理异步渲染的细节,最终返回一个 HTML 响应。它可以帮助你在 Axum 框架中更方便地处理异步渲染任务。在调用该函数时,你需要传递一个异步闭包,该闭包负责实际的渲染工作,并返回一个 Future,其 Output 类型是 String。函数内部会处理异步操作,确保返回一个完整的 HTML 响应对象。

4. 页面响应交互事件

创建一些静态或近似静态内容是很简单的。

但是,一个静态的网页对于我们开发来说,就像下孩子过家家一样。

下面,我们将在页面中新增一个button,用于记录按钮被点击的次数。

代码语言:javascript复制
use maud::{html, Markup};

static mut COUNTER: u32 = 0;

fn counter() -> Markup {
    // 从一个真实的数据库中获取数据,而不是访问全局状态
    let c = unsafe { COUNTER };
    html! {
        div {
            p { "总数为:" (c) }
            button { "数字加1" }
        }
    }
}

async fn page1() -> impl IntoResponse {
    html! {
        h1 { "页面可交互" }
        (counter())
    }
}

点击按钮目前还没有任何反应。

我们将使用htmx[7] JavaScript库。通过编写适量的JavaScript代码,就可以响应一下事件回掉。

❝当用户点击按钮时,它将发送一个POST /components/counter/increment请求到我们的服务器,服务器会在其全局状态中更新计数器并返回更新后计数器的修改后的HTML。 ❞

让我们在Axum的路由器中注册一个新的counter_increment()路由处理程序。对于响应,我们可以重用之前定义的counter()函数。

代码语言:javascript复制
// 向axum路由器注册特定于此组件的新路由
fn register(router: Router) -> Router {
    router.route("/components/counter/increment", post(counter_increment))
}

static mut COUNTER: u32 = 0;

async fn counter_increment() -> Markup {
    // 更新状态
    unsafe { COUNTER  = 1 };
    // 返回更新后的HTML
    counter()
}

fn counter() -> Markup {
    // 从一个真实的数据库中获取数据,而不是访问全局状态
    let c = unsafe { COUNTER };
    html! {
        div {
            p { "总数为:" (c) }
            button { "数字加1" }
        }
    }
}

async fn page1() -> impl IntoResponse {
    html! {
        h1 { "页面可交互" }
        (counter())
    }
}

这里有一点需要注意:我们需要将之前定义的Router对象传人。所以,在main中,我们也需要做一次改造。

代码语言:javascript复制
async fn main() {
    
    let app = Router::new().route("/page1", get(page1));
    let app = register(app);
    // 省略下面代码。
}

这样,当用户点击按钮时,服务器将处理请求并更新计数器,然后返回更新后的计数器HTML,从而实现交互性。

我们可以使用curl轻松测试我们的新端点:

代码语言:javascript复制
$ curl -XPOST http://localhost:3000/components/counter/increment
<div><p>总数为: 1</p><button>数字加1</button></div>

$ curl -XPOST http://localhost:3000/components/counter/increment
<div><p>总数为: 2</p><button>数字加1</button></div>

到这步了,说明我们的应用可以根据对应的请求,返回指定的HTML结构了。

为了使其具有交互性,让我们添加一个「点击」事件,该事件发送相同的HTTP请求并用响应替换其内容:

代码语言:javascript复制
fn counter() -> Markup {
    
    let c = unsafe { COUNTER };
    html! {
        div {
            p { "总数为:" (c) }
            button
                // 目标元素将被替换
                hx-target="closest div"
                // 请求的方法和URL
                hx-post="/components/counter/increment"
                // 由于它是一个按钮,默认情况下,htmx会将触发器设置为点击事件
                { "数字加1" }
        }
    }
}

async fn page1() -> impl IntoResponse {
    html! {
         // 从CDN添加htmx
        script src="https://unpkg.com/htmx.org@1.9.3" {}
        h1 { "页面可交互" }
        (counter())
    }
}

就是这样,它就可以工作了:

上面的hx-XXhtmx的语法,在这里我们就不展开说明。感兴趣的同学,可以前往htmx官网[8]学习。

❝上面的代码使用maud语法构建的组件,如果有兴趣,可以换成rscx。效果是等同的。 ❞

5. 新增 <Suspense />特性

上面的例子,虽然代码很少,但是也算的上是一个功能完备的RSC了。我们还想更进一步,针对Suspense特性,让我们的页面有更好的用户交互体验。

React中的Suspense组件真正的用途是:在需要渲染想要的展示的组件时候,在服务器上仍渲染时一个回退组件。 ❞

考虑我们之前的counter()组件;想象一下我们需要从一个需要500毫秒的第三方服务中检索该数字。在关键内容渲染之后,我们应该让客户端延迟获取counter()组件。

我们继续使用htmx,事件绑定问题。首先,我将创建一个新的GET /components/counter路由,只返回counter组件:

代码语言:javascript复制
fn register(router: Router) -> Router {
    router
        .route("/components/counter", get(counter_get))
        .route("/components/counter/increment", post(counter_increment))
}

async fn counter_get() -> Markup {
    counter()
}

而且,因为我们不再想渲染counter(),所以让我们用一个占位符div来代替,这个div将在页面准备好后触发GET请求:

代码语言:javascript复制
async fn page1() -> impl IntoResponse {
    html! {
        script src="https://unpkg.com/htmx.org@1.9.3" {}
        h1 { "Suspence" }
        // 这个div将在页面加载后立即被替换
        div hx-trigger="load" hx-get="/components/counter" {
            p { "总数为: 正在加载中...." }
        }
    }
}

我们也可以编写自己的suspense()组件来保持整洁!

代码语言:javascript复制
fn suspense(route: &str, placeholder: Markup) -> Markup {
    html! {
        // 这个div将在页面加载后立即被替换
        div hx-trigger="load" hx-get=(route) {
            (placeholder)
        }
    }
}

这样我们可以在我们想要的地方调用它

代码语言:javascript复制
async fn page1() -> impl IntoResponse {
    html! {
         // 从CDN添加htmx
        script src="https://unpkg.com/htmx.org@1.9.3" {}
        h1 { "页面可交互" }
        // 这个div将在页面加载后立即被替换
        { (suspense("/components/counter",html!{"总数为: 正在加载中...."}))}
    }
}

在页面加载过程中,我们会看到页面中有一瞬间显示的是,Suspence的内容

待做的部分

上面的内容,我们利用axummaud或者rscxhtmx构建了一个功能完备的RSC服务,其实还有很多东西没完善。

比方说:

  1. 组件库 - dioxus[9]
  2. 引入样式 - tailwindcss[10]
  3. 状态管理
  4. ....

Reference

[1]

tokio.rs 官网: https://tokio.rs/

[2]

axum官网: https://github.com/tokio-rs/axum

[3]

rscx 官网: https://crates.io/crates/rscx

[4]

format!(): https://doc.rust-lang.org/stable/alloc/fmt/index.html

[5]

Maud: https://maud.lambda.xyz/

[6]

Web framework integration: https://maud.lambda.xyz/web-frameworks.html

[7]

htmx: https://htmx.org/

[8]

htmx官网: https://htmx.org/

[9]

dioxus: https://github.com/DioxusLabs/dioxus

[10]

tailwindcss: https://tailwindcss.com/

0 人点赞