ruweb::webclient模块提供统一的HTTP客户端接口,支持同步(基于ureq)与异步(基于reqwest)双实现。通过WebClientTrait抽象出get2ret、post2ret等泛型方法,自动处理状态码检查与JSON反序列化,返回RuResult<T>。同步版依赖注入单例注册,异步版直接使用async fn并复用reqwest连接池。WebMsg结构体支持消息驱动请求,灵活适配各类场景。整体设计兼顾易用性与性能,适用于高并发或简单调用需求。
# `ruweb::webclient` 模块讲解
## 概述
`webclient` 是 **ruweb 框架的 HTTP 客户端模块**,负责向外部服务发起 RESTful 请求。该模块采用**双实现设计**——同步(基于 `ureq`)与异步(基于 `reqwest`)并存,上层通过统一的 trait 抽象调用。
---
## 📁 目录结构
```
src/ruweb/webclient/
├── mod.rs # 模块入口,导出所有子模块
├── web_client_trait.rs # 统一 HTTP 方法抽象 trait
├── web_client.rs # 同步实现(基于 ureq)
├── web_client_init.rs # 依赖注入自动注册(单例 Bean)
├── web_client_test.rs # 同步客户端测试
└── webasync/ # 异步子模块
├── mod.rs
├── web_client.rs # 异步实现(基于 reqwest)
├── web_client_init.rs # 异步版本依赖注入
└── web_client_test.rs # 异步客户端测试
```
---
## 一、统一接口层:`WebClientTrait` trait
文件:[web_client_trait.rs](file:///E:/soft/gitee.com/ruwebframe/src/ruweb/webclient/web_client_trait.rs)
这是整个 HTTP 客户端的**抽象契约**,定义了所有支持的 HTTP 方法:
### 基础 HTTP 方法(返回原始 `ureq::Response`)
| 方法 | 说明 |
|------|------|
| `get(url)` | GET 请求 |
| `post(url, body)` | POST 请求,body 为 JSON 字符串 |
| `delete(url)` | DELETE 请求 |
| `put(url, body)` | PUT 请求 |
| `patch(url, body)` | PATCH 请求 |
| `head(url)` | HEAD 请求 |
### 高级封装方法(自动解析为 `RuResult<T>`)
| 方法 | 说明 |
|------|------|
| `get2ret<T>(url)` | GET → 自动反序列化为 `RuResult<T>` |
| `post2ret<T>(url, body)` | POST → 自动反序列化为 `RuResult<T>` |
| `put2ret<T>(url, body)` | PUT → 自动反序列化为 `RuResult<T>` |
| `delete2ret<T>(url)` | DELETE → 自动反序列化为 `RuResult<T>` |
| `patch2ret<T>(url, body)` | PATCH → 自动反序列化为 `RuResult<T>` |
### 分页专用方法
| 方法 | 说明 |
|------|------|
| `get2page_ret<T>(url)` | GET 分页数据 |
| `post2page_ret<T>(url, body)` | POST 分页数据 |
### 消息驱动方法
| 方法 | 说明 |
|------|------|
| `http2ret<T>(&WebMsg)` | 通过 `WebMsg` 结构体发请求 |
| `res2ret<T>(Response)` | 原始 Response → `RuResult<T>` |
| `res2page_ret<T>(Response)` | 原始 Response → 分页 `RuResult<T>` |
**设计亮点**:`*2ret` 泛型方法的核心价值在于——调用方无需关心 HTTP 状态码检查和 JSON 反序列化过程,框架自动完成,直接得到业务层所需的 `RuResult<T>` 结构。
---
## 二、同步实现:`WebClient`(基于 `ureq`)
文件:[web_client.rs](file:///E:/soft/gitee.com/ruwebframe/src/ruweb/webclient/web_client.rs)
### 结构体定义
```rust
pub struct WebClient {
pub client_dto: ClientDto, // 客户端配置(testUrl、超时等)
}
```
### 核心机制
**1. URL 构建逻辑** (`build_url`):
- 如果 URL 以 `http://` 或 `https://` 开头 → 直接当作完整 URL 处理
- 否则 → 通过 `build_server_url` 判断是否启用测试地址模式(`testUrl`),如果启用了则自动拼接前缀
```rust
// 例如:testUrl = "http://localhost:6001"
// build_url("conf") → "http://localhost:6001/conf"
// build_url("http://api.example.com") → "http://api.example.com"
```
**2. HTTP 请求实现**(以 `get` 为例):
```rust
fn get(&self, url: &str) -> Response {
let urls = self.build_url(url.to_string());
ureq::get(urls.as_str())
.set("Content-Type", "application/json")
.set("Authorization", "Bearer token123")
.call()
.unwrap()
}
```
每个请求都会自动注入 `Content-Type: application/json` 和 `Authorization: Bearer token123` 请求头。
**3. 响应解析** (`res2ret`):
```rust
fn res2ret<T>(&self, res: Response) -> ru_result::RuResult<T> {
// 1. 检查 HTTP 状态码是否为 200
// 2. 读取 body 字符串
// 3. 使用 rutils::json2struct 反序列化为 RuResult<T>
// 4. 若任何环节失败,返回 code=500 的错误结果
}
```
**4. 消息驱动请求** (`http2ret`):
通过 `WebMsg` 结构体传递完整的请求信息(method、url、headers、body),内部根据 `method` 字段动态路由到对应的 HTTP 方法执行。这使得**调用方可以统一通过一个方法发送任意类型的请求**。
---
## 三、异步实现:`WebClient`(基于 `reqwest`)
文件:[webasync/web_client.rs](file:///E:/soft/gitee.com/ruwebframe/src/ruweb/webclient/webasync/web_client.rs)
### 与同步版本的关键差异
| 特性 | 同步版本 | 异步版本 |
|------|---------|---------|
| HTTP 库 | `ureq` | `reqwest` |
| 方法签名 | `fn get(&self) -> Response` | `async fn get(&self) -> Result<Response, Error>` |
| 额外字段 | 无 | `client: RwLock<reqwest::Client>` |
| Trait 实现 | 实现了 `WebClientTrait` | **未实现 trait**,直接是 struct 方法 |
### 为什么异步版本不实现 trait?
由于 Rust 的 trait 中对 async fn 的支持尚有限制(需要 `async_trait` 或 `AFIT`),异步版本选择直接在 `WebClient` struct 上定义 `async fn` 方法,调用方使用 `.await` 等待。
### 异步版本的结构体
```rust
pub struct WebClient {
pub client_dto: ClientDto,
pub client: RwLock<reqwest::Client>, // 复用 reqwest::Client 连接池
}
```
`reqwest::Client` 内部维护连接池,支持 HTTP/2,适合高并发场景。用 `RwLock` 包装确保线程安全。
### 方法对比
同步版:`fn get2ret<T>(&self, url: &str) -> RuResult<T>`
异步版:`pub async fn get2ret<T>(&self, url: &str) -> RuResult<T>`
调用方式差别:
```rust
// 同步
let ret = client.get2ret::<ConfDto>("conf");
// 异步
let ret = client.get2ret::<ConfDto>("conf").await;
```
---
## 四、依赖注入与单例注册
文件:
- [web_client_init.rs](file:///E:/soft/gitee.com/ruwebframe/src/ruweb/webclient/web_client_init.rs)(同步版)
- [webasync/web_client_init.rs](file:///E:/soft/gitee.com/ruwebframe/src/ruweb/webclient/webasync/web_client_init.rs)(异步版)
两者完全对称,核心代码:
```rust
use crate::rubase::{BaseEntitySingle, BeanSingle};
use ctor::ctor;
#[ctor(unsafe)]
pub fn init() {
WebClient::register_bean_singleton(); // 程序启动时自动注册
}
pub fn find_bean_web_client() -> Option<Arc<WebClient>> {
WebClient::find_bean() // 全局访问入口
}
impl BeanSingle for WebClient {
fn new_bean() -> Self {
let mut bean = WebClient::new();
bean.init(); // 从全局配置加载 ClientDto
bean
}
}
```
**关键机制**:
1. `#[ctor(unsafe)]` —— 这是 Rust 的 `ctor` crate,允许在程序 `main()` 执行之前自动运行初始化函数
2. `BeanSingle` trait —— 框架的单例 Bean 注册机制,确保 WebClient 全局只有一个实例
3. `find_bean_web_client()` —— 全局获取 WebClient 实例的入口函数,返回 `Option<Arc<WebClient>>`
4. `init()` 方法 —— 从全局配置 `RuConfig` 中读取 `web_client` 相关配置(如 `testUrl`)
---
## 五、`WebMsg` —— 消息驱动的请求封装
在 `http2ret` 方法中使用,将 HTTP 请求封装为结构体:
```rust
pub struct WebMsg {
pub method: String, // "get" | "post" | "delete" | "put" | "patch" | "head"
pub url: String, // 请求 URL
pub body: String, // 请求体 JSON
pub headers: Vec<(String, String)>, // 自定义请求头
}
```
支持添加 token、自定义 header,并通过 `set_header()` 统一应用到 `ureq::Request`。
---
## 六、使用示例总结
```rust
// 1. 获取全局单例
let client = find_bean_web_client().unwrap();
// 2. 基础 GET 请求(返回原始 Response)
let resp = client.get("http://localhost:6001/conf");
// 3. 高级 GET(自动解析为 ConfDto)
let ret = client.get2ret::<ConfDto>("conf");
// ret 是 ru_result::RuResult<ConfDto>
// 4. POST 请求
let ret = client.post2ret::<ConfDto>("conf", json_body);
// 5. 消息驱动
let mut msg = WebMsg::default();
msg.init_get(String::from("conf"));
let ret = client.http2ret::<ConfDto>(&msg);
// 6. 异步版本(在 async 上下文中)
let ret = async_client.get2ret::<ConfDto>("conf").await;
```
---
## 整体架构图
```
┌──────────────────────────────────────────────────┐
│ 调用方代码 │
│ (通过 find_bean_web_client 获取) │
└──────────────┬───────────────────────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌────────────────────┐
│ WebClientTrait │ │ WebClient(异步) │
│ (trait) │ │ (直接定义 async fn) │
└────────┬────────┘ └─────────┬──────────┘
│ │
┌────────▼────────┐ ┌─────────▼──────────┐
│ WebClient(同步) │ │ reqwest::Client │
│ 基于 ureq │ │ 连接池复用 │
└────────┬────────┘ └────────────────────┘
│
┌────────▼────────┐
│ build_url() │──→ 自动拼接 testUrl 前缀
│ res2ret() │──→ HTTP→RuResult<T> 转换
│ http2ret() │──→ WebMsg 消息路由
└─────────────────┘
```
总的来说,`webclient` 模块的设计体现了以下原则:
1. **统一抽象**:通过 `WebClientTrait` 定义契约,同步/异步各有实现
2. **开箱即用**:依赖注入 + `#[ctor]` 自动初始化,调用方只需 `find_bean_web_client()`
3. **灵活切换**:同步适用于简单场景,异步适用于高并发 IO 密集场景