前言
在前面的文章中,我们介绍过React
的RSC
和Rust
。
- React Server Components手把手教学
- Rust学习笔记
而如果想使用RSC
,就需要使用Next.js
的最新版本。而今天,我们做一次技术尝试。
「用Rust
搭建适配RSC
的Web服务器」。
我们在致所有渴望学习Rust的人的信中也介绍过,Rust
在Web开发
中也能大放异彩。
最近,在评论区,有些人说Rust
在国内的工作岗位少,其实如果大家细心去找的话,还是有些区块链或者类似的公司都有招聘计划。
从我个人角度来看的话,Rust
其实不是啥屠龙术,更多的是给自己的另外一种机会。
好了,天不早了,干点正事哇。
前置知识点
❝「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」 同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。 ❞
Tokio.rs
你没看错,这个单词和「小日子」有关。
Tokio.rs
(通常称为Tokio
)是一个用于异步编程的Rust
编程语言的库和运行时系统。它提供了一个基于futures
和async/await
的高性能异步编程框架,使开发者能够编写高效、非阻塞的网络应用程序和系统服务。
- 「异步编程」:
Tokio.rs
的核心特性是异步编程。它利用Rust
的async/await
语法来编写异步代码,使得编写非阻塞的程序更加直观和容易。- 异步编程可以在单个线程上处理大量的并发连接和
I/O
操作,从而提高了应用程序的性能和资源利用率。
- 「事件驱动」:
Tokio
基于事件驱动的模型,应用程序被构建成响应事件的方式,例如,网络套接字上的数据到达、定时器触发等。- 这种模型使得应用程序可以有效地管理多个并发任务,而不必为每个任务分配一个独立的线程,从而减少了资源开销。
- 「核心组件」:
- 「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
语法的库。
- 「生态系统」:
Tokio.rs
有一个丰富的生态系统,包括许多第三方库和插件,用于构建各种类型的应用程序,从Web服务器到分布式系统。- 一些流行的Web框架,如
Actix
和warp
,都建立在Tokio.rs
之上。
- 「性能」:
Tokio.rs
被设计成高性能的异步运行时系统,通过有效地管理事件循环和任务调度,使得应用程序能够处理大量并发连接而不会出现性能瓶颈。
- 「安全性」:
Rust
的内存安全性和线程安全性特性也适用于Tokio.rs
应用程序,帮助开发者避免了许多常见的并发和内存错误。
- 「使用场景」:
Tokio.rs
广泛用于构建高性能的网络服务器、代理、数据库连接池、消息队列等异步应用程序。- 它也适用于需要大规模并发处理的任务,如网络爬虫和实时数据处理。
下图是用简单的命令,构建了一个服务器。
更详细的介绍,可以参考tokio.rs 官网[1]
Axum.rs
Axum.rs
(通常简称为Axum
)是一个用于构建异步Rust
应用程序的Web框架。它专注于提供简单而强大的工具,使得开发者能够构建高性能、可维护的异步Web服务。
- 「异步和性能」:
Axum.rs
是一个异步的Web框架,它利用Rust
的async/await
语法来编写「非阻塞的Web应用程序」。- 异步编程模型允许应用程序有效地处理大量并发请求,提高性能和资源利用率。
- 「基于Actix和Hyper」:
Axum.rs
构建在Actix
和Hyper
之上,这两个项目都是Rust生态系统中非常受欢迎的Web框架和HTTP库。- 它继承了Actix的优秀特性,如Actor模型和中间件支持,以及Hyper的高性能HTTP请求处理能力。
- 「简洁和模块化」:
Axum
的设计目标之一是提供清晰、模块化的API,使得开发者能够轻松构建和维护Web应用程序。- 它鼓励将应用程序划分为各种小模块,每个模块负责处理特定的请求和响应。
- 「中间件支持」:
Axum
支持中间件,这些中间件可以用于处理请求和响应,例如身份验证、日志记录、跨域资源共享(CORS)等。- 这使得开发者可以轻松地扩展和自定义应用程序的行为。
- 「路由」:
Axum
提供了强大的路由功能,可以根据不同的HTTP请求路径和HTTP方法来映射到处理程序(handlers)。- 开发者可以定义多个路由,并且可以通过宏来声明路由和处理程序。
- 「Type-Safe」:
Axum.rs
是类型安全的,它利用Rust的类型系统来防止常见的编程错误,如空指针和数据竞争。
- 「生态系统」:
- 虽然Axum.rs是相对较新的项目,但它已经开始吸引了一些关注,并且正在建立一个生态系统,包括与数据库连接、模板引擎和其他常见Web开发工具的集成。
- 「使用场景」:
- Axum.rs适用于构建高性能的Web服务,特别是需要处理大量并发请求的应用程序,如实时通信服务、游戏服务器、物联网后端等。
下面是利用axum
简单的构建了一个web应用。访问localhost:3000/
就会返回Hello Rust
。
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
语法构建页面结构的例子。
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
)是一个用于传输数据的命令行工具和库,广泛用于各种操作系统中,包括Linux
、Unix
、macOS
和Windows
。它支持多种协议,包括HTTP
、HTTPS
、FTP
、FTPS
等,因此可以用来与各种远程服务器进行数据交互。
- 「功能特点」:
- 「数据传输」:
curl
可以用于上传和下载文件,发送和接收数据。 - 「多协议支持」:支持多种协议,使其适用于各种网络通信需求。
- 「命令行工具」:
curl
是一个命令行工具,可以通过终端或脚本来使用。 - 「跨平台」:可在多种操作系统上运行,包括
Linux
、Unix
、macOS
和Windows
。 - 「开源」:
curl
是开源软件,可以自由使用和修改。
- 「数据传输」:
- 「基本用法」:
- 下载文件:
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
,将服务器响应保存到指定文件中。
- 下载文件:
- 「高级用法」:
- 「自定义请求头」:通过
-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
中引入axum
和tokio
。这两具体干啥的,我们在前面介绍了,这里就不过赘述了。
[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>")
}
我们对其中核心代码做简单的分析和解释:
main
函数:#[tokio::main]
注解标识main
函数是「异步」的,这意味着它可以在一个异步运行时环境中执行。- 在
main
函数中,首先通过Router::new()
创建了一个Axum
应用程序app
,然后使用.route()
方法定义了两个路由规则:一个是/page1
,另一个是/page2
,分别映射到page1
和page2
函数。 - 接着,通过
SocketAddr
创建了服务器地址对象,指定服务器监听的地址和端口。 - 最后,使用
Axum
的Server::bind()
方法绑定服务器地址,并使用.serve()
方法启动服务器。服务器将处理传入的请求,并根据路由规则调用相应的处理函数。
page1
和page2
函数:- 这两个函数是「路由处理函数」,它们接收请求并返回响应。
- 这里的
page1
函数返回一个Html
响应,其中包含一个简单的标题标签<h1>Page 1</h1>
。 - 同样,
page2
函数返回一个Html
响应,其中包含<h1>Page 2!!!</h1>
。
使用cargo run
后,我们可以在浏览器中,通过访问对应的页面地址进行页面展示。
页面共有逻辑抽离
在我们页面开发中,总是会有「多个页面拥有共同的布局和样式」,我们可以对其进行抽离。也就是消除重复的HTML
,此时我们可以使用format!()[4]来操作。
// 代码省略
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
组件。
// 一个JSX组件
function Count({ count }) {
if (count < 0) {
return <p>数字为负数</p>;
}
return <h1>{count}</h1>;
}
等价于Rust
服务器组件
fn count(count: i64) -> String {
if count < 0 {
"<p>数字为负数</p>".into()
} else {
format!("<h1>{count}</h1>")
}
}
代码语言:javascript复制❝可能大家没注意到,这里做一下拓展,在
React
中我们定义组件的时候,我们有一个不成文的规定,组件「首字母大写」,例如上面的例子中Count
。 而到了Rust
中定义组件时候,组件名称变成了小写了(count
)。其实这也是Rust
的不成文规定。这是因为Rust
代码使用蛇形命名法Snake Case 来作为规范函数和变量名称的风格。而蛇形命名法「只使用小写的字母进行命名,并以下画线分隔单词」。 ❞
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]
使用Maud
的RSC
代码语言:javascript复制fn count(count: i64) -> Markup {
// 我们仍然可以在这里放一些其他逻辑。
// 但是,Maud模板方便地支持if、let、match和循环。
html! {
@if (count < 0) {
p { "数字为负数" }
} else {
h1 { (count) }
}
}
}
我们现在将format!()
用maud
替换。代码如下:
// ...省略
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
,最浓墨重彩的一笔是,Maud
的Render特性
。
默认情况下,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
中引入对应的项目
[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 响应。
以下是对这段代码的详细解释:
async fn render_with_meta<F>(...) -> axum::response::Html<String>
:- 这是一个异步函数,它返回一个 HTML 响应,响应内容是一个
String
。 - 函数接受一个名为
render_fn
的参数,该参数是一个闭包,闭包的返回值是一个实现了Future
trait 的类型(F
)。
- 这是一个异步函数,它返回一个 HTML 响应,响应内容是一个
where F: futures::Future<Output = String> Send 'static
:- 这是一个「泛型约束」,限定了闭包
render_fn
返回的类型F
必须是一个实现了Future
trait 的类型,并且其Output
类型是String
。 Send
表示F
必须是可跨线程发送的。'static
表示F
必须具有静态生命周期,这意味着它可以在整个程序运行期间保持有效。
- 这是一个「泛型约束」,限定了闭包
- 函数体:
- 函数体开始时调用了
rscx::axum::render
函数,该函数似乎是用于渲染的工具函数,接受一个异步闭包作为参数。 - 在这个异步闭包中,我们使用
async move { render_fn().await }
来调用传入的render_fn
,并等待它的结果。这部分代码负责实际的渲染工作。
- 函数体开始时调用了
rscx::axum::render
返回的结果再次被await
,这表示整个异步函数render_with_meta
将等待渲染完成,然后返回一个 HTML 响应对象,响应内容是渲染结果的String
。
这个函数的主要目的是将渲染逻辑封装在一个异步函数中,并处理异步渲染的细节,最终返回一个 HTML 响应。它可以帮助你在 Axum 框架中更方便地处理异步渲染任务。在调用该函数时,你需要传递一个异步闭包,该闭包负责实际的渲染工作,并返回一个 Future
,其 Output
类型是 String
。函数内部会处理异步操作,确保返回一个完整的 HTML 响应对象。
4. 页面响应交互事件
创建一些静态或近似静态内容是很简单的。
但是,一个静态的网页对于我们开发来说,就像下孩子过家家一样。
下面,我们将在页面中新增一个button
,用于记录按钮被点击的次数。
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()
函数。
// 向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
中,我们也需要做一次改造。
async fn main() {
let app = Router::new().route("/page1", get(page1));
let app = register(app);
// 省略下面代码。
}
这样,当用户点击按钮时,服务器将处理请求并更新计数器,然后返回更新后的计数器HTML,从而实现交互性。
我们可以使用curl
轻松测试我们的新端点:
$ 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-XX
是htmx
的语法,在这里我们就不展开说明。感兴趣的同学,可以前往htmx官网[8]学习。
❝上面的代码使用
maud
语法构建的组件,如果有兴趣,可以换成rscx
。效果是等同的。 ❞
5. 新增 <Suspense />
特性
上面的例子,虽然代码很少,但是也算的上是一个功能完备的RSC
了。我们还想更进一步,针对Suspense
特性,让我们的页面有更好的用户交互体验。
❝
React
中的Suspense
组件真正的用途是:在需要渲染想要的展示的组件时候,在服务器上仍渲染时一个回退组件。 ❞
考虑我们之前的counter()
组件;想象一下我们需要从一个需要500毫秒的第三方服务中检索该数字。在关键内容渲染之后,我们应该让客户端延迟获取counter()
组件。
我们继续使用htmx
,事件绑定问题。首先,我将创建一个新的GET /components/counter
路由,只返回counter
组件:
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请求:
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()
组件来保持整洁!
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
的内容
待做的部分
上面的内容,我们利用axum
和maud
或者rscx
和htmx
构建了一个功能完备的RSC
服务,其实还有很多东西没完善。
比方说:
- 组件库 - dioxus[9]
- 引入样式 - tailwindcss[10]
- 状态管理
- ....
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/