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
#[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
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
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
)。
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 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请求并用响应替换其内容:
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官网学习。
上面的代码使用
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) } } }
这样我们可以在我们想要的地方调用它
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
- 引入样式 - tailwindcss
- 状态管理
- ....
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。