本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务。
创建项目将加入到 workspace 中:
cargo new blog-frontend
添加依赖
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 }
}
}
cate
:连接到分类服务category-srv
的 gRPC 客户端topic
:连接到文章服务topic-srv
的 gRPC 客户端tera
:模板引擎
获取文章列表
let query_category_id = match params.category_id.clone() {
Some(cid) => {
if cid > 0 {
Some(cid)
} else {
None
}
}
None => None,
};
let mut tpc = state.topic.clone();
let resp = tpc
.list_topic(tonic::Request::new(ListTopicRequest {
page: params.page.clone(),
category_id: query_category_id,
keyword: params.keyword.clone(),
is_del: Some(false),
dateline_range: None,
}))
.await
.map_err(|err| err.to_string())?;
let reply = resp.into_inner();
let mut topic_list: Vec<blog_types::Topic> = Vec::with_capacity(reply.topics.capacity());
for reply_topic in reply.topics {
let mut t: blog_types::Topic = reply_topic.into();
// 查找分类
for cate in &cate_list {
if cate.id == t.category_id {
t.category_name = cate.name.clone();
break;
}
}
topic_list.push(t);
}
let paginate = blog_types::Paginate {
page: reply.page,
page_size: reply.page_size,
page_totoal: reply.page_totoal,
record_total: reply.record_total,
data: topic_list,
};
文章列表需要接收多个可选参数,我们对其进行定义,并通过 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
的实现
pub async fn detail(
Extension(state): Extension<Arc<AppState>>,
Path(id): Path<i64>,
) -> Result<Html<String>, String> {
let mut ctx = Context::new();
// 获取分类列表
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());
}
ctx.insert("cate_list", &cate_list);
// 获取文章详情
let mut tpc = state.topic.clone();
let resp = tpc
.get_topic(tonic::Request::new(GetTopicRequest {
id,
inc_hit: Some(true),
is_del: Some(false),
}))
.await
.map_err(|err| err.to_string())?;
let reply = resp.into_inner();
let mut t: blog_types::Topic = match reply.topic {
Some(topic) => topic.into(),
None => {
return Err("不存在的文章".to_string());
}
};
// 查找分类
for cate in &cate_list {
if cate.id == t.category_id {
t.category_name = cate.name.clone();
break;
}
}
ctx.insert("topic", &t);
let out = state
.tera
.render("detail.html", &ctx)
.map_err(|err| err.to_string())?;
Ok(Html(out))
}
启动前台WEB服务
#[tokio::main]
async fn main() {
let addr = "0.0.0.0:39527";
let cate = CategoryServiceClient::connect("http://127.0.0.1:19527")
.await
.unwrap();
let topic = TopicServiceClient::connect("http://127.0.0.1:29527")
.await
.unwrap();
let tera = Tera::new("blog-frontend/templates/**/*.html").unwrap();
let app = Router::new()
.route("/", get(handler::index))
.route("/detail/:id", get(handler::detail))
.layer(Extension(Arc::new(model::AppState::new(cate, topic, tera))));
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
- 首先初始化
AppState
所需要的 gRPC 客户端连接和模板引擎 - 定义路由
本章代码位于04/实现前台服务