Skip to content

在 2020 年用 Rust 写前端什么体验

Rust 语言是一门有趣的语言,在学习 Rust 后我想找点东西实践下,然后就发现了由 Rust 编写可以用 Rust 编写网页的 Yew 框架。由于对 Rust 相关工具链的不熟悉,我感觉自己回到了刚刚接触 React + Webpack 的时候,一脸懵逼,什么都没有头绪的样子。那个时候,我写了个 Todo 应用来帮助自己熟悉工具链,现在当然是继续重复作为菜鸟时做的事情,写一个 Todo 应用来熟悉工具链!

介绍

什么是 Yew?

Yew 是一个设计先进的 Rust 框架,目的是使用 WebAssembly 来创建多线程的前端 web 应用。
  • 基于组件的框架,可以轻松的创建交互式 UI。拥有 React 或 Elm等框架经验的开发人员在使用 Yew 时会感到得心应手。
  • 高性能 ,前端开发者可以轻易的将工作分流至后端来减少 DOM API 的调用,从而达到异常出色的性能。
  • 支持与 JavaScript 交互 ,允许开发者使用 NPM 包,并与现有的 JavaScript 应用程序结合。

应用外观

 
我们先来看下应用外观,为了能专注于 使用 Rust 写前端 这一目的,我直接复用了了《 I created the exact same app in React and Vue. Here are the differences 》 的样式代码
 
 
notion image
 

目录结构

 
├── Cargo.lock ├── Cargo.toml ├── README.md ├── docs // 编译后后的文件 | ├── README.md | ├── assets | | ├── rust.svg | | ├── style // 应用 css | | └── yew.svg | ├── index.html | ├── package.json // 编译产物 | ├── wasm.d.ts // 编译产物 | ├── wasm.js // 编译产物 | ├── wasm_bg.wasm // 编译产物 | └── wasm_bg.wasm.d.ts // 编译产物 ├── src // 应用代码 | ├── app.rs // 应用入口 | ├── components // 组件 | | ├── mod.rs | | ├── todo | | | └── mod.rs // Todo 组件 | | └── todo_item | | └── mod.rs // TodoItem 组件 | ├── lib.rs | ├── model.rs // 类型存放处 | └── utils.rs // 一些工具函数 └── tests └── web.rs

一个 Yew 组件

notion image
这就是一个 Yew 组件的样子,我们类比着讲述下现代前端框架的基础概念对应到 Yew 上应该是什么样子

State 存储在哪里?

 
pub struct Todo { link: ComponentLink<Self>, list: Vec<model::TodoItem>, input: String, show_error: bool, } impl Component for TodoItem { // ... codes }
 
你可以近似的认为这声明了 TodoItem 类,带有 link,list,input,show_error 属性,但它还不是一个 Yew 组件!因为它还没有 " ... extends React.Component ",必须加上下方的 impl Component for TodoItem 才能算一个组件
 
如果要类比成 React 组件,就是这样
 
class Todo extends React.Component { constructor(props) { super(props) this.state = { list: [], input: '', show_error: false, }; } }
 
一个 Yew 的组件的 state 存储在自身,或者近似的认为直接挂载 this 上,而非 this.state 上。
 
