组件化

在上一节中,我们已经实现了一个基础的计数器 Demo,并分析了其渲染循环机制。接下来,我们将引入“组件化”思想,进一步提升代码的可维护性和扩展性。

1. 为什么要组件化?

随着 UI 结构的复杂化,所有渲染和状态逻辑都堆叠在主循环中会导致代码臃肿、难以维护。组件化的核心思想是:将 UI 拆分为独立、可复用的单元,每个组件只关注自身的渲染和状态。

这种模式不仅提升了代码的可读性和复用性,也为后续引入更高级的特性(如 Hook)打下基础。

2. 用函数式组件重构计数器 Demo

我们可以将 UI 的不同部分封装为独立的渲染函数:

fn render_title() -> Line<'static> {
    Line::from("Counter Application")
        .style(Style::default().bold().light_blue())
        .centered()
}

fn render_count(count: i32) -> Paragraph<'static> {
    Paragraph::new(format!("Count: {}", count).light_green()).centered()
}

fn render_info() -> Line<'static> {
    Line::from("Press q or Ctrl+C to quit, + to increase, - to decrease")
        .style(Style::default().yellow())
        .centered()
}

在主循环中,我们只需调用这些组件函数来渲染对应区域:

// ...existing code...
let title = render_title();
let text = render_count(count);
let info = render_info();
// ...existing code...

不过,这样的函数式组件用起来还是有不少槽点:

  • 组件只能一层一层平铺,没法嵌套。想让一个组件里再放别的组件,或者页面结构多层嵌套,根本做不到。界面一复杂,代码就乱套。
  • 区域(area)划分全靠自己手动算。每个组件要显示在哪,都得在主循环里提前把 area 算好,一个个传进去。页面结构一变,所有 area 计算都得跟着改,非常容易出错。
  • 组合和复用很麻烦。想把几个组件组合成一个大组件,或者让某个组件在不同地方用,基本只能复制粘贴,没法像搭积木一样灵活组合。

所以,我们需要一个能支持嵌套、自动分配区域、方便组合的组件化结构和布局系统,来解决这些问题。

3. 组件化结构

要让组件能像树一样嵌套、组合,首先得有一套“组件协议”。我们可以新建一个 component 模块,先定义一个基础的组件 trait。因为每个组件可能需要不同的参数,所以用关联类型来描述 props。

component/mod.rs 里可以这样写:

pub trait Component: Any {
    type Props<'a>
    where
        Self: 'a;

    fn new(props: &Self::Props<'_>) -> Self;

    fn draw(&self, _frame: &mut ratatui::Frame<'_>, _area: ratatui::layout::Rect) {}
}

由于 Component trait 包含了关联类型参数,因此它不具备对象安全性(Object Safety),无法直接作为 trait 对象使用。为此,我们需要定义一个新的对象安全 trait AnyComponent 来实现这一点。

pub trait AnyComponent {
    fn draw(&self, frame: &mut ratatui::Frame<'_>, area: ratatui::layout::Rect);
}

// 为所有实现了 Component trait 的类型自动实现 AnyComponent trait
impl<T> AnyComponent for T
where
    T: Component,
{
    /// 调用具体组件的 draw 方法,实现多态分发
    fn draw(&self, frame: &mut ratatui::Frame<'_>, area: ratatui::layout::Rect) {
        Component::draw(self, frame, area);
    }
}

然后,创建一个组件容器 InstantiatedComponent 来管理组件及其子组件:

use std::ops::{Deref, DerefMut};

use crate::component::AnyComponent;

pub struct InstantiatedComponent {
    pub component: Box<dyn AnyComponent>,
    pub children: Components,
}

#[derive(Default)]
pub struct Components {
    pub components: Vec<InstantiatedComponent>,
}

impl Deref for Components {
    type Target = Vec<InstantiatedComponent>;

    fn deref(&self) -> &Self::Target {
        &self.components
    }
}

impl DerefMut for Components {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.components
    }
}

|> 思考: 如何创建一个组件实例?

由于使用了 AnyComponent trait,我们没法知道具体的组件类型,所以不能直接调用 new 方法。我们需要一种额外的方式保存组件的类型信息,同时需要一个抽象的 Props 类型来传递参数。

4. AnyProps

新建一个props模块,定义一个类型擦除的容器AnyProps,用于保存组件的props参数。

// 定义一个用于释放原始指针内存的 trait
trait DropRaw {
    fn drop_raw(&self, raw: *mut ());
}

// 通过 PhantomData 标记类型 T 来保留结构体与泛型参数的关联关系
struct DropRawImpl<T> {
    _marker: std::marker::PhantomData<T>,
}

// 为 DropRawImpl 实现 DropRaw trait
impl<T> DropRaw for DropRawImpl<T> {
    // 将 *mut () 转换为 *mut T 并通过 Box::from_raw 构造智能指针来安全释放对应的堆内存
    fn drop_raw(&self, raw: *mut ()) {
        unsafe {
            let _ = Box::from_raw(raw as *mut T);
        }
    }
}

// 一个可以持有任意类型属性的结构体
pub struct AnyProps<'a> {
    raw: *mut (),                                  // 指向实际数据的原始指针
    drop: Option<Box<dyn DropRaw + 'a>>,           // 用于释放 raw 所指向的数据
    _marker: std::marker::PhantomData<&'a mut ()>, // 标记生命周期信息
}

impl<'a> AnyProps<'a> {
    pub(crate) fn owned<T: 'a>(props: T) -> Self {
        // 将堆分配的值转换为原始指针,用于手动内存管理
        let raw = Box::into_raw(Box::new(props));

