Rust中的Web应用程序开发

该材料的作者(我们今天将发表其翻译)说,他在软件项目体系结构领域的最新实验是仅使用Rust语言并以最少的模板代码使用量创建一个可运行的Web应用程序。 在本文中,他希望与读者分享他通过开发应用程序并回答Rust是否准备在Web开发的各个领域中使用它的问题而发现的发现。



项目概况


可以在GitHub上找到将在此处讨论的项目代码。 应用程序的客户端和服务器部分位于同一存储库中,这样做是为了简化项目的维护。 应该注意的是,Cargo将需要编译具有不同依赖关系的前端和后端应用程序。 在这里,您可以查看正在运行的应用程序。

我们的项目是认证机制的简单演示。 它允许您使用所选的用户名和密码登录(必须相同)。

如果用户名和密码不同,则身份验证将失败。 成功认证后, JWT令牌(JSON Web令牌)既存储在客户端,又存储在服务器端。 通常不需要在此类应用程序中将令牌存储在服务器上,但是我只是出于演示目的。 例如,这可用于查找已登录的用户数。 可以使用单个Config.toml文件配置整个应用程序,例如,指定用于访问数据库的凭据或服务器的地址和端口号。 这是我们应用程序中该文件的标准代码。

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

应用程序客户端开发


为了开发应用程序的客户端,我决定使用yew 。 这是受Elm,Angular和React启发的现代Rust框架。 它旨在使用WebAssembly (Wasm)创建多线程Web应用程序的客户端部分。 当前,该项目正在积极开发中,但没有太多稳定版本。

yew框架依赖于cargo-web工具,该工具旨在在Wasm中交叉编译代码。

cargo-web工具是直接的yew依赖,可简化Wasm中Rust代码的交叉编译。 通过此工具可以达到以下三个主要的编译Wasm目标:

  • asmjs-unknown-emscripten通过Emscripten使用asmjs-unknown-emscripten
  • wasm32-unknown-emscripten通过Emscripten使用WebAssembly
  • wasm32-unknown-unknown将WebAssembly与本机Rust后端一起用于WebAssembly


网络组装

我决定使用最后一个选项,该选项要求使用Rust编译器的“夜间”程序集,但最大程度地展示了Rust的本机Wasm功能。
如果我们谈论WebAssembly,那么今天谈论Rust是最热门的话题。 正在Wasm中交叉编译Rust并将其集成到Node.js生态系统中(使用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"! If you need to serve any extra files put them in the 'static' directory in the root of your crate; they will be served alongside your application. You can also put a 'static' directory in your 'src' directory. Your application is being served at '/app.js'. It will be automatically rebuilt if you make any changes in your code. You can access the web server at `http://0.0.0.0:8000`. 

yew框架具有一些非常有趣的功能。 其中包括对可重用组件体系结构的支持。 此功能将我的应用程序细分为三个主要组件:

RootComponent 。 该组件直接安装在网站的<body>标签上。 他决定接下来应加载哪个子组件。 如果在页面的第一个入口找到了JWT令牌,它将尝试通过与应用程序的服务器部分联系来更新此令牌。 如果失败,则LoginComponentLoginComponent组件的转换。

LoginComponent 。 此组件是RootComponent组件的后代;它包含带有用于输入凭据的字段的表单。 此外,它与应用程序的后端进行交互,以基于用户名和密码的验证来组织简单的身份验证方案,并且,如果身份验证成功,则将JWT保存在cookie中。 另外,如果用户能够进行身份验证,则他将过渡到ContentComponent组件。


LoginComponent组件的外观

内容组件 该组件是RootComponent组件的另一个后代。 它包含在应用程序主页上显示的内容(目前只是标题和退出系统的按钮)。 可以通过RootComponent (如果应用程序在启动时设法找到了有效的会话令牌)来访问它,也可以通过LoginComponent (在成功通过身份验证的情况下)获得访问权限。 当用户单击注销按钮时,此组件与后端交换数据。


ContentComponent组件

路由器组件 该组件存储包含内容的组件之间的所有可能路由。 此外,它还包含应用程序loadingerror的初始状态。 它直接连接到RootComponent

现在将讨论的以下yew关键概念之一是服务。 它们使您可以在不同的组件中重用相同的逻辑。 假设这些可以是记录界面或支持cookie使用的工具。 服务不存储某些全局状态;它们是在组件初始化时创建的。 除了服务, yew支持代理的概念。 它们可用于组织各个组件之间的数据共享,以维护应用程序的一般状态,例如负责路由的代理所需的状态。 为了组织覆盖所有组件的应用程序的路由系统,我们在此处实现了我们自己的代理和路由服务yew没有标准的路由器,但是在框架存储库中,您可以找到一个支持各种URL操作的路由器实现示例

我很高兴地注意到yew使用Web Workers API在各种线程上运行代理,并使用附加到该线程的本地调度程序来解决并行任务。 这样就可以在Rust上开发具有高度多线程的浏览器应用程序。

每个组件都实现了自己的Renderable特性 ,这使我们可以使用html!{} Macro将HTML代码直接包含在Rust源代码中。

这是一个很棒的功能,当然,其正确使用由编译器控制。 这是LoginComponent组件中的Renderable实现Renderable

 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>       }   } } 

