模板引擎与前端集成
What — 是什么
模板引擎将 Python 数据与 HTML 模板结合,动态生成网页内容。Jinja2 是 Python 生态最流行的模板引擎,也是 Flask/Django 的默认模板后端。前端集成则涉及服务端渲染(SSR)与客户端渲染(CSR)的选择、API 驱动的前后端分离架构。
核心概念:
- Jinja2:模板语言,支持变量插值、控制流、继承、宏、过滤器
- 模板继承:
{% extends "base.html" %}+{% block content %},避免重复 - SSR vs CSR:服务端渲染(Python 生成 HTML)vs 客户端渲染(JS 框架生成)
- 前后端分离:后端只提供 API,前端用 React/Vue 独立开发
关键特性:
- Jinja2 自动转义防 XSS(
{{ var }}自动 HTML 转义) - 模板继承实现布局复用,
block定义可覆盖区域 - 自定义过滤器和全局函数扩展模板能力
include引入子模板片段,macro定义可复用组件- 支持异步模板渲染(
enable_async=True)
Jinja2 核心语法:
{# 变量输出(自动转义) #}
{{ user.name }}
{# 控制流 #}
{% if user.is_admin %}
<span>管理员</span>
{% elif user.is_staff %}
<span>员工</span>
{% else %}
<span>普通用户</span>
{% endif %}
{# 循环 #}
{% for item in items %}
<li>{{ loop.index }}: {{ item.name }}</li>
{% endfor %}
{# 过滤器 #}
{{ content|truncate(100) }}
{{ price|round(2) }}
{{ date|datetimeformat('%Y-%m-%d') }}
{# 继承 #}
{% extends "base.html" %}
{% block title %}首页{% endblock %}
Why — 为什么
适用场景:
- Django/Flask 传统 Web 应用(SSR)
- 邮件模板、PDF 报表生成
- 配置文件生成(Nginx/K8s 配置)
- 前后端分离项目的前端部署
渲染模式对比:
| 维度 | SSR(服务端渲染) | CSR(客户端渲染) | 同构渲染 |
|---|---|---|---|
| 首屏速度 | 快(直出 HTML) | 慢(需加载 JS) | 快 |
| SEO | 友好 | 不友好 | 友好 |
| 交互体验 | 传统(全页刷新) | 流畅(局部更新) | 流畅 |
| 服务器负载 | 高 | 低 | 中 |
| 技术栈 | Jinja2/Django | React/Vue | Next.js/Nuxt |
| 适合场景 | 内容站/管理后台 | SPA 应用 | 全场景 |
优缺点:
- ✅ 优点:
- SSR 首屏快、SEO 好、无需 JS 框架
- Jinja2 语法简洁,学习成本低
- 模板继承减少重复代码
- Django/Flask 原生集成
- ❌ 缺点:
- SSR 交互体验差(全页刷新)
- 复杂 UI 难以用模板实现
- 前后端耦合,前端无法独立开发
- Jinja2 模板不能在浏览器运行
How — 怎么用
快速上手:Jinja2 模板继承
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>{% block title %}MyApp{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
<nav>
<a href="/">首页</a>
<a href="/blog">博客</a>
{% if current_user.is_authenticated %}
<a href="/logout">退出</a>
{% else %}
<a href="/login">登录</a>
{% endif %}
</nav>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer>{% block footer %}© 2026{% endblock %}</footer>
</body>
</html>
<!-- templates/blog/post_list.html -->
{% extends "base.html" %}
{% block title %}博客 - {{ super() }}{% endblock %}
{% block content %}
<h1>最新文章</h1>
{% for post in posts %}
<article>
<h2><a href="{{ url_for('blog.detail', slug=post.slug) }}">{{ post.title }}</a></h2>
<p>{{ post.content|truncate(200) }}</p>
<span class="meta">{{ post.created_at|datetimeformat }}</span>
</article>
{% else %}
<p>暂无文章</p>
{% endfor %}
{% endblock %}
代码示例1:宏、自定义过滤器和全局函数
# Flask 集成自定义过滤器
from flask import Flask
app = Flask(__name__)
@app.template_filter('datetimeformat')
def datetimeformat(value, format='%Y-%m-%d %H:%M'):
from datetime import datetime
if isinstance(value, str):
value = datetime.fromisoformat(value)
return value.strftime(format)
@app.template_global()
def format_currency(amount, currency='CNY'):
symbols = {'CNY': '¥', 'USD': '$', 'EUR': '€'}
return f"{symbols.get(currency, '')}{amount:,.2f}"
@app.context_processor
def inject_utils():
return {
'now': datetime.now,
'app_name': 'MyBlog',
}
<!-- templates/macros.html -->
{% macro pagination(pagination, endpoint) %}
<nav class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for(endpoint, page=pagination.prev_num) }}">« 上一页</a>
{% endif %}
{% for page in pagination.iter_pages() %}
{% if page %}
{% if page == pagination.page %}
<span class="active">{{ page }}</span>
{% else %}
<a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
{% endif %}
{% else %}
<span class="ellipsis">...</span>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<a href="{{ url_for(endpoint, page=pagination.next_num) }}">下一页 »</a>
{% endif %}
</nav>
{% endmacro %}
{% macro card(title, content, image=None) %}
<div class="card">
{% if image %}
<img src="{{ image }}" alt="{{ title }}">
{% endif %}
<h3>{{ title }}</h3>
<p>{{ content|truncate(150) }}</p>
</div>
{% endmacro %}
<!-- 使用宏 -->
{% from "macros.html" import pagination, card %}
{% for post in posts %}
{{ card(post.title, post.content, post.cover_image) }}
{% endfor %}
{{ pagination(posts_pagination, 'blog.list') }}
代码示例2:前后端分离架构集成
# 方案1:Django + DRF API + Vue/React 前端
# 后端只提供 API
# frontend/ 目录放前端项目,构建后收集到 Django static
# settings.py
STATICFILES_DIRS = [
BASE_DIR / "frontend" / "dist",
]
# 方案2:Django 模板 + Vue/React 组件(渐进增强)
# 模板中挂载 Vue/React 组件
# templates/dashboard.html
{% extends "base.html" %}
{% block content %}
<div id="app"></div>
<script type="module" src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
{% endblock %}
# 方案3:Django 模板 + HTMX(轻量级交互增强)
# 无需前端框架,用 HTML 属性声明式交互
# templates/partials/task_list.html
{% for task in tasks %}
<div class="task" hx-target="#task-{{ task.id }}" hx-swap="outerHTML">
<span>{{ task.title }}</span>
<button hx-post="/tasks/{{ task.id }}/toggle"
hx-target="#task-{{ task.id }}"
hx-swap="outerHTML">
{% if task.done %}✅{% else %}⬜{% endif %}
</button>
</div>
{% endfor %}
# Django view 返回局部 HTML
from django.template.loader import render_to_string
from django.http import HttpResponse
def toggle_task(request, task_id):
task = Task.objects.get(id=task_id)
task.done = not task.done
task.save()
html = render_to_string('partials/task_item.html', {'task': task})
return HttpResponse(html)
代码示例3:Jinja2 独立使用(非 Web 场景)
from jinja2 import Environment, FileSystemLoader, select_autoescape
# 独立使用 Jinja2
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=select_autoescape(['html', 'xml']),
trim_blocks=True,
lstrip_blocks=True,
)
# 生成配置文件
nginx_template = env.from_string("""
server {
listen {{ port }};
server_name {{ domain }};
{% for location in locations %}
location {{ location.path }} {
proxy_pass {{ location.upstream }};
{% if location.websocket %}
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
{% endif %}
}
{% endfor %}
}
""")
config = nginx_template.render(
port=8080,
domain="example.com",
locations=[
{"path": "/", "upstream": "http://127.0.0.1:3000", "websocket": False},
{"path": "/ws", "upstream": "http://127.0.0.1:8000", "websocket": True},
]
)
print(config)
# 批量生成邮件
email_template = env.get_template('email/welcome.html')
for user in users:
html = email_template.render(user=user, app_name="MyApp")
send_email(user.email, "欢迎加入", html)
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| XSS 攻击 | `{{ var | safe }}` 跳过转义 |
| 模板继承多层覆盖 | 子 block 覆盖父 block | 用 {{ super() }} 保留父 block 内容 |
| 模板中业务逻辑 | 模板里写复杂 Python 逻辑 | 逻辑放在视图,模板只做展示 |
| 静态文件缓存 | 浏览器缓存旧版本 | url_for('static', v=app_version) 加版本号 |
| CSRF 缺失 | 表单未加 CSRF Token | {% csrf_token %}(Django)或手动添加 |
最佳实践
- 模板只做展示,业务逻辑放在视图层
- 基础模板定义布局,子模板只覆盖
block - 宏用于可复用 UI 组件(分页、卡片、表单)
- 自定义过滤器封装格式化逻辑
- 用户输入永远自动转义,不用
|safe - 新项目优先前后端分离(API + 前端框架)
- 渐进增强用 HTMX,比全 SPA 更轻量
面试题
Q1: Jinja2 的自动转义是如何工作的?
Jinja2 对
{{ var }}输出的内容自动进行 HTML 实体转义(<→<等),防止 XSS 攻击。{{ var|safe }}跳过转义,仅用于可信内容。Flask 默认对.html/.htm/.xml/.xhtml启用自动转义,Django 默认对所有模板启用。开发者不应关闭自动转义,只对确认安全的内容使用safe过滤器。
Q2: 模板继承和 {% include %} 有什么区别?
继承(
extends)是整体布局替换:子模板继承父模板的完整结构,用block覆盖指定区域。一个模板只能继承一个父模板。include是片段插入:将子模板的渲染结果插入当前位置,不涉及 block 覆盖。继承用于页面整体布局(导航/侧栏/页脚),include 用于可复用片段(评论组件、广告位)。include 可以传递变量:{% include "item.html" with item=post %}。
Q3: SSR 和 CSR 如何选择?
SSR 适合:内容型网站(博客/新闻/文档)、SEO 要求高、管理后台、首屏速度优先。CSR 适合:交互密集型应用(在线编辑器/仪表盘)、需要离线能力、团队前后端分离开发。混合方案:Next.js/Nuxt.js 的同构渲染,首屏 SSR + 后续 CSR。Django/Flask 项目可结合 HTMX 实现”SSR + 轻量交互”,比全 SPA 更简单。
Q4: Jinja2 的宏和 Python 函数有什么区别?
宏(
macro)是模板级别的可复用片段,输出 HTML,在模板中用{% macro name() %}定义,{{ name() }}调用。Python 函数是业务逻辑,在视图层定义,通过context_processor注入模板。宏适合 UI 组件(分页/卡片),函数适合数据计算。宏不能访问 Python 对象的方法,只能操作传入的参数。宏可导入:{% from "macros.html" import pagination %}。
Q5: 如何在 Django 中集成 Vue/React 前端?
三种方案:1) 前后端完全分离——Django 只提供 API(DRF),Vue/React 独立项目,构建后放到 Django 的 static 目录或独立部署;2) Django 模板 + Vue/React 组件——模板中
<div id="app">挂载 Vue/React 应用,适合渐进式迁移;3) Django 模板 + HTMX——无前端框架,用 HTML 属性实现交互,最简单。新项目推荐方案 1,已有项目迁移推荐方案 2 或 3。
Q6: Jinja2 的 call 块有什么用途?
call块允许向宏传递一段模板内容(类似 React 的 children/插槽)。定义:{% macro dialog(title) %}<div><h1>{{ title }}</h1>{{ caller() }}</div>{% endmacro %}。调用:{% call dialog("确认") %}<p>确定删除?</p>{% endcall %}。caller()在宏内部输出 call 块包裹的内容。这比纯参数传递更灵活,适合组件化的模板开发。
Q7: 如何防止模板注入攻击?
Jinja2 的自动转义防止 XSS,但还有其他风险:1) 不要用
|safe处理用户输入;2) 不要在模板中直接eval用户数据;3) 沙箱模式(SandboxedEnvironment)限制模板可访问的对象和属性;4) 不要让用户提交模板内容并渲染;5) 敏感操作在视图中完成,模板只负责展示。Flask 默认使用沙箱环境,Django 模板语言更安全(默认不允许调用方法)。
Q8: Django 模板语言和 Jinja2 有什么区别?
Django 模板语言(DTL)更严格:不允许在模板中调用方法(
user.get_name()不行)、不支持表达式运算、过滤器语法不同。Jinja2 更灵活:支持 Python 表达式、方法调用、宏、call块、行语句。Django 1.8+ 可以切换模板引擎为 Jinja2,但大多数 Django 项目仍用 DTL(因为 Admin/第三方包基于 DTL)。Flask 默认 Jinja2。如果项目同时用 Django 和 Jinja2,需要注意模板语法差异。
相关链接: