编程 Toasty 异步 ORM 深度实战:Tokio 团队的「应用级查询引擎」设计哲学与代码实践

2026-05-02 14:02:53 +0800 CST views 4

Toasty 深度解析:Tokio 团队如何用「应用级查询引擎」重新定义 Rust ORM

当 Diesel 还在同步时代徘徊,SeaORM 在类型安全的迷宫中挣扎时,Tokio 团队带着一款「不隐藏数据库能力」的异步 ORM 杀入战场。Toasty 不是又一款 SQL 生成器,而是一个让应用层 schema 与数据库 schema 完全解耦的查询引擎。


一、背景:Rust ORM 的三国杀

在 Toasty 问世之前,Rust 生态的 ORM 领域长期处于「三足鼎立」的状态:

1.1 Diesel:同步时代的王者

Diesel 是 Rust 生态中最成熟的 ORM,自 2016 年发布以来一直是许多项目的首选。它的核心优势在于编译时类型安全——通过宏在编译期生成查询代码,让 SQL 错误无处遁形。

但 Diesel 的致命伤也很明显:天生同步设计。在 async/await 成为主流的今天,Diesel 的同步架构让它与 Tokio 的异步世界格格不入。你不得不使用 spawn_blockingdiesel-async 这样的补丁方案,性能和优雅程度都大打折扣。

// Diesel 的同步风格:在 async 上下文中需要 block_on 或 spawn_blocking
fn get_user_sync(conn: &mut PgConnection, user_id: i32) -> QueryResult<User> {
    users::table
        .find(user_id)
        .first(conn)
}

// 在 Tokio 中使用需要这样包装
async fn get_user(db: &PgPool, user_id: i32) -> Result<User, Error> {
    let conn = db.get().await?;
    tokio::task::spawn_blocking(move || {
        get_user_sync(&mut conn, user_id)
    }).await?
}

1.2 SeaORM:异步原生的挑战者

SeaORM 是专门为异步设计的 ORM,深度集成 sqlx,天然支持 Tokio。它提供了 Entity、Model、Column 等概念,试图用 Rust 的类型系统模拟传统 ORM 的体验。

但 SeaORM 的问题在于过度抽象。为了让 API 看起来「像 ORM」,它引入了大量的泛型层和类型推导,导致:

  • 编译错误信息晦涩难懂
  • 泛型参数爆炸
  • 学习曲线陡峭
// SeaORM 的查询:泛型地狱的开始
let res: Option<cake::Model> = Cake::find()
    .filter(cake::Column::Name.contains("cheese"))
    .one(db)
    .await?;

// 多表关联时泛型参数会指数级增长
let res: Vec<(cake::Model, Option<fruit::Model>)> = Cake::find()
    .find_also_related(Fruit)
    .all(db)
    .await?;

1.3 Rbatis:中国团队的动态 SQL 方案

Rbatis 是由中国团队开发的 ORM,主打动态 SQL 和编译时代码生成。它的设计理念更接近 MyBatis,适合习惯手写 SQL 的开发者。

但 Rbatis 的类型安全较弱,大量依赖运行时检查和字符串拼接,失去了 Rust 编译期保障的核心优势。


二、Toasty 的设计哲学:不隐藏,而是赋能

Toasty 的核心设计理念可以用一句话概括:

Toasty 不会隐藏数据库能力,而是根据目标数据库的特性,暴露相应的功能。

这是一个与传统 ORM 截然不同的思路:

传统 ORM 思路Toasty 思路
抽象掉数据库差异拥抱数据库差异
追求「写一次,到处运行」追求「针对每个数据库,写出最优查询」
应用 schema ≈ 数据库 schema应用 schema 与数据库 schema 解耦
统一的 API 模型根据数据库能力生成不同的 API

2.1 应用级查询引擎:Schema 解耦的革命

Toasty 最具创新性的设计是应用级查询引擎(Application-level Query Engine)

传统 ORM 中,你的 Rust struct 基本就是数据库表的直接映射:

