Hyper Webapp 模板

2025-06-08

Hyper Webapp 模板

和我们很多人一样,我很懒。开发 Web 应用时,很多核心功能在不同的代码库中都是相同的。你需要响应 HTTP 请求、生成并返回 HTML 主体、提供静态资源、处理未知路由。没有必要为了渲染一个新网页而从头开始重新设计所有这些功能。

这就是我们拥有框架的原因,我们不喜欢一遍又一遍地做这些事情。

不过,和我们很多人一样,我也很挑剔。我经常会想起这张XKCD :

xkcd

大多数 CLI 脚手架工具都给我这种感觉。我知道所有额外的样板代码其实都是我想要的东西,但我不知道它们到底什么。

所以,我做了自己的。

如果你像我一样,你不会用这个模板或任何其他模板。但是,如果你是我,你就会用,因为它是你创建的!否则,它可能对你创建自己的模板有帮助。

这是GitHub 仓库。您可以点击便捷的“使用此模板”按钮开始使用。

以下是重点内容:

  • Hyper——没有框架,只是一个小而快的 HTTP 服务器库。
  • Askama - Typesafe,编译模板。
  • TailwindCSS - 为不知道如何使用实际 CSS 的人提供细粒度的样式。
  • Docker——简单、快速部署。
  • Github Action - 在您的提交旁边获得一个漂亮的绿色复选标记。

让我们快速浏览一下。

$ tree
.
├── Cargo.lock                    # Rust package lockfile
├── Cargo.toml                    # Rust package metadata
├── Dockerfile                    # Docker container build instructions
├── LICENSE                       # I use the BSD-3-Clause License
├── README.md                     # Markdown readme file
├── package.json                  # NPM package metadata
├── pnpm-lock.yaml                # NPM package lockfile
├── postcss.config.js             # CSS processing configuration
├── src
│   ├── assets
│   │   ├── config.toml           # Set runtime options (port, address)
│   │   ├── images
│   │   │   └── favicon.ico       # Any static images can live here
│   │   ├── main.css              # Postcss-compiled styelsheet - don't edit this one
│   │   ├── manifest.json         # WebExtension API metadata file
│   │   └── robots.txt            # Robots exclusion protocol
│   ├── config.rs                 # Read config.toml/CLI options, init logging
│   ├── css
│   │   └── app.css               # App-wide stylesheet - DO edit this one
│   ├── handlers.rs               # Rust functions from Requests to Responses
│   ├── main.rs                   # Entry point
│   ├── router.rs                 # Select proper handler from request URI
│   ├── templates.rs              # Type information for templates
│   └── types.rs                  # Blank - use how you like!
├── stylelintrc.json              # CSS style lint options
└── templates
    ├── 404.html                  # Not Found stub
    ├── index.html                # Main page template
    └── skel.html                 # App-wide skeleton markup

5 directories, 24 files
Enter fullscreen mode Exit fullscreen mode

其中一个奇怪之处(也是很酷的一点)是,该assets/目录实际上位于 里面src/。这是因为所有这些文本文件资产都通过include_str!()宏以静态字符串的形式直接包含在二进制文件中。部署时,这些额外的东西都不存在。如果不使用 Docker,部署目录将如下所示:

$ tree
.
├── LICENSE                       # I use the BSD-3-Clause License
├── README.md                     # Markdown readme file
├── images
│    └── favicon.ico              # Favicon
└── hyper-template                # Executable
Enter fullscreen mode Exit fullscreen mode

只需运行它!

我将简要地解压其中几个文件。main.rs首先让我们看一下:

#[tokio::main]
async fn main() {
    init_logging(2); // set INFO level
    let addr = format!("{}:{}", OPT.address, OPT.port)
        .parse()
        .expect("Should parse net::SocketAddr");
    let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(router)) });

    let server = Server::bind(&addr).serve(make_svc);

    info!("Serving {} on {}", env!("CARGO_PKG_NAME"), addr);

    if let Err(e) = server.await {
        eprintln!("Server error: {}", e);
    }
}
Enter fullscreen mode Exit fullscreen mode

该文件中您唯一需要接触的部分是调用部分make_service_fn。此存根假设您的处理程序不会失败,并使用std::convert::Infallible。这意味着,在此调用中弹出的任何错误(也就是您的路由器和处理程序)都需要使用unwrap()或 来处理expect()。您可以通过简单地换成 来获得更多灵活性anyhow::Error!这样,所有这些unwrap()s 都可以变成?s。这是我个人在使用此模板时所做的,但我决定不为您做出这样的选择——这感觉在极简模板中有点过度。

