使用 Rust 和 WebAssembly 來進行 Web 開發

星川かえで
2021-10-03

直到一段時間以前,如果要進行 Web 開發,幾乎只有使用 JavaScript 這一個選項。但是隨著 WebAssembly 的發展,使用非 JavaScript 語言來進行 Web 開發似乎也變得越來越可行。本文就簡單介紹一下如何使用 Rust 和 WebAssembly 來進行 Web 開發。

什麼是 WebAssembly

WebAssembly

WebAssembly 是一種非常高效的,可跨平台運行的低階二進制形式。 它可以被瀏覽器非常高效的執行。Chrome,Safari 和 Firefox 都支援 WebAssembly。

關於 WebAssembly,可以參考這個網站: https://webassembly.org/

為什麼選用 Rust?

Rust

自己選用 Rust 有這麼幾個理由:

  1. 最終可執行檔案的大小:由於要構建網站,所有東西都需要從網路下載。 於是如何控制大小便成為了一個很重要的課題。如果選擇一個運行時較大的語言 ,就會導致網站的載入速度變慢。
  2. 速度:使用過 React 的人都知道,有時 React 再次渲染時由於 Component 過多,或是某些運算過於依賴 CPU,這時就會導致卡頓。(當然,可以優化。)所以,既然要使用 WebAssembly,就不如選取一個高效的語言。
  3. 成熟度:現今在可以使用 WebAssembly 的語言中,Rust 可以說是最成熟的。
  4. 安全:Rust 一直都以安全著稱。它可以在編譯時發現很多問題。

選用框架

Yew

現在如果使用 JavaScript 構建網站,通常不會從頭開始,而是選用諸如 React 或是 Vue 等的框架進行構建。使用 Rust 來構建也是一樣。在 Rust 社群中較成熟的框架有兩個:YewSeed

Yew 是採用類 React 架構,而 Seed 是參考的 Elm 架構。

由於自己平時使用 React 較多,所以自己選用的是 Yew。

0. 準備工作

  1. Rust + wasm32 編譯工具鏈

    請參考:https://rustup.rs/

    由於本篇並不是介紹如何使用 Rust,所以略過安裝 Rust 的部分。

    安裝完 Rust 後還需要使用下列命令來添加 wasm32 工具鏈:

    rustup target add wasm32-unknown-unknown
    
  2. Trunk

    在 macOS 下,可以使用下列命令進行安裝:

    brew install trunk
    

    其它平台請參考:https://trunkrs.dev

    利用 React 構建網站時,如果使用 Webpack Dev Server 就可以在開發時實時查看變更。即使使用 WebAssembly,雖然也可以使用 Webpack,但是還是需要一定 JavaScript 才能使用。那樣就大大減少使用 WebAssembly 的意義了。如果使用 Trunk,就可以在不使用任何 JavaScript 的情況下查看變更了。

1. 創建倉庫

要創建 Rust 倉庫,最簡單的方法是使用 Cargo。Cargo 是 Rust 的軟體包管理器,類似於 Pythonpip 或是 JavaScriptnpmyarn

在終端中輸入:

cargo new --bin first-yew-app

cargo add yew  # 需要 cargo-edit
# 你可以使用 cargo install cargo-edit 來安裝 cargo-edit。

這樣 cargo 就會創建一個叫做 first-yew-app 的檔案夾,並將 Rust 檔案放到裏面。

2. 創建 index.html

要使用 Trunk,在 first-yew-app 檔案夾裡面,還需要一個 index.html

index.html 裏面,不需要定義 <body /> 只需要 <head />

在此附上一個簡單的 index.html:

<!DOCTYPE HTML>
<html>
  <head>
    <title>First Yew App</title>
  </head>
</html>

關於更詳細的 index.html 的配置方法,請參考 Trunk 文檔。

3. 創建第一個 Component

和 React 一樣,Yew 也是基於 Component 的設計模式。Yew 目前只有 Struct Component(對應 React 的 Class Component),但是在將來的 0.19 版本中也將會引入 Function Component。

