使用 Rust 的 Loco 框架入门指南

更新于 2026-01-18

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 中的中间件可以通过以下几种方式实现:

  1. 为某个结构体或枚举实现 axum::FromRequestParts(或 FromRequest
  2. app.rs 中的 Hooks trait 中实现可选的 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 开发,现在正是最佳时机!