[譯] Rust 開發完整的 Web 應用程式

0x7e2發表於2018-10-29

我在軟體架構方面最新的嘗試,是在 Rust 中使用盡可能少的模板檔案來搭建一個真實的 web 應用程式。在這篇文章中我將和大家分享我的發現,來回答實際上有多少網站在使用 Rust 這個問題。

這篇文章提到的專案都可以在 GitHub 上找到。為了提高專案的可維護性,我將前端(客戶端)和後端(服務端)放在了一個倉庫中。這就需要 Cargo 為整個專案去分別編譯有著不同依賴關係的前端和後端二進位制檔案。

請注意,目前這個專案正在快速迭代中可以在 rev1 這個分支上找到所有相關的程式碼。你可以點選此處閱讀這個本系列部落格的第二部分。

這個應用是一個簡單的身份驗證示範,它允許你選一個使用者名稱和密碼(必須相同)來登入,當它們不同就會失敗。驗證成功後,將一個 JSON Web Token (JWT) 同時儲存在客戶端和服務端。通常服務端不需要儲存 token,但是出於演示的目的,我們還是儲存了。舉個栗子,這個 token 可以被用來追蹤實際登入的使用者數量。整個專案可以通過一個 Config.toml 檔案來配置,比如去設定資料庫連線憑證,或者伺服器的 host 和 port。

[server]
ip = "127.0.0.1"
port = "30080"
tls = false

[log]
actix_web = "debug"
webapp = "trace"

[postgres]
host = "127.0.0.1"
username = "username"
password = "password"
database = "database"
複製程式碼

webapp 預設的 Config.toml 檔案

前端 —— 客戶端

我決定使用 yew 來搭建應用程式的客戶端。Yew 是一個現代的 Rust 應用框架,受到 Elm、Angular 和 ReactJS 的啟發,使用 WebAssembly(Wasm) 來建立多執行緒的前端應用。該專案正處於高度活躍發展階段,並沒有釋出那麼多穩定版。

cargo-web 工具是 yew 的直接依賴之一,能直接交叉編譯出 Wasm。實際上,在 Rust 編譯器中使用 Wasm 有三大主要目標:

  • _asmjs-unknown-emscripten _— 通過 Emscripten 使用 asm.js
  • wasm32-unknown-emscripten — 通過 Emscripten 使用 WebAssembly
  • _wasm32-unknown-unknown _— 使用帶有 Rust 原生 WebAssembly 後端的 WebAssembly

[譯] Rust 開發完整的 Web 應用程式

我決定使用最後一個,需要一個 nightly Rust 編譯器,事實上,演示 Rust 原生的 Wasm 可能是最好的。

WebAssembly 目前是 Rust 最熱門 ? 的話題之一。關於編譯 Rust 成為 Wasm 並將其整合到 nodejs(npm 打包),世界上有很多開發者為這項技術努力著。我決定採用直接的方式,不引入任何 JavaScript 依賴。

當啟動 web 應用程式的前端部分的時候(在我的專案中用 make frontend), cargo-web 將應用編譯成 Wasm,並且將其與靜態資源打包到一起。然後 cargo-web 啟動一個本地 web 伺服器,方便應用程式進行開發。

