域名 AXUM.RS 将于2025年10月到期。我们无意再对其进行续费,我们希望你能够接续这个域名,让更多 AXUM 开发者继续受益。现在,我们已启用新域名 AXUM.EU.ORG
  • 方案AXUM.RS 域名 = 3000
如果你有意接续这份 AXUM 情怀,请与我们取得联系。
说明:
  1. 如果有人购买 AXUM.RS 域名,或者该域名到期,本站将使用免费域名 AXUM.EU.ORG 继续提供服务。

实现前台web服务

本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务。

创建项目将加入到 workspace 中:

添加依赖

[dependencies]
tokio = {version = "1", features = ["full"]}
prost = "0.11"
prost-types = "0.11"
tonic = "0.8"
axum = "0.5"
tera = "1"
serde = { version="1", features = ["derive"] }
chrono = "0.4"
blog-proto = {path="../blog-proto"}
blog-types = {path="../blog-types"}

由于采用了微服务,所以并没有直接的数据库操作。

blog-types web 所需的数据结构

由于 web 需要的数据结构和 pb 生成的并不一定相同,比如需要序列化和反序列化、比如可能根据需要增加/删除某些结构体、字段等。

基于此,我们需要定义 web 所需要的数据结构,并提供 From/Into 等方法,方便和 pb 生成的数据结构进行转换。

代码相对简单,请直接在 git 上查看

博客首页 handler::index 的实现

获取分类列表

    let mut cate = state.cate.clone();
    let resp = cate
        .list_category(tonic::Request::new(ListCategoryRequest {
            name: None,
            is_del: Some(false),
        }))
        .await
        .map_err(|err| err.to_string())?;
    let reply = resp.into_inner();
    let mut cate_list: Vec<blog_types::Category> = Vec::with_capacity(reply.categories.len());
    for reply_cate in reply.categories {
        cate_list.push(reply_cate.into());
    }

我们通过调用分类服务的 list_category 方法来获取分类列表。为了共享 gRPC 客户端连接,我们将各种 gRPC 客户端连接通过 AppState 进行共享。

AppState 状态共享

model.rs中,我们定义了 AppState 以实现handler间的状态共享:

pub struct AppState {
    pub cate: CategoryServiceClient<tonic::transport::Channel>,
    pub topic: TopicServiceClient<tonic::transport::Channel>,
    pub tera: Tera,
}

impl AppState {
    pub fn new(
        cate: CategoryServiceClient<tonic::transport::Channel>,
        topic: TopicServiceClient<tonic::transport::Channel>,
        tera: Tera,
    ) -> Self {
        Self { cate, topic, tera }
    }
}

获取文章列表

文章列表需要接收多个可选参数,我们对其进行定义,并通过 axum 的 Query 进行获取:

#[derive(Deserialize, Serialize)]
pub struct QueryParams {
    pub page: Option<i32>,
    pub category_id: Option<i32>,
    pub keyword: Option<String>,
}

为了方便在模板中组合成url参数,我们还定义了对应的结构体,并对其实现 From<QueryParams>,方便从获取到的参数转换成模板中所需要的参数:

#[derive(Deserialize, Serialize)]
pub struct QueryParamsForUrl {
    pub category_id: i32,
    pub keyword: String,
    pub page: i32,
}

impl From<QueryParams> for QueryParamsForUrl {
    fn from(p: QueryParams) -> Self {
        Self {
            category_id: match p.category_id {
                Some(cid) => cid,
                None => 0,
            },
            keyword: match p.keyword {
                Some(kw) => kw,
                None => "".to_string(),
            },
            page: p.page.unwrap_or(0),
        }
    }
}

请点击查看 handler::index()的完整代码。

博客文章详情 handler::detail 的实现

相对于 index()detail()的实现就简单多了。

启动前台WEB服务

  • 首先初始化AppState 所需要的 gRPC 客户端连接和模板引擎
  • 定义路由

本章代码位于04/实现前台服务

要查看完整内容,请先登录