静态文件就是磁盘上的原样内容,服务器不加工,读出来就发走。CSS、图片、JS 都属于这一类。
模板是「带空位的 HTML」。{{ }} 里放变量,{% %} 里放控制语句,渲染完才发出去。
Jinja2 默认开启 HTML 转义。 少了这一层,任何能留言的人都能塞一段 script 进来,
别人一打开就被盗号——这就是 XSS。
上一篇的页面只有一个字符串,这一篇让它长得像样。
教材第 9 章中间两节是【访问静态网页文件】和【认识样板与模板引擎】。
两件事一起讲,因为一个真页面两者都要:样式和图走静态文件,数据走模板。
演示里的模板渲染是真跑的:Flask 起在后台线程,requests 把渲染结果拿回来打印。
那个转义效果也是真的,你能看到 <script>。
一、静态文件:原样发,不过模板
CSS、图片、JS、字体,这些文件的内容是固定的,服务器不需要加工,读出来发走就行。这类叫静态文件。
Flask 默认把 static 目录整个映射到 /static/ 下。 你把 style.css 放进 static/,
访问 /static/style.css 就能拿到。
演示第一段:/static/style.css 返回 200,类型 text/css;/static/nope.css 返回 404。
返回的正文里有 \r\n,那是 Windows 的换行符。
服务器原样发,不转成 \n。 静态文件的特点就是这个「原样」。
目录名和网址前缀都可以改,但要改得一致,否则就是一片 404。
改法是在 Flask(__name__, static_folder='...', static_url_path='...') 里指定。
二、模板:数据填进骨架
要展示一张随时会变的报价表,不能每次都拼字符串。
做法是先把 HTML 写好,把要变的地方留成空位,运行时再填。
这个「带空位的 HTML」叫模板。Flask 用的是 Jinja2。
模板文件放在 templates/ 目录下,跟 static/ 平级。
| 写法 | 干什么 |
|---|---|
{{ 变量 }} |
把值填进来 |
{% for x in xs %} |
循环,要配 {% endfor %} |
{% if 条件 %} |
判断,要配 {% endif %} |
{{ x|length }} |
过滤器:取长度 |
{{ x|format(...) }} |
过滤器:格式化 |
演示第二段渲染出来的 HTML 里,三行 li 是 for 展开的,其中一个带 class="hot" 是 if 决定的,
末尾「共 3 项,合计 4.20 元」是过滤器算的。
模板里写不了复杂的逻辑,只能做这种简单变换。这是故意的——免得业务代码跑到页面里来,
最后没人说得清某个数是在哪儿算的。
渲染用 render_template('文件名.html', 变量=值)。变量以关键字参数传进去,模板里直接用名字引用。
三、模板继承:头尾只写一次
一个网站十几个页面,每个都有导航和页脚。复制粘贴十几份,改一个字要改十几遍,而且总有一个漏掉。
模板继承解这个:写一个父模板,把公共部分(导航、页脚)放进去,
中间留一个 {% block 名字 %}{% endblock %} 当空位。
子模板第一行写 {% extends 'base.html' %},然后只填那个 block。
渲染时,父模板的导航和页脚自动带上,中间换成子模板的内容。
改一处,整站生效。 这是继承唯一但足够充分的理由。
演示第三段渲染出来的是:
<nav>首页 | 台账 | 帮助</nav> 加子模板的 <h1>今日报价</h1> 再加 <footer>© 2026</footer>。
三块拼在一起,但只有中间那块是子模板写的。
一个父模板可以有多个 block(标题、侧栏、正文),子模板想填哪个填哪个,不填就用父模板的默认内容。
四、模板会自动转义
演示第四段往模板里塞了一段 <script>alert("x")</script>,
返回的正文变成了 <script>alert("x")</script>。
尖括号被换成了 < 和 >,浏览器就把这段当成文字显示,不会当代码执行。
这叫 HTML 转义,Jinja2 默认就开着。你不用做任何事。
如果没这一层会怎样? 任何能写留言的人都可以塞一段 script 进来。
别人一打开那个页面,脚本就在他的浏览器里跑起来——可以读他的 Cookie、用他的身份发请求。
这就是 XSS(跨站脚本)。
第 12 章做留言板会再遇到它。
到那时候你会发现:转义是默认的,反而是「不转义」需要显式声明(写 |safe)。
这个默认值选得很聪明。
什么时候该用 |safe?几乎不该。
只有你完全确定那段内容是安全的 HTML(比如自己生成的表格片段)才用。用户输入一律不许 |safe。
五、动手 Demo:静态文件和模板
起一个带 static/ 和 templates/ 的 Flask 应用,先请求静态文件。
再让模板渲染一张报价表,看 for、if、过滤器各自的效果。
最后塞一段 script 进去,看它怎么被转义掉。
import logging, os, tempfile, threading
from flask import Flask, render_template_string
from werkzeug.serving import make_server
import requests
logging.getLogger('werkzeug').setLevel(logging.ERROR)
d = tempfile.mkdtemp()
os.makedirs(os.path.join(d, 'static'))
open(os.path.join(d, 'static', 'style.css'), 'w').write(
'body { font-family: sans-serif; }\n.price { color: #c00; }\n')
app = Flask(__name__, static_folder=os.path.join(d, 'static'))
TPL = '''<h1>今日材料报价</h1>
<ul>
{% for it in items %}
<li class="{{ 'hot' if it.hot else '' }}">{{ it.name }} — {{ '%.2f' % it.price }} 元</li>
{% endfor %}
</ul>
<p>共 {{ items|length }} 项,合计 {{ '%.2f' % (items|sum(attribute='price')) }} 元</p>'''
class It:
def __init__(self, name, price, hot=False):
self.name, self.price, self.hot = name, price, hot
@app.route('/')
def home():
items = [It('六角螺栓 M8', 1.5), It('膨胀螺栓 10mm', 2.3, True), It('不锈钢垫片', 0.4)]
return render_template_string(TPL, items=items)
@app.route('/echo')
def echo():
from flask import request
return render_template_string('<p>你输入的是:{{ s }}</p>', s=request.args.get('s', ''))
srv = make_server('127.0.0.1', 0, app)
B = 'http://127.0.0.1:%d' % srv.server_port
threading.Thread(target=srv.serve_forever, daemon=True).start()
print('① 静态文件:原样发,不过模板')
for path in ('/static/style.css', '/static/nope.css'):
r = requests.get(B + path, timeout=5)
body = repr(r.text[:38]) if r.status_code == 200 else '-'
print(' %-20s %d %s %s' % (path, r.status_code, r.headers.get('Content-Type', ''), body))
print(' 静态文件就是磁盘上的原样内容,服务器不加工,只是读出来发走。')
print('② 模板:数据填进骨架')
r = requests.get(B + '/', timeout=5)
for ln in r.text.strip().splitlines():
if ln.strip():
print(' ' + ln)
print(' {{ }} 里放变量,{% %} 里放控制语句。for 循环把列表展开成多行,if 决定某个 class 要不要加。')
print(' |length 是过滤器,取长度;|sum 也是过滤器。')
print('③ 模板继承:头尾只写一次')
BASE = ('<html><head><title>今日报价</title></head><body>'
'<nav>首页 | 台账 | 帮助</nav>{% block main %}{% endblock %}'
'<footer>© 2026</footer></body></html>')
CHILD = '{% extends "base" %}{% block main %}<h1>今日报价</h1>{% endblock %}'
from jinja2 import DictLoader, Environment
env = Environment(loader=DictLoader({'base': BASE, 'page': CHILD}), autoescape=True)
print(' ' + env.get_template('page').render())
print(' 子模板 extends 父模板,只填 block 里的空。导航和页脚写在父模板一次,所有页面自动带上。')
print('④ 模板会自动转义')
r = requests.get(B + '/echo', params={'s': '<script>alert("x")</script>'}, timeout=5)
print(' 模板里塞进一段 script,返回的正文是:')
print(' ' + r.text.strip())
print(' 尖括号被换成了 < 和 >,浏览器就把这段当成文字显示,不会当代码执行。')
print(' 这叫 HTML 转义,Jinja2 默认就开着。')
srv.shutdown()
实跑输出:
--------------------------------------------------------------------
静态文件和模板
--------------------------------------------------------------------
① 静态文件:原样发,不过模板
--------------------------------------------------------------------
/static/style.css 200 text/css; charset=utf-8
正文 'body { font-family: sans-serif; }\r\n.pric'
/static/nope.css 404
静态文件就是磁盘上的原样内容,服务器不加工,只是读出来发走。CSS、图片、JS 都属于这一类。
Flask 默认把 static 目录整个映射到 /static/ 下。
目录名和网址前缀都可以改,但要改得一致,否则就是一片 404。
② 模板:数据填进骨架
--------------------------------------------------------------------
<h1>今日材料报价</h1>
<ul>
<li class="">六角螺栓 M8 — 1.50 元</li>
<li class="hot">膨胀螺栓 10mm — 2.30 元</li>
<li class="">不锈钢垫片 — 0.40 元</li>
</ul>
<p>共 3 项,合计 4.20 元</p>
{{ }} 里放变量,{% %} 里放控制语句。for 循环把列表展开成多行,if 决定某个 class 要不要加。
|length 是过滤器,取长度;|format 也是过滤器,跟 Python 的 % 一样。
模板里写不了复杂的逻辑,只能做这种简单变换——这是故意的,免得业务代码跑到页面里来。
③ 模板继承:头尾只写一次
--------------------------------------------------------------------
<html><head><title>今日报价</title></head><body>
<nav>首页 | 台账 | 帮助</nav>
<h1>今日报价</h1>
<footer>© 2026</footer></body></html>
子模板 extends 父模板,只填 block 里的空。
导航和页脚写在父模板一次,所有页面自动带上——改一处,整站生效。
没有继承的话,十几个页面各有一份导航,改一个字要改十几遍,而且总有一个漏掉。
④ 模板会自动转义
--------------------------------------------------------------------
模板里塞进一段 script,返回的正文是:
<p>你输入的是:<script>alert("x")</script></p>
尖括号被换成了 < 和 >,浏览器就把这段当成文字显示,不会当代码执行。
这叫 HTML 转义,Jinja2 默认就开着。
如果没这一层,任何能写留言的人都可以塞一段 script 进来,别人一打开页面就被盗号——这就是 XSS。
第 12 章做留言板会再遇到它。
第一段两个路径的对照很直观:有文件就 200 加内容,没文件就 404。 注意正文里的换行是 \r\n,说明服务器原样发,没做任何加工。 第二段渲染出来的 HTML 要看两处:li 有三行(for 展开的),第二个带 class=hot(if 决定的), 末尾那句是过滤器算的合计。 第三段那个继承渲染结果只有一行,但里面有 nav、h1、footer 三块,只有 h1 是子模板写的。 改 nav 只需要改父模板一次。 第四段最值得看:输入的是尖括号,输出的是 < 和 >。 这一层挡住的正是 XSS——没有它,谁都能在你的页面上跑脚本。
30 秒实验
- 把演示第二段的
<li class="{{ 'hot' if it.hot else '' }}">改成固定写class="hot", 看三行是不是都带上了。 - 在演示第四段的 URL 里把 params 换成手动拼
?s=<script>,看返回的正文有区别没有。 - 把演示第三段的
autoescape=True改成False,再渲染一次,看 nav 里的内容有没有被转义(提示: 这时候如果模板里含用户输入,就危险了)。
常见误区
- 「静态文件也要过模板。」不过。服务器原样读出来发走,一个字都不改。
这也意味着你不能在 CSS 里用
{{ }}。 - 「模板里可以写复杂的 Python 逻辑。」只能做简单变换。 这是故意的,免得业务代码跑到页面里,最后没人说得清某个数在哪算的。
- 「转义会影响显示,用户输入直接用
|safe就好。 」|safe关掉的正是防 XSS 的那一层。 用户输入一律不许用它。