距离上一篇写一个Yew版的UI库,已经一个多月了。一直在写Yew,有些认知已经变化了,总结一下。
类组件
结构式组件(Struct components),我更喜欢叫它类组件,以下是一个计数器的例子。
use yew::prelude::*;
pub enum Msg {
Add,
Sub
}
pub struct CounterPage {
counter: i32
}
impl Component for CounterPage {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self {
counter:0
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Add=>{
self.counter += 1;
true
},
Msg::Sub=>{
self.counter -= 1;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
<h1>{ "Counter测试" }</h1>
<div>{"counter: "}{self.counter}</div>
<div>
<button onclick={ctx.link().callback(move|_|Msg::Add)}>{ "增加" }</button>
<button onclick={ctx.link().callback(move|_|Msg::Sub)}>{ "减少" }</button>
</div>
</div>
}
}
}
这种形式的重点是生命周期,上面的例子只使用了最常用的create
、update
、view
这三个方法。下面是所有的生命周期的方法:
pub trait BaseComponent: Sized + 'static {
/// The Component's Message.
type Message: 'static;
/// The Component's Properties.
type Properties: Properties;
/// Creates a component.
fn create(ctx: &Context<Self>) -> Self;
/// Updates component's internal state.
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool;
/// React to changes of component properties.
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool;
/// Returns a component layout to be rendered.
fn view(&self, ctx: &Context<Self>) -> HtmlResult;
/// Notified after a layout is rendered.
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool);
/// Notified before a component is destroyed.
fn destroy(&mut self, ctx: &Context<Self>);
/// Prepares the server-side state.
fn prepare_state(&self) -> Option<String>;
}
不过Yew的类组件在状态更新方面跟React不一样。比如React:
// 初始化状态
this.state = {
counter: 0
}
// 设置状态
this.setSatate({
counter: 1
})
Yew主要依赖于消息,每次要更新状态先发送消息,然后在update
方法里面处理消息并决定视图是否需要重新渲染。
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Add=>{
self.counter += 1;
true
},
Msg::Sub=>{
self.counter -= 1;
true
}
}
}
这种模式实际上是受到了Elm这个语言的影响。这个是Elm官网上的Button例子。
module Main exposing (..)
-- Press buttons to increment and decrement a counter.
--
-- Read how it works:
-- https://guide.elm-lang.org/architecture/buttons.html
--
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model = Int
init : Model
init =
0
-- UPDATE
type Msg
= Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
函数式组件
这个受到React的函数式组件的启发,上面计数器的例子可以这样写:
#[function_component]
pub fn CounterPage() -> Html {
let counter = use_state(|| 0);
let add_callback = {
let counter_clone = counter.clone();
Callback::from(move |_| {
counter_clone.set(*counter_clone+1);
})
};
let sub_callback = {
let counter_clone = counter.clone();
Callback::from(move |_| {
counter_clone.set(*counter_clone-1);
})
};
html! {
<div>
<h1>{ "Counter测试" }</h1>
<div>{"counter: "}{*counter}</div>
<div>
<button onclick={add_callback}>{ "增加" }</button>
<button onclick={sub_callback}>{ "减少" }</button>
</div>
</div>
}
}
从形式上这个会比类组件更加的简洁。Yew函数式组件是一种完整构建组件的方法,主要是依赖Hooks,比如上面的use_state
,还有很多比如use_node_ref
、use_effect_with_deps
。如果你用过React的函数式组件,那么只要适当的参考下Yew的相关文档,很容易上手。不熟悉函数式组件的,建议阅读最新的React函数式文档,或者读一本书,比如我读的是《React学习手册第二版》。
如何选择?
Yew的官方文档这样描述:
You are currently reading about function components - the recommended way to write components when starting with Yew and when writing simple presentation logic.
There is a more advanced, but less accessible, way to write components - Struct components. They allow very detailed control, though you will not need that level of detail most of the time.
这给人感觉类组件更强大,我一开始使用类组件,但是后来因为用到了上下文消息的监听的问题,见这个例子contexts,里面的发布消息,是用函数式组件写的,当时没有反应过来如何用类组件写发布消息,后来发现类组件也很简单,这是后话。后面我就有意开始使用函数式组件,发现用函数式组件似乎实现起来也问题不大,所以后面的组件又都是函数式组件写的。
函数式组件一开始我觉得,也有跟类组件类似的生命周期的东西,虽然在组织逻辑复杂的代码方面似乎没有类组件方便,后来我慢慢的使用类似过程式的方式,把一些相似的逻辑拆分到方法里面。如下:
#[function_component]
pub fn YELTag(props: &YELTagProps) -> Html {
let on_click = {
let on_click = props.on_click.clone();
Callback::from(move |e| {
on_click.emit(e);
})
};
let on_click_clone = on_click.clone();
html! {
<span
class={get_span_classes(props)}
style={if props.color.is_empty() {"".to_string()} else {format!("background-color: {}", props.color)}}
onclick={on_click}
>
{props.children.clone()}
if props.closable {
<i class="el-tag__close el-icon-close" onclick={ on_click_clone }></i>
}
</span>
}
}
fn get_span_classes(props: &YELTagProps) -> Vec<String> {
let mut span_classes = vec!["el-tag".to_string()];
if let Some(t) = props.tag_type.clone() {
span_classes.push(format!("el-tag--{}", t));
}
if let Some(e) = props.effect.clone() {
span_classes.push(format!("el-tag--{}", e));
}
if let Some(s) = props.size.clone() {
span_classes.push(format!("el-tag--{}", s));
}
if props.hit {
span_classes.push("is-hit".to_string());
}
span_classes
}
把class
单独通过fn get_span_classes(props: &YELTagProps) -> Vec<String>
来处理,这样就可以避免函数组件内代码的膨胀。
但就在刚发布这篇文章后的第二天,我遇到了如下的问题。
#[function_component]
pub fn YELInputNumber(props: &YELInputNumberProps) -> Html {
let on_decrease = {
let value = props.value.clone();
Callback::from(move |e| {
let v = decrease(props, value);
// log!("v:", v);
})
};
let on_increase = { Callback::from(|e| {}) };
html! {
<div class={get_div_classes(props)}>
if props.controls {
<span
class="el-input-number__decrease"
role="button"
onclick={on_decrease}
>
<i class={format!("el-icon-{}", {
if get_controls_at_right(props) {
"arrow-down"
} else {
"minus"
}
})}></i>
</span>
<span
class="el-input-number__increase"
role="button"
onclick={on_increase}
>
<i class={format!("el-icon-{}", {
if get_controls_at_right(props) {
"arrow-down"
} else {
"plus"
}
})}></i>
</span>
}
<YELInput value={get_display_value(props)}/>
</div>
}
}
fn decrease(props: &YELInputNumberProps, val: f64) -> f64 {
let precision_factor = js_sys::Math::pow(10.0, get_num_precision(props));
let num = precision_factor * val - precision_factor * props.step as f64;
return to_precision(props, num, None);
}
fn to_precision(props: &YELInputNumberProps, num: f64, precision_option: Option<i32>) -> f64 {
let precision = if let Some(p) = precision_option {
p
} else {
get_num_precision(props) as i32
};
let a = js_sys::Math::round(num * js_sys::Math::pow(10.0, precision as f64));
let b = js_sys::Math::pow(10.0, precision as f64);
return a / b;
}
fn get_precision(val: f64) -> i32 {
let value_string = val.to_string();
let dot_position = value_string.find('.');
if let Some(p) = dot_position {
return (value_string.len() as i32) - (p as i32) - 1;
}
0
}
这段代码会报错,因为我在闭包中使用了props这个引用,这个引用不满足'Static
的这个要求。
| pub fn YELInputNumber(props: &YELInputNumberProps) -> Html {
| ----- - let's call the lifetime of this reference `'1`
| |
| `props` is a reference that is only valid in the function body
...
41 | / Callback::from(move |e| {
42 | | let v = decrease(props, value);
43 | | // log!("v:", v);
44 | | })
| | ^
| | |
| |__________`props` escapes the function body here
| argument requires that `'1` must outlive `'static`
出错的代码:
#[function_component]
pub fn YELInputNumber(props: &YELInputNumberProps) -> Html {
let on_decrease = {
let value = props.value.clone();
Callback::from(move |e| {
let v = decrease(props, value);
// log!("v:", v);
})
};
这之前使用闭包都是复制了props的值,没想道这次因为使用了props引用本身却出错了。因为decrease这个方法会调用其他的方法,也需要props中的参数。这个错误似乎无法解决,props这个参数我无法修改成'Static
的,因为这个是Yew自己提供的。一种就是我彻底把所有后续用到的参数复制出来,通过decrease传给下面的调用,即使decrease本身不需要这些参数,但这样一来反而增加了复杂性。但如果改成类组件的话,就不存在上面的问题,每个类组件可以包含props属性,在实例方法中可以调用。
通过以上这个问题,打破了我原来觉得函数式组件是一种完备方法的结论,即使我努力去尝试适应,但函数式组件在复杂逻辑代码中有它的缺陷,这终究是Rust本身的决定,也是框架的问题。我相信JS版本没有这样的问题,所以React中函数式组件的可以大行其道有它的道理,但是在Yew中却受到语言特性的限制。
总结
类组件和函数式都可以使用,两者是不同思想,见上分析。虽然Yew的函数式组件看起来跟React的函数式组件很像,但还是有限制的,所以如果遇到问题,那么回到类组件的方式,至少可以解决问题。但为什么不直接使用类组件就得了,所以Yew团队也不知道怎么想的,创造了两种方式,但又不是很完美,反而增加了困扰,或许我根本不需要了解React。
Vue 用户
我就是Vue的用户,更准确的是应该Vue 2用户,Vue 3还没有来得及学习。Element UI 也是基于Vue2的组件库。一开始我觉得这只是JS和Rust的不同,很多时候我都是用Vue的思维在学习使用Yew这个框架,但渐渐发现Yew与Vue在设计思想上有很大不同,本质上这实际上是React与Vue的不同,尤其是React的函数式组件思想。
Rust vs JavaScript
动态性
在写Table这个组件时,JS版的Table中使用很多动态获取的东西,这种方式对于Rust而言就无法做到,首先是数组,一般都是固定类型,所以这里需要使用范型,范型问题不大。动态性,我想到了反射这样的东西,官方提供反射功能很有限,是一种编译时的反射。后来也找到了一些第三方的库,不过最后发现还是要结合范型,并不能动态的判断类型,进行值的转换。
另外比如Element UI组件的属性有些同时支持Number、String、Function,我一开始想到用范型,但是在实际使用中还需要动态的判断类型,这个Rust无法做到,目前这类属性的实现还没有好的方法。
确定性
使用Rust好处就是类型确定性,比如还是组件的属性,JS版使用的是字符串,这个实际上就有个验证的问题。但是如果用Rust写的话,可以直接使用枚举类型。
总结
用Rust写前端,大部分问题实际上就跟你用C#或者Java写前端一样,只不过Rust对比后者而言少了更多的动态性,另外就是它的所有权法则,早期的时候,非常不习惯,顿不顿就被编译器暂停了。Rust项目编译慢,这个确实是个问题,我的电脑都是32G的8核的配置。
题外话:从前端的角度,TypeScript确实弥补了JavaScript在类型上的问题,这也是很多大项目转向了TypeScript。