Rwf 是一个全面的框架,用于在 Rust 中构建 Web 应用程序。采用经典的 MVC(模型-视图-控制器)模式编写,Rwf 配备了构建快速且安全 Web 应用程序所需的一切标准功能。
文档
官方的文档 **在此提供[1]**。
功能概览
HTTP 服务器[2] 用户友好的 ORM[3],轻松构建 PostgreSQL 查询 动态模板[4] 认证[5] & 内置用户会话 中间件[6] 后台任务[7] 和 定时任务[8] 数据库迁移 内置 REST 框架[9] 支持 JSON 序列化 WebSockets 支持 静态文件[10] 托管 与 Hotwired Turbo[11] 紧密集成,构建 后端驱动的 SPA[12] 环境特定配置 日志记录和指标 CLI[13] 用于从 Django/Flask 应用 迁移[14] 的 WSGI 服务器
快速开始
要将 Rwf 添加到你的技术栈中,创建一个 Rust 二进制应用程序,并将 rwf
和 tokio
添加到你的依赖项中:
cargo add rwf
cargo add tokio@1 --features full
构建应用程序就像这样简单:
use rwf::prelude::*;
use rwf::http::Server;
#[derive(Default)]
struct IndexController;
#[async_trait]
impl Controller for IndexController {
async fn handle(&self, request: &Request) -> Result<Response, Error> {
Ok(Response::new().html("<h1>Hey Rwf!</h1>"))
}
}
#[tokio::main]
async fn main() {
Server::new(vec![
route!("/" => IndexController),
])
.launch("0.0.0.0:8000")
.await
.unwrap();
}
架构
Rwf 建立在古老的 模型-视图-控制器(MVC)[15] 架构之上。根据我的经验,这是唯一经受住时间考验并且能够扩展到数百万行代码的架构。
这份文档尚不完整。如果你愿意,欢迎随时添加内容。
模块
Rwf 被划分为不同的模块和源代码文件。以下是模块的文档,没有特定的顺序。
controller
MVC 中的 "C",控制器模块负责处理来自客户端的 HTTP 请求。有三个(3)个特性最值得关注:
rwf::controller::Controller
这个特性是所有希望由 HTTP 服务器服务的控制器所必需的。它只有一个必需的方法(handle
),其他所有方法都是可选的,并且已经用默认值实现了。所有方法都可以被覆盖,使其非常可定制。
rwf::controller::RestController
这个特性实现了 REST 框架,特别是 Controller
特性的 handle
方法,并将传入的请求分割成 6 个 REST 动词。
rwf::controller::ModelController
与上述相同,但它还使用指定的 Model
类型实现了所有 6 个 REST 动词。它直接链接到 ORM,并读取、创建、更新和删除所需的记录。
rwf::controller::WebsocketController
实现 HTTP -> WebSocket 协议升级和通信。
controller::middleware
这个模块实现了控制器中间件。一旦你阅读了 mod.rs
,这个模块就非常不言自明了。
http
HTTP 服务器和请求路由到控制器。
job
后台和定时任务。
model
ORM 和连接池。
view
动态模板 —— 基本上是我们自己的 Rails ERB 实现。
crypto
使用 AES-128 轻松加密/解密数据。我认为 应该足够了[16],但如果需要,我很高兴加入其他密码。
comms
通过 Websockets 和 Tokio broadcast
模块,HTTP 客户端之间的通信。有助于将 Rwf 的一个部分与另一个部分连接起来。仍在早期开发中,我认为我们可以在这里添加更多东西,例如 Django 风格的信号 / Rails 风格的回调。
功能路线图
Rwf 是全新的,但 Web 开发历史悠久。许多功能缺失或不完整。
ORM
[ ] 左连接(LEFT JOINs) [ ] 右连接(RIGHT JOINs) [ ] MySQL 支持 [ ] SQLite 支持 [ ] 分布式锁(使用 Postgres,而不是 Redis) [ ] 更多测试
HTTP 服务器
[ ] HTTP/2, HTTP/3 [ ] TLS [ ] 模糊测试(不是四条腿的可爱动物,而是将垃圾输入路由器并试图使其崩溃的测试) [ ] 事件流(EventStreams) [ ] 集成测试
动态模板
[ ] 更好的错误消息,例如语法错误、未定义的变量、函数等。 [ ] 更多数据类型支持,例如 UUID、时间戳、任何我们忘记添加的 Rust 数据类型。 [ ] 更多测试 [x] context!
宏,从键值映射生成Context
[x] render!
宏,加载并渲染模板为Ok(Response::new().html())
[ ] 允许使用用户定义的函数(在启动时定义)扩展模板语法
后台与定时任务
[ ] 运行中/待处理/失败任务的统计信息(可以使用视图完成) [ ] 更多测试 [ ] 支持更多 crontab 语法扩展
认证与用户会话
[ ] 添加默认用户模型,以便我们可以在没有任何样板代码的情况下支持账户(就像 Django 一样) [ ] 内置 OAuth2(Google/GitHub 等)支持,用户只需要添加密钥/密文
文档和指南
[ ] 更多 README 风格的文档 [ ] 每个公共结构、函数、枚举等的代码文档(rust doc)
从 Python 迁移
[ ] 并行运行 Django/Flask 应用
更多?
请随时向此列表添加更多功能。
控制器基础 Rwf 提供了多个预构建的控制器,可以开箱即用,例如,用于处理 WebSocket 连接、REST 风格的交互或服务静态文件。对于其他所有情况,可以通过实现 Controller trait 来处理任何类型的 HTTP 请求。
什么是控制器? 控制器是 MVC 中的 "C":它处理用户与 Web 应用程序的交互,并代表他们执行操作。控制器负责处理用户输入,如表单,以及对应用程序的所有其他 HTTP 请求。
编写控制器 控制器是一个实现 Controller trait 的普通 Rust 结构体。例如,让我们编写一个返回当前 UTC 时间的控制器。
导入类型
use rwf::prelude::*;
prelude 模块包含了使用 Rwf 所需的大部分类型和特性。包含它会节省编写代码时的时间和努力,但不是必需的。
定义结构体
#[derive(Default)]
struct CurrentTime;
这个结构体没有字段,但你可以添加任何你想要跟踪的内部状态。自动推导出 Default trait,以提供一种方便的实例化方式。
实现 Controller trait
#[async_trait]
impl Controller for CurrentTime {
/// 这个函数处理传入的 HTTP 请求。
async fn handle(&self, request: &Request) -> Result<Response, Error> {
let time = OffsetDateTime::now_utc();
// 这创建了一个 HTTP "200 OK" 响应,
// 带有 "Content-Type: text/plain" 头。
let response = Response::new()
.text(format!("The current time is: {:?}", time));
Ok(response)
}
}
Controller trait 是异步的。Rust 中对异步 trait 的支持还不完整,因此我们使用 async_trait 库来简化使用。trait 本身有几个方法,其中大多数都有合理的默认值。唯一需要手工编写的方法是 async fn handle()。
handle handle 方法接受一个 Request 并必须返回一个 Response。响应可以是任何有效的 HTTP 响应,包括 404 或甚至 500。
错误 如果在 async fn handle 函数中发生错误,Rwf 会自动返回 HTTP 500 并将错误显示给客户端。
连接控制器 一旦实现了控制器,将其添加到应用程序中需要将其映射到一个路由。路由是一个唯一的 URL,从应用程序的根开始。例如,/signup 是一个可以映射到 Signup 控制器的路由,并允许用户创建账户。
在服务器启动时将控制器添加到应用程序中。服务器可以从代码中的任何异步任务启动,但通常从 main 函数开始:
use rwf::prelude::*;
use rwf::http::{self, Server};
#[tokio::main]
async fn main() -> Result<(), http::Error> {
Server::new(vec![
// 将 `/time` 路由映射到 `CurrentTime` 控制器。
route!("/time" => CurrentTime),
])
.launch("0.0.0.0:8000")
.await
}
注意
route! 宏是调用 CurrentTime::default().route("/time") 的简写。我们使用它因为它看起来很酷,但不是必需的。你可以以任何需要的方式实例化你的控制器结构体,并在将其添加到服务器时调用 Controller::route 方法。或者,你可以像我们在本例中所做的那样实现 Default trait 并使用宏。
使用 cURL 测试 一旦服务器运行,你可以使用 cURL(或任何常规浏览器,如 Firefox)测试你的端点:
cURL
输出
curl localhost:8000/time -w '\n'
分离 GET 和 POST 实现 Controller trait 的控制器不区分 HTTP 请求方法,并在一个函数中处理所有方法。大多数网站通过 GET 请求显示页面,并通过 POST 请求接受表单提交。为了避免在 handle 方法中编写样板代码,Rwf 有另一种类型的控制器 PageController,它将这两种方法分别拆分成自己的函数:async fn get 和 async fn post:
#[derive(Default, macros::PageController)]
struct Login;
impl PageController for Login {
/// 处理 GET 请求。
async fn get(&self, request: &Request) -> Result<Response, Error> {
/* 通过渲染模板显示页面 */
}
/// 处理 POST 请求。
async fn post(&self, request: &Request) -> Result<Response, Error> {
let form = request.form_data();
/* 处理表单提交并重定向 */
}
}
ORM 基础 介绍 Rwf 带有自己的 ORM(对象关系映射)。Rwf ORM 非常灵活,支持从基本的按主键查询到多表连接和复杂的自定义查询。
什么是 ORM? ORM 是 MVC 设计中的 M:模型。它允许你轻松检索数据库表中存储的数据,并在应用程序中显示,而无需手动编写复杂的 SQL 查询。
它通过将自己附加到 Rust 结构体,并将表列中的数据映射到结构体字段(反之亦然),在此过程中自动将它们从数据库类型转换为 Rust 数据类型。
入门 使用 ORM 很简单,只需要为每个模型(或数据库表)定义一个结构体。例如,大多数 Web 应用程序都会有一个用户模型,它将数据存储在 "users" 表中:
列 | 数据库数据类型 | Rust 数据类型 |
---|---|---|
id | BIGINT | i64 |
VARCHAR | String | |
created_at | TIMESTAMPTZ | time::OffsetDateTime |
可以这样定义模型的 Rust 结构体:
use rwf::prelude::*;
#[derive(Clone, macros::Model)]
struct User {
id: Option<i64>,
email: String,
created_at: OffsetDateTime,
}
使用以下查询可以创建数据库中的相同表:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
注意
id 列使用了可选的 Rust i64 整数。这是因为结构体将用于从表中插入和选择数据。插入时,id 列应为 None,并将由数据库自动分配。这确保了表中的所有行都有一个唯一的主键。
命名约定 结构体字段与数据库列同名,数据类型与它们各自的 Rust 类型相匹配。数据库中的表名对应于结构体的名称,小写并复数化。例如,User 模型将引用数据库中的 "users" 表。
包含模型数据的数据库表中的一行称为记录。macros::Model 宏自动实现了数据库到 Rust 以及反之的类型转换,并将列值映射到结构体字段。
查询数据 在 Rust 中定义了模型后,ORM 会自动实现 SQL 查询的编写。例如,要按主键获取记录,可以这样做:
let user = User::find(15)
.fetch(&mut conn)
.await?;
find 方法由 Model trait 为 User 结构体自动实现。它接受一个 Rust 整数,并产生以下查询:
SELECT * FROM "users" WHERE id = $1
fetch 方法组装查询,将其发送到数据库,并返回一行。该行被转换为 User 结构体的实例:
println!("user email: {}", user.email);
获取多行 可以使用 fetch_all 而不是 fetch 来查询多行,例如:
let users = User::all()
.order("id")
.limit(25)
.fetch_all(&mut conn)
.await?;
这将从 "users" 表中获取 25 条用户记录,按主键排序。结果将是一个 Vec
for user in &users {
println!("{}: {}", user.id, user.email);
}
ORM 可以用来编写简单和复杂的查询,而无需学习 SQL。Rwf 目前支持 PostgreSQL,但 SQLite 和 MySQL 等其他数据库也在路线图上。
模板概览 动态模板是 HTML 和一种编程语言的混合体,它指导 HTML 如何显示。例如,如果你的 Web 应用程序为用户有个人资料页面,你会希望每个用户都有专属于他们的页面。为了实现这一点,你只需要编写一个模板,并使用模板变量替换每个用户的独特部分,例如:
<div class="profile">
<h2><%= username %></h2>
<p><%= bio %></p>
</div>
变量 username
和 bio
可以被替换为每个用户独特的值,例如:
use rwf::prelude::*;
let template = Template::from_str(r#"
<div class="profile">
<h2><%= username %></h2>
<p><%= bio %></p>
</div>
"#)?;
let ctx = context!(
"username" => "Alice",
"bio" => "I like turtles"
);
let html = template.render(&ctx)?;
println!("{}", html);
输出将是:
<div class="profile">
<h2>Alice</h2>
<p>I like turtles</p>
</div>
模板帮助重用 HTML(以及 CSS、JavaScript),就像常规函数和结构体帮助重用代码一样。通过使用模板,你可以为不同的用户提供个性化的内容,而无需为每个用户编写单独的 HTML 页面。模板引擎会在运行时将模板变量替换为实际的数据,从而生成最终的 HTML 输出。这种方法不仅提高了开发效率,还使得维护和更新网站内容变得更加容易。
启用配置
要配置 Rwf,将名为 rwf.toml
的文件放置于你的应用工作目录中。在开发期间,这通常是你的 Cargo 项目的根目录。应用启动时,Rwf 会自动加载该文件中的配置设置。
可用设置
配置文件使用 TOML 语言编写。如果你不熟悉 TOML,它是一种简洁且富有表现力的语言,常用于 Rust 编程领域。
Rwf 的配置文件分为多个部分。[general] 部分控制着日志设置、加密使用的密钥等不同的选项。[database] 部分用于配置数据库连接设置,如数据库 URL、连接池大小等。
[general] 部分
log_queries:开启或关闭 ORM 执行的所有 SQL 查询的日志记录。默认为 false
。secret_key:用于加密的密钥,使用 base64 编码。默认为随机生成。 cache_templates:开启或关闭动态模板的缓存。在调试模式下默认为 false
,在发布模式下默认为true
。
密钥
密钥是一个使用 base64 编码的随机生成数据字符串。一个有效的密钥包含 256 位的熵,必须使用安全的随机数生成器生成。
如果你的系统中安装了 Python,可以用几行代码为 Rwf 生成一个密钥:
import base64
import secrets
secret = base64.b64encode(secrets.token_bytes(int(256/8)))
print(secret)
[database] 部分
name:要连接的数据库名称。默认与环境变量 $USER
相同。若未设置,则默认为postgres
。user:连接数据库的用户名。默认为 $USER
,若未设置,则默认为postgres
。url:完整的数据库连接字符串。格式为 postgresql://{user}/localhost:5432/{name}
,其中{user}
和{name}
分别对应配置中的用户名和数据库名。
数据库 URL 遵循 The Twelve Factor App 标准,使用 URL 格式指定数据库连接。连接 PostgreSQL 时,驱动程序为 postgresql
(或也可接受 postgres
)。
driver://user:password@host:port/database_name
后台任务
任务概览 后台任务,也称为异步任务,是可以独立于主 HTTP 请求/响应生命周期运行的代码。在后台任务中执行代码允许你执行有用的工作,而无需让客户端等待任务完成。后台任务的例子包括发送电子邮件或与第三方 API 通信。
Rwf 拥有自己的后台任务队列和工作器,可以执行这些任务。
定义任务
后台任务是任何实现了 Job trait 的 Rust 结构体。任务需要实现的唯一 trait 方法是异步的 execute
方法,该方法接受用 JSON 编码的任务参数。
例如,如果我们想在用户注册我们的 Web 应用程序时发送欢迎电子邮件,我们可以将其作为后台任务来完成:
use rwf::prelude::*;
use rwf::job::{Error as JobError};
use serde::{Serialize, Deserialize};
#[derive(Default, Debug, Serialize, Deserialize)]
struct WelcomeEmail {
email: String,
user_name: String,
}
#[async_trait]
impl Job for WelcomeEmail {
/// 此函数中的代码将在后台执行。
async fn execute(&self, args: serde_json::Value) -> Result<(), JobError> {
let args: WelcomeEmail = serde_json::from_value(args)?;
// 用给定的电子邮件地址向用户发送电子邮件。
Ok(())
}
}
生成工作器
一旦我们有了后台任务,我们需要创建将在单独的线程(实际上是 Tokio 任务)中运行并执行这些任务的工作器。通常在 main
函数中生成工作器,但可以在代码的任何地方进行:
use rwf::job::Worker;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() -> Result<(), Error> {
// 使用 4 个线程创建一个新的工作器。
let worker = Worker::new(vec![
WelcomeEmail::default().job()
])
worker.start().await?;
// 使主任务无限期休眠。
sleep(Duration::MAX).await;
}
共享进程 可以在应用程序内部生成工作器,而无需创建单独的二进制应用程序。由于大多数任务将运行异步代码,Tokio 将有效地负载均衡前台(HTTP 请求/响应)和后台工作负载。
在 Web 应用程序内生成工作器时,使用上述代码而不包含 sleep
。Worker::start
方法几乎立即返回,因为它只是在单独的 Tokio 任务上生成一个工作器。
调度任务
随着后台任务的定义和工作器的运行,我们可以开始安排任务在后台运行。可以通过在代码的任何地方调用 queue_async
方法来安排任务运行:
let email = WelcomeEmail {
email: "new-user@example.com".to_string(),
user_name: "Alice".to_string(),
};
// 尽快在后台安排任务运行。
queue_async(&email).await?;
queue_async
方法在队列中创建任务记录并立即返回,而不执行实际工作。这使得这种方法非常快速,因此你可以在控制器内安排多个任务,而不会显著影响端点延迟。
加密
加密
Rwf 使用 AES-128 对用户会话和私有 Cookie 进行加密。同样的功能通过 rwf::crypto
模块提供,用于加密和解密任意数据。
加密数据
要使用 AES-128 和应用密钥加密数据,可以使用 encrypt
函数,例如:
use rwf::crypto::encrypt;
use serde_json::json;
let data = json!({
"user": "test",
"password": "hunter2"
});
// JSON 被转换为字节数组。
let data = serde_json::to_vec(&data).unwrap();
// 使用 AES 对数据进行加密。
let encrypted = encrypt(&data).unwrap();
任何类型的数据都可以加密,只要它能被序列化为字节数组。通常可以使用 serde
来实现序列化。
加密会产生一个 base64 编码的 UTF-8 字符串。你可以将这个字符串保存在数据库中,或者通过不安全的介质如电子邮件发送。
解密数据
要解密数据,可以对 encrypt
函数生成的字符串调用 decrypt
函数。解密算法会自动将 base64 编码的字符串转换为字节,并使用密钥进行解密,例如:
use rwf::crypto::decrypt;
use serde_json::from_slice;
let decrypted = decrypt(&encrypted).unwrap();
let json = from_slice::<Value>(&decrypted).unwrap();
assert_eq!(json["user"], "test");
在这里,Value
是 serde_json
库中用于表示 JSON 数据的结构体。这段代码假设你已经包含了必要的 serde_json
和 rwf::crypto
模块。
日志
日志记录
Rwf 使用 log
crate 进行日志记录。该 crate 使用标准的 INFO(信息)、WARN(警告)、ERROR(错误)和 DEBUG(调试)级别来输出不同重要性的信息。如果你有日志记录偏好,例如你想使用没有颜色的 JSON 结构化日志,你可以选择使用你喜欢的日志订阅器。或者,你可以使用 Rwf 内置的日志记录器,如下所示:
use rwf::prelude::*;
#[tokio::main]
async fn main() {
// 确保在你的应用中只调用一次。
Logger::init();
/* ... */
}
记录查询
默认情况下,对数据库执行的查询不会被记录。如果你想看到执行了哪些操作(以及查询返回结果需要多长时间),可以在配置中切换 log_queries
设置。
记录请求 所有对 Rwf 的 HTTP 请求都以 INFO 级别记录。这在生产环境中很有用,可以帮助检测应用程序活动并调试任何问题(例如,负载均衡器配置错误)。
默认日志级别
默认情况下,Rwf 应用程序以 INFO 日志级别启动。由于 Rwf Logger 使用了 tracing-subscriber
,你可以通过设置 RUST_LOG
环境变量来更改它,例如:
export RUST_LOG=debug
这将设置 Rust 应用程序的默认日志级别为 DEBUG,让你能够看到更详细的日志信息。
从 python 迁移
从 Python 迁移 Rwf 用 Rust 编写,因此如果你有现有的应用程序想要迁移到 Rust,你有几种选择。Rwf 内置了 WSGI 服务器,所以你可以在 Rwf 控制器旁边无修改地运行现有的 Django 或 Flask 应用程序。
使用 WSGI 注意
Rwf 的 WSGI 服务器仍然是实验性的,不如流行的 uWSGI 项目那样先进。
将 WSGI 应用程序添加到你的 Rwf 服务器非常简单。首先,确保 Python 项目在你的 PYTHONPATH 中,例如:
export PYTHONPATH=/path/to/python/project
Rwf 将直接将你的 Python 应用加载到其自己的内存中(使用 pyo3),所以它需要能够在导入你的应用模块时找到它。
Django Django 应用程序带有 WSGI 接口,Rwf 可以直接使用。通常,接口位于它自己的文件中,例如 project/wsgi.py。WsgiController 在初始化时接受 Python 模块作为参数,在这种情况下,是 project.wsgi。
一旦初始化,控制器可以被添加到服务器中,并安装在 /*(根,通配符)路径上。这确保所有请求都由 Django 处理:
use rwf::prelude::*;
use rwf::http::Server;
use rwf::controller::WsgiController;
#[tokio::main]
async fn main() {
Server::new(vec![
WsgiController::new("project.wsgi")
.wildcard("/"),
])
.launch("0.0.0.0:8000")
.await
.unwrap();
}
Python 依赖项 你的应用程序很可能有其他依赖项,例如 django 或 Flask 包等。为了确保它们在加载到 Rwf 时正常工作,要么在启动服务器之前创建并激活虚拟环境,要么全局安装这些包(例如,在 Docker 中部署时)。
将流量转移到 Rust 当你将端点重写为使用 Rwf 和 Rust 时,你可以一次移动一个路由的流量,而不会干扰你的用户。例如,如果你准备将路由 /users 移动到 Rust,将控制器添加到服务器中:
/// 你新的 "/users" 控制器
/// 用 Rust 编写。
use crate::controllers::Users;
#[tokio::main]
async fn main() {
Server::new(vec![
WsgiController::new("project.wsgi")
.wildcard("/"),
route!("/users" => Users),
])
.launch("0.0.0.0:8000")
.await
.unwrap();
}
Rwf 路由算法将匹配请求到 /users 到 Users 控制器,而不是将其发送到 WSGI,因为 Users 控制器路径更具体,优先级高于通配符路由。
总结
Rwf 是一个 Rust Web 框架,支持文件和环境变量配置。它具备内置的 WSGI 服务器,允许直接在 Rwf 中运行 Django 或 Flask 应用,便于从 Python 迁移到 Rust。Rwf 使用 AES-128 加密技术保护用户会话和私有 Cookie,并提供加密和解密任意数据的功能。日志系统基于 log
crate,支持不同的日志级别,可通过环境变量调整日志输出。对于希望逐步迁移到 Rust 的开发者,Rwf 提供了平滑过渡的方案,允许在同一个应用中同时运行 Python 和 Rust 代码,逐步将流量转移到 Rust 控制器。
在此提供: https://levkk.github.io/rwf/
[2]HTTP 服务器: examples/quick-start
[3]ORM: examples/orm
[4]动态模板: examples/dynamic-templates
[5]认证: examples/auth
[6]中间件: examples/middleware
[7]后台任务: examples/background-jobs
[8]定时任务: examples/scheduled-jobs
[9]REST 框架: examples/rest
[10]静态文件: examples/static-files
[11]Hotwired Turbo: https://turbo.hotwired.dev/
[12]后端驱动的 SPA: examples/turbo
[13]CLI: rwf-cli
[14]迁移: examples/django
[15]模型-视图-控制器(MVC): https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
[16]应该足够了: https://security.stackexchange.com/questions/14068/why-most-people-use-256-bit-encryption-instead-of-128-bit