关于 rust-ui-architecture
为使用 GPUI 构建可维护 Rust UI 应用提供架构指导,涵盖分层设计、按功能组织模块、状态管理、测试与扩展模式。
主要功能
- 01四层应用架构
- 02按功能组织模块
- 03单向状态流
- 04通过依赖注入支持测试
- 05插件与组件模式
适用场景
- 01规划 GPUI 项目结构
- 02拆分 UI、领域与应用逻辑
- 03设计状态所有权与更新
- 04让外部依赖易于测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ geoffjay/ claude-plugins --skill rust-ui-architecture安装后在 Claude Code 中输入/rust-ui-architecture确认已加载。
详细介绍
Rust UI 架构
元数据
此技能提供使用 GPUI 构建可扩展、易维护 Rust UI 应用的全面架构指导,涵盖项目结构、设计模式和最佳实践。
说明
应用结构
推荐的项目布局
my-gpui-app/
├── Cargo.toml
├── src/
│ ├── main.rs # Application entry point
│ ├── app.rs # Main application struct
│ ├── ui/ # UI layer
│ │ ├── mod.rs
│ │ ├── views/ # High-level views
│ │ │ ├── mod.rs
│ │ │ ├── main_view.rs
│ │ │ ├── sidebar.rs
│ │ │ └── editor.rs
│ │ ├── components/ # Reusable components
│ │ │ ├── mod.rs
│ │ │ ├── button.rs
│ │ │ ├── input.rs
│ │ │ └── modal.rs
│ │ └── theme.rs # Theme definitions
│ ├── models/ # Application state
│ │ ├── mod.rs
│ │ ├── document.rs
│ │ ├── project.rs
│ │ └── settings.rs
│ ├── services/ # External integrations
│ │ ├── mod.rs
│ │ ├── file_service.rs
│ │ └── api_client.rs
│ ├── domain/ # Core business logic
│ │ ├── mod.rs
│ │ └── operations.rs
│ └── utils/ # Utilities
│ ├── mod.rs
│ └── helpers.rs
├── examples/ # Example applications
│ └── basic.rs
└── tests/ # Integration tests
├── integration/
└── ui/
分层
四层架构
┌─────────────────────────────────┐
│ UI Layer (Views) │ - GPUI views and components
│ │ - User interactions
│ │ - Render logic
├─────────────────────────────────┤
│ Application Layer (Models) │ - Application state (Model<T>)
│ │ - State coordination
│ │ - Business logic orchestration
├─────────────────────────────────┤
│ Service Layer (Services) │ - File I/O
│ │ - Network requests
│ │ - External APIs
├─────────────────────────────────┤
│ Domain Layer (Core) │ - Pure business logic
│ │ - Domain types
│ │ - No dependencies on UI/GPUI
└─────────────────────────────────┘
实现示例
// Domain Layer (pure logic)
pub mod domain {
#[derive(Clone, Debug)]
pub struct Document {
pub id: DocumentId,
pub content: String,
pub language: Language,
}
impl Document {
pub fn word_count(&self) -> usize {
self.content.split_whitespace().count()
}
pub fn is_empty(&self) -> bool {
self.content.trim().is_empty()
}
}
}
// Service Layer (external integration)
pub mod services {
use super::domain::*;
pub trait FileService: Send + Sync {
fn read(&self, path: &Path) -> Result<String>;
fn write(&self, path: &Path, content: &str) -> Result<()>;
}
pub struct RealFileService;
impl FileService for RealFileService {
fn read(&self, path: &Path) -> Result<String> {
std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read: {}", e))
}
fn write(&self, path: &Path, content: &str) -> Result<()> {
std::fs::write(path, content)
.map_err(|e| anyhow::anyhow!("Failed to write: {}", e))
}
}
}
// Application Layer (state management)
pub mod models {
use super::domain::*;
use super::services::*;
pub struct DocumentModel {
document: Document,
file_service: Arc<dyn FileService>,
is_modified: bool,
}
impl DocumentModel {
pub fn new(document: Document, file_service: Arc<dyn FileService>) -> Self {
Self {
document,
file_service,
is_modified: false,
}
}
pub fn update_content(&mut self, content: String) {
self.document.content = content;
self.is_modified = true;
}
pub async fn save(&mut self) -> Result<()> {
self.file_service.write(&self.document.path, &self.document.content)?;
self.is_modified = false;
Ok(())
}
}
}
// UI Layer (views)
pub mod ui {
use gpui::*;
use super::models::*;
pub struct DocumentView {
model: Model<DocumentModel>,
_subscription: Subscription,
}
impl DocumentView {
pub fn new(model: Model<DocumentModel>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
impl Render for DocumentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let model = self.model.read(cx);
div()
.child(format!("Words: {}", model.document.word_count()))
.when(model.is_modified, |this| {
this.child("(modified)")
})
}
}
}
组件层级
容器-呈现器模式
// Container: Manages state and logic
pub struct EditorContainer {
document: Model<DocumentModel>,
_subscription: Subscription,
}
impl EditorContainer {
pub fn new(document: Model<DocumentModel>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&document, |_, _, cx| cx.notify());
Self { document, _subscription }
}
fn handle_save(&mut self, cx: &mut ViewContext<Self>) {
let document = self.document.clone();
cx.spawn(|_, mut cx| async move {
cx.update_model(&document, |doc, _| {
doc.save().await
}).await?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
impl Render for EditorContainer {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let doc = self.document.read(cx);
EditorPresenter::new(
doc.document.content.clone(),
doc.is_modified,
cx.listener(|this, content, cx| {
this.document.update(cx, |doc, _| {
doc.update_content(content);
});
}),
)
}
}
// Presenter: Pure rendering
pub struct EditorPresenter {
content: String,
is_modified: bool,
on_change: Box<dyn Fn(String, &mut WindowContext)>,
}
impl EditorPresenter {
pub fn new(
content: String,
is_modified: bool,
on_change: impl Fn(String, &mut WindowContext) + 'static,
) -> Self {
Self {
content,
is_modified,
on_change: Box::new(on_change),
}
}
}
impl Render for EditorPresenter {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.child(
textarea()
.value(&self.content)
.on_input(|value, cx| {
(self.on_change)(value, cx);
})
)
.when(self.is_modified, |this| {
this.child("Unsaved changes")
})
}
}
模块组织
基于功能的结构
src/
├── features/
│ ├── editor/
│ │ ├── mod.rs
│ │ ├── model.rs # EditorModel
│ │ ├── view.rs # EditorView
│ │ ├── commands.rs # Editor actions
│ │ └── components/ # Editor-specific components
│ ├── sidebar/
│ │ ├── mod.rs
│ │ ├── model.rs
│ │ ├── view.rs
│ │ └── components/
│ └── statusbar/
│ ├── mod.rs
│ ├── model.rs
│ └── view.rs
优势:
- 功能边界清晰
- 易于理解和导航
- 可随团队规模良好扩展
- 支持基于功能的开发
状态管理架构
单向数据流
User Action → Action Dispatch → State Update → View Rerender
↑ ↓
└──────────────── Event Handlers ─────────────┘
实现:
// Define actions
actions!(app, [AddTodo, ToggleTodo, DeleteTodo]);
// State model
pub struct TodoListModel {
todos: Vec<Todo>,
}
impl TodoListModel {
pub fn add_todo(&mut self, text: String) {
self.todos.push(Todo {
id: TodoId::new(),
text,
completed: false,
});
}
pub fn toggle_todo(&mut self, id: TodoId) {
if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
}
}
}
// View with action handlers
pub struct TodoListView {
model: Model<TodoListModel>,
}
impl TodoListView {
fn register_actions(&mut self, cx: &mut ViewContext<Self>) {
cx.on_action(cx.listener(|this, action: &AddTodo, cx| {
this.model.update(cx, |model, cx| {
model.add_todo(action.text.clone());
cx.notify();
});
}));
cx.on_action(cx.listener(|this, action: &ToggleTodo, cx| {
this.model.update(cx, |model, cx| {
model.toggle_todo(action.id);
cx.notify();
});
}));
}
}
状态所有权模式
单一事实来源:
pub struct AppModel {
// Root owns all state
documents: Vec<Model<DocumentModel>>,
settings: Model<Settings>,
ui_state: Model<UiState>,
}
层级所有权:
pub struct WorkspaceModel {
// Workspace owns workspace-level state
panes: Vec<Model<PaneModel>>,
}
pub struct PaneModel {
// Pane owns pane-level state
tabs: Vec<Model<TabModel>>,
active_index: usize,
}
关注点分离
明确的边界
// ✓ GOOD: Clear responsibilities
// Domain logic (no GPUI)
pub mod document {
pub struct Document {
content: String,
}
impl Document {
pub fn insert(&mut self, pos: usize, text: &str) {
self.content.insert_str(pos, text);
}
}
}
// Application logic (uses GPUI models)
pub mod editor_model {
use gpui::*;
use super::document::Document;
pub struct EditorModel {
document: Document,
cursor_position: usize,
}
impl EditorModel {
pub fn insert_at_cursor(&mut self, text: &str) {
self.document.insert(self.cursor_position, text);
self.cursor_position += text.len();
}
}
}
// UI logic (GPUI views)
pub mod editor_view {
use gpui::*;
use super::editor_model::EditorModel;
pub struct EditorView {
model: Model<EditorModel>,
}
impl Render for EditorView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// Rendering logic
}
}
}
可测试性模式
依赖注入
// Define trait for external dependencies
pub trait FileService: Send + Sync {
fn read(&self, path: &Path) -> Result<String>;
fn write(&self, path: &Path, content: &str) -> Result<()>;
}
// Production implementation
pub struct RealFileService;
impl FileService for RealFileService {
// Real implementation
}
// Test implementation
#[cfg(test)]
pub struct MockFileService {
read_results: HashMap<PathBuf, Result<String>>,
written_files: RefCell<Vec<(PathBuf, String)>>,
}
#[cfg(test)]
impl FileService for MockFileService {
fn read(&self, path: &Path) -> Result<String> {
self.read_results
.get(path)
.cloned()
.unwrap_or_else(|| Err(anyhow::anyhow!("File not found")))
}
fn write(&self, path: &Path, content: &str) -> Result<()> {
self.written_files
.borrow_mut()
.push((path.to_path_buf(), content.to_string()));
Ok(())
}
}
// Model accepts any FileService
pub struct DocumentModel {
file_service: Arc<dyn FileService>,
}
// Tests use mock
#[cfg(test)]
mod tests {
#[test]
fn test_save() {
let mock_service = Arc::new(MockFileService::new());
let model = DocumentModel::new(mock_service.clone());
model.save().unwrap();
assert_eq!(mock_service.written_files.borrow().len(), 1);
}
}
插件架构
扩展系统
// Define plugin trait
pub trait EditorPlugin: Send + Sync {
fn name(&self) -> &str;
fn on_document_open(&self, doc: &Document) -> Result<()>;
fn on_document_save(&self, doc: &Document) -> Result<()>;
}
// Plugin manager
pub struct PluginManager {
plugins: Vec<Box<dyn EditorPlugin>>,
}
impl PluginManager {
pub fn register(&mut self, plugin: Box<dyn EditorPlugin>) {
self.plugins.push(plugin);
}
pub fn notify_document_open(&self, doc: &Document) -> Result<()> {
for plugin in &self.plugins {
plugin.on_document_open(doc)?;
}
Ok(())
}
}
// Example plugin
pub struct AutoSavePlugin {
interval: Duration,
}
impl EditorPlugin for AutoSavePlugin {
fn name(&self) -> &str {
"AutoSave"
}
fn on_document_open(&self, doc: &Document) -> Result<()> {
// Start auto-save timer
Ok(())
}
fn on_document_save(&self, doc: &Document) -> Result<()> {
println!("Document saved: {}", doc.path.display());
Ok(())
}
}
资源
设计模式
架构模式:
- Model-View 模式(GPUI 专用)
- 容器-呈现器模式(关注点分离)
- 面向服务模式(外部依赖)
- 插件架构(可扩展性)
代码组织:
- 基于功能的模块
- 分层
- 明确的边界
- 依赖注入
状态管理:
- 单向数据流
- 单一事实来源
- 层级所有权
- 响应式更新
最佳实践
- 关注点分离:将 UI、逻辑和数据分开
- 依赖注入:使用 trait 提高可测试性
- 功能组织:按功能归类相关代码
- 状态所有权:明确所有权层级
- 可测试设计:从一开始就为测试而设计
- 文档:记录架构决策
- 模块化:采用小型、专注的模块
- 可扩展性:为发展而设计
常见模式
- 仓储模式:数据访问抽象
- 命令模式:操作系统
- 观察者模式:订阅
- 工厂模式:组件创建
- 策略模式:可插拔行为
- 外观模式:简化接口
评论
支持 Markdown