前端和后端之间的连接基于每个客户端使用的WebSocket连接。 WebSocket技术的优势在于它适合传输二进制消息,并且服务器可以在必要时将推送通知发送给客户端。 yew有一个标准的WebSocket服务,但我决定创建自己的版本用于演示,主要是因为直接在服务内部对连接进行了“惰性”初始化。 如果要在组件初始化期间创建WebSocket服务,则必须监视许多连接。


协议Cap'n协议

我决定使用Cap'n Proto协议(而不是诸如JSONMessagePackCBOR之类的协议)作为传输应用程序数据的层,这是出于速度和紧凑性的考虑。 值得注意的是,我没有使用Cap'n Proto中可用的RPC协议接口,因为它的Rust实现不是为WebAssembly编译的(由于toxio-rs Unix依赖性)。 正确类型的请求和响应的选择有些复杂,但是可以使用结构良好的API解决此问题。 这是该应用程序的Cap'n Proto协议声明。

 @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;       }   } } 

您可以看到这里有两个不同版本的登录请求。

一个用于LoginComponent (在这里获取令牌,使用名称和密码),另一个用于RootComponent (用于更新现有令牌)。 协议工作所需的一切都打包在协议服务中,这使得在前端的各个部分中重用相应的功能很方便。


UIkit-一个紧凑的模块化前端框架,用于开发快速而强大的Web界面

该应用程序客户端部分的用户界面基于UIkit框架,其3.0.0版将在不久的将来发布。 专门准备的build.rs脚本会自动下载所有必需的UIkit依赖项并编译生成的样式表。 这意味着您可以将自己的样式添加到单个style.scss文件中,该文件可以应用于整个应用程序。 非常方便。

front测试前端


我认为测试我们的解决方案存在一些问题。 事实是,测试单个服务非常简单,但是yew没有为开发人员提供测试组件和代理的便捷方法。 现在,在纯Rust框架中,前端的集成和端到端测试不可用。 在这里,您可以使用CypressProtractor之类的项目,但采用这种方法,您将不得不在项目中包含很多模板JavaScript / TypeScript代码,因此我决定放弃此类测试的实现。

顺便说一下,这是一个新项目的想法:一个用Rust编写的端到端测试框架。

应用服务器端开发


为了实现应用程序的服务器端,我选择了actix-web框架。 这是一个紧凑,实用且快速的基于Rust的actor模型框架。 它支持所有必要的技术,例如WebSockets,TLS和HTTP / 2.0 。 该框架支持各种处理程序和资源,但是在我们的应用程序中,仅使用了两条主要路线:

  • /ws是WebSocket通信的主要资源。
  • / -可以访问静态前端应用程序的主处理程序。