另外,值得注意的是,Rustasync/await现在有了!对于一些已经存在的功能(例如 Futures),这是一种非常酷的语法,使整个 Rust 更容易上手。不再需要那些疯狂的 120 个字符类型!入门指南请点击此处

你其实不需要动这个。它只是设置了异步运行时,并将你的实际应用程序转换为可以使用它的状态机。在本例中,我们的实际应用程序是函数router()。它看起来像这样:

pub async fn router(req: Request<Body>) -> HandlerResult {
    let (method, path) = (req.method(), req.uri().path());
    info!("{} {}", method, path);
    match (method, path) {
        (&Method::GET, "/") | (&Method::GET, "/index.html") => index().await,
        (&Method::GET, "/main.css") => {
            string_handler(include_str!("assets/main.css"), "text/css", None).await
        }
        (&Method::GET, "/manifest.json") => {
            string_handler(include_str!("assets/manifest.json"), "text/json", None).await
        }

        (&Method::GET, "/robots.txt") => {
            string_handler(include_str!("assets/robots.txt"), "text", None).await
        }
        (&Method::GET, path_str) => {
            // Otherwise...
            // is it an image?
            if let Some(ext) = path_str.split('.').nth(1) {
                match ext {
                    "ico" | "svg" => image(path).await,
                    _ => four_oh_four().await,
                }
            } else {
                four_oh_four().await
            }
        }
        _ => {
            warn!("{}: 404!", path);
            four_oh_four().await
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

所有处理程序最终都会传递到此函数:

/// Top-level handler that DEFLATE compresses and responds with from a &[u8] body
/// If None passed to status, 200 OK will be returned
pub async fn bytes_handler(
    body: &[u8],
    content_type: &str,
    status: Option<StatusCode>,
) -> HandlerResult {
    // Compress
    let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
    e.write_all(body).unwrap();
    let compressed = e.finish().unwrap();
    // Return response
    Ok(Response::builder()
        .status(status.unwrap_or_default())
        .header(header::CONTENT_TYPE, content_type)
        .header(header::CONTENT_ENCODING, "deflate")
        .body(Body::from(compressed))
        .unwrap())
}
Enter fullscreen mode Exit fullscreen mode

它将你的响应主体作为字节切片,并在返回之前进行压缩,并添加适当的标头。很多资源都是 HTML,但我们总是会用到它:

pub async fn string_handler(
    body: &str,
    content_type: &str,
    status: Option<StatusCode>,
) -> HandlerResult {
    bytes_handler(body.as_bytes(), content_type, status).await
}

pub async fn html_str_handler(body: &str) -> HandlerResult {
    string_handler(body, "text/html", None).await
}
Enter fullscreen mode Exit fullscreen mode

这些模板都具有特定的结构:

use askama::Template;

#[derive(Default, Template)]
#[template(path = "skel.html")]
pub struct SkelTemplate {}

#[derive(Default, Template)]
#[template(path = "404.html")]
pub struct FourOhFourTemplate {}

#[derive(Default, Template)]
#[template(path = "index.html")]
pub struct IndexTemplate {}
Enter fullscreen mode Exit fullscreen mode

它们目前是空白的,没有数据流过。如果你想将字符串传递到索引中,它可能看起来像这样:

#[derive(Default, Template)]
#[template(path = "index.html")]
pub struct IndexTemplate<'a> {
    pub name: &'a str,
}
Enter fullscreen mode Exit fullscreen mode

现在您需要使用正确类型的数据实例化该结构,然后可以{{ name }}在模板文件中使用。

此模板预先与 Tailwind 挂钩 - 以下是app.css

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

再说一遍,这是你的应用,不是我的。当你准备好添加样式时,只需在这些指令下方开始 - 或者直接在你的模板中!提供的 NPM 脚本会src/assets/main.css在编译 Rust 二进制文件之前将所有 CSS 编译进去,因此它也可以作为静态字符串包含在内。

差不多就是这样了!这个应用极其简陋,就像我喜欢的模板一样。我刚刚成功地用这个模板构建了一个更复杂的应用,包含数据库和一些数据抓取逻辑,而且从这里开始比从零开始节省了几个小时。效果因人而异。

请继续关注两个不太简单的变体 - 一个用于构建静态博客,另一个用于连接数据库和 ORM!

这里肯定还有改进的空间。代码可以重构(比如中间件?),需要测试等等。我最终会做到的,不过我也会提交 PR :)

照片由 Shiro hatori 在 Unsplash 上拍摄

鏂囩珷鏉ユ簮锛�https://dev.to/decidously/hyper-webapp-template-4lj7
PREV
有趣的棋盘游戏机制 Nerd Alert
NEXT
开始编写方案