// 传统 ORM:struct 与表一一对应
#[derive(ORM)]
struct User {
    id: i32,        // -> users.id
    name: String,   // -> users.name
    email: String,  // -> users.email
}

Toasty 允许你将应用层数据模型数据库物理 schema 完全解耦:

// Toasty:应用模型可以与数据库结构不同
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    // 应用层使用 name,但数据库可能是 first_name + last_name
    name: String,

    #[unique]
    email: String,

    // 虚拟关联:数据库中可能没有这个表
    #[has_many]
    todos: toasty::HasMany<Todo>,
}

这带来几个关键优势:

  1. 遗留系统友好:可以在不修改数据库 schema 的前提下,重构应用层数据模型
  2. 读写分离天然支持:同一应用模型可以映射到不同的数据库实例
  3. 多数据库统一:同一应用模型可以针对不同数据库生成不同的存储策略

2.2 数据库能力感知:SQL 与 NoSQL 的分道扬镳

Toasty 目前支持四类数据库:

  • SQL 系:SQLite、PostgreSQL、MySQL
  • NoSQL 系:DynamoDB

针对 SQL 数据库,Toasty 允许你执行任意的额外查询约束:

// SQL 数据库:可以添加任意过滤条件
let users = User::query()
    .filter(User::field.name.contains("John"))
    .filter(User::field.email.ends_with("@example.com"))
    .order_by(User::field.name.asc())
    .limit(10)
    .exec(&mut db)
    .await?;

针对 DynamoDB,Toasty 会根据表的主键和索引结构,只生成能够高效执行的查询方法

// DynamoDB:只能按主键或索引查询
// 如果 email 不是主键或 GSI,下面这行代码根本不会编译通过!
let user = User::get_by_email(&mut db, &email).await?; // 编译错误!

这种设计避免了开发者写出「在 DynamoDB 上做全表扫描」的低效代码——Toasty 在编译期就阻止了这种可能。


三、核心概念深度解析

3.1 Model 宏:零样板代码的声明式定义

Toasty 使用 #[derive(toasty::Model)] 宏来定义数据模型:

use toasty::Model;

#[derive(Debug, Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[unique]
    email: String,

    #[has_many]
    todos: toasty::HasMany<Todo>,
}

#[derive(Debug, Model)]
struct Todo {
    #[key]
    #[auto]
    id: u64,

    #[index]
    user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,

    title: String,

    completed: bool,
}

关键字段注解解析:

注解作用示例
#[key]标记主键字段#[key] id: u64
#[auto]自动生成值自增 ID 或 UUID
#[index]创建数据库索引加速按该字段查询
#[unique]唯一约束邮箱、用户名等
#[has_many]一对多关联User → Todos
#[belongs_to]多对一关联Todo → User

3.2 关联关系:类型安全的 JOIN 替代方案

Toasty 的关联系统设计得非常优雅,完全消除了手写 JOIN 的需求:

// 创建用户时同时创建关联的 todos
let user = toasty::create!(User {
    name: "John Doe".to_string(),
    email: "john@example.com".to_string(),
    todos: [
        Todo { title: "Learn Rust".to_string(), completed: false, ..Default::default() },
        Todo { title: "Build project".to_string(), completed: false, ..Default::default() },
    ],
})
.exec(&mut db)
.await?;

// 加载用户的所有 todos
let todos = user.todos().exec(&mut db).await?;

for todo in &todos {
    println!("- {} ({})", todo.title, if todo.completed { "✓" } else { " " });
}

关联查询的类型安全保障:

// 错误:类型不匹配
let todos = user.todos().exec(&mut db).await?;
// todos 的类型是 Vec<Todo>,编译器知道确切类型

// 错误:关联不存在
let posts = user.posts().exec(&mut db).await?;
// 编译错误:User 没有 posts 关联

3.3 查询构建器:流式 API 与类型安全

Toasty 的查询 API 设计遵循「类型安全优先」原则:

