模板与静态资源 (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>&copy; 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 处理跨域即可。



练习

以下题目用于验证本章所学内容:

题号题目链接涉及知识点
本章无对应力扣题请用动手练习题自检