用 Rust 寫前端什麼體驗

Rust 語言是一門有趣的語言,在學習 Rust 後我想找點東西實踐下,然後就發現了由 Rust 編寫可以用 Rust 編寫網頁的 Yew 框架。由於對 Rust 相關工具鏈的不熟悉,我感覺自己回到了剛剛接觸 React + Webpack 的時候,一臉懵逼,什麼都沒有頭緒的樣子。那個時候,我寫了個 Todo 應用來幫助自己熟悉工具鏈,現在當然是繼續重複作爲菜鳥時做的事情,寫一個 Todo 應用來熟悉工具鏈!

介紹

什麼是 Yew?

Yew 是一個設計先進的 Rust 框架,目的是使用 WebAssembly 來創建多線程的前端 web 應用。

應用外觀

我們先來看下應用外觀,爲了能專注於 使用 Rust 寫前端 這一目的,我直接複用了了《 I created the exact same app in React and Vue. Here are the differences 》 的樣式代碼

目錄結構

├── 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 上。

如何接受父組件的數據

#[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