模板与静态资源 (Templates & Static Files)
章节概述
Web 应用不只是 JSON API——浏览器期望的是 HTML 页面。本章讲解 Jinja2 模板引擎(变量渲染、循环、条件、模板继承、过滤器),静态文件(CSS/JS/图片)的部署方式,以及如何在 HTML 中渲染 C 程序的输出表格。最后简要对比传统服务端渲染与现代 SPA + API 的前后端分离架构。
核心理念:模板的作用是”将数据嵌入 HTML”。对于 C 程序员来说,Jinja2 模板就像增强版的
printf格式化——{{ var }}类似printf("%s", var),{% for %}类似 C 的for循环拼装字符串,但模板引擎自动处理了 HTML 转义、继承和复用,让你不必用 C 手写 HTML 拼接。
第一节:Jinja2 基础 — 变量、循环与条件
Jinja2 的三种核心语法块:
| 语法 | 作用 | C 类比 |
|---|---|---|
{{ 变量 }} | 输出变量值(自动 HTML 转义) | printf("%s", var) |
{% 控制语句 %} | 循环、条件、继承等逻辑 | if/for 控制流 |
{# 注释 #} | 模板注释(不渲染到输出) | /* 注释 */ |
Flask 中渲染模板:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/user/<name>')
def profile(name):
return render_template('user.html', # 模板文件名
username=name, # 传给模板的变量
age=25,
skills=['C', 'Python', 'Rust'])基础模板 templates/user.html:
<h1>{{ username }} 的个人主页</h1>
<p>年龄: {{ age }}</p>
<h2>技能列表</h2>
<ul>
{% for skill in skills %}
<li>{{ loop.index }}. {{ skill }}</li> {# loop.index 从 1 开始 #}
{% endfor %}
</ul>
{% if age >= 18 %}
<p>成年人</p>
{% else %}
<p>未成年人</p>
{% endif %}循环特殊变量:
| 变量 | 含义 |
|---|---|
loop.index | 当前迭代计数(从 1 开始) |
loop.index0 | 当前迭代计数(从 0 开始) |
loop.first | 是否为第一次迭代 |
loop.last | 是否为最后一次迭代 |
loop.length | 序列总长度 |
字典遍历:
<table>
{% for key, val in config.items() %}
<tr><td>{{ key }}</td><td>{{ val }}</td></tr>
{% endfor %}
</table>第二节:过滤器 — 变量后处理
过滤器对变量进行格式化转换,用管道符号 | 连接:
<p>{{ name | upper }}</p> <!-- 转大写 -->
<p>{{ price | round(2) }}</p> <!-- 四舍五入 -->
<p>{{ items | length }}</p> <!-- 列表长度 -->
<p>{{ html_content | safe }}</p> <!-- 不转义 HTML(慎用) -->
<p>{{ timestamp | datetimeformat }}</p> <!-- 自定义过滤器 -->常用内置过滤器:
| 过滤器 | 作用 | 示例 |
|---|---|---|
upper / lower | 大小写转换 | "hello" → "HELLO" |
capitalize | 首字母大写 | "hello" → "Hello" |
round(precision) | 四舍五入 | 3.14159 → 3.14 |
default(val) | 变量为 None 时用默认值 | None → val |
length | 序列长度 | [1,2,3] → 3 |
join(sep) | 列表拼接 | [1,2,3] → "1,2,3" |
safe | 标记为安全(不转义 HTML) | — |
tojson | 转为 JSON 字符串 | dict → JSON |
自定义过滤器(Python 侧):
@app.template_filter('timestamp')
def format_timestamp(ts):
from datetime import datetime
return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
# 模板中: {{ 1700000000 | timestamp }} → "2023-11-15 06:13:20"
safe过滤器仅在你确认变量内容不包含用户输入时才使用,否则可能造成 XSS 攻击——这类似于 C 中printf(user_input)的格式字符串漏洞。
第三节:模板继承 — DRY 原则
模板继承让你定义通用页面骨架,各页面只填充差异部分。这类似于 C 中的”头文件 + 函数实现”分离:
父模板 templates/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}默认标题{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav>
<a href="/">首页</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% block footer %}
<p>© 2024</p>
{% endblock %}
</footer>
</body>
</html>子模板 templates/index.html:
{% extends "base.html" %}
{% block title %}首页 - 我的网站{% endblock %}
{% block content %}
<h1>欢迎,{{ username }}!</h1>
<p>这是首页内容。</p>
{% endblock %}
{# footer 使用父模板的默认值,无需重写 #}{% block %} 的核心用法:
{% block name %}— 定义一个可被覆盖的块{% block name %}默认内容{% endblock %}— 带默认值的块{{ super() }}— 在子模板中调用父模板的同名块内容(类似 C++ 的Base::method())
{% block head %}
{{ super() }} <!-- 先保留父模板的 head 内容 -->
<script src="extra.js"></script> <!-- 再添加自己的 -->
{% endblock %}第四节:静态文件部署
Flask 默认将项目根目录下的 static/ 文件夹作为静态资源目录,URL 路径固定为 /static/。
目录约定:
graph TB ROOT["project/"] ROOT --> APP["app.py"] ROOT --> STATIC["static/"] STATIC --> CSS["css/"] CSS --> STYLE["style.css"] STATIC --> JS["js/"] JS --> MAIN_JS["main.js"] STATIC --> IMG["images/"] IMG --> LOGO["logo.png"] ROOT --> TPL["templates/"] TPL --> INDEX["index.html"]
模板中引用静态文件:
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">始终使用
url_for('static', filename='...')而非硬编码/static/...——这让你可以通过配置修改静态文件前缀,也支持 CDN 部署等场景。
生产环境优化:静态文件通常由 Nginx 直接处理,不经过 Python(见 第 5 章 部署)。
第五节:渲染 C 程序输出为 HTML 表格
典型场景:C 后端程序输出 CSV 或 TSV 格式数据,Flask 解析后在 HTML 模板中呈现为表格。
C 程序 bin/data-gen 输出 CSV:
#include <stdio.h>
int main() {
printf("id,name,score\n");
printf("1,Alice,95.5\n");
printf("2,Bob,87.0\n");
printf("3,Charlie,72.3\n");
return 0;
}Flask 路由解析并渲染:
import subprocess, csv, io
@app.route('/report')
def report():
result = subprocess.run(['./bin/data-gen'],
capture_output=True, text=True)
reader = csv.DictReader(io.StringIO(result.stdout))
rows = list(reader)
return render_template('report.html', rows=rows)模板 templates/report.html:
{% extends "base.html" %}
{% block content %}
<h1>数据报告</h1>
<table>
<thead>
<tr>
<th>ID</th><th>姓名</th><th>分数</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row.id }}</td>
<td>{{ row.name }}</td>
<td>{{ row.score }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}对于结构更复杂的 C 输出(JSON 格式),用 json.loads() 解析后直接传给模板即可。
第六节:SPA + API 前后端分离简介
传统模板渲染(MVC)与现代 SPA 架构的对比:
模板渲染 (Flask + Jinja2):
浏览器 → HTTP GET → Flask → render_template → HTML 页面
浏览器 → POST 表单 → Flask → 重定向 → 新 HTML 页面
SPA + API:
浏览器 ← 首次加载 → Flask/任何静态服务器 → 一个空 HTML + JS 包
浏览器 ← 后续交互 → FastAPI → JSON API → JS 更新 DOM
| 维度 | 模板渲染 | SPA + API |
|---|---|---|
| 前后端 | 耦合(同一项目) | 分离(独立部署) |
| 交互体验 | 页面刷新 | 无刷新局部更新 |
| 搜索引擎 | 好(直接出 HTML) | 需 SSR 辅助 |
| 适合场景 | 简单后台、内部工具 | 复杂交互、多端共享 API |
本教程侧重于”快速出活”——用模板渲染为 C 后端搭建 Web UI 是最短路径。如果你的项目需要复杂的交互界面,FastAPI 提供 JSON API,前端用 React/Vue 单独开发,FastAPI + FastAPI 的 CORSMiddleware 处理跨域即可。
练习
以下题目用于验证本章所学内容:
| 题号 | 题目 | 链接 | 涉及知识点 |
|---|---|---|---|
| — | 本章无对应力扣题 | — | 请用动手练习题自检 |