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 對應類型的實現如下:

TTKscs

第一個必傳參數 (此處爲 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