        Self {
            // 将 *mut T 转换为 *mut () 实现类型擦除
            // 保留指向具体类型的指针信息,但隐藏具体类型
            raw: raw as *mut (),
            drop: Some(Box::new(DropRawImpl::<T> {
                _marker: std::marker::PhantomData,
            })),
            _marker: std::marker::PhantomData,
        }
    }

    pub(crate) fn borrowed<T>(props: &'a mut T) -> Self {
        // 创建一个不负责内存释放的 AnyProps 实例
        // 用于持有对 T 类型数据的引用
        Self {
            // 将 &mut T 转换为 *mut (),实现类型擦除
            raw: props as *const _ as *mut (),

            // 不负责内存释放,因此 drop 设置为 None
            drop: None, // 不负责内存释放

            // 使用 PhantomData 标记生命周期信息
            _marker: std::marker::PhantomData,
        }
    }

    // 创建一个新的 AnyProps 实例,共享当前实例的 raw 指针
    // 不获取所有权,也不负责释放内存
    pub(crate) fn borrow(&mut self) -> Self {
        Self {
            raw: self.raw,
            drop: None, // 不负责内存释放
            _marker: std::marker::PhantomData,
        }
    }

    // 不安全地将内部指针向下转换为具体类型的不可变引用
    // 必须保证当前 raw 指针确实指向 T 类型的数据
    pub(crate) unsafe fn downcast_ref_unchecked<T>(&self) -> &T {
        unsafe { &*(self.raw as *const T) }
    }

    // 不安全地将内部指针向下转换为具体类型的可变引用
    // 必须保证当前 raw 指针确实指向 T 类型的数据
    pub(crate) unsafe fn downcast_mut_unchecked<T>(&mut self) -> &mut T {
        unsafe { &mut *(self.raw as *mut T) }
    }
}

impl Drop for AnyProps<'_> {
    fn drop(&mut self) {
        // 如果 drop 字段存在,则调用其 drop_raw 方法释放内存
        if let Some(drop) = self.drop.take() {
            drop.drop_raw(self.raw);
        }
    }
}

小结:

  • AnyProps 通过类型擦除和原始指针封装,实现了对任意类型 props 的统一存储和生命周期管理。
  • 结合 DropRaw trait,既能安全释放堆分配的 props,也能灵活支持借用场景。
  • 这种设计为后续组件树的动态创建、嵌套和复用提供了基础能力,是实现对象安全组件协议的关键一环。

5. 组件实例化

在前面处理 props 类型擦除和内存释放时,我们采用了 DropRaw trait 搭配结构体的方式,解决了类型信息丢失后的资源管理问题。组件实例化其实也面临类似的挑战:类型擦除后,如何动态创建具体的组件实例?

这里我们同样借鉴 DropRaw 的思路,定义一个 trait(ComponentHelperExt)和一个泛型结构体(ComponentHelper<T>),专门用于保存组件类型信息和实例化方法。这样,每种组件类型都可以有一个对应的“工厂”,负责根据类型擦除的 props 创建具体组件。

component/component_helper.rs 中实现如下:

use crate::{
    component::{AnyComponent, Component},
    props::AnyProps,
};

// 组件实例化工厂 trait,支持类型擦除下的动态创建
pub trait ComponentHelperExt {
    fn new_component(&self, props: AnyProps) -> Box<dyn AnyComponent>;
    fn copy(&self) -> Box<dyn ComponentHelperExt>;
}

// 泛型结构体,保存组件类型信息
pub(crate) struct ComponentHelper<T> {
    _marker: std::marker::PhantomData<T>,
}

impl<T> ComponentHelper<T>
where
    T: Component,
{
    pub fn boxed() -> Box<dyn ComponentHelperExt> {
        Box::new(Self {
            _marker: std::marker::PhantomData,
        })
    }
}

impl<T> ComponentHelperExt for ComponentHelper<T>
where
    T: Component,
{
    fn new_component(&self, props: AnyProps) -> Box<dyn AnyComponent> {
        Box::new(T::new(unsafe { props.downcast_ref_unchecked() }))
    }
    fn copy(&self) -> Box<dyn ComponentHelperExt> {
        Self::boxed()
    }
}

通过这种设计,每个组件类型都能注册自己的实例化工厂,解决了类型擦除后无法直接 new 的问题。

接下来,扩展 InstantiatedComponent,让它持有 helper,并提供统一的实例化方法:

pub struct InstantiatedComponent {
    component: Box<dyn AnyComponent>,
    children: Components,
    helper: Box<dyn ComponentHelperExt>,
}

impl InstantiatedComponent {
    pub fn new(mut props: AnyProps, helper: Box<dyn ComponentHelperExt>) -> Self {
        let component = helper.new_component(props.borrow());
        Self {
            component,
            children: Components::default(),
            helper,
        }
    }
}

这样,InstantiatedComponent 只需持有 helper 和 props,就能动态创建和管理具体组件实例,并支持递归管理子组件。

总结

本节我们梳理了组件化实现中的类型擦除、内存管理和动态实例化等关键技术,介绍了如何借助 trait+结构体的设计,让组件系统既灵活又安全。通过这些机制,组件可以像积木一样自由组合和复用,也为后续实现组件树的渲染与布局打下了坚实的基础。

下一节将聚焦于组件树的渲染与布局系统,介绍如何让组件能够嵌套、自动分配区域,并实现灵活的 UI 结构。