Iced 是一个 Rust 语言下的前端框架。目前而言,Rust 生态里面的 Diesel ORM 我觉得远不如 GORM,SeaORM 准备了解,但 Rocket 远超 Gin,不清楚 Iced 框架怎么样。不过 Iced 首先在能跨的平台种类上就不是很多,Web 构建非常不方便(理论上可以搞一个 CLI Tool),只能跨 Win / Linux / Mac. 但这玩意可是用来做 2D 游戏引擎的啊!应该会很强吧!
插入
在调试网络环境的时候遇到 curl 能通直接连通不了的问题,原因是 DNS 配置有误,以后可以参考。
和 Yew 对比
之前有考虑过使用 Yew 框架,但 Yew 很多地方还是 HTML 那套思路,看起来乱七八糟的。以计数器为例,先看看 Yew 的。我将注释出所有我认为需要注意的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| use gloo::console; use yew::{html, Component, Context, Html};
pub enum Msg { Increment, Decrement, }
pub struct App { value: i64, }
impl Component for App { type Message = Msg; type Properties = ();
fn create(_ctx: &Context<Self>) -> Self { Self { value: 0 } }
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::Increment => { self.value += 1; true } Msg::Decrement => { self.value -= 1; true } } }
fn view(&self, ctx: &Context<Self>) -> Html { html! { <div> <div class="panel"> <button class="button" onclick={ctx.link().callback(|_| Msg::Increment)}> { "+1" } </button> <button onclick={ctx.link().callback(|_| Msg::Decrement)}> { "-1" } </button> </div> <p class="counter"> { self.value } </p> </div> } } }
fn main() { yew::Renderer::<App>::new().render(); }
|
Iced 则精简不少:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| use iced::widget::{button, column, text}; use iced::{Alignment, Element, Sandbox, Settings};
pub fn main() -> iced::Result { Counter::run(Settings::default()) }
struct Counter { value: i32, }
#[derive(Debug, Clone, Copy)] enum Message { IncrementPressed, DecrementPressed, }
impl Sandbox for Counter { type Message = Message;
fn new() -> Self { Self { value: 0 } }
fn title(&self) -> String { String::from("Counter - Iced") }
fn update(&mut self, message: Message) { match message { Message::IncrementPressed => { self.value += 1; } Message::DecrementPressed => { self.value -= 1; } } }
fn view(&self) -> Element<Message> { column![ button("Increment").on_press(Message::IncrementPressed), text(self.value).size(50), button("Decrement").on_press(Message::DecrementPressed) ] .padding(20) .align_items(Alignment::Center) .into() } }
|
不过因为要限定 Message 的可能性,实际上写起来会比 Vue 在这方面内容多点,优势主要在于元件比较符合直觉。顺带一提这里返回 Element<Message>
实际上已经把 Element
视为 Monad!
中型实例分析:Game of Life
看看 Ices 的组件传参机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| pub fn new<F>(label: impl Into<String>, is_checked: bool, f: F) -> Self where F: 'a + Fn(bool) -> Message, { Checkbox { is_checked, on_toggle: Box::new(f), label: label.into(), width: Length::Shrink, size: Self::DEFAULT_SIZE, spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, icon: Icon { font: Renderer::ICON_FONT, code_point: Renderer::CHECKMARK_ICON, size: None, line_height: text::LineHeight::default(), shaping: text::Shaping::Basic, }, style: Default::default(), } }
|
注意这里的 new 纯粹只是一个数据结构,需要和 Application trait 里面有个强制带 flags 的 new 区别开。
使用 Message 有个很逆天的好处就是不需要像 React 一样把回调函数传给子组件,不需要像 Vue 混淆 getter 和 setter,也不需要单独的全局状态管理插件!我现在才发现!而且这样的话更符合我「产生什么实例就做什么事」的编程观,不过实际上是同构的。
正常的编程语言搞带参数的 enum 特别困难,所以实现这种 Message 制度有障碍。但在 Rust 里面,这个问题直接瞬间得到解决:
1 2 3 4 5 6 7 8 9 10 11
| #[derive(Debug, Clone)] enum Message { Grid(grid::Message, usize), Tick(Instant), TogglePlayback, ToggleGrid(bool), Next, Clear, SpeedChanged(f32), PresetPicked(Preset), }
|
Rust 作为我心目中最好的 C-like 语言,已经有一种 Idris2 平替的感觉了!
函数式的影子
看,甚至还有 Functor:
1 2 3 4 5 6
| let content = column![ self.grid .view() .map(move |message| Message::Grid(message, version)), controls, ];
|
注意 canvas::Program<Message>
trait 中的一个函数:
1 2 3 4 5 6 7 8 9
| fn update( &self, _state: &mut Self::State, _event: Event, _bounds: Rectangle, _cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { (event::Status::Ignored, None) }
|
注意它返回 Message,因此从 Canvas 的实例可以构造出一个 Element<Message>
:
1 2 3 4 5 6
| pub fn view(&self) -> Element<Message> { Canvas::new(self) .width(Length::Fill) .height(Length::Fill) .into() }
|
具体解释了前面提到的 Element 是 Monad. 有 Monad 就有回合制,这里回合的一方是前端设计顶层,只摆出有哪些组件(其返回什么信息什么是未知的),接收到组件返回的信息后再更新自己的状态,这里的 Monad 借助了组件状态来实现,实际上和正常的 Monad 是同构的,也更方便没有函数式编程基础的人理解;回合的另一方是底层组件,在收到具体的用户请求的时候再去生成 Message, 不需要管这个 Message 被如何处理。
相比之下,Vue、React 这种框架需要自己把 setter 往下传,就显得很小丑。私以为 Iced 这种才是合格的解耦。
Affine Types
在 Affine Types 的加持下,处理状态更新的时候我们可以更准确地对内存建模:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Message::Tick(_) | Message::Next => { self.queued_ticks = (self.queued_ticks + 1).min(self.speed);
if let Some(task) = self.grid.tick(self.queued_ticks) { if let Some(speed) = self.next_speed.take() { self.speed = speed; }
self.queued_ticks = 0;
let version = self.version;
return Command::perform(task, move |message| { Message::Grid(message, version) }); } }
|
注意我们通过 Command::perform 执行了一个异步函数 task,首先这个 task 因为是 Affine 的不会被疯狂执行;take 函数某种意义上只是个压行,不能体现出 Affine Types 的好处;perform 的 callback 里面有个 move 标签,可以以最高效的方式使用内存,虽然省下来一点用都没有,但写这种程序的时候可以有浓浓的正义感,说 Rust 是编程语言的「原神」一点都不为过。
结语
以后有机会分析 Canvas 的绘制机制和用户输入的处理机制,等到需要写自己后端了还可以学 SeaORM 和 GraphQL!
Rewrite Everything in Rust!