我们的博客分为“前台”和“后台”两部分。前台用于展示博客内容,后台用于管理博客。本章我们将编写前台和后台的基础模板以及对应的路由。
目录结构
前台模板位于 templates/frontend
,后台模板位于templates/backend
。
前台
我们的前台模板基于 Bootstrap的Blog 修改而来
布局
首先,我们对前台页面的布局来做一个全局总览。
注意,以下示意图链接到 github,如果你无法查看,请更换网络环境
- 【页眉】显示博客名称
- 【博客分类】从数据库中读取所有分类,并以导航栏的形式显示在页面上
- 【关于我们】和【友情链接】本项目中,它们都是硬编码在页面上的
- 【存档】按月份显示,点击其中的月份会跳转到显示该月份所有博客的列表页面
- 【页脚】显示版权信息
- 【页面标题】动态定义当前页面的标题
- 【页面内容】动态定义当前页面的内容
母模板
基于以上对布局的说明,我们的母模板 templates/frontend/base.html
需要定义两个块(block):
- 对应【页面标题】的
{% block category_name %}分类名称{%endblock%}
- 对应【页面内容】的
{% block content%}页面内容{%endblock%}
虽然其它区域是固定的,但有些区域的内容还是需要从数据库中获取数据来填充。现阶段而言,我们只定义了【页面标题】对应的块,其它区域都保持硬编码的文字。
首页模板
有了母模板,我们就可以通过extends
来衍生出业务需要的特定模板了,比如现阶段我们定义的首页的模板:
<!-- templates/frontend/index.html -->
{%extends "./base.html"%}
{%block category_name%}最新博文{%endblock%}
视图类
下面,我们为前台首页定义一下视图类:
// src/view/frontend/index.rs
use askama::Template;
#[derive(Template)]
#[template(path="frontend/index.html")]
pub struct Index {}
handler
有了视图类,就可以将模板进行渲染:
// src/handler/frontend/index.rs
pub async fn index()->Result<HtmlView> {
let handler_name = "frontend/index/index";
let tmpl = Index{};
render(tmpl).map_err(log_error(handler_name))
}
在这个 handler 中,出现了几个新元素:
HtmlView
:自定义的类型,用于返回HTML视图render()
:用于通过视图类来渲染模板log_error()
:使用日志记录AppError
HtmlView
// src/handler/mod.rs
type HtmlView = axum::response::Html<String>;
render()
// src/handler/mod.rs
fn render<T>(tmpl: T) ->Result<HtmlView> where T:Template {
let html = tmpl.render().map_err(AppError::from)?;
Ok(Html(html))
}
泛型T
必须满足askama::Template
约束。由于我们定义视图类的时候使用了 #[derive(Template)]
,所以我们的视图类满足这一约束。
该函数用于渲染模板,模板文件的路径通过视图类的#[template(path="frontend/index.html")]
指定。默认模板引擎会在项目根目录的templates
目录下,查找path
指定的模板文件。
如果发生错误,通过map_err
将其转换为AppError
。
log_error()
// src/handler/mod.rs
fn log_error(handler_name:&str) -> Box<dyn Fn(AppError)->AppError> {
let handler_name = handler_name.to_string();
Box::new(move |err| {
tracing::error!("操作失败:{:?}, {}", err, handler_name);
err
})
}
该函数用于记录产生的AppError
日志。
路由
我们还需要定义路由。
// src/handler/frontend/mod.rs
pub fn router()->Router {
Router::new().route("/", get(index::index))
}
后台
布局
我们来看一下后台页面的布局:
- 【页眉】显示应用名称以【退出登录】链接
- 【侧边菜单】显示可用的菜单
- 【页面标题】显示当前页面的标题
- 【工具栏】显示当前页面可用的工具栏
- 【提示信息】该区域和【页面内容】平行,示意图上没有画出。用于显示提示信息。
- 【页面内容】显示当前页面的内容
- 【分页】如果需要分页,则这里显示分页按钮
母模板
通过以上分析,我们需要定义以下块:
- 【页面标题】:
{% block title%}标题{%endblock%}
- 【工具栏】:
{% block toolbar %}{%endblock%}
- 【提示信息】
{% block msg%} {%endblock%}
- 【页面内容】
{%block content%}内容{%endblock%}
当然,现阶段而言,我们并不需要定义它们,随着项目的推进,会对其进行补全。
首页模板
根据母模板,我们来定义后台首页模板:
<!-- templates/backend/index.html -->
{% extends "./base.html" %}
视图类
// src/view/backend/index.rs
use askama::Template;
#[derive(Template)]
#[template(path="backend/index.html")]
pub struct Index {}
handler
// src/handler/backend/index.rs
pub async fn index()->Result<HtmlView>{
let handler_name="backend/index/index";
let tmpl = Index{};
render(tmpl).map_err(log_error(handler_name))
}
路由
// src/handler/backend/mod.rs
pub fn router() -> Router {
Router::new().route("/", get(index))
}
启动axum服务
// src/main.rs
use axum::Router;
use axum_rs_blog::handler::{backend, frontend};
#[tokio::main]
async fn main() {
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "axum_rs_blog=debug");
}
tracing_subscriber::fmt::init();
tracing::info!("服务已启动");
let frentend_routers = frontend::router();
let backend_routers = backend::router();
let app = Router::new()
.nest("/", frentend_routers)
.nest("/admin", backend_routers);
axum::Server::bind(&"127.0.0.1:9527".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
本章代码位于02/模板