在Rust中实现类JSX特性:静态类型Props优化方案问询
嘿,这个在Rust里实现类JSX的思路很棒!我看了你当前的实现,这里有几个可以优化的方向,不管是类型安全、代码简洁性还是后续宏的实现都能帮上忙:
1. 明确区分原生元素与自定义组件,强化类型安全
你当前用泛型Element<T, P>兼容了原生元素和自定义组件,但这种设计会让类型边界模糊,后续扩展也容易出问题。可以试试把两者做清晰拆分:
- 原生DOM元素:用枚举固定标签类型,比如
enum NativeTag { Div, P, Span },这样get_tag可以直接返回对应字符串,不用依赖Debugtrait格式化,既高效又避免出错。 - 自定义组件:单独抽离
Componenttrait,明确它的职责是接收Props和子元素,返回渲染后的Element:
trait Component { type Props; fn render(&self, props: &Self::Props, children: Children) -> Rc<dyn Render>; }
之后Element可以设计成枚举,清晰区分两种类型:
enum Element { Native(NativeTag, NativeProps, Children), Custom(Box<dyn Component>, Box<dyn AnyProps>, Children), }
(注:AnyProps可以用dyn Any实现,或者给Props单独加一个 trait 来约束)
2. 优化Children与Rc的使用
- 考虑用
SmallVec<[Rc<Render>; 4]>替代Vec<Rc<Render>>(需要引入smallveccrate),大部分场景下子元素数量不多,SmallVec能减少堆分配,提升性能。 - 文本节点不要直接让
String实现Render,专门写一个Text结构体:struct Text(String),语义更清晰,后续扩展文本相关特性也更方便。 - 如果渲染是单线程场景,
Rc是合理的,但如果后续要支持多线程,可以换成Arc。
3. 拆分Render trait的职责
现在Render trait既负责组件渲染逻辑,又负责字符串转换,违反了单一职责原则。可以把字符串渲染逻辑抽离成单独的函数:
trait Render { fn render(&self) -> Option<Rc<dyn Render>>; fn is_native(&self) -> bool; // 提供原生元素的属性和子元素访问方法 fn as_native(&self) -> Option<(&NativeTag, &NativeProps, &Children)>; } fn render_to_string(element: &dyn Render) -> String { if element.is_native() { let (tag, props, children) = element.as_native().unwrap(); let props_str = render_props(props); // 单独实现Props转字符串逻辑 let children_str = children.iter().map(render_to_string).collect::<Vec<_>>().join("\n"); format!("<{tag}{props_str}>{children_str}</{tag}>") } else { match element.render() { Some(child) => render_to_string(&*child), None => String::new(), } } }
这样Render trait更简洁,逻辑也更清晰。
4. 宏实现的简化思路
要实现<div id="myId">...</div>这种语法,用**过程宏(proc_macro)**会比 declarative macro更合适,因为它能处理复杂的嵌套结构:
- 宏解析XML风格标签,区分原生标签和自定义组件;
- 原生标签自动生成对应的Props实例,用
..Default::default()补全默认值(需要Props实现Default); - 自定义组件自动调用
Componenttrait的render方法; - 递归处理子元素,转换成
Children类型。
可以参考yew或dioxus的宏实现逻辑,但不用照搬,简化成你需要的核心功能即可。
当前实现代码
use std::fmt; use std::rc::Rc; use std::fmt::{Debug, Display}; type Children = Vec<Rc<Render>>; trait ElementParts { fn get_children(&self) -> Option<&Children>; fn get_tag(&self) -> String; } // Mostly for custom elements trait Render: ElementParts + Debug + Display { // Dom elements don't have to render anything fn render(&self) -> Option<Rc<Render>> { None } // Tell whether it's a custom component that will render more things or not. fn should_render(&self) -> bool { true } fn render_to_string(&self) -> String { if self.should_render() { match self.render() { Some(element) => element.render_to_string(), None => String::new(), } } else { format!("<{tag}>{}<\/{tag}>", self.render_children_to_string(), tag=self.get_tag()) } } fn render_children_to_string(&self) -> String { if let Some(children) = self.get_children() { children.iter() .map(|child| child.render_to_string()) .collect::<Vec<String>>() .join("\n") } else { String::new() } } } #[derive(Debug)] struct Element<T: Debug + Clone + 'static, P: Debug + Clone + 'static> { etype: T, props: P, children: Children, } impl<P: Debug + Clone + 'static, T: Debug + Clone + 'static> Element<T, P> where Element<T, P>: Render { fn new(etype: T, props: P, children: Children) -> Rc<Render> { Rc::new( Element { etype: etype, children: children, props: props, } ) } } impl<P: Debug + Clone + 'static, T: Debug + Clone + 'static> ElementParts for Element<T, P> { fn get_children(&self) -> Option<&Children> { Some(&self.children) } fn get_tag(&self) -> String { format!("{:?}", self.etype) } } impl<P: Debug + Clone + 'static, T: Debug + Clone + 'static> Display for Element<T, P> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "- {:?}, {:?}\n", self.etype, self.props)?; for c in &self.children { write!(f, " {}\n", c)?; } write!(f, "\n") } } // Text node impl ElementParts for String { fn get_children(&self) -> Option<&Children> { None } fn get_tag(&self) -> String { "TextNode".to_string() } } impl Render for String { fn should_render(&self) -> bool { false } fn render_to_string(&self) -> String { self.clone() } } #[derive(Debug, Clone)] struct Div; #[derive(Debug, Clone)] struct DivProps { class: String, } impl Render for Element<Div, DivProps> { fn should_render(&self) -> bool { false } } #[derive(Debug, Clone)] struct P; #[derive(Debug, Clone)] struct PProps { class: String, text: Option<String>, } impl Render for Element<P, PProps> { fn should_render(&self) -> bool { false } } #[derive(Debug, Clone)] struct Name; #[derive(Debug, Clone)] struct NameProps { show: bool, name: String, } impl Render for Element<Name, NameProps> { fn render(&self) -> Option<Rc<Render>> { // In here custom logic will come if !self.props.show { return Some(Element::new( P, PProps{class: String::new(), text: None,}, vec![ Rc::new("NOT_SHOWING".to_string()), ] )) } Some( Element::new( Div, DivProps { class: "form-control".to_string()}, vec![ Rc::new("SHOWING".to_string()), Rc::new(self.props.name.clone()) ] ) ) } } // And this is how I would use it fn main() { let e = Element::new(Name, NameProps{ show: true, name: "Fran".to_string() }, vec![]); println!("{:?}", e); println!("{}", e.render_to_string()); }
内容的提问来源于stack exchange,提问作者franleplant
相关产品推荐
相关产品推荐

