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
下面这段代码是构建一个监听指定端口的服务器。并对指定路径的做出反应。
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!()来操作。
// 代码省略 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>") } }
可能大家没注意到,这里做一下拓展,在
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
引入到我们的页面中。
注意:上面例子中,layout(content: String)
的函数签名变成接受String
了。(不过这里不是很重要)
优化组件定义方式
我们上面的代码示例中,是利用fromat!()
对字符串进行页面结构的拼装,在一些简单的页面这种方式还是很有作用的,但是如果阁下遇到页面逻辑复杂,层级众多的时候,使用format!()
就有点捉襟见肘了。
我们可以使用Maud crate,来重新处理上面的组件。
下面,我们做一次简单的改造。
首先,我们引入了一个新的依赖项。
[dependencies] maud = { varsion = "0.25.0", features = ["axum"] }
这里的features
是适配各种Rust
框架的,具体情况可以参考Web framework integration
使用Maud
的RSC
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
来自定义其输出。
这对于创建自己的组件非常有用:
我们可以在各自页面或者共有页面中引入对应的样式信息。对应的代码如下。
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)} } } }