  • .link 属性存储着 ComponentLink ,它是组件和 Yew 沟通的桥梁,我们需要它来触发组件的重渲染,其作用和 React 的 this.setState() 很相似
  • .list 存储着我们需要渲染出来的 todo 列表的数据,它的类型可以近似的认为是 JavaScript 中的 Array
  • .input 存储着我们在 <input /> 框中的输入的内容
  • .show_error 用来控制是否显示错误信息
 

如何接受父组件的数据

#[derive(Properties, Clone)] pub struct TodoItemProps { pub item: model::TodoItem, pub delete: Callback<i32>, } pub struct TodoItem { link: ComponentLink<Self>, props: TodoItemProps, } impl Component for TodoItem {}
 
和 React 一样,Yew 也是单向数据流。通过声明一个新的 Props Struct 类型,并将其赋予到组件上,你就可以通过 .props 访问父组件传递来的数据。上述代码的 Props 声明意味着 TodoItem 这个组件接受 TodoItem 类型的数据和一个回调函数。
 
并且,由于 Rust 是 强类型 语言,因此在编译期就会检查你传递数据的类型是否 吻合 ,如果不吻合就无法通过编译,提前发现错误。

render 函数

impl Component for TodoItem { // ... codes fn view(&self) -> Html { // render 函数 html! { <div class="ToDoItem"> <p class="ToDoItem-Text">{&self.props.item.text}</p> <button onclick={self.link.callback(|_| Msg::OnClick)} class="ToDoItem-Delete" > { "-" } </button> </div> } } // ... codes }
 
对应着 React 中 render() 概念的是 view() 方法,这里最值得一提的是你看到的上述代码是 完全符合 Rust 语法规则 的!Rust 拥有强大的宏机制,可以在编译期动态的生成代码。通过利用宏,可以很容易在 Rust 中实现 DSL,而不需要 babel 这样的转译工具

如何更新组件状态

pub enum Msg { UpdateInput(String), AddTodoItem, DeleteItem(i32), None, }
 
Msg 是一个枚举类型,起到的作用和 Redux 的 Action 很相似,你可以近似的认为上述代码等于以下代码
 
const updateInputAction = { type: 'UpdateInput', payload: str, }; const addTodoItem = { type: 'AddTodoItem', }; const deleteItem = { type: 'DeleteItem', payload: id, };
 
接受 Msg 的是 update() 方法,这个方法很像 shouldComponentUpdate()reducer() 的结合体,我们在 update() 中进行副作用
 
impl Component for Todo { type Message = Msg; // ... codes fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { Msg::UpdateInput(input) => { self.input = input; true }, Msg::AddTodoItem => { if self.input.trim().len() == 0 { self.show_error = true; } else { self.list.push(model::TodoItem { id: self.list.len() as i32, text: self.input.clone(), }); } self.input = String::new(); true }, Msg::DeleteItem(id) => { self.list = self.list.clone().into_iter().filter(|item| item.id != id).collect(); true } _ => true } } // ... codes }
 
我们在 <button onclick=self.link.callback(|_| Msg::AddTodoItem) /> 上绑定了 onclick 事件处理的函数,当按钮被点击时,处理函数的返回值会 Msg::AddTodoItem 被发送到 update() 方法,我们根据传入的 Msg 类型来修改自身状态或调用父组件传递的回调函数,返回布尔值来告诉 Yew 是否需要重新渲
如果返回 true , Yew 会去重新执行 view() 函数,因为我们已经修改了自身状态,所以此时 view() 会根据新的状态返回相应的虚拟 DOM 树,就完成数据驱动视图的闭环
Rust 的枚举类型非常富有表现力,再配合上其强大模式匹配功能,相当于你获得了一个绝对类型安全的 Redux。这里顺便说下,通过配合 Typescript ,Redux 也能做到类型安全,但是其写法比较复杂,需要做 类型体操 :)。

父子间如何通信

#[derive(Properties, Clone)] pub struct TodoItemProps { pub item: model::TodoItem, pub delete: Callback<i32>, } impl Component for TodoItem { // ... codes fn update(&mut self, msg: Self::Message) -> ShouldRender { // 单单针对 state 变化的 shouldComponentUpdate // 同时起到一个局部 reducer 的作用 match msg { Msg::OnClick => { let id = self.props.item.id.clone(); self.props.delete.emit(id); // 触发回调 return false; }, } true } // ... codes }
 
和 React 很相似,父子间的通信也是通过回调函数进行的。 <Todo /><TodoItem delete={self.link.callback(|id: i32| Msg::DeleteItem(id))} item={item} />delete 属性上设置了一个闭包函数,当这个函数被子组件执行的时候其返回值 Msg::DeleteItem(id) 会发送到 todo 的 update() 函数,进而更新自身状态,完成通信

问题

Yew 对于 CSS 文件的引入还没有很好的解决方法,因为缺少类似 Webpack 的打包工具,你不能像写 JavaScript 一样,不能简单的通过一句 import 'index.css' 解决。这个项目中的组件 CSS 文件是我在 index.html 文件中手动引入的,我觉得这对于组件化开发是不可接受的
而其他的,诸如异步组件,tree shaking 等现代前端已经习以为常的东西就更是缺少了,这导致 Yew 暂时只能停留在玩具级别,没法上生产环境。不过未来还是值得畅想的,特别是在面向 wasm 的 DOM API 出来后

最后

你可能会很疑惑用 Rust 写网页的意义在哪里,当然是因为可以。