默认情况下, actix-web以与本地计算机上可用处理器核心数量相对应的数量启动工作流。 这意味着如果应用程序具有状态,则必须在所有线程之间安全地共享它,但是由于可靠的Rust并行计算模板,这不是问题。 尽管如此,后端应该是一个无状态系统,因为它的许多副本可以在云环境(如Kubernetes )中并行部署。 结果,形成应用程序状态的数据必须与后端分开。 例如,它们可能在Docker容器的单独实例中。


PostgreSQL DBMS和Diesel项目

作为主要数据仓库,我决定使用PostgreSQL DBMS。 怎么了 这一选择确定了一个出色的Diesel项目的存在,该项目已经支持PostgreSQL,并为其提供了安全和可扩展的ORM系统以及查询构建工具。 所有这些完全符合我们项目的需求,因为actix-web已经支持Diesel。 结果,在这里,要使用有关数据库中会话的信息来执行CRUD操作,可以使用一种考虑了Rust的特殊性的特殊语言。 这是基于Diesel.rs的actix-web的示例UpdateSession处理程序。

 impl Handler<UpdateSession> for DatabaseExecutor {   type Result = Result<Session, Error>;   fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result {       //         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())   } } 

r2d2项目用于在actix-web和Diesel之间建立连接。 这意味着我们(除了具有工作流程的应用程序之外)具有应用程序的共享状态,该状态将许多数据库连接作为一个连接池支持。 这大大简化了后端的扩展,使该解决方案变得灵活。 在这里,您可以找到负责创建服务器实例的代码。

▍测试后端


通过启动测试服务器实例并连接到已经运行的数据库,可以执行我们项目中的后端集成测试 。 然后,您可以使用标准的WebSocket客户端(我使用钨合金 )将使用Cap'n Proto协议生成的数据发送到服务器,并将结果与​​预期结果进行比较。 该测试设置已被证明是出色的。 我没有使用actix-web专用测试服务器 ,因为配置和运行真实服务器不需要做很多工作。 预期,后端单元测试是一项相当简单的任务;进行此类测试不会引起任何问题。

项目部署


该应用程序非常容易使用Docker映像进行部署。


码头工人

使用make deploy您可以创建一个名为webapp的映像,其中包含静态链接的后端可执行文件,当前的Config.toml文件,TLS证书和静态前端内容。 使用rust-musl-builder Docker映像的修改版本,实现Rust中完全静态链接的可执行程序的汇编。 可以使用make run命令测试完成的Web应用程序,该命令将启动启用网络的容器。 PostgreSQL容器必须与应用程序容器并行运行,以确保系统正常工作。 通常,部署我们的系统的过程非常简单,此外,由于这里使用的技术,我们可以谈论其足够的灵活性,简化其对开发中的应用程序的适应性。

项目开发中使用的技术


这是应用程序依赖关系图。


用于在Rust中开发Web应用程序的技术

前端和后端使用的唯一组件是Cap'n Proto的Rust版本,该版本需要在本地安装的Cap'n Proto编译器才能创建。

结果。 Rust已准备好进行Web生产吗?


这是一个大问题。 这是我能回答的。 从服务器的角度来看,我倾向于回答“是”,因为Rust生态系统除了actix-web ,还具有非常成熟的HTTP堆栈以及用于快速开发服务器API和服务的许多不同框架

如果我们谈论前端,那么,由于对WebAssembly的普遍关注,现在正在进行许多工作。 但是,在此区域中创建的项目必须达到与服务器项目相同的成熟度。 对于API稳定性和测试功能尤其如此。 因此,现在我对在前端使用Rust表示“不”,但我不得不指出它正在朝着正确的方向发展。

亲爱的读者们! 您是否在Web开发中使用Rust?

Source: https://habr.com/ru/post/zh-CN416933/


All Articles