Rust前端框架Yew:写一个Input输入框组件

405 阅读5分钟

WX20230715-163510@2x.png

上次写颜色拾取器时,就需要一个输入框的组件,当时用文本替代的。输入框是个常用的组件,干脆一些算了,直接一写。

也不直接上代码了,喜欢的直接到工程中看源码yew-lab

以下主要是在实现中的遇到的一些点。

三目运算符

Rust没有三目运算符,所以在处理简单HTML标签属性时,就很麻烦。

<textarea
   maxlength={format!("{}", self.props.max_length)}
/>

maxlength这样的属性可能会没有设置,所以最好定义为Option<i32>这样的类型。

<textarea
   maxlength={format!("{}", self.props.max_length.unwrap())}
/>

这样写如果max_length为None会报错,

panicked at 'called `Option::unwrap()` on a `None` value'

如果有三目运算符,或许我们可以这么写。

<textarea
   maxlength={format!("{}", self.props.max_length.is_some()?self.props.max_length.unwrap():"")}
/>

但是实际是没有的,这时可以写一个方法,在方法里面写判断就没有问题了。

<textarea
   maxlength={self.get_max_length()}
/>
pub fn get_max_length(&self) -> String {
    if let Some(len) = self.props.max_length {
        return format!("{}", len);
    }
    "".to_string();
}

但是这样渲染出来的html源码,如果maxlength为None,就会出现这样情况。

<textarea maxlength/>

虽然不影响使用,但总觉得怪怪的,最终用代码的方式解决吧。添加一个对textarea引用

<textarea
   ref = {&self.textarea_ref}
/>

然后在第一次渲染完后,根据条件动态设置。


fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
    if first_render {
        if self.props.input_type == "textarea" {
            let textarea = self.textarea_ref.cast::<HtmlTextAreaElement>().unwrap();
            if let Some(maxlength) = self.props.max_length {
                 textarea.set_max_length(maxlength);
            }
        } else if self.props.input_type == "text" {
            let input = self.input_ref.cast::<HtmlInputElement>().unwrap();
            if let Some(maxlength) = self.props.max_length {
                input.set_max_length(maxlength);
            }
        }
    }
}

Slot

Element UI中使用了Slot这个东西,在输入框中就有不少的Slot,前缀、后缀等等。一开始我有点不知道该怎么实现这个,想着在属性中定义需要的Slot,最后发现了Yew有个Children这样的东西。

<YewInput><span slot="suffix">{"test"}</span></YewInut>

可以获取到YewInput标签中间的内容,实际上Element UI的实现也是从中间的标签中查找对应的Slot。首先需要在属性中定义一个children的属性。

#[derive(Clone, PartialEq, Properties)]
pub struct YewInputProps {
    // ...
    #[prop_or_default]
    pub children: Children,
    // ...
}

children就代表标签中间的内容,以下是如何从children中获取Slot,这个比较Rust特色。

pub fn get_slot(&self, slot_name: String) -> Option<VNode> {
    for i in self.props.children.clone().into_iter() {
        match i {
            VNode::VTag(ref vtag) => {
                match vtag.attributes {
                    yew::virtual_dom::Attributes::Static(vev) => {
                        for g in vev {
                            if g.0 == "slot" {
                                // log!(format!("{:?}", g.1));
                                if g.1 == slot_name {
                                    return Some(i);
                                }
                            }
                        }
                    }
                    _ => {}
                }
            }
            _ => {}
        }
    }
    None
}

这里返回使用了Option,这个我还蛮喜欢的,之前在Java 8中就有这样的东西,然后在渲染的时候就可以动态的判断。


html!{
    if let Some(node) = self.get_slot("prefix".to_string()) {
        {node}
    }
}

内部可修改性

这个是也是因为Slot这个东西,我一开始觉得每次都去遍历,想着缓存下吧。于是在结构内部定义了HashMap。


pub struct YewInput {
    textarea_ref: NodeRef,
    input_ref: NodeRef,
    password_visible: bool,
    hovering: bool,
    focused: bool,
    need_focus: bool,
    // 缓存查找的
    slot_map: HashMap<String, bool>,
    props: YewInputProps,
}

然后我会在查找Slot是否存在方法中使用这个。

pub fn has_slot(&mut self, name: String) -> bool {
    if let Some(h) = self.slot_map.get(&name.clone()) {
        return *h;
    }
    match self.get_slot(name.clone()) {
        Some(_) => {
            self.slot_map.insert(name.clone(), true);
            return true;
        }
        None => {
            self.slot_map.insert(name.clone(), false);
            return false;
        }
    }
}

这个方法之前是&self,因为改变了slot_map变成这样&mut self。这么修改后发现报出了一堆的如下的错误。

`self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

这要类的其他方法中调用这个,那么都需要改成&mut self这样的,一开始我按照提示修改了全部的报错,但最后发现这些方法会在view中调用。

fn view(&self, ctx: &Context<Self>) -> Html

这个是BaseComponent中定义的,BaseComponent是个Trait,类似于Java的接口,是不可能修改的,折腾一番最后发现白折腾。在写这几个组件的过程中,Rust语言方面的问题从没有现在遇到这个让我头大,传统的这些语言中哪会有这些问题,冷静下来一思考,觉得不可能吧,还是有一些我没有掌握的东西。翻开《Rust程序设计》这本书,在第九章结构体发现了解决方案,书中叫内部可修改性

pub struct YewInput {
    textarea_ref: NodeRef,
    input_ref: NodeRef,
    password_visible: bool,
    hovering: bool,
    focused: bool,
    need_focus: bool,
    // 缓存查找的
    slot_map: RefCell<HashMap<String, bool>>,
    props: YewInputProps,
}

slot_map定义为这样slot_map: RefCell<HashMap<String, bool>>。这样就可以不需要获取mut权限了。

pub fn has_slot(&self, name: String) -> bool {
    let clone_name = name.clone();
    let mut ref_cell = self.slot_map.borrow_mut();
    let c = ref_cell.get(&clone_name.clone());
    if c.is_some() {
        return *c.unwrap();
    }
    match self.get_slot(name) {
        Some(_) => {
            ref_cell.insert(clone_name.clone(), true);
            return true;
        }
        None => {
            ref_cell.insert(clone_name.clone(), false);
            return false;
        }
    }
}

总结

学习一门语言最好的方式就是用他来写点项目。Rust的文档不错,很多时候都是看文档来解决,最常用的是wasm-bindgen的文档。Yew相关的也更多的是看源码或者文档为主,另外也通过Element UI的源码实现,反过来再看Yew,查找相似的解决方案。另外Vue、React这类的方案已经很成熟了,所以Yew的源码阅读中也多少会有所帮助。

虽然编译器提示的错误是个很有用的东西,有些情况下按着提示会把问题解决了,就像我一开始写记事本的时候,当时遇到很多编译错误,最后按着提示终于没有错误了,后来我重读了《Rust程序设计》中关于所有权和引用相关章节的后,我几乎把当时的解决方案给重写了,所以按着提示是没有错,但或许不是最佳的方案,只是没有错误了。但像上面遇到的问题,你一步步按编译器的提示修改,最后还是解决不了问题,这时可能在换个思路了。

关于WebAssembly的东西,Mozilla这个文档非常的浅显易懂。另外《WebAssembly实战》这本书也不错,虽然是讲C/C++的,但JS和WebAssembly的相关部分,作者都是先实现了一个使用Emscripten插件的,也会提供一个没有使用插件的实现,这些都是通用的,是基础。另外既然学习了Rust了,C/C++这些其实都是一个层级的,都是会用到的。