用 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 》 的樣式代碼
- 應用演示地址: https://iheyunfei.github.io/yew-todo-demo/
- Github 地址: https://github.com/iheyunfei/yew-todo-demo
目錄結構
├── 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
οnclick={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 寫網頁的意義在哪裏,我想了好多理由,但最後覺得還是這句話最有說服力。
"因爲山就在那裏!"
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://blog.csdn.net/yunfeihe233/article/details/108883963