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 》 的样式代码
- 应用演示地址: iheyunfei.github.io/yew-todo-de…
- Github 地址: github.com/iheyunfei/y…
目录结构
├── 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 组件
这就是一个 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 {
// ... codes
}
和 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 也能做到类型安全,但是其写法比较复杂,需要做 类型体操 :),有兴趣的同学可以看看我写的一篇 《如何利用 Typescript 的类型编程自动推断 Redux reducer 的类型》
父子间如何通信
#[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 写网页的意义在哪里,我想了好多理由,但最后觉得还是这句话最有说服力。
"因为山就在那里!"