> make frontend
   Compiling webapp v0.3.0 (file:///home/sascha/webapp.rs)
    Finished release [optimized] target(s) in 11.86s
    Garbage collecting "app.wasm"...
    Processing "app.wasm"...
    Finished processing of "app.wasm"!

如果需要對任何其他檔案啟動服務,將其放入專案根目錄下的 'static' 目錄;然後它們將和你的應用程式一起提供給使用者。
同樣可以把靜態資源目錄放到 ‘src’ 目錄中。
你的應用通過 '/app.js' 啟動,如果有任何程式碼上的變動,都會觸發自動重建。
你可以通過 `http://0.0.0.0:8000` 訪問 web 伺服器
複製程式碼

Yew 有些很好用的功能,就像可複用的元件架構,可以很輕鬆的將我的應用程式分為三個主要的元件:

  • 根元件: 直接掛載在網頁的 <body> 標籤,決定接下來載入哪一個子元件。如果在進入頁面的時候發現了 JWT,那麼將嘗試和後端通訊來更新這個 token,如果更新失敗,則路由到 登入元件
  • 登入元件: 根元件 的一個子元件包含登入表單欄位。它同樣和後端進行基本的使用者名稱和密碼的身份驗證,並在成功後將 JWT 儲存到 cookie 中。成功驗證身份後路由到 內容元件

[譯] Rust 開發完整的 Web 應用程式

登入元件
  • 內容元件: 根元件的 的另一個子元件,包括一個主頁面內容(目前只有一個頭部和一個登出按鈕)。它可以通過 根元件 訪問(如果有效的 session token 已經可用)或者通過 登入元件 (成功認證)訪問。當使用者按下登出按鈕後,這個元件將會和後端進行通訊。

[譯] Rust 開發完整的 Web 應用程式

內容元件
  • 路由元件: 儲存包含內容的元件之間的所有可能路由。同樣包含應用的一個初始的 “loading” 狀態和一個 “error” 狀態,並直接附加到 根元件 上。

服務是 yew 的下一個關鍵概念之一。它允許元件間重用相同的邏輯,比如日誌記錄或者 cookie 處理。在元件的服務是無狀態的,並且服務會在元件初始化的時候被建立。除了服務, yew 還包含了代理(Agent)的概念。代理可以用來在元件間共享資料,提供一個全域性的應用狀態,就像路由代理所需要的那樣。為了在所有的元件之間完成示例程式的路由,實現了一套自定義的路由代理和服務。Yew 實際上沒有獨立的路由,但他們的示例提供了一個支援所有型別 URL 修改的參考實現。

太讓人驚訝了,yew 使用 Web Workers API 在獨立的執行緒中生成代理,並使用附加到執行緒的本地的任務排程程式來執行併發任務。這使得使用 Rust 在瀏覽器中編寫高併發應用成為可能。

每個元件都實現了自己的 `Renderable` 特性,這讓我們可以直接通過 [html!{}](https://github.com/DenisKolodin/yew#jsx-like-templates-with-html-macro) 巨集在 rust 原始碼中包含 HTML。這非常棒,並且確保了使用編輯器內建的 borrow checker 進行檢查!

impl Renderable<LoginComponent> for LoginComponent {
    fn view(&self) -> Html<Self> {
        html! {
            <div class="uk-card uk-card-default uk-card-body uk-width-1-3@s uk-position-center",>
                <form onsubmit="return false",>
                    <fieldset class="uk-fieldset",>
                        <legend class="uk-legend",>{"Authentication"}</legend>
                        <div class="uk-margin",>
                            <input class="uk-input",
                                   placeholder="Username",
                                   value=&self.username,
                                   oninput=|e| Message::UpdateUsername(e.value), />
                        </div>
                        <div class="uk-margin",>
                            <input class="uk-input",
                                   type="password",
                                   placeholder="Password",
                                   value=&self.password,
                                   oninput=|e| Message::UpdatePassword(e.value), />
                        </div>
                        <button class="uk-button uk-button-default",
                                type="submit",
                                disabled=self.button_disabled,
                                onclick=|_| Message::LoginRequest,>{"Login"}</button>
                        <span class="uk-margin-small-left uk-text-warning uk-text-right",>
                            {&self.error}
                        </span>
                    </fieldset>
                </form>
            </div>
        }
    }
}
複製程式碼

登入元件 Renderable 的實現

每個客戶端從前端到後端的通訊(反之亦然)通過 WebSocket 連線來實現。WebSocket 的好處是可以使用二進位制資訊,並且如果需要的話,服務端同時可以向客戶端推送通知。Yew 已經發行了一個 WebSocket 服務,但我還是要為示例程式建立一個自定義的版本,主要是因為要在服務中的延遲初始化連線。如果在元件初始化的時候建立 WebSocket 服務,那麼我們就得去追蹤多個套接字連線。

[譯] Rust 開發完整的 Web 應用程式

出於速度和緊湊的考量。我決定使用一個二進位制協議 —— Cap’n Proto,作為應用資料通訊層(而不是JSONMessagePack 或者 CBOR這些)。值得一提的是,我沒有使用 Cap’n Proto 的RPC 介面協議,因為其 Rust 實現不能編譯成 WebAssembly(由於tokio-rs’ unix 依賴項)。這使得正確區分請求和響應型別稍有困難,但是結構清晰的 API 可以解決這個問題:

@0x998efb67a0d7453f;

struct Request {
    union {
        login :union {
            credentials :group {
                username @0 :Text;
                password @1 :Text;
            }
            token @2 :Text;
        }
        logout @3 :Text; # The session token
    }
}

struct Response {
    union {
        login :union {
            token @0 :Text;
            error @1 :Text;
        }
        logout: union {
            success @2 :Void;
            error @3 :Text;
        }
    }
}
複製程式碼

應用程式的 Cap’n Proto 協議定義

你可以看到我們這裡有兩個不同的登入請求變體:一個是 登入元件 (使用者名稱和密碼的憑證請求),另一個是 根元件 (已經存在的 token 重新整理請求)。所有需要的協議實現都包含在協議服務中,這使得它在整個前端中可以被輕鬆複用。

[譯] Rust 開發完整的 Web 應用程式

UIkit - 用於開發快速且功能強大的 Web 介面的輕量級模組化前端框架

前端的使用者介面由 UIkit 提供支援,其 3.0.0 版將在不久的將來發布。自定義的 build.rs 指令碼會自動下載 UIkit 所需要的全部依賴項並編譯整個樣式表。這就意味著我們可以在單獨的一個 style.scss 檔案中插入自定義的樣式,然後在應用程式中使用。安排!(PS: 原文是 Neat!

前端測試

在我的看來,測試可能會存在一些小問題。測試獨立的服務很容易,但是 yew 還沒有提供一個很優雅的方式去測試單個元件或者代理。目前在 Rust 內部也不可能對前端進行整合以及端到端測試。或許可以使用 Cypress 或者 Protractor 這類專案,但是這會引入太多的 JavaScript/TypeScript 樣板檔案,所以我跳過了這個選項。

但是呢,或許這是一個新專案的好起點:用 Rust 編寫一個端到端測試框架!你怎麼看?

後端 —— 服務端

我選擇的後端框架是 actix-web: 一個小而務實且極其快速的 Rust actor 框架。它支援所有需要的技術,比如 WebSockets、TLS 和 HTTP/2.0. Actix-web 支援不同的處理程式和資源,但在示例程式中只用到了兩個主要的路由:

  • **/ws**: 主要的 websocket 通訊資源。
  • **/**: 路由到靜態部署的前端應用的主程式處理控制程式碼(handler)

預設情況下,actix-web 會生成與本地計算機邏輯 CPU 數量一樣多的 works(譯者注: 翻譯參考了Actix中文文件中伺服器一節的多執行緒部分)。這就意味著必須線上程之間安全的共享可能的應用程式狀態,但這對於 Rust 無所畏懼的併發模式來說完全不是問題。儘管如此,整個後端應該是無狀態的,因為可能會在雲端(比如 Kubernetes)上並行部署多個副本。所以應用程式狀態應該在單個 Docker 容器例項中的後端服務之外。

[譯] Rust 開發完整的 Web 應用程式

我決定使用 PostgreSQL 作為主要的資料儲存。為什麼呢?因為令人敬畏的 Diesel 專案 已經支援 PostgreSQL,並且為它提供了一個安全、可擴充的物件關係對映(ORM)和查詢構建器(query builder)。這很棒,因為 actix-web 已經支援了 Diesel。這樣的話,就可以自定義慣用的 Rust 域特定語言來建立、讀取、更新或者刪除(CRUD)資料庫中的會話,如下所示:

impl Handler<UpdateSession> for DatabaseExecutor {
    type Result = Result<Session, Error>;

    fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result {
        // Update the session
        debug!("Updating session: {}", msg.old_id);
        update(sessions.filter(id.eq(&msg.old_id)))
            .set(id.eq(&msg.new_id))
            .get_result::<Session>(&self.0.get()?)
            .map_err(|_| ServerError::UpdateToken.into())
    }
}
複製程式碼

Diesel.rs 提供的 actix-web 的 UpdateSession 處理程式

至於 actix-web 和 Diesel 之間的連線的處理,使用 r2d2 專案。這就意味著我們(應用程式和它的 works)具有共享的應用程式狀態,該狀態將多個連線儲存到資料庫作為單個連線池。這使得整個後端非常靈活,很容易大規模擴充。這裡可以找到整個伺服器示例。

後端測試

後端的整合測試通過設定一個測試用例並連線到已經執行的資料庫來完成。然後可以使用標準的 WebSocket 客戶端(我使用 tungstenite)將與協議相關的 Cap'n Proto 資料傳送到伺服器並驗證預期結果。這很好用!我沒有用 actix-web 特定的測試伺服器,因為設定一個真正的伺服器並費不了多少事兒。後端其他部分的單元測試工作像預期一樣簡單,沒有任何棘手的陷阱。

部署

使用 Docker 映象可以很輕鬆地部署應用程式。

[譯] Rust 開發完整的 Web 應用程式

Makefile 命令 make deploy 建立一個名為 webapp 的 Docker 映象,其中包含靜態連結(staticlly linked)的後端可執行檔案、當前的 Config.toml、TLS 證照和前端的靜態資源。在 Rust 中構建一個完全的靜態連結的可執行檔案是通過修改的 rust-musl-builder 映象變體實現的。生成的 webapp 可以使用 make run 進行測試,這個命令可以啟動容器和主機網路。PostgreSQL 容器現在應該並行執行。總的來說,整體部署不應該是這個工程的重要部分,應該足夠靈活來適應將來的變動。

總結

總結一下,應用程式的基本依賴棧如下所示:

[譯] Rust 開發完整的 Web 應用程式

前端和後端之間唯一的共享元件是 Cap'n Proto 生成的 Rust 源,它需要本地安裝的 Cap’n Proto 編譯器。

那麼, 我們的 web 完成了嗎(用於生產環境)?

這是一個大問題,這是我的個人觀點:

後端部分我傾向於說“是”。因為 Rust 有包含非常成熟的 HTTP 技術棧的各種各樣的框架,類似 actix-web。用於快速構建 API 和後端服務。

前端部分的話,由於 WebAssembly 的炒作,目前還有很多正在進行中的工作。但是專案需要和後端具有相同的成熟度,特別是在穩定的 API 和測試的可行性方面。所以前端應該是“不”。但是我們依然在正確的方向。

[譯] Rust 開發完整的 Web 應用程式

非常感謝你能讀到這裡。 ❤

我將繼續完善我的示例程式,來不斷探索 Rust 和 Web 應用的連線點。持續 rusting!

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章