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

我在軟件架構方面最新的嘗試,是在 Rust 中使用盡量少的模板文件來搭建一個真實的 web 應用程序。在這篇文章中我將和你們分享個人發現,來回答實際上有多少網站在使用 Rust 這個問題。css

這篇文章提到的項目均可以在 GitHub 上找到。爲了提升項目的可維護性,我將前端(客戶端)和後端(服務端)放在了一個倉庫中。這就須要 Cargo 爲整個項目去分別編譯有着不一樣依賴關係的前端和後端二進制文件。html

請注意,目前這個項目正在快速迭代中能夠在 rev1 這個分支上找到全部相關的代碼。你能夠點擊此處閱讀這個本系列博客的第二部分。前端

這個應用是一個簡單的身份驗證示範,它容許你選一個用戶名和密碼(必須相同)來登陸,當它們不一樣就會失敗。驗證成功後,將一個 JSON Web Token (JWT) 同時保存在客戶端和服務端。一般服務端不須要存儲 token,可是出於演示的目的,咱們仍是存儲了。舉個栗子,這個 token 能夠被用來追蹤實際登陸的用戶數量。整個項目能夠經過一個 Config.toml 文件來配置,好比去設置數據庫鏈接憑證,或者服務器的 host 和 port。node

[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 文件android

前端 —— 客戶端

我決定使用 yew 來搭建應用程序的客戶端。Yew 是一個現代的 Rust 應用框架,受到 Elm、Angular 和 ReactJS 的啓發,使用 WebAssembly(Wasm) 來建立多線程的前端應用。該項目正處於高度活躍發展階段,並無發佈那麼多穩定版。ios

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

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

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

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

當啓動 web 應用程序的前端部分的時候(在個人項目中用 make frontend), cargo-web 將應用編譯成 Wasm,而且將其與靜態資源打包到一塊兒。而後 cargo-web 啓動一個本地 web 服務器,方便應用程序進行開發。sql

> 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 中。成功驗證身份後路由到 內容組件

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

內容組件
  • 路由組件: 保存包含內容的組件之間的全部可能路由。一樣包含應用的一個初始的 「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 服務,那麼咱們就得去追蹤多個套接字鏈接。

出於速度和緊湊的考量。我決定使用一個二進制協議 —— 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 刷新請求)。全部須要的協議實現都包含在協議服務中,這使得它在整個前端中能夠被輕鬆複用。

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 容器實例中的後端服務以外。

我決定使用 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 鏡像能夠很輕鬆地部署應用程序。

Makefile 命令 make deploy 建立一個名爲 webapp 的 Docker 鏡像,其中包含靜態連接(staticlly linked)的後端可執行文件、當前的 Config.toml、TLS 證書和前端的靜態資源。在 Rust 中構建一個徹底的靜態連接的可執行文件是經過修改的 rust-musl-builder 鏡像變體實現的。生成的 webapp 可使用 make run 進行測試,這個命令能夠啓動容器和主機網絡。PostgreSQL 容器如今應該並行運行。總的來講,總體部署不該該是這個工程的重要部分,應該足夠靈活來適應未來的變更。

總結

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

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

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

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

後端部分我傾向於說「是」。由於 Rust 有包含很是成熟的 HTTP 技術棧的各類各樣的框架,相似 actix-web。用於快速構建 API 和後端服務。

前端部分的話,因爲 WebAssembly 的炒做,目前還有不少正在進行中的工做。可是項目須要和後端具備相同的成熟度,特別是在穩定的 API 和測試的可行性方面。因此前端應該是「不」。可是咱們依然在正確的方向。

很是感謝你能讀到這裏。 ❤

我將繼續完善個人示例程序,來不斷探索 Rust 和 Web 應用的鏈接點。持續 rusting!

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


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

相關文章
相關標籤/搜索