Hyper Webapp 模板
和我们很多人一样,我很懒。开发 Web 应用时,很多核心功能在不同的代码库中都是相同的。你需要响应 HTTP 请求、生成并返回 HTML 主体、提供静态资源、处理未知路由。没有必要为了渲染一个新网页而从头开始重新设计所有这些功能。
这就是我们拥有框架的原因,我们不喜欢一遍又一遍地做这些事情。
不过,和我们很多人一样,我也很挑剔。我经常会想起这张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
其中一个奇怪之处(也是很酷的一点)是,该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
只需运行它!
我将简要地解压其中几个文件。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);
}
}
该文件中您唯一需要接触的部分是调用部分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
}
}
}
所有处理程序最终都会传递到此函数:
/// 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())
}
它将你的响应主体作为字节切片,并在返回之前进行压缩,并添加适当的标头。很多资源都是 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
}
这些模板都具有特定的结构:
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 {}
它们目前是空白的,没有数据流过。如果你想将字符串传递到索引中,它可能看起来像这样:
#[derive(Default, Template)]
#[template(path = "index.html")]
pub struct IndexTemplate<'a> {
pub name: &'a str,
}
现在您需要使用正确类型的数据实例化该结构,然后可以{{ name }}
在模板文件中使用。
此模板预先与 Tailwind 挂钩 - 以下是app.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
再说一遍,这是你的应用,不是我的。当你准备好添加样式时,只需在这些指令下方开始 - 或者直接在你的模板中!提供的 NPM 脚本会src/assets/main.css
在编译 Rust 二进制文件之前将所有 CSS 编译进去,因此它也可以作为静态字符串包含在内。
差不多就是这样了!这个应用极其简陋,就像我喜欢的模板一样。我刚刚成功地用这个模板构建了一个更复杂的应用,包含数据库和一些数据抓取逻辑,而且从这里开始比从零开始节省了几个小时。效果因人而异。
请继续关注两个不太简单的变体 - 一个用于构建静态博客,另一个用于连接数据库和 ORM!
这里肯定还有改进的空间。代码可以重构(比如中间件?),需要测试等等。我最终会做到的,不过我也会提交 PR :)
照片由 Shiro hatori 在 Unsplash 上拍摄
鏂囩珷鏉ユ簮锛�https://dev.to/decidously/hyper-webapp-template-4lj7