Rust web开发 ActixWeb 入门

avatar
作者
筋斗云
阅读量:0

Rust Actix Web 入门

在这里插入图片描述

目前,Actix Web 仍然是 Rust Web 后端生态系统中极其强大的竞争对手。尽管之前的事件可能对其产生影响,但它仍然很强大,并且是 Rust 中最受推荐的 Web 框架之一。最初基于同名的 actor 框架 ( actix ),后来它已经消失,并且 actix 现在仅用于 websocket 端点。

本文将主要讨论 v4.4。

Actix Web 入门

首先,使用 cargo init example-api 生成项目,将 cd 到该文件夹中,然后使用以下命令将 actix-web crate 添加到项目中:

cargo add actix-web 

现在已经准备好开始了!如果想复制样板文件以直接编写您的应用程序,如下所示:

use actix_web::{web, App, HttpServer, Responder};  #[get("/")] async fn index() -> impl Responder {     "Hello world!" }  #[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new().service(             // prefixes all resources and routes attached to it...             web::scope("/")                 // ...so this handles requests for `GET /app/index.html`                 .route("/", web::get().to(index)),         )     })     .bind(("127.0.0.1", 8080))?     .run()     .await } 

Actix Web 中的路由

使用 Actix Web 时,大多数返回 实现actix_web::Responder trait 的函数都可以用作路由。请参阅下面的基本 Hello World 示例:

#[get("/")] async fn index() -> impl Responder {     "Hello world!" } 

然后可以将此处理函数输入到 actix_web::App 中,然后将其作为参数传递给 HttpServer

#[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new().service(             // prefixes all resources and routes attached to it...             web::scope("/")                 // ...so this handles requests for the base route                 .route("/index.html", web::get().to(index)),         )     })     .bind(("127.0.0.1", 8080))?     .run()     .await } 

现在,每当您访问 /index.html 时,它都应该返回“Hello world!”。但是,如果您想创建多个 子路由 类型,然后最后将它们全部合并到应用程序中,您可能会发现这种方法有点缺乏。在这种情况下,您将需要 ServiceConfig 类型,您也可以这样写:

use actix_web::{web, App, HttpResponse};  // this function could be located in different module fn config(cfg: &mut web::ServiceConfig) {     cfg.service(web::resource("/test")         .route(web::get().to(|| HttpResponse::Ok()))         .route(web::head().to(|| HttpResponse::MethodNotAllowed()))     ); }  #[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new().configure(config)     })     .bind(("127.0.0.1", 8080))?     .run()     .await } 

Actix Web 中的提取器(Extractor)正是这样的:实现类型安全的请求,当传递到处理函数时,将尝试从处理函数的请求中提取相关数据。例如, actix_web::web::Json 提取器将尝试从请求正文中提取 JSON。然而,要成功反序列化 JSON,您需要使用 serde 包 - 最好与 derive 函数一起使用,为您的结构添加自动反序列化和序列化派生宏。您可以通过执行以下命令来安装 serde:

cargo add serde -F derive 

现在可以将其用作派生宏,如下所示:

use actix_web::web; use serde::Deserialize;  #[derive(Deserialize)] struct Info {     username: String, }  // deserialize `Info` from request's body #[post("/submit")] async fn submit(info: web::Json<Info>) -> String {     format!("Welcome {}!", info.username) } 

Actix Web 还支持 paths, queries and forms。您还需要在此处使用 serde - 尽管对于paths,您还需要使用 Actix Web 路由宏来声明路径参数的确切位置。我们可以在下面找到所有这 3 种方法的示例:

#[derive(Deserialize)] struct Info {     username: String, }  // extract path info using serde #[get("/users/{username}")] // <- define path parameters async fn index(info: web::Path<Info>) -> String {     format!("Welcome {}!", info.username) }  // data is passed in here through query parameters in the URL // for example, google.com/?hello=world #[get("/")] async fn index(info: web::Query<Info>) -> String {     format!("Welcome {}!", info.username) }  // data is passed into the Form extractor through a HTML Form element #[post("/")] async fn index(form: web::Form<Info>) -> actix_web::Result<String> {     Ok(format!("Welcome {}!", form.username)) } 

有兴趣编写自己的提取器吗?你可以做到的!编写自己的提取器只需要实现 FromRequest 特征。查看 HTTP 标头提取器的这段代码,它向您展示了它的具体工作原理:

use actix_web::dev::Payload; use actix_web::{FromRequest,     http::header::Header as ParseHeader,     HttpRequest, error::ParseError };  #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Header<T>(pub T);  impl<T> FromRequest for Header<T> where     T: ParseHeader, {     type Error = ParseError;     type Future = Ready<Result<Self, Self::Error>>;      #[inline]     fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {         match T::parse(req) {             Ok(header) => ok(Header(header)),             Err(e) => err(e),         }     } } 

请注意, T: ParseHeader trait 约束 特定于此特征实现,因为为了使header有效,它需要能够成功解析为header,并在实现 actix_web::error::Error 。虽然我们也有 extract 作为提供的方法,但 from_request 是这里唯一需要实现的方法,它返回 Self::Future 。也就是说,我们需要返回一个 已准备好等待的结果(ready to be awaited) - 您可以在此处找到有关 Ready 结构的更多信息。其他提取器(例如 JSON 提取器)也允许您更改其配置 - 您可以在此文档页面中找到有关它的更多信息。

一般来说,responses只需要实现 actix_web::Responder trait就能够响应。尽管在实际响应类型方面已经有广泛的实现,因此您通常不需要实现自己的类型,但在某些特定的用例中这可能会很有帮助;例如,能够记录您的应用程序可能具有的所有类型的响应。

连接到数据库

通常,在设置数据库时,您可能需要设置数据库连接:

use sqlx::PgPoolOptions;  #[actix_web::main] async fn main() -> std::io::Result<()> {     let dbconnection = PgPoolOptions::new()         .max_connections(5)         .connect(r#"<db-connection-string-here>"#).await;      //... rest of your code } 

然后,您需要配置自己的 Postgres 实例,无论是本地安装在计算机上、通过 Docker 还是其他方式配置。但是,使用 Shuttle,我们可以消除这种情况,因为运行时会为您配置数据库:

use actix_web::{get, web::ServiceConfig}; use shuttle_actix_web::ShuttleActixWeb;  #[shuttle_runtime::main] async fn actixweb(     #[shuttle_shared_db::Postgres] pool: PgPool, ) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {     let state = AppState { pool };      // .. the rest of your code } 

在本地,这是通过 Docker 完成的,但在部署中,有一个总体流程可以为您完成此操作!不需要额外的工作。我们还有一个 AWS RDS 数据库产品,需要零 AWS 知识才能设置 - 请访问此处了解更多信息。

Actix Web 中的应用程序状态

路由 非常棒(连接数据库也非常容易!),但是当您需要 存储变量时,您可能想要寻找可以在应用程序中存储和使用它们的东西。这就是共享可变状态的用武之地:您在跨整个应用程序构建服务时声明它,然后您可以将它用作处理程序函数中的提取器。例如,您可能需要共享数据库池、计数器或 Websocket 订阅者的共享hashmap。您可以像这样声明和使用状态:

use sqlx::PgPool;  #[derive(Clone)] struct AppState {     db: PgPool }  #[get("/")] async fn index(data: web::Data<AppState>) -> String {     let res = sqlx::query("SELECT 'Hello World!'").fetch_all(&data.db).await.unwrap();      format!("{res}") } 

然后你可以像这样实现它:

#[actix_web::main] async fn main() -> std::io::Result<()> {     let db = connect_to_db();     let state = web::Data::new(AppState { db });      HttpServer::new(move || {         // move app state into the closure         App::new()             .app_data(state.clone()) // <- register the created data             .route("/", web::get().to(index))     })     .bind(("127.0.0.1", 8080))?     .run()     .await } 

Actix Web 中的中间件

