Flask + Jinja2 模板继承实战:6 个独立 HTML 到统一 base_layout
一、问题:每个页面都是一座孤岛
venxine.vip 最初只有一两个页面,每页一个独立 HTML 文件,各自包含完整的 <html>、<head>、<body>、导航栏、主题切换脚本。没问题,简单直接。
但当页面数增长到 6 个时(/system/、/demo/、/blog/、/blog/xxx/、/token/、/skills/),问题爆发了:
每个页面都要维护:
├── 导航栏(返回中枢按钮)
├── 主题切换(日/夜模式 + localStorage)
├── 暗色模式防闪烁脚本(<head> 内的 inline script)
├── Glass Card 样式(毛玻璃背景 + hover 效果)
├── 交错入场动画(fadeSlideUp + stagger 延迟)
└── 全局排版(max-width、字体、颜色)
每改一个全局样式,要改 6 个文件。 而且改不完全,有的页面用了旧版导航栏,有的用了新版,视觉一致性全面崩盘。
这就是模板继承要解决的问题。
二、方案:base_layout.html + block 设计
Jinja2 的 {% extends %} + {% block %} 机制天然适合这个场景:
base_layout.html ← 所有页面的"骨架"
├── <html>/<head> ← 全局 meta、暗色防闪、Tailwind CDN
├── <nav> ← 统一导航栏(返回中枢 + 页面标签 + 主题切换)
├── <main> ← 统一容器(max-w-6xl + scrollbar-gutter)
│ └── {% block content %} ← 子模板填充区
└── <script> ← 主题切换 JS
子模板(如 system.html)只需:
{% extends "base_layout.html" %}
{% block page_title %}系统信息{% endblock %}
{% block page_label %}系统版本 / SYSTEM{% endblock %}
{% block sidebar %}{% endblock %} {# 不需要侧边栏,覆盖为空 #}
{% block page_css %}
/* 页面专属 CSS */
{% endblock %}
{% block content %}
<!-- 页面专属内容 -->
{% endblock %}
关键设计决策:
| Block | 用途 | 示例 |
|---|---|---|
page_title |
<title> 标签内容 |
系统信息 · Venxine.Vip |
page_label |
导航栏中间的文字 | 系统版本 / SYSTEM |
accent_pulse_color |
导航栏脉冲点的颜色 | text-rose-400 |
sidebar |
侧边栏内容,不需要就覆盖为空 | {% block sidebar %}{% endblock %} |
page_css |
页面专属 CSS,注入 <style> 内 |
.tabs { ... } |
content |
主体内容区 | 每个页面自己的 HTML |
page_scripts |
页面专属 JS | IntersectionObserver 等 |
三、实战:blog 迁移的全过程
/blog/ 和 /blog/xxx/ 是最晚迁移的两个页面。迁移前它们是 200 行级别的独立 HTML,各自包含完整的导航栏、主题切换逻辑。
迁移步骤:
Step 1:提取公共部分到 base_layout
检查两个 blog 模板的公共部分: - 导航栏 → base_layout 已有 - 主题切换 → base_layout 已有 - 暗色模式脚本 → base_layout 已有 - Glass Card 样式 → base_layout 已有
提取 blog 独有部分: - 分类标签颜色系统(4 种 cat-accent) - 卡片左色条 hover 效果 - 文章排版样式(.article-body) - 滚动揭示动画(IntersectionObserver)
Step 2:改造子模板
blog.html 迁移前的开头:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>技术博客 · Venxine.Vip</title>
<!-- 100+ 行重复的头内容 -->
</head>
<body>
<nav><!-- 40 行重复的导航栏 --></nav>
<main><!-- 博客内容 --></main>
<script><!-- 30 行重复的主题切换 JS --></script>
</body>
</html>
迁移后:
{% extends "base_layout.html" %}
{% block page_title %}技术博客 · Venxine.Vip{% endblock %}
{% block page_label %}技术博客 / BLOG{% endblock %}
{% block accent_pulse_color %}text-amber-400{% endblock %}
{% block sidebar %}{% endblock %}
{% block page_css %}
/* 仅保留 blog 独有的 CSS */
{% endblock %}
{% block content %}
<!-- 仅保留博客内容 HTML -->
{% endblock %}
文件从 193 行缩减到 ~150 行,去掉了 100% 的重复代码。
Step 3:处理 CSS 命名冲突
base_layout 已经定义了 .glass-card,但 blog 原来也有一套 .card 样式。解决方案:将 blog 的 .card 重命名为 .blog-card,避免样式污染。
Step 4:模板缓存坑
改完文件 → touch 模板 → systemctl restart fund-web → 打开网页 → 还是旧的!
Flask 在生产模式下会缓存 Jinja2 编译后的模板字节码。touch 更新了文件 mtime,但正在运行的 Python 进程不会自动重载。
正确做法:
# 1. 先确保旧进程完全停止(可能占着端口)
sudo fuser -k 5000/tcp
# 2. 再启动
sudo systemctl start fund-web
systemctl restart 有时候 stop + start 之间有空窗期导致旧进程还没死透,新进程就尝试绑定端口失败。先 kill 再 start 是最稳妥的。
四、意外收获:scrollbar-gutter 解决页面抖动
所有页面统一到 base_layout 后,发现一个长期存在的视觉问题:/system/ 的 Tab3 内容比 Tab1 高,切换时垂直滚动条出现/消失导致 mx-auto 居中参考点变化,内容左移 7.5px。
在 base_layout.html 的 <html> 上加一行就全局修复:
html {
scrollbar-gutter: stable;
}
浏览器始终预留 15px 滚动条空间,无论内容是否溢出。6 个页面全部受益,而这在各自维护独立 HTML 的时代,需要改 6 次。
五、模板继承的收益总结
| 迁移前 | 迁移后 | |
|---|---|---|
| 导航栏代码 | 6 份 × 40 行 = 240 行 | 1 份 × 40 行 |
| 主题切换代码 | 6 份 × 30 行 = 180 行 | 1 份 × 15 行 |
| 全局 CSS | 6 份,版本不一致 | 1 份,统一维护 |
| 改一个全局样式 | 改 6 个文件 | 改 1 个文件 |
| scrollbar-gutter | 不存在 | 全局生效 |
| 单页面文件大小 | 200+ 行 | 100-150 行 |
核心原则:DRY(Don't Repeat Yourself)不只是代码的事,HTML 模板同样适用。