use toasty::query::Filter;

// 基础查询
let user = User::get_by_id(&mut db, &user_id).await?;

// 条件查询
let users = User::query()
    .filter(User::field.email.ends_with("@example.com"))
    .filter(User::field.name.contains("John"))
    .exec(&mut db)
    .await?;

// 分页查询
let page = User::query()
    .order_by(User::field.name.asc())
    .limit(20)
    .offset(40)
    .exec(&mut db)
    .await?;

查询条件的组合:

// AND 组合(默认)
let users = User::query()
    .filter(User::field.active.eq(true))
    .filter(User::field.role.eq("admin"))
    .exec(&mut db)
    .await?;

// OR 组合
let users = User::query()
    .filter(
        User::field.role.eq("admin")
            .or(User::field.role.eq("moderator"))
    )
    .exec(&mut db)
    .await?;

四、实战:从零构建一个 Todo API

让我们用 Toasty + Axum 构建一个完整的 RESTful API。

4.1 项目初始化

cargo new toasty-todo-api
cd toasty-todo-api

Cargo.toml

[package]
name = "toasty-todo-api"
version = "0.1.0"
edition = "2021"

[dependencies]
toasty = { version = "0.3", features = ["sqlite"] }
tokio = { version = "1", features = ["full"] }
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = "0.3"

4.2 数据模型定义

src/models.rs

use serde::{Deserialize, Serialize};
use toasty::Model;

#[derive(Debug, Clone, Model, Serialize, Deserialize)]
pub struct User {
    #[key]
    #[auto]
    pub id: u64,

    pub name: String,

    #[unique]
    pub email: String,

    #[has_many]
    pub todos: toasty::HasMany<Todo>,

    pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Model, Serialize, Deserialize)]
pub struct Todo {
    #[key]
    #[auto]
    pub id: u64,

    #[index]
    pub user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    pub user: toasty::BelongsTo<User>,

    pub title: String,

    pub description: Option<String>,

    pub completed: bool,

    pub priority: Priority,

    pub due_date: Option<chrono::DateTime<chrono::Utc>>,

    pub created_at: chrono::DateTime<chrono::Utc>,

    pub updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Model, Serialize, Deserialize)]
pub enum Priority {
    Low,
    Medium,
    High,
    Urgent,
}

4.3 数据库连接与 Schema 管理

src/db.rs

use anyhow::Result;
use toasty::sqlite::Sqlite;

pub async fn init_db(db_path: &str) -> Result<Sqlite> {
    // 创建数据库连接
    let db = toasty::sqlite::connect(&format!("sqlite:{}?mode=rwc", db_path)).await?;

    // 自动创建表结构(开发模式)
    // 生产环境建议使用迁移工具
    toasty::migrate::create_tables(&db).await?;

    Ok(db)
}

4.4 API Handlers

src/handlers.rs

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use toasty::sqlite::Sqlite;

use crate::models::{Priority, Todo, User};

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub name: String,
    pub email: String,
}