在 Actix Web 中,中间件用作能够向(一组)路由添加通用功能的介入层,方法是在处理程序函数运行之前获取请求,执行一些操作,运行实际的处理程序函数本身,然后中间件进行额外的处理(如果需要)。默认情况下,Actix Web 有几个我们可以使用的默认中间件,包括日志记录、路径规范化、访问外部服务和修改应用程序状态(通过 ServiceRequest 类型)。

请参阅下面的示例,了解如何实现默认 Logger 中间件:

use actix_web::{middleware::Logger, App};  #[actix_web::main] async fn main() -> std::io::Result<()> {     // access logs are printed with the INFO level so ensure it is enabled by default     env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));      let app = App::new()         .wrap(Logger::default());      // ... rest of your code } 

此外,您还可以在 Actix Web 中编写自己的中间件!对于许多用例,我们可以使用 crate actix-web-lab 中方便的 middleware::from_fn 帮助器(在即将发布的版本中将提升为 actix-web 本身)。例如,在请求处理流程的不同部分打印消息,如下所示:

use actix_web::{body::MessageBody, dev::{ServiceRequest, ServiceResponse}}; use actix_web_lab::middleware::{from_fn, Next};  async fn print_before_and_after_handler(     req: ServiceRequest,     next: Next<impl MessageBody>, ) -> Result<ServiceResponse<impl MessageBody>, Error> {     println!("Hi from start. You requested: {}", req.path());     let res = next.call(req).await?;     println!("Hi from response");     Ok(res) }  let app = App::new()     .wrap(from_fn(print_before_and_after_handler))     .route(         "/",         web::get().to(|| async { "Hello from handler!" }), ); 

为了能够编写更复杂的中间件,我们实际上需要实现两个traits - Service<Req> 用于实现实际的中间件本身以及 Transform<S, Req> 这是构建器所需的处理请求的实际服务(就我们构建服务而言,我们实际上将使用构建器,而不是中间件!外部服务将在检测到 HTTP 请求时自动调用中间件)。

对于实际的中间件实现,让我们看一下编写一个仅打印消息的中间件。我们可以通过实现 Transform 特征来创建中间件的构建器:

use std::{future::{ready, Ready, Future}, pin::Pin};  use actix_web::{     dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},     web, Error, };  pub struct SayHi;  // `S` - type of the next service // `B` - type of response's body impl<S, B> Transform<S, ServiceRequest> for SayHi where     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,     S::Future: 'static,     B: 'static, {     // setting up the types for the middleware to work     type Response = ServiceResponse<B>;     type Error = Error;     type InitError = ();     type Transform = SayHiMiddleware<S>;     type Future = Ready<Result<Self::Transform, Self::InitError>>;      // this immediately returns the middleware     fn new_transform(&self, service: S) -> Self::Future {         ready(Ok(SayHiMiddleware { service }))     } } 

现在我们可以编写中间件本身了!在内部,中间件必须实现一个泛型类型 - 然后在 Service 特征中声明。请注意,我们手动重新实现了 futures_util 中名为 LocalBoxFuture 的类型 - 也就是说,未来不需要 Send 特征并且是安全的使用它是因为它在解除引用时实现了 Unpin ,这会自动取消任何先前的线程安全保证。

