yew框架中组件属性构造器的实现方法

2022-03-28 14:45:55 浏览数 (1)

yew是rust生态中一个优秀的前端mvvm框架。由于rust的强类型特点,在javascript中看似很容易的功能,放到rust语言上来实现就不是那么容易了。平时只是光顾着用,没有想到这个简单的功能,背后竟是靠一大堆代码才实现的。

比如,在yew中有个组件Person的属性是PersonProp,代码如下:

代码语言:javascript复制
#[derive(PartialEq, Properties)]
struct PersonProp {
    pub id: i64,
    pub name: String,
    pub job: Option<String>,
    pub telphone: Option<String>,
    pub address: Option<String>,
}

struct Person {};

impl Component for Person {
    type Message = ();
    type Properties = PersonProp;

    fn create(ctx: &Context<Self>) -> Self {
        Person {}
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        html! {
            <span></span>
        }
    }
    //其他trait方法
}

在使用它来构建视图的时候,用的宏来模拟html的语法

代码语言:javascript复制
#[function_component]
fn App() -> Html {
    html! {
        <Person name="zhangsan" id={1}>
        </Person>
    }
}

生成视图树的时候是要通过参数name和id构建出PersonProp的,注意job、telphone、address这些Option的参数并没有传递,yew给我们使用了默认值None赋值,如果是javascript来实现,直接一个对象,依次对每个参数赋值就完了,job、telphone、address这些不传照样构造出对象。但是对于rust来说,好难。对rust来说,所有参数要一起备齐,要是要求使用者传递所有参数,就没人用这个框架了,浏览器的dom节点有几十个事件监听器,全部都要显式传递一遍的话真是噩梦。一般人都能想到,给PersonProp加个Default的约束,这样就可以不必传每个参数了。形如如下:

代码语言:javascript复制
PersonProp {
    id: 1,
    name: "zhangsan".into(),
    ..PersonProp::default()
}

或者

代码语言:javascript复制
let mut props = PersonProp::default();
props.id = 1;
props.name = "zhangsan".into();

但是yew对Properties并没有Default的要求,也不是每个参数都一定能够满足Default约束,有些参数就只能用的时候再传递。

既然这样,可以考虑另一种方法,构造一个中间类型,属性全搞成Option,就满足Default了,最后再从Option里面强行unwrap出来。比如:

代码语言:javascript复制
#[derive(Default)]
struct PersonPropTemp {
    pub id: Option<i64>,
    pub name: Option<String>,
    pub job: Option<String>,
    pub telphone: Option<String>,
    pub address: Option<String>,
}

impl PersonPropTemp {
    fn id(mut self, id: i64) -> Self {
        self.id = Some(id);
        return self;
    }
    fn name(mut self, name: String) -> Self {
        self.name = Some(name);
        return self;
    }
    fn job(mut self, job: Option<String>) -> Self {
        self.job = job;
        return self;
    }
	
    fn telphone(mut self, telphone: Option<String>) -> Self {
        self.telphone = telphone;
        return self;
    }
	
    fn address(mut self, address: Option<String>) -> Self {
        self.address = address;
        return self;
    }
	
    pub fn build(self) -> PersonProp {
        PersonProp {
            id: self.id.unwrap(),
            name: self.name.unwrap(),
            job: self.job,
            telphone: self.telphone,
            address: self.address,
        }
    }
}

这样,勉强可以实现功能,但是有个大问题,如果使用者一个参数都不传,编译是能够通过的,只是在运行的时候发生panic,这样对必传参数的约束就形同虚设,没起到作用,程序的可靠性完全靠程序员的认真仔细来确保,程序没有一点儿健壮性可言。

如果不是想自己造轮子,是不会想到这些问题的,想了几天也没想到好方法,不得不翻看yew的源码,看它是怎么弄的。初看一下,它的实现也是构造中间类型,来进行链式调用,最后build返回需要的类型,像第三种方法。但是它是怎么做到编译时必传约束的呢?

由于自己平时很少有看开源框架源代码,之前也没有写过过程宏,看了一些时间看不太懂里面的逻辑,过程宏的东西,难以厘清逻辑。不过它里面有个对属性排序的操作,还分组了,必传的一组,非必传的一组,这给了我启发。一旦排序了之后进行链式调用,就可以在中间类型上做文章,我看到链式调用习惯性地以为都是返回自身的,而这个yew里面的中间类型,返回的不是自身,实际上是有好几个中间类型,每个必传参数都对应一个中间类型,调用一个必传参数的setter方法之后就扭转成下一个类型(像一个状态机),然后给每个类型上添加不同的setter方法来约束,如果必传参数都给了,通过调用顺序的归一化,就能保证最终收集到所有必传参数,如果少传了部分必传参数,中间类型因为没有对应的方法,在编译期间就报错了。最后把yew过程宏生成的代码打印出来看,印证了我的猜测。

按照这个思路,属性排序之后,顺序如下address、id(必传)、job、name(必传)、telphone,可以用宏生成以下参考代码:

代码语言:javascript复制
impl PersonProp {
    fn builder() -> PersonPropStageId {
        Default::default()
    }
}

#[derive(Default)]
struct PersonPropStageId {
    pub address: Option<String>,
}

impl PersonPropStageId {
    fn address(mut self, address: Option<String>) -> Self {
        self.address = address;
        self
    }

    fn id(self, id: i64) -> PersonPropStageName {
        PersonPropStageName {
            address: self.address,
            id: id,
            job: Default::default(),
        }
    }
}

struct PersonPropStageName {
    pub address: Option<String>,
    pub id: i64,
    pub job: Option<String>,
}

impl PersonPropStageName {
    fn job(mut self, job: Option<String>) -> Self {
        self.job = job;
        self
    }

    fn name(self, name: String) -> PersonPropStageFinal {
        PersonPropStageFinal {
            address: self.address,
            id: self.id,
            job: self.job,
            name: name,
            telphone: Default::default(),
        }
    }
}

struct PersonPropStageFinal {
    pub address: Option<String>,
    pub id: i64,
    pub job: Option<String>,
    pub name: String,
    pub telphone: Option<String>,
}

impl PersonPropStageFinal {
    fn telphone(mut self, telphone: Option<String>) -> Self {
        self.telphone = telphone;
        self
    }

    fn build(self) -> PersonProp {
        PersonProp {
            address: self.address,
            id: self.id,
            job: self.job,
            name: self.name,
            telphone: self.telphone,
        }
    }
}

每一个必传属性对应一个类型,PersonProp包含2个必传属性id和name。类型里面包含的属性是排在它之前的所有属性,包含的setter方法只有当前属性和到上一个必传属性之间的非必传属性,而且非必传参数的setter方法返回的是自身,并没有进行状态切换,调用当前属性的setter方法之后,之前的属性在上一个状态里取,当前属性在参数里取,从当前必传属性开始,到下一个必传属性中间的非必传属性用默认值填充。

例如第二个必传参数name对应类型的实现如下:

address

id(必传)

job

name(必传)

telphone

包含的属性

包含的setter

扭转状态时的数据来源

上一个状态

上一个状态

上一个状态

参数

默认值

第一个必传参数(此处为id)对应的状态类型只包含0到多个非必传属性,是可以全部用默认值填充的,支持Default约束。

yew中的实现还有些细节处理,所以生成的状态机不太一样,但是思路一样。另外必传和非必传参数的区分,通过其他的属性过程宏(prop_or, prop_or_else, prop_or_default)来打标记,Option类型的貌似免了。

使用html!宏对PersonProp进行构造就可以生成如下链式调用代码(也需要先对属性名进行排序)

代码语言:javascript复制
PersonProp::builder()
    .address(Some("guangdong".into())) //非必传参数部分可以没有
    .id(1)
    .job(Some("it".into())) //非必传参数部分可以没有
    .name("zhangsan".into())
    .telphone(Some("88888888".into())) //非必传参数部分可以没有
    .build();

注意各个setter方法的调用一定是按属性排序之后的顺序调用。如果少传了必传参数id或者name,会因为没有后续的setter方法而编译失败,从而实现在编译期进行约束。通过如此巧妙的设计,才实现了允许不传支持默认值的参数这个看似理所当然的功能。

0 人点赞