内容简介:我在软件架构方面最新的尝试,是在 Rust 中使用尽可能少的模板文件来搭建一个真实的 web 应用程序。在这篇文章中我将和大家分享我的发现,来回答实际上有多少网站在使用 Rust 这个问题。这篇文章提到的项目请注意,目前这个项目正在快速迭代中可以在
我在软件架构方面最新的尝试,是在 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
我决定使用最后一个,需要一个 nightly Rust 编译器,事实上,演示 Rust 原生的 Wasm 可能是最好的。
WebAssembly 目前是 Rust 最热门 :fire: 的话题之一。关于编译 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 中。成功验证身份后路由到 内容组件 。
- 内容组件 : 根组件的 的另一个子组件,包括一个主页面内容(目前只有一个头部和一个登出按钮)。它可以通过 根组件 访问(如果有效的 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,作为应用数据通信层(而不是JSON、MessagePack 或者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 : 一个小而务实且极其快速的 Rustactor 框架。它支持所有需要的技术,比如 WebSockets、TLS 和HTTP/2.0. Actix-web 支持不同的处理程序和资源,但在示例程序中只用到了两个主要的路由:
**/ws** **/**
默认情况下,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 编译器。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Android里应用程序,应用程序窗口和视图对象之间的关系
- 这几天在C程序中有哪些应用程序?
- 在iOS应用程序中登录系统使用的标准程序是什么?
- 使用 Bluemix、Watson Discovery 和 Cloudant 构建移动应用程序来分析其他应用程序
- 用Visual Studio的.NET程序员开发dapp应用程序
- 程序员疯狂记事:如何利用众多技术栈构建一个 Web 应用程序?!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Base64 编码/解码
Base64 编码/解码
HEX HSV 转换工具
HEX HSV 互换工具