项目架构:从模型到视图
GPUI 不强制 MVC、MVVM 或 Redux,但它天然鼓励一种清晰的 Entity-View-Model 组织方式:业务数据保持为普通 Rust 类型,视图持有 UI 状态和组件句柄,实体负责让状态拥有生命周期、订阅关系和重绘通知。
HiPoster 的目录边界
HiPoster 的核心目录可以整理为:
src/
├── main.rs # 平台入口,只调用库层 run()
├── lib.rs # 应用启动、菜单、Action、窗口创建
├── model.rs # HTTP 请求和响应模型
├── http.rs # reqwest 异步网络层
├── app.rs # 根视图:Tabs、历史记录、主题、全局布局
├── api_tab.rs # 单个请求 Tab:表单、发送、响应展示
├── theme.rs # 主题枚举、颜色表和主题转换
├── assets.rs # 应用资源、图标与静态文件
└── about.rs # 关于窗口
这种拆法的优点是:
model.rs不依赖 GPUI,方便序列化、测试和迁移。http.rs不依赖具体 UI,方便替换请求库或做集成测试。app.rs只管理全局状态,单个请求编辑器的细节下沉到api_tab.rs。- 主题、资源、关于窗口都是独立模块,不污染请求业务。
Model:保持纯粹
HTTP 调试器的业务模型可以是完全普通的 Rust 类型:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum HttpMethod {
GET,
POST,
PUT,
DELETE,
PATCH,
HEAD,
OPTIONS,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpRequest {
pub method: HttpMethod,
pub url: String,
pub headers: Vec<Header>,
pub params: Vec<Header>,
pub body: HttpBody,
pub content_type: String,
pub auth: AuthData,
}
模型层的原则:
- 能
Clone,因为 UI 事件和异步任务经常需要移动数据。 - 能
Serialize/Deserialize,方便历史记录和配置落盘。 - 不直接引用
Entity<T>、Window、Context<T>或组件状态。 - 用枚举表达状态空间,例如
HttpBody、AuthType,避免魔法字符串扩散。
Network:隔离 IO
网络层只接收 HttpRequest,返回 HttpResponse:
pub async fn execute_request(request: &HttpRequest) -> anyhow::Result<HttpResponse> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let response = client
.request(reqwest::Method::GET, &request.url)
.send()
.await?;
Ok(HttpResponse {
status_code: response.status().as_u16(),
status_text: response.status().canonical_reason().unwrap_or("").to_string(),
headers: Vec::new(),
body: response.text().await?,
size: 0,
elapsed_ms: 0,
})
}
真实项目里还要处理方法映射、自定义 Header、认证、Body、Multipart、错误消息和耗时统计。重点是:网络层不应该知道按钮、输入框、Tab 或主题。
View:状态容器与渲染函数
GPUI 里的 View 通常是一个结构体加一个 Render 实现:
pub struct ApiTab {
pub url_input: Entity<InputState>,
pub body_input: Entity<InputState>,
pub request: HttpRequest,
pub response: Option<HttpResponse>,
pub loading: bool,
}
impl Render for ApiTab {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.v_flex()
.size_full()
.child(self.render_url_bar(window, cx))
.child(self.render_response(cx))
}
}
视图层可以持有两类状态:
- 业务快照:
request、response、loading、当前选中的 Tab。 - 组件句柄:
Entity<InputState>、子视图Entity<ApiTab>、订阅Subscription。
不要把所有字段都丢到一个大结构体里。根视图只管理应用级状态;业务面板管理自己的局部状态;纯函数负责从输入状态收集请求模型。
Entity:让状态进入 GPUI 运行时
普通结构体只有 Rust 生命周期;Entity<T> 才有 GPUI 生命周期。创建一个视图时通常使用:
let tab = cx.new(|cx| ApiTab::new(window, cx));
Entity<T> 是句柄,不是数据本体。读取和修改要通过上下文:
let url = tab.read(cx).request.url.clone();
tab.update(cx, |tab, cx| {
tab.loading = true;
cx.notify();
});
这让 GPUI 可以跟踪谁变了、谁需要通知、哪些窗口需要重绘。
Root View:应用的边界
HiPoster 的根视图 Hiposter 管理:
tabs: Vec<Entity<ApiTab>>active_tab_indexhistorytheme- 子视图订阅列表
这是一种非常实用的边界:根视图负责“应用级导航与全局资源”,Tab 负责“单个请求编辑和响应展示”。当项目继续增长时,还可以再拆出 Sidebar、RequestPanel、ResponsePanel、ThemePicker 等视图。
推荐架构模板
新建 GPUI 项目时,可以先按这个模板开局:
src/
├── main.rs
├── lib.rs
├── model.rs
├── services/
│ ├── mod.rs
│ └── http.rs
├── views/
│ ├── mod.rs
│ ├── app.rs
│ └── request_tab.rs
├── theme.rs
├── assets.rs
└── persistence.rs
当逻辑变多时,优先拆服务和纯函数;当 UI 变复杂时,优先拆视图和渲染辅助函数。GPUI 的 Render 会反复运行,所以不要把昂贵计算藏在渲染树中,后面的性能章节会专门展开。