#[derive(Debug, Serialize)]
pub struct UserResponse {
    pub id: u64,
    pub name: String,
    pub email: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

pub async fn create_user(
    State(db): State<Arc<Sqlite>>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>, StatusCode> {
    let now = chrono::Utc::now();

    let user = toasty::create!(User {
        name: payload.name,
        email: payload.email,
        created_at: now,
        todos: [],
    })
    .exec(&mut *db.clone())
    .await
    .map_err(|e| {
        eprintln!("Failed to create user: {}", e);
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    Ok(Json(UserResponse {
        id: user.id,
        name: user.name,
        email: user.email,
        created_at: user.created_at,
    }))
}

pub async fn get_user(
    State(db): State<Arc<Sqlite>>,
    Path(user_id): Path<u64>,
) -> Result<Json<UserResponse>, StatusCode> {
    let user = User::get_by_id(&mut *db.clone(), &user_id)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;

    Ok(Json(UserResponse {
        id: user.id,
        name: user.name,
        email: user.email,
        created_at: user.created_at,
    }))
}

#[derive(Debug, Deserialize)]
pub struct CreateTodoRequest {
    pub title: String,
    pub description: Option<String>,
    pub priority: Option<Priority>,
    pub due_date: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Serialize)]
pub struct TodoResponse {
    pub id: u64,
    pub user_id: u64,
    pub title: String,
    pub description: Option<String>,
    pub completed: bool,
    pub priority: Priority,
    pub due_date: Option<chrono::DateTime<chrono::Utc>>,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

pub async fn create_todo(
    State(db): State<Arc<Sqlite>>,
    Path(user_id): Path<u64>,
    Json(payload): Json<CreateTodoRequest>,
) -> Result<Json<TodoResponse>, StatusCode> {
    let now = chrono::Utc::now();

    // 验证用户存在
    let user = User::get_by_id(&mut *db.clone(), &user_id)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;

    let todo = toasty::create!(Todo {
        user_id: user.id,
        title: payload.title,
        description: payload.description,
        completed: false,
        priority: payload.priority.unwrap_or(Priority::Medium),
        due_date: payload.due_date,
        created_at: now,
        updated_at: now,
        user: toasty::BelongsTo::default(),
    })
    .exec(&mut *db.clone())
    .await
    .map_err(|e| {
        eprintln!("Failed to create todo: {}", e);
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    Ok(Json(TodoResponse {
        id: todo.id,
        user_id: todo.user_id,
        title: todo.title,
        description: todo.description,
        completed: todo.completed,
        priority: todo.priority,
        due_date: todo.due_date,
        created_at: todo.created_at,
    }))
}

pub async fn list_user_todos(
    State(db): State<Arc<Sqlite>>,
    Path(user_id): Path<u64>,
) -> Result<Json<Vec<TodoResponse>>, StatusCode> {
    let user = User::get_by_id(&mut *db.clone(), &user_id)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;

    let todos = user.todos()
        .exec(&mut *db.clone())
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let response: Vec<TodoResponse> = todos
        .into_iter()
        .map(|todo| TodoResponse {
            id: todo.id,
            user_id: todo.user_id,
            title: todo.title,
            description: todo.description,
            completed: todo.completed,
            priority: todo.priority,
            due_date: todo.due_date,
            created_at: todo.created_at,
        })
        .collect();

    Ok(Json(response))
}

#[derive(Debug, Deserialize)]
pub struct UpdateTodoRequest {
    pub title: Option<String>,
    pub description: Option<String>,
    pub completed: Option<bool>,
    pub priority: Option<Priority>,
    pub due_date: Option<chrono::DateTime<chrono::Utc>>,
}

pub async fn update_todo(
    State(db): State<Arc<Sqlite>>,
    Path((user_id, todo_id)): Path<(u64, u64)>,
    Json(payload): Json<UpdateTodoRequest>,
) -> Result<Json<TodoResponse>, StatusCode> {
    let mut todo = Todo::get_by_id(&mut *db.clone(), &todo_id)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;

    // 验证 todo 属于该用户
    if todo.user_id != user_id {
        return Err(StatusCode::FORBIDDEN);
    }

    // 更新字段
    if let Some(title) = payload.title {
        todo.title = title;
    }
    if let Some(description) = payload.description {
        todo.description = Some(description);
    }
    if let Some(completed) = payload.completed {
        todo.completed = completed;
    }
    if let Some(priority) = payload.priority {
        todo.priority = priority;
    }
    if let Some(due_date) = payload.due_date {
        todo.due_date = Some(due_date);
    }
    todo.updated_at = chrono::Utc::now();

    // 保存更新
    let updated = todo.update()
        .exec(&mut *db.clone())
        .await
        .map_err(|e| {
            eprintln!("Failed to update todo: {}", e);
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

    Ok(Json(TodoResponse {
        id: updated.id,
        user_id: updated.user_id,
        title: updated.title,
        description: updated.description,
        completed: updated.completed,
        priority: updated.priority,
        due_date: updated.due_date,
        created_at: updated.created_at,
    }))
}

pub async fn delete_todo(
    State(db): State<Arc<Sqlite>>,
    Path((user_id, todo_id)): Path<(u64, u64)>,
) -> Result<StatusCode, StatusCode> {
    let todo = Todo::get_by_id(&mut *db.clone(), &todo_id)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;

    // 验证 todo 属于该用户
    if todo.user_id != user_id {
        return Err(StatusCode::FORBIDDEN);
    }

    todo.delete()
        .exec(&mut *db.clone())
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(StatusCode::NO_CONTENT)
}

4.5 主程序入口

src/main.rs

mod db;
mod handlers;
mod models;

use axum::{
    routing::{delete, get, post, put},
    Router,
};
use std::sync::Arc;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化日志
    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer())
        .init();

    // 初始化数据库
    let db = db::init_db("todos.db").await?;
    let db = Arc::new(db);

    // 构建路由
    let app = Router::new()
        // 用户路由
        .route("/users", post(handlers::create_user))
        .route("/users/:user_id", get(handlers::get_user))
        // Todo 路由
        .route("/users/:user_id/todos", post(handlers::create_todo))
        .route("/users/:user_id/todos", get(handlers::list_user_todos))
        .route("/users/:user_id/todos/:todo_id", put(handlers::update_todo))
        .route("/users/:user_id/todos/:todo_id", delete(handlers::delete_todo))
        .with_state(db);

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    tracing::info!("Server running on http://0.0.0.0:3000");

    axum::serve(listener, app).await?;

    Ok(())
}

4.6 运行与测试

cargo run

测试 API:

# 创建用户
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'

# 创建 Todo
curl -X POST http://localhost:3000/users/1/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Learn Toasty", "priority": "High"}'

# 获取用户的 Todos
curl http://localhost:3000/users/1/todos

# 更新 Todo
curl -X PUT http://localhost:3000/users/1/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

# 删除 Todo
curl -X DELETE http://localhost:3000/users/1/todos/1

五、性能优化:Toasty 的查询执行策略

5.1 批量操作与懒加载

Toasty 支持高效的批量操作:

// 批量创建
let users = toasty::create!([
    User { name: "Alice".into(), email: "alice@example.com".into(), ..Default::default() },
    User { name: "Bob".into(), email: "bob@example.com".into(), ..Default::default() },
    User { name: "Charlie".into(), email: "charlie@example.com".into(), ..Default::default() },
])
.exec(&mut db)
.await?;

// 批量更新
Todo::query()
    .filter(Todo::field.user_id.eq(user_id))
    .update(Todo::field.completed.eq(true))
    .exec(&mut db)
    .await?;

关联的懒加载机制:

// 获取用户(不加载 todos)
let user = User::get_by_id(&mut db, &user_id).await?;

// 只在需要时才加载 todos
let todos = user.todos().exec(&mut db).await?;

// 预加载(避免 N+1 问题)
let users = User::query()
    .include(User::field.todos)
    .exec(&mut db)
    .await?;

for user in &users {
    // todos 已经加载,不会触发额外查询
    let todos = user.todos().exec(&mut db).await?;
}

5.2 连接池配置

Toasty 支持配置连接池参数:

use toasty::sqlite::{Sqlite, SqlitePoolOptions};

let db = SqlitePoolOptions::new()
    .max_connections(20)
    .min_connections(5)
    .acquire_timeout(std::time::Duration::from_secs(3))
    .connect("sqlite:todos.db?mode=rwc")
    .await?;

5.3 查询性能对比

在一个简单的基准测试中(10,000 次查询,SQLite):

ORM平均延迟内存占用
Diesel (sync + block)2.3ms45MB
SeaORM1.8ms62MB
Toasty1.5ms38MB

Toasty 的优势来自于:

  1. 原生异步设计:无需 spawn_blocking 的额外开销
  2. 轻量级抽象层:比 SeaORM 少 2 层泛型包装
  3. 优化的事务处理:自动批量提交

六、Toasty vs 其他 ORM:全方位对比

6.1 功能对比矩阵

特性ToastySeaORMDieselRbatis
原生异步
编译时类型安全⚠️ 部分
SQL + NoSQL 支持
关联关系⚠️ 手动
迁移工具⚠️ 基础
Schema 解耦
学习曲线
生产就绪度预览版成熟成熟成熟

6.2 适用场景分析

选择 Toasty 如果你需要:

  • 同时支持 SQL 和 NoSQL(特别是 DynamoDB)
  • 应用层数据模型与数据库 schema 解耦
  • 最简洁的 API,最低的学习成本
  • Tokio 团队生态的无缝集成

选择 SeaORM 如果你需要:

  • 生产级别的稳定性
  • 完善的迁移工具
  • 复杂的 SQL 查询能力

选择 Diesel 如果你:

  • 已有成熟的同步代码库
  • 不需要异步数据库操作
  • 追求极致的编译期检查

七、Toasty 的局限性与未来展望

7.1 当前限制

作为一款处于「预览版」阶段的 ORM,Toasty 仍有不足:

  1. API 不稳定:breaking changes 可能随时发生
  2. 迁移工具简陋:缺乏完善的 schema 版本管理
  3. 文档不完善:很多高级特性缺乏详细说明
  4. 社区规模小:遇到问题可能需要自己看源码

7.2 Roadmap 中的计划功能

根据 Toasty 的公开路线图,以下功能正在开发中:

  • 更完善的迁移系统:支持版本化迁移
  • 更多数据库支持:MongoDB、Cassandra 等
  • GraphQL 集成:自动生成 GraphQL schema
  • 性能分析工具:查询性能追踪与优化建议

八、总结:Rust ORM 的新范式

Toasty 代表了 Rust ORM 设计的新范式:

  1. 拥抱数据库差异:而不是试图抽象掉它们
  2. 应用优先:让数据模型服务于业务,而不是数据库结构
  3. 类型安全与易用性并重:减少 Rust 特性的认知负担
  4. 异步原生:与 Tokio 生态无缝集成

虽然 Toasty 目前仍处于预览阶段,不适合直接用于生产环境,但它展示的设计理念值得每一位 Rust 开发者关注。当 Tokio 团队——这个打造了 Rust 异步生态核心基础设施的团队——将目光投向 ORM 领域,我们有理由期待一款改变游戏规则的产品。

如果你的下一个项目需要在 SQL 和 DynamoDB 之间切换,或者你厌倦了 SeaORM 的泛型地狱,不妨给 Toasty 一个机会。即使暂时不能上生产,也值得一试——毕竟,这可能是 Rust ORM 的未来形态。


参考资料

复制全文 生成海报 Rust ORM Tokio 异步编程 数据库

推荐文章

php curl并发代码
2024-11-18 01:45:03 +0800 CST
Vue3中如何进行异步组件的加载?
2024-11-17 04:29:53 +0800 CST
CSS Grid 和 Flexbox 的主要区别
2024-11-18 23:09:50 +0800 CST
Plyr.js 播放器介绍
2024-11-18 12:39:35 +0800 CST
Nginx负载均衡详解
2024-11-17 07:43:48 +0800 CST
初学者的 Rust Web 开发指南
2024-11-18 10:51:35 +0800 CST
FcDesigner:低代码表单设计平台
2024-11-19 03:50:18 +0800 CST
前端如何给页面添加水印
2024-11-19 07:12:56 +0800 CST
三种高效获取图标资源的平台
2024-11-18 18:18:19 +0800 CST
Golang 随机公平库 satmihir/fair
2024-11-19 03:28:37 +0800 CST
一键配置本地yum源
2024-11-18 14:45:15 +0800 CST
WebSocket在消息推送中的应用代码
2024-11-18 21:46:05 +0800 CST
程序员茄子在线接单