Joshua Mo 2023-12-28
入门准备
要开始使用 Loco,首先需要安装 Loco 的命令行工具(CLI),运行以下命令:
cargo install loco-cli
Loco 还使用 sea-orm-cli 来执行数据库迁移。你可以通过以下命令安装它:
cargo install sea-orm-cli
现在我们已经安装了所有必需的包,可以初始化我们的项目了。我们可以使用 loco new 命令并选择 SaaS 应用程序模板,这将为我们生成一个功能完整的应用程序。本教程中我们将应用命名为 example_app。别忘了进入项目文件夹!
当你编写 API 时,可能希望启动一个本地的 Docker 数据库用于测试。此时,可以使用以下 Docker 命令:
$ docker run -d -p 5432:5432 -e POSTGRES_USER=loco -e POSTGRES_DB=example_app_development -e POSTGRES_PASSWORD="loco" postgres:15.3-alpine
Loco 中的路由
第一步是生成一个 “脚手架(scaffold)”。这会同时生成控制器(controller)、模型(model)和迁移(migration)。你还可以预先指定字段名及其类型,更多详情请参见此处。下面是我们创建脚手架的命令:
cargo loco generate scaffold item name:string! description:string quantity:int!
该命令将为名为 items 的数据表生成控制器、模型和迁移,包含以下字段:
- 一个非空的
name字段(string!表示非空) - 一个可为空的
description字段 - 一个非空的
quantity字段(整数类型)
实体(entities)也会自动生成,因此你无需手动创建它们。
脚手架生成完成后,你会注意到 Loco 已自动将控制器和其他相关内容添加到 app.rs 文件中,因此无需手动注册。
现在我们可以打开新生成的控制器文件,路径应为 src/controllers/item.rs。打开后,你应该看到类似以下内容:
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
pub async fn echo(req_body: String) -> String {
req_body
}
pub async fn hello(State(_ctx): State<AppContext>) -> Result<String> {
format::string("Hello world!")
}
pub fn routes() -> Routes {
Routes::new()
.prefix("item")
.add("/", get(hello))
.add("/echo", post(echo))
}
现在我们可以开始为这些路由编写逻辑了!
如果你查看 AppContext 的源码(链接),会看到如下结构:
#[derive(Clone)]
#[allow(clippy::module_name_repetitions)]
pub struct AppContext {
/// 应用程序运行的环境。
pub environment: Environment,
#[cfg(feature = "with-db")]
/// 应用程序使用的数据库连接。
pub db: DatabaseConnection,
/// 可选的 Redis 连接池,用于工作队列任务。
pub redis: Option<Pool<RedisConnectionManager>>,
/// 应用程序的配置设置。
pub config: Config,
/// 可选的邮件发送组件,可用于发送电子邮件。
pub mailer: Option<EmailSender>,
}
这意味着我们只需使用 ctx.db 即可访问数据库连接。下面我们来看一个从数据库中获取所有 item 记录的简单请求示例:
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use crate::models::_entities::items::Entity as Item;
use crate::models::_entities::items::Model as ItemModel;
pub async fn hello(State(ctx): State<AppContext>) -> Result<Json<Vec<ItemModel>>> {
let items = Item::find().all(&ctx.db).await?;
format::json(items)
}
注意:我们需要从 entities 文件夹导入模型和实体。
我们可以在此基础上扩展,实现一个完整的 CRUD 控制器:
use crate::models::_entities::items::ActiveModel;
pub async fn view_item_by_id(Path<id>: Path<i32>, State(ctx): State<AppContext>) -> Result<Json<ItemModel>> {
let item: Option<ItemModel> = Item::find_by_id(id).one(&ctx.db).await?;
let item: ItemModel = item.unwrap();
format::json(item)
}
pub async fn create_item(
State(ctx): State<AppContext>,
Json(item): Json<ItemModel>
) -> Result<String> {
let item: ActiveModel = item.into();
item.insert(&ctx.db).await?;
format::text("Created")
}
#[derive(Deserialize)]
struct ItemQty { qty: i32 }
pub async fn update_item_quantity(
State(ctx): State<AppContext>,
Path(id): Path<i32>,
Json(json): Json<ItemQty>
) -> Result<String> {
let item: Option<ItemModel> = Item::find_by_id(id).one(&ctx.db).await?;
let mut item: ActiveModel = item.unwrap().into();
item.quantity = Set(json.qty);
let updated_item = item.update(&ctx.db).await?;
format::text("Updated")
}
pub async fn delete_item(
Path<id>: Path<i32>,
State(ctx): State<AppContext>
) -> Result<String> {
let item: Option<ItemModel> = Item::find_by_id(id).one(&ctx.db).await?;
let item: ItemModel = item.unwrap();
let res: DeleteResult = item.delete(&ctx.db).await?;
format::text("Deleted")
}
编写完所有路由后,需要确保在控制器文件的 routes() 函数中将它们挂载到路由器上:
pub fn routes() -> Routes {
Routes::new()
.prefix("items")
.add("/", get(get_all_items).post(create_item))
.add("/:id", get(view_item_by_id).put(update_item_quantity).delete(delete_item))
}
恭喜!你刚刚创建了第一个完整的 CRUD 路由器。
添加验证逻辑
在此基础上,我们可以为保存 item 时添加一些验证逻辑。Loco 本身重新导出了 validator crate,允许你验证某个结构体是否满足特定要求。Loco 将其与 sea_orm 集成,以便在将数据保存到数据库之前进行验证。一个验证器结构体可能如下所示:
#[derive(Debug, Validate, Deserialize)]
pub struct ModelValidator {
#[validate(range(min = 0, message = "Item must have at least a quantity of 0."))]
pub quantity: i32,
}
完成上述定义后,我们需要为验证器结构体实现 From<&ActiveModel>,并为 ActiveModel 实现 ActiveModelBehavior trait。注意:由于我们总是需要先将 Model 转换为 ActiveModel 才能保存,因此 before_save 函数总会被触发。
impl From<&ActiveModel> for ModelValidator {
fn from(value: &ActiveModel) -> Self {
Self {
quantity: *value.quantity.as_ref(),
}
}
}
#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::items::ActiveModel {
async fn before_save<C>(self, _db: &C, _insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
self.validate()?;
Ok(self)
}
}
你也可以为 Model 本身实现方法以扩展其行为!注意:你需要将数据库连接作为函数参数传入。例如,以下是在预生成的 users::Model 模型中查找用户邮箱的函数:
// src/models/users.rs
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
let user = users::Entity::find()
.filter(users::Column::Email.eq(email))
.one(db)
.await?;
user.ok_or_else(|| ModelError::EntityNotFound)
}
Loco 中的中间件
Loco 中的中间件可以通过以下几种方式实现:
- 为某个结构体或枚举实现
axum::FromRequestParts(或FromRequest) - 在
app.rs中的Hookstrait 中实现可选的after_routes()方法
使用 FromRequest 是为特定路由实现中间件的最简单方式,而 after_routes() 更适合全局中间件(例如超时处理)。
使用 FromRequestParts
尽管 FromRequestParts(以及 FromRequest)看起来难以实现,但只要记住:状态本身只需实现 Send + Sync —— 这意味着你可以直接使用 AppContext!以下是一个完整实现示例:
#[derive(Deserialize)]
pub struct MyMiddlewareState(String);
#[async_trait::async_trait]
impl<AppContext> FromRequestParts<AppContext> for MyMiddlewareState
{
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, _state: &AppContext) -> Result<Self, Self::Rejection> {
let string = "Hello world!".to_string();
if string != *"Hello world!" {
return Err(ApiError::Unknown);
}
Ok(MyMiddlewareState(string))
}
}
enum ApiError { Unknown }
impl IntoResponse for ApiError {
// ... 为 ApiError 实现 IntoResponse,使其能返回 HTTP 响应
}
当然,在真实应用中,逻辑会比简单地赋值 "Hello world!" 并返回结构体复杂得多。
使用全局中间件
如前所述,你也可以在 Hooks trait 中使用 after_routes() 函数。该函数签名如下:
async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
Ok(router)
}
由于 Axum 路由器作为参数传入,你可以附加任何类型的 Axum 中间件或 Layer。这也意味着你可以添加 tower-http 的服务层!下面我们演示如何添加一个超时层,以防止慢速攻击(Slowloris)。
首先,添加 tower-http 并启用 timeout 特性:
cargo add tower-http -F timeout
然后在 after_routes() 中添加该层:
use std::time::Duration;
use tower_http::timeout::TimeoutLayer;
async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
let router = router.layer(TimeoutLayer::new(Duration::from_secs(10)));
Ok(router)
}
现在,任何超过 10 秒的请求都会自动中止,并返回 408 Request Timeout 响应。非常实用!
你也可以实现自己的 Tower 服务,更多详情请参见 Tower 文档。
在 Loco 中提供前端服务
在 Loco 中提供前端服务非常简单:进入 frontend 文件夹,运行 npm i 安装依赖,然后运行 npm run build 构建应用。当你使用 cargo loco start 启动服务并访问 localhost:8000 时,应该能看到 Loco.rs 的主页(但可能是空白的)。
注意:默认前端方案使用 React。你可以自由替换为其他框架。唯一需要确保的是,你提供的前端资源路径需与配置文件中的
static部分匹配(默认folder键值为/frontend/dist)。如果你是 Svelte 或 Vue 用户,或者想使用 Leptos、Dioxus,都可以自由切换!如果你不熟悉上述框架,也可以直接使用原生 HTML/CSS/JS,尤其当你只需要提供少量静态内容时,这种方式可能更合适。
部署 Loco 应用
Loco 提供了专门的命令用于生成部署配置。运行以下命令即可:
cargo loco generate deployment
它会根据你的选择生成 Dockerfile 或 Shuttle 部署配置。
如果你使用 Shuttle 部署,请记得在 Shuttle.toml 文件中将前端资源添加到 assets 字段:
name = "<your-project-name>"
assets = ["frontend/dist/*"]
然后运行以下命令部署应用(别忘了先安装 cargo-shuttle):
shuttle deploy
我们计划推出 Loco 的原生数据库集成!敬请期待第 2 部分,我们将深入探讨如何构建你梦想中的 Web 应用程序。
结语
感谢阅读!希望这篇 Rust Loco 入门指南对你有所帮助。如果你想开始 Rust Web 开发,现在正是最佳时机!