pub struct SayHiMiddleware<S> {     /// The next service to call     service: S, }  // This future doesn't have the requirement of being `Send`. // See: futures_util::future::LocalBoxFuture type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;  // `S`: type of the wrapped service // `B`: type of the body - try to be generic over the body where possible impl<S, B> Service<ServiceRequest> for SayHiMiddleware<S> where     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,     S::Future: 'static,     B: 'static, {     type Response = ServiceResponse<B>;     type Error = Error;     type Future = LocalBoxFuture<Result<Self::Response, Self::Error>>;      // This service is ready when its next service is ready     forward_ready!(service);      fn call(&self, req: ServiceRequest) -> Self::Future {         println!("Hi from start. You requested: {}", req.path());          // A more complex middleware, could return an error or an early response here.          // we do not immediately await this, which means nothing happens         // this future gets moved into a Box         let fut = self.service.call(req);          Box::pin(async move {             // this future gets awaited now             let res = fut.await?;              // we can now do any work we need to after the request             println!("Hi from response");             Ok(res)         })     } } 

现在已经编写了中间件,现在可以将其添加到应用程序中:

#[actix_web::main] async fn main() -> std::io::Result<()> {     let app = App::new()          .wrap(SayHi);      // ... rest of your code } 

Actix Web 中的静态文件

Actix Web 中简单、简洁的静态文件服务是通过 actix_files 箱完成的 - 要添加它,只需通过 Cargo 添加它,如下所示:

cargo add actix-files 

设置静态文件服务的路由如下所示:

use actix_files::NamedFile; use actix_web::{HttpRequest, Result}; use std::path::PathBuf;  #[get("/")] async fn index(req: HttpRequest) -> Result<NamedFile> {     let path: PathBuf = req.match_info().query("filename").parse().unwrap();     Ok(NamedFile::open(path)?) } 

该路由允许我们提供任何可以找到并与文件名匹配的文件 - 例如,如果我们有一个为该路由提供服务的基本路由,如果我们然后运行我们的应用程序并转到 /index.html ,则该路由将尝试在项目根目录中查找名为 index.html 的文件。

请注意,您在任何情况下都不应该尝试使用带有 .* 的路径尾部来返回 NamedFile - 这会产生严重的安全隐患,并且会打开您的 Web 服务以进行路径遍历!这在 Actix Web 文档中进行了记录,您可以在此处找到有关路径遍历攻击的更多信息。为了防止这种情况,您可以尝试使用 std::fs::canoncalize 来尝试验证路径文件是否正确或未尝试遍历目标文件夹之外。

然而,当您需要提供多个文件时,这有点笨拙 - 例如,特别是如果您需要提供 HTML 文件的文件夹。为了能够从您的网络服务提供文件文件夹,最好的方法是使用 actix_files::Files 并将其附加到您的 App

use actix_files as fs; use actix_web::{App, HttpServer};  #[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new().service(             fs::Files::new("/static", ".")                 .use_last_modified(true),         )     })     .bind(("127.0.0.1", 8080))?     .run()     .await } 

请注意,您还可以使用多个选项来扩展 Files 结构,例如在基本路径上显示文件目录本身(用于文件服务)并允许隐藏文件,您可以在此处找到更多信息。

此外,我们还可以使用 askama 的 HTML 模板功能来增强我们的 HTML 文件服务!我们可以像这样开始:

cargo add askama askama-actix-web -F askama/with-actix-web 

这会添加 askama 本身以及 askama::Template 类型的 Responder 特征实现。 askama 期望您的文件默认位于项目根目录的名为 templates 的子文件夹中,因此让我们创建该文件夹,然后使用以下内容创建一个 index.html 文件HTML 代码位于:

Hello, {{name}}! 

要在我们的应用程序中使用 Askama,我们需要声明一个使用 Template 派生宏的结构体,并使用 Askama 的 template 宏将该结构体指向我们希望它使用的文件:

#[derive(Template)] #[template(path = "index.html")] struct IndexTemplate<'a> {     name: &'a str }  #[get("/")] async fn index_route() -> impl Responder {     IndexTemplate { name: "Shuttle" } } 

然后我们可以将其添加为我们的 Actix Web 服务中的常规处理程序函数,我们就可以开始了!当您转到返回 index_route 的路径时,您应该看到“Hello, Shuttle!”作为 HTML 响应。

部署

一般来说,由于必须使用 Dockerfile,使用 Rust 后端程序进行部署可能不太理想,尽管如果您已经有使用 Docker 的经验,这对您来说可能不是一个问题 - 特别是如果您使用 cargo-chef .但是,如果您使用的是 Shuttle,则只需使用 cargo shuttle deploy 即可完成。无需设置。

尾声

谢谢阅读! Actix Web 是一个强大的框架,您可以用它来增强您的 Rust 产品组合,如果您想构建您的第一个 Rust API,那么它也是一个深入了解 Rust 的绝佳框架。


原文地址:Getting Started with Actix Web in Rust

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!