這裏,讓我們創建一個最簡單的 Component:

use yew::prelude::*;

pub struct App;

impl Component for App {
    type Message = ();
    type Properties = ();

    fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
        Self
    }

    fn update(&mut self, _msg: Self::Message) -> ShouldRender {
        false
    }

    fn change(&mut self, _props: Self::Properties) -> ShouldRender {
        false
    }

    fn view(&self) -> Html {
        html! {
            <div>{"Hello, World!"}</div>
        }
    }
}

在這裏附上一個 React 與 Yew Component 的語意轉換列表:

  • constructor() -> create()
  • render() -> view()
  • componentDidMount() -> rendered()
  • shouldComponentUpdate() -> update()
  • componentWillUpdate() -> change()
  • componentWillUnmount() -> destroy()

4. 渲染 App

將 main 函數替換為下列內容:

fn main() {
    yew::start_app::<App>();
}

然後在 first-yew-app 檔案夾下使用下列命令:

trunk serve --open

不出意外的話,你的瀏覽器將會自動打開一個寫著 Hello World! 的頁面。

5. 實現一個簡單的計數器

要對 Component 的內部狀態實現更新,需要使用 Message

Message 可以為任意枚舉類型。

首先定義一個簡單的 Message

#[derive(Debug)]
pub enum AppMsg {
    Increment,
}

然後將 App 變更為:

pub struct App {
    counter: u64,
    link: ComponentLink<Self>,
}

App 類型現在有兩個成員,分別是 counterlinkcounter 存儲有計數器變量。link 則是一個特殊類型 ComponentLink<Self>ComponentLink<Self> 是一個指向當前 ComponentComponentLinkComponentLink 可以用來將 Message 傳回 Component

實現部分也需要替換為下列內容:

impl Component for App {
    type Message = AppMsg;
    type Properties = ();

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self { counter: 0, link }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            AppMsg::Increment => {
                self.counter += 1;
            }
        }

        true
    }

    fn change(&mut self, _props: Self::Properties) -> ShouldRender {
        false
    }

    fn view(&self) -> Html {
        let increment = self.link.callback(|_| AppMsg::Increment);

        html! {
            <div>
                {format!("Current Counter: {}", self.counter)}
                <br />
                <button onclick=increment>{"Increment"}</button>
            </div>
        }
    }
}

首先,需要將關聯類型 Message 指定為 AppMsg,這樣 AppComponentLink 才可以接收 AppMsg。每次 ComponentLink 收到一個 Message 都會調用 update 方法,在 update 方法中可以對 App 的成員進行變更。如果返回 true 則表示需要重新渲染。在 view 中可以定義渲染內容,在這裏創建一個輸出計數器的 div 和遞增計數器用的按鈕。

使用 self.link.callback 可以創建一個用於 Event 的回調。將這個回調賦予給按鈕的 onclick 就可以觸發 update 方法中處理 AppMsg::Increment 的邏輯了。

6. 最終結果

完整代碼: https://github.com/futursolo/fl-www-examples/tree/master/first-yew-app

7. 使用 Rust 進行 Web 開發的限制

由於使用 Rust 進行 Web 開發還是處在起步的階段,生態系統還不成熟。 所以可能會遇到各種問題。比如:

  1. 沒有 SSR(伺服器端渲染)
  2. 沒有比較可用的類似 Styled Components 的 CSS 庫(所以自己造了一個
  3. 沒有 Function Component
  4. 沒有延遲加載(React.lazy

等等。

8. 寫在最後

其實使用 Rust 進行 Web 開發是在某種程度上可行的,但是由於當前 WebAssembly 和 Yew 的成熟度問題,在某些問題上還是需要一番探索。

讀到這裏,你也許已經猜到了,這個部落格也是使用 Rust + Yew + WebAssembly 寫成的。

大家也可以參考本站的原始碼:

https://github.com/futursolo/furtherland-www

評論

Coming Soon...