Rust web开发 ActixWeb 入门
目前,Actix Web 仍然是 Rust Web 后端生态系统中极其强大的竞争对手。尽管之前的事件可能对其产生影响,但它仍然很强大,并且是 Rust 中最受推荐的 Web 框架之一。最初基于同名的 actor 框架 (actix),后来它已经消失,并且actix现在仅用于 websocket 端点。本文将主要讨论 v4.4。
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 的绝佳框架。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)