yew 框架中組件屬性構造器的實現方法
yew 是 rust 生態中一個優秀的前端 mvvm 框架。由於 rust 的強類型特點,在 javascript 中看似很容易的功能,放到 rust 語言上來實現就不是那麼容易了。平時只是光顧着用,沒有想到這個簡單的功能,背後竟是靠一大堆代碼才實現的。
比如,在 yew 中有個組件 Person 的屬性是 PersonProp,代碼如下:
#[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 的語法
#[function_component]
fn App() -> Html {
html! {
<Person id={1}>
</Person>
}
}
生成視圖樹的時候是要通過參數 name 和 id 構建出 PersonProp 的,注意 job、telphone、address 這些 Option 的參數並沒有傳遞,yew 給我們使用了默認值 None 賦值,如果是 javascript 來實現,直接一個對象,依次對每個參數賦值就完了,job、telphone、address 這些不傳照樣構造出對象。但是對於 rust 來說,好難。對 rust 來說,所有參數要一起備齊,要是要求使用者傳遞所有參數,就沒人用這個框架了,瀏覽器的 dom 節點有幾十個事件監聽器,全部都要顯式傳遞一遍的話真是噩夢。一般人都能想到,給 PersonProp 加個 Default 的約束,這樣就可以不必傳每個參數了。形如如下:
PersonProp {
id: 1,
name: "zhangsan".into(),
..PersonProp::default()
}
或者
let mut props = PersonProp::default();
props.id = 1;
props.name = "zhangsan".into();
但是 yew 對 Properties 並沒有 Default 的要求,也不是每個參數都一定能夠滿足 Default 約束,有些參數就只能用的時候再傳遞。
既然這樣,可以考慮另一種方法,構造一箇中間類型,屬性全搞成 Option,就滿足 Default 了,最後再從 Option 裏面強行 unwrap 出來。比如:
#[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,可以用宏生成以下參考代碼:
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 對應類型的實現如下:
第一個必傳參數 (此處爲 id) 對應的狀態類型只包含 0 到多個非必傳屬性,是可以全部用默認值填充的,支持 Default 約束。
yew 中的實現還有些細節處理,所以生成的狀態機不太一樣,但是思路一樣。另外必傳和非必傳參數的區分,通過其他的屬性過程宏(prop_or, prop_or_else, prop_or_default)來打標記,Option 類型的貌似免了。
使用 html! 宏對 PersonProp 進行構造就可以生成如下鏈式調用代碼(也需要先對屬性名進行排序)
PersonProp::builder()
.address(Some("guangdong".into())) //非必傳參數部分可以沒有
.id(1)
.job(Some("it".into())) //非必傳參數部分可以沒有
.name("zhangsan".into())
.telphone(Some("88888888".into())) //非必傳參數部分可以沒有
.build();
注意各個 setter 方法的調用一定是按屬性排序之後的順序調用。如果少傳了必傳參數 id 或者 name,會因爲沒有後續的 setter 方法而編譯失敗,從而實現在編譯期進行約束。通過如此巧妙的設計,才實現了允許不傳支持默認值的參數這個看似理所當然的功能。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5kVo0heSHHnQh2N-6POuAQ