基于HTML 模板的 Rust Web 应用源码


如果你来自像Go这样拥有庞大标准库的语言,你应该知道Rust是比较轻量级的。该语言已经决定提供一个纤细的标准库和一个顶级的包管理器和工具。由社区来提供像网络服务器或模板的包。

如果你来自Python,即使有一个大的标准库,在做网络时,也不会被开发者使用。相反,你可能习惯于像Django、Jinja2或Fastapi这样的库。如果是这样的话,你就会对下面的堆栈感到熟悉。

这让我想知道......Go最终会不会和python走在同一个方向?在标准库中是否有一些社区不使用的东西,而依赖于第三方的包?无论如何...

如果你来自Javascript,你可能已经习惯了安装许多依赖性,所以你已经熟悉了接下来的内容。

Axum
Axum是 rust 领域最受欢迎的 Web 框架之一。它不像 Actix Web 那样成熟(它继续摇滚)。但它获得了很大的吸引力,因为它与 tokio 及其生态系统很好地整合在一起。AxumEmbarkStudiosDavid Pedersen创建。这家公司似乎正在将Rust提升到一个新的水平

Minijinja
Minijinja是 Python 的 Jinja2 的 rust 实现,作者是同样出色的作者:mitsuhiko。很多人已经熟悉它,并且有充分的理由,它很容易使用。

Tokio
最流行的异步运行时。它非常适合编写网络应用程序。就像我们使用 Axum 构建的 Web 应用程序一样。 Tokio拥有一个庞大的生态系统,从跟踪到数据库驱动程序。

Serde
Serde再次是 rust 中最流行的序列化和反序列化数据结构的方法。您将一种格式序列化或反序列化为另一种格式。例如,如果您从 HTTP 请求中接收 JSON 作为字节,则使用 serde 您将能够读取不同的字段,或将其中一些信息加载到struct.

设置
您已经安装了 rust,所以转到您的项目文件夹并创建一个新的 rust 项目。

cargo new web-template-rs cd web-template-rs/

具有以下依赖项。
cargo add axum \
    tokio -F tokio/full \
    serde -F serde/derive \
    minijinja -F minijinja/builtins


我们使用-F标志来表示要从这些 crate 中包含哪些功能。

代码可在github.com/woile/web-template-rs-example上获得

源码
返回没有模板的 HTML 的基本网络服务器如下所示:

use axum::{response::Html, routing::get, Router};

async fn home() -> Html<&'static str> {
    Html("hello world")
}

#[tokio::main]
async fn main() {
   
// build our application with a single route
    let app = Router::new()
        .route(
           
"/",
            get(home),
        );

   
// run it with hyper on localhost:3000
    axum::Server::bind(&
"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

运行:
cargo run

因为我们没有配置任何日志记录,所以运行后你不会在终端看到任何东西,这很好。如果你想学习如何做,请查看tracing-example.。

同时,在你的浏览器中转到0.0.0.0:3000,观察 "hello world "的文字。

你的浏览器将制作的请求,将看起来像这样:

GET / HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0
Accept: */*

模板
假设我们要创建一个 HTML,其中包含用户列表:

<ul>
    <li>Timmy</li>
    <li>Benji</li>
    <li>Mimi</li>
</ul>

当我们有一个新的用户时会发生什么?我们必须手动编辑它,对吗?我们能不能让它 "动态 "地表现出来?

是的,使用模板。Minijinja是对流行的python的jinja2的一个rust实现。一个流行的 "模板引擎",有自己的语言。模板引擎层出不穷,而且它们的语法也不尽相同。Python django的模板引擎,Go模板,JJSX(对吗?),lodash模板,等等。它们最终都是相似的,它们都有一个迭代、显示数据或使用条件的方法。

<ul>
    {% for user in users %}
    <li>user.name</li>
    {% endfor %}
</ul>

这样一来,我们虚构的用户服务,就可以接受这个模板,获取用户,并通过模板,呈现用户列表。

模板可能是一个怪胎。大多数时候,围绕它们的工具不是很好,你不会得到一个错误,直到你真正尝试渲染它们,你可能不会得到语法高亮。例如,在Kubernetes周围有一个著名的包管理器 "Helm",它在YAML之上使用模板。YAML 已经是一种有争议的语言,如果在上面再加上一个模板层,会变得非常难以阅读和维护。尽管如此,模板还是很方便的,我不知道有什么更好的选择。

Axum with templates
我们将创建2个虚构的结构,我们将把它们作为例子。

use serde::Serialize;

#[derive(Debug, Serialize)]
struct Items {
    id: i32,
    name: String,
}

#[derive(Debug, Serialize)]
struct Profile {
    full_name: String,
    items: Vec<Items>,
}

你可以看到我们正在使用serde的Serialize,所以minijinja可以使用它们。而Profile.items是一个Vec,这样我们可以在模板中展示一个迭代的例子。

正如我们所看到的模板,我们已经准备好用rust编写类似jinja2的模板。

const PROFILE_TEMPLATE: &'static str = r#"
<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>A Basic HTML5 Template</title>
  <meta name="description" content="A basic HTML5 Template for new projects.">
  <meta name="author" content="Woile">
</head>

<body>
    <h1>Profile of {{ profile.full_name|title }}</h1>
    <p>This is a template example to show some functionality</p>
    <h2>Items</h3>
    <ul>
        {% for item in profile.items %}
        <li>{{ item.name }} ({{ item.id }})</li>
        {% endfor %}
    <ul>
</body>
</html>

就在HTML的正文中,我们显示了profile.full_name,然后我们遍历每个项目,显示名字和id。

对于Axum,我们添加一个新的路由,它将创建一些示例结构。

use axum::{response::Html, routing::get, Router, extract::Path};
use minijinja::render;

async fn home() -> Html<&'static str> {
    Html("hello world")
}

async fn get_profile(Path(profile_name): Path<String>) -> Html<String> {
    let orders_example = vec![
        Items {
            id: 1,
            name: "Article banana".into(),
        },
        Items {
            id: 2,
            name: "Article apple".into(),
        },
    ];
    let profile_example = Profile {
        full_name: profile_name,
        items: orders_example,
    };
    let r = render!(PROFILE_TEMPLATE, profile => profile_example );
    Html(r)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route(
            "/",
            get(home),
        )
        .route(
            "/:profile_name",
            get(get_profile),
        );
    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}


"/:profile_name",
get(get_profile),

第一行是路径,它是一个根/+变量值:profile_name。在下一行,我们调用get_profile,我们可以看到它是如何使用从路径中提取的profile_name变量的。

之后,我们创建示例结构。在一个真实的例子中,这些信息可能来自数据库。

然后,在get_profile里面,我们有

let r = render!(PROFILE_TEMPLATE, profile => profile_example );
Html(r)

其中render!是minijinja的宏,它接收我们之前声明的模板,然后我们在模板中使用的变量和锈蚀变量之间提供某种 "映射"。
这里的一个缺点是,如果我们的模板有错误,我们只能在运行时看到它。但从好的方面看,这个介绍是一个快速开始使用Rust中模板和axum的方法。

 github.com/woile/web-template-rs-example