Jasper Ji

开口不在舌头上

0%

写一个Yew版的UI库

前言

去年的时候我就尝试用Yew写了一个记事本的应用,当时可以说是刚接触Yew,Rust也没写什么代码,好在那个应用在UI方面也比较简单。一年过去了,最近又开始折腾Yew这个框架,一开始只是想把网络请求以及图片显示等测试一下,后来觉得是不是可以写一个UI库试下,虽然也有几个在Yew推荐的UI库,但感觉审美不在一个线上。之前写系统后台管理端时,使用的是vue-element-admin这个框架,想着也写一个Yew版的吧,不过首先得把Element UI库给写出来,实际上Yew能不能大范围的使用还是跟类似Element UI这样的企业级库有关系。另一方面对于Yew,我也想通过写这个UI库,来进一步学习Yew这个框架,同时也可以练习Rust的使用。

组件

这些组件的实现都是参考了Element UI的源码,样式部分基本原封不动的使用了Element UI的样式,Element UI是基于Vue的,现在等于把Vue换成了Yew,Javascript换成了Rust,实现逻辑上依旧使用Element UI的,但在具体实现上则是根据Yew框架和Rust的特点有所不同,尽可能在功能上保持跟Element UI一致。

Button 按钮

一开始我选择了Button这个最常用的组件,这个组件相对来说比较简单。


use yew::prelude::*;

pub enum Msg {}
pub struct YewButton {
    props:YewButtonProps
}

#[derive(Clone, PartialEq, Properties)]
pub struct YewButtonProps {
    #[prop_or_default]
    pub disabled: bool,
    #[prop_or_default]
    pub style: String,
    pub title: AttrValue,
    pub on_clicked: Callback<MouseEvent>,
    #[prop_or_default]
    pub loading:bool,

    #[prop_or_default]
    pub plain:bool,
    
    // medium / small / mini
    #[prop_or_default]
    pub size:String
}

impl Component for YewButton {
    type Message = Msg;
    type Properties = YewButtonProps;

    fn create(ctx: &Context<Self>) -> Self {
        Self {
            props:ctx.props().clone()
        }
    }

    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {}
    }
    fn view(&self, ctx: &Context<Self>) -> Html {
        let title = ctx.props().title.clone();
        let disabled = ctx.props().disabled.clone();
        let style = ctx.props().style.clone();
        let loading = ctx.props().loading.clone();
        
        let onclick = ctx.props().on_clicked.reform(move |event: MouseEvent| {
            event.stop_propagation();
            event.clone()
        });

    
        let mut classes = Vec::new();
        classes.push(String::from("el-button"));

        if !style.is_empty() {
            let ss = format!("el-button--{}", style);
            classes.push(ss);
        }
        if disabled {
            classes.push(String::from("is-disabled"));
        }

        if self.props.plain {
            classes.push(String::from("is-plain"));
        }

        // TODO 需要对字符串进行检查
        if !self.props.size.is_empty() {
            classes.push(format!("el-button--{}", self.props.size));
        }

        html! {
            <button class={classes!(classes.clone())} {onclick} disabled={disabled.clone()} >
            {title.clone()}
            if loading {
                <i class="el-icon-loading"></i>
            }
            </button>
        }
    }
}

这个里面代码目前没有优化,基本上时当时写的样子,应该是没有完全实现所有Element UI版的按钮功能,但核心功能是可以用的。

Rate 评分

评分组件在以前iOS的时候也是自己写过,相对按钮组件,这个就相对复杂些了。主要问题是实现半星效果的功能时,发现Yew的鼠标事件不能穿透,一开始按照原版的实现来写,结果老是出错,后来才发现是这个不能穿透的问题。

if self.is_rate_disabled() {
    return false;
}
if self.props.allow_half {
    let element: Element = e.target_unchecked_into();
    let mut target = None;
    // 这段代码原本是Element UI的实现,但是Yew的鼠标移动事件并不能穿透,所以这段代码弃用
    // if element.class_list().contains("el-rate__item") {
    //     target = element.query_selector(".el-rate__icon").unwrap();
    // }
    // if target.is_some()&& target.clone().unwrap().class_list().contains("el-rate__decimal") {
    //     target = target.clone().unwrap().parent_element();
    // } else if element.class_list().contains("el-rate__decimal") {
    //     target = element.parent_element();
    // }
    if target.is_none() {
        target = Some(element);
    }
    if target.is_some() {
        let offset_x = e.offset_x()*2;
        let client_width = target.clone().unwrap().client_width();
        self.pointer_at_left_half = offset_x <= client_width;
        self.current_value = if self.pointer_at_left_half {
            (index+1) as f64 - 0.5
        } else {
            (index+1) as f64
        };
    } 
} else {
    self.current_value = (index+1) as f64;
}
self.hover_index = index+1;
true

原版的实现专门对穿透做了处理,既然不支持穿透实现反而简单了,这个应该是Yew自己的设置了。

ColorPicker 颜色选择器

完成了评分组件后,想到找个再复杂一点的,于是选中了颜色选择器这个组件,一看这个组件是由好几子组件组成的,就是他了。颜色是个比较专业的东西,原版专门有个Color模块来处理,一开始我打算用Rust重写Color模块,不过鉴于Javascript的动态类型,一开始写一个Hsv转RGB的时候就卡住了,于是就找了一些第三方的库,先把功能拼凑出来,这也是颜色选择器中没有使用原版的实现的一部分。另一个遇到的问题是一些参数面板原版是支持滑动来操作的,但是我试了一下还是有问题。于是只支持了点击设置值的操作。

另一个遇到的大问题是原版支持输入颜色的操作,但是我的输入组件还没有写好,看了写源码,这个组件还需要单独去写。于是目前的功能不支持输入。

颜色处理,最后使用了csscolorparser这个库,把原来零散的库给替换了。

最后这个组件功能我觉得能用,支持十六进制颜色以及RGB和RGBA的形式,实际上csscolorparser这个库支持的很全。目前存在的问题,原版使用了Vue特有的transition标签,我一开始以为这是HTML的,后来才发现。原版弹框时会有动画,而且也会把弹出框定位到颜色按钮的下方适当位置。目前这本版本暂时没有这样的效果。

总结

Yew这个框架,我也尝试的看了下源码,这非常有帮助,因为这是文档有时无法学习到的。基本的运行原理以及生命周期之类的都有了一定的认识。另外通过这三个组件的练习,跟Vue的实现一对比,发现Vue还是包装的更深,更对开发者友好。Yew基本的功能实现是没有问题的,只是有些类似上面提到的transition这样的,可能就需要自己实现。

Rust方面,一方面我看了一些源码,这些源码使用了更深层次的Rust功能,比如宏、范型。另一方面,Rust写前端,毕竟运行环境不是操作系统,所以有些库也是特定平台的。比如数学模块,我一开始想着使用Rust自带之类的标准库,但最后还是使用js-sys的Math模块,实际证明这个很好用。

下一步,目前只写了三组件,总共有90个组件,完全是冰山一角,工作量之巨大,只能慢慢来。目前来看用Rust写前端,只是一种实验性的东西,毕竟Javascript的应用面很广,小程序,移动端,Rust更广阔的应该还是在系统应用这个层级,但面对C/C++这类系统级别的语言也是挑战,另外服务端又有各种语言占据。Rust的使用场景看似很广,但却不得不名对先占市场的问题,所以这也是我为什么要用Rust来写前端这个初衷,就是想用Rust。

最后有对Yew这个框架感兴趣的,可以关注下yew-lab这个项目,组件相关的代码都在里面。