第 3 章 · Jinja2 模板与静态资源
本章目标:在 api-demo 的 admin 蓝图中使用 Jinja2 渲染 HTML 管理页;掌握 extends / block 模板继承与 macro 复用片段;正确配置 url_for('static', ...) 引用 CSS/JS;使用 flash 与 get_flashed_messages 展示操作反馈;理解 Flask 模板目录约定并与 Django 模板对照。
学时建议:4~5 小时(含 2 小时跟练)
前置:完成 flask-web ch01~ch02(蓝图、url_for、admin 蓝图占位)。
3.1 场景说明:轻量管理页
api-demo 以 API 为主,但运营仍需浏览器管理页维护商品(完整 CRUD 在 ch04~ch05)。与 shop-demo 的 Django Admin 不同,这里用自定义模板打造极简后台。
| 页面 | 路径 | 模板 | 说明 |
|---|---|---|---|
| 仪表盘 | /admin/ | admin/dashboard.html | 统计卡片 |
| 商品列表 | /admin/products/ | admin/product_list.html | 表格 + flash |
| 布局基类 | — | base.html | 导航、静态资源 |
虚构对外 API 文档仍指向 https://api.example.com;管理页仅本地 http://127.0.0.1:5000/admin/ 访问。
3.2 模板与静态目录结构
Flask 默认从应用根下的 templates/ 与 static/ 加载。
api-demo/
├── api_demo/
│ ├── templates/
│ │ ├── base.html
│ │ ├── partials/
│ │ │ └── flash.html
│ │ ├── macros/
│ │ │ └── ui.html
│ │ └── admin/
│ │ ├── dashboard.html
│ │ └── product_list.html
│ ├── static/
│ │ ├── css/admin.css
│ │ └── js/admin.js
│ └── routes/admin.py
| 资源类型 | 目录 | 引用方式 |
|---|---|---|
| HTML 模板 | templates/ | render_template("admin/dashboard.html") |
| CSS/JS/图片 | static/ | url_for('static', filename='css/admin.css') |
创建应用时指定(可选,默认已正确):
app = Flask(__name__, template_folder="templates", static_folder="static")
3.3 基模板 base.html
api_demo/templates/base.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}api-demo 管理{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin.css') }}">
</head>
<body>
<header class="topbar"><strong>api-demo</strong></header>
<nav class="sidebar">
<a href="{{ url_for('admin.dashboard') }}">仪表盘</a>
<a href="{{ url_for('admin.product_list') }}">商品</a>
<a href="{{ url_for('api.product_list') }}">API 预览</a>
</nav>
<main class="content">
{% include "partials/flash.html" %}
{% block content %}{% endblock %}
</main>
<script src="{{ url_for('static', filename='js/admin.js') }}"></script>
</body>
</html>
api_demo/templates/partials/flash.html:
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class="flash-list">
{% for category, message in messages %}
<li class="flash flash-{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
3.4 extends 与 block 继承
子模板通过 extends 填充父模板的 block。
api_demo/templates/admin/dashboard.html:
{% extends "base.html" %}
{% block title %}仪表盘 · api-demo{% endblock %}
{% block content %}
<h1>仪表盘</h1>
<div class="cards">
<div class="card">
<h3>商品数</h3>
<p>{{ product_count }}</p>
</div>
<div class="card">
<h3>API 基址</h3>
<p><code>{{ api_base }}</code></p>
</div>
</div>
{% endblock %}
api_demo/templates/admin/product_list.html:
{% extends "base.html" %}
{% block title %}商品列表 · api-demo{% endblock %}
{% block content %}
<h1>商品列表</h1>
<p><a class="btn" href="{{ url_for('admin.product_create') }}">新建商品</a></p>
<table class="table">
<thead>
<tr><th>ID</th><th>名称</th><th>价格</th><th>库存</th></tr>
</thead>
<tbody>
{% for p in products %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>¥{{ "%.2f"|format(p.price) }}</td>
<td>{{ p.stock }}</td>
</tr>
{% else %}
<tr><td colspan="4">暂无商品</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
| Jinja2 | Django 模板 | 说明 |
|---|---|---|
{% extends %} | {% extends %} | 相同 |
{% block %} | {% block %} | 相同 |
{% for %}...{% else %} | {% empty %} | Flask 用 else 分支 |
format 过滤器(如 "%.2f" 格式化价格) | 过滤器 | Jinja2 用竖线调用过滤器 |
3.5 macro 宏复用
api_demo/templates/macros/ui.html:
{% macro badge(text, kind="default") %}
<span class="badge badge-{{ kind }}">{{ text }}</span>
{% endmacro %}
{% macro page_header(title, subtitle="") %}
<header class="page-header">
<h1>{{ title }}</h1>
{% if subtitle %}<p class="muted">{{ subtitle }}</p>{% endif %}
</header>
{% endmacro %}
在列表页引用: