mcpField
R
rust-ui-architecture

rust-ui-architecture

github.com/geoffjay开发847 次安装52 周前更新
代码仓库

关于 rust-ui-architecture

为使用 GPUI 构建可维护 Rust UI 应用提供架构指导,涵盖分层设计、按功能组织模块、状态管理、测试与扩展模式。

主要功能

  1. 01四层应用架构
  2. 02按功能组织模块
  3. 03单向状态流
  4. 04通过依赖注入支持测试
  5. 05插件与组件模式

适用场景

  1. 01规划 GPUI 项目结构
  2. 02拆分 UI、领域与应用逻辑
  3. 03设计状态所有权与更新
  4. 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 专用)
  • 容器-呈现器模式(关注点分离)
  • 面向服务模式(外部依赖)
  • 插件架构(可扩展性)

代码组织:

  • 基于功能的模块
  • 分层
  • 明确的边界
  • 依赖注入

状态管理:

  • 单向数据流
  • 单一事实来源
  • 层级所有权
  • 响应式更新
最佳实践
  1. 关注点分离:将 UI、逻辑和数据分开
  2. 依赖注入:使用 trait 提高可测试性
  3. 功能组织:按功能归类相关代码
  4. 状态所有权:明确所有权层级
  5. 可测试设计:从一开始就为测试而设计
  6. 文档:记录架构决策
  7. 模块化:采用小型、专注的模块
  8. 可扩展性:为发展而设计
常见模式
  • 仓储模式:数据访问抽象
  • 命令模式:操作系统
  • 观察者模式:订阅
  • 工厂模式:组件创建
  • 策略模式:可插拔行为
  • 外观模式:简化接口

评论

支持 Markdown