编写应用程序

application.md
commit - 4d8d53cea59bca095ca5c02ef81f0b1791736855 - 2020.09.12

actix-web 提供了各种原语,以使用 Rust 程序设计语言构建 web 服务器和应用程序。它提供路由、中间件、预处理请求,以及响应的后置处理等。

所有 actix-web 服务器都是围绕 App(应用程序)实例构建的,其用于注册资源路由和中间件。另外,在同一作用域(scope)中,actix-web 服务器也存储所有 handler 之间共享的应用程序状态。

应用程序的作用域(scope)充当所有路由的命名空间,也就是说,特定的应用程序作用域内,所有路由都具有相同的 url 路径前缀。应用程序的路由前缀始终包含前导斜杠 “/”;如果提供的前缀不包含前导斜杠 “/”,则会自动补入该前缀。除了前导斜杠 “/”,路由前缀也应该包含路径值。

比如,应用程序的作用域为 /app,即路径前缀为 /app。那么,路径为 /app/app/,或者 /app/test 的请求都可以匹配;但是,路径 /application 不能匹配。

use actix_web::{web, App, HttpServer, Responder};

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("/app")
                // ...so this handles requests for `GET /app/index.html`
                .route("/index.html", web::get().to(index)),
        )
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

上述例子中,创建了具有 /app 前缀和 index.html 页面资源的应用程序。此资源可通过 url 路径 /app/index.html 获得。

要获取更多信息,请参阅 URL 调度 - 使用作用域前缀一节。

状态(state)

应用程序状态(state)被同一作用域(scope)内的所有路由和资源共享。可以使用数据提取器 web::Data<T> 访问状态(state),其中泛型参数 T 表示状态类型。另外,中间件也可以访问状态。

让我们编写一个简单的应用程序,并将应用程序名称存储在状态中:

use actix_web::{get, web, App, HttpServer};

// This struct represents state
struct AppState {
    app_name: String,
}

#[get("/")]
async fn index(data: web::Data<AppState>) -> String {
    let app_name = &data.app_name; // <- get app_name

    format!("Hello {}!", app_name) // <- response with app_name
}

并在 App 初始化时传入状态(state),然后启动应用程序:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .data(AppState {
                app_name: String::from("Actix-web"),
            })
            .service(index)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

在应用程序中,可以注册任意数量的状态(state)类型。

共享可变状态

HttpServer 接受应用程序工厂,而非应用程序实例。HttpServer 为每个线程构造一个应用程序实例。因此,必须多次构造应用程序数据。如果你想在不同的线程之间共享数据,应该使用一个可共享的对象,例如 Send + Sync

web::Data 内部使用 Arc(原子引用计数器)。因此,为了避免创建两个 Arc(原子引用计数器),我们应该在使用 App::app_data() 方法注册数据之前,先行创建数据。

在下面的示例中,我们将编写一个应用程序,其具有可变的、共享的状态(state)。首先,我们定义状态并创建 handler

use actix_web::{web, App, HttpServer};
use std::sync::Mutex;

struct AppStateWithCounter {
    counter: Mutex<i32>, // <- Mutex is necessary to mutate safely across threads
}

async fn index(data: web::Data<AppStateWithCounter>) -> String {
    let mut counter = data.counter.lock().unwrap(); // <- get counter's MutexGuard
    *counter += 1; // <- access counter inside MutexGuard

    format!("Request number: {}", counter) // <- response with count
}

然后,在 App 中注册数据:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let counter = web::Data::new(AppStateWithCounter {
        counter: Mutex::new(0),
    });

    HttpServer::new(move || {
        // move counter into the closure
        App::new()
            // Note: using app_data instead of data
            .app_data(counter.clone()) // <- register the created data
            .route("/", web::get().to(index))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

使用作用域组合应用程序

web::scope() 方法允许设置资源组前缀。此作用域表示一个预添加的资源前缀——在由资源配置添加的所有资源模式中,该前缀将被预先附加。这有助于将一组新编写的路由挂载到不同位置,从而与以前开发者设计的位置分离,但仍然保持相同的资源名称。

我们来看实际例子:

#[actix_web::main]
async fn main() {
    let scope = web::scope("/users").service(show_users);
    App::new().service(scope);
}

在上面的示例中,show_users 路由的有效路由模式将是 /users/show,而非 /show,因为应用程序的 scope 参数将附加在模式前面。仅当 URL 路径为 /users/show 时,路由才将匹配。使用路由名称 show_users 调用函数 HttpRequest.url_for(),它将生成具有相同路径的 URL。

应用程序卫语句及虚拟主机

可以将卫语句看作是一个简单的函数,它接受 request 对象引用,并返回 true 或者 false。从形式上讲,卫语句是实现了 Guard trait 的任何对象。actix-web 提供了多种卫语句,要详细了解,请查看 API 文档的函数部分

Header 是 actix-web 提供的卫语句之一,它可以用作基于请求头信息的过滤器。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/")
                    .guard(guard::Header("Host", "www.rust-lang.org"))
                    .route("", web::to(|| HttpResponse::Ok().body("www"))),
            )
            .service(
                web::scope("/")
                    .guard(guard::Header("Host", "users.rust-lang.org"))
                    .route("", web::to(|| HttpResponse::Ok().body("user"))),
            )
            .route("/", web::to(|| HttpResponse::Ok()))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

配置

为了简洁和可重用,Appweb::Scope 均提供了 configure 方法,此函数用于将配置的部分移动到不同的模块甚至库中。例如,资源的某些配置可以移动到其它模块。

use actix_web::{web, App, HttpResponse, HttpServer};

// this function could be located in a different module
fn scoped_config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::resource("/test")
            .route(web::get().to(|| HttpResponse::Ok().body("test")))
            .route(web::head().to(|| HttpResponse::MethodNotAllowed())),
    );
}

// this function could be located in a different module
fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::resource("/app")
            .route(web::get().to(|| HttpResponse::Ok().body("app")))
            .route(web::head().to(|| HttpResponse::MethodNotAllowed())),
    );
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .configure(config)
            .service(web::scope("/api").configure(scoped_config))
            .route("/", web::get().to(|| HttpResponse::Ok().body("/")))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

上述示例的结果是:

/         -> "/"
/app      -> "app"
/api/test -> "test"

每一个 ServiceConfig 可以有自己的数据(data)路由(route),以及服务(services)