我们的博客分为“前台”和“后台”两部分。前台用于展示博客内容,后台用于管理博客。本章我们将编写前台和后台的基础模板以及对应的路由。

目录结构

前台模板位于 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/模板