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方法而编译失败,从而实现在编译期进行约束。通过如此巧妙的设计,才实现了允许不传支持默认值的参数这个看似理所当然的功能。