0%

Flask基础开发

需要的前置知识有Python基础和前端老三样基础,以及Linux的基础
这些我都没什么问题,要是学习过程中遇到什么奇奇怪怪的东西,到时候再去学习也是可以的

全过程我都会用wsl来操作

前置操作

我曾经的日常操作,但是因为太久没操作导致生疏?那不妨好好记录一下

  • 创建虚拟环境
1
2
3
4
5
6
/mnt/f/watchlist$ python3 -m venv env
/mnt/f/watchlist$ .env/bin/activate
-bash: .env/bin/activate: No such file or directory
/mnt/f/watchlist$ . env/bin/activate
(env) /mnt/f/watchlist$ deactivate
/mnt/f/watchlist$

python3直接调用内置的venv模块,然后==. env/bin/activate==激活,要退出的话就直接deactivate

我操,我这虚拟环境没自带pip,破防了,还得自己装

OK,装flask的过程中还是遇到了不测,Ubuntu的通病

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(env) caochuhan@DESKTOP-B9Q8MAA:/mnt/f/watchlist$ pip install Flask
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.

If you wish to install a non-Debian-packaged Python package,
create a virtual environment using python3 -m venv path/to/venv.
Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
sure you have python3-full installed.

If you wish to install a non-Debian packaged Python application,
it may be easiest to use pipx install xyz, which will manage a
virtual environment for you. Make sure you have pipx installed.

See /usr/share/doc/python3.12/README.venv for more information.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

没话说,查查怎么解决

最后还是屈服了,使用pipx(很奇怪,难道是我虚拟环境的创建过程有误才导致了这种结果?)
而且pipx装的flask库根本不在虚拟环境中,而是在我的bin目录下!
教程有问题,我重新解决这个问题

成功解决,接下来我来说一下我的方法

自打ubuntu24面世以来,就有pip install会产生上述报错的问题,但是我很奇怪的是,我明明启用了venv,按道理来说是不会有报错的
除非,我的pip不是虚拟环境中的pip,而是我本地bin目录下的pip
验证

1
2
which pip
/usr/bin/pip

果然被做局了
因此只能重新创建一个虚拟环境,这个虚拟环境得自带pip

  1. 安装系统的 venv 支持
1
2
sudo apt update
sudo apt install python3-full python3-venv
  1. 删除旧环境
1
rm -rf env
  1. 重建虚拟环境
1
python3 -m venv env
  1. 激活并安装 Flask
1
2
3
source env/bin/activate
python -m pip install --upgrade pip
pip install flask
  1. 验证
1
2
which pip
/mnt/f/watchlist/env/bin/flask

终于解决问题了

helloFlask

简单的引导

1
2
3
4
5
6
7
from flask import Flask

app=Flask(__name__)

@app.route('/')
def Hello():
return 'Welcome to my watchlist'

很容易理解,搞web的基本上不用看就能知道是什么意思
导入Flask类,然后通过实例化这个类,创建了对象app

然后外面直接==注册==一个处理函数,用于处理某个请求(官方叫做视图函数),其实就是一个请求处理函数
所谓注册,就是给这个函数戴上一个装饰器帽子。我们使用 app.route() 装饰器来为这个函数绑定对应的 URL,当用户在浏览器访问这个 URL 的时候,就会触发这个函数,获取返回值,并把返回值显示到浏览器窗口

填入 app.route() 装饰器的第一个参数是 URL 规则字符串,这里的 /指的是根地址。
我们只需要写出相对地址,主机地址、端口号等都不需要写出。所以说,这里的 / 对应的是主机名后面的路径部分,完整 URL 就是 http://localhost:5000/。
如果我们这里定义的 URL 规则是 /hello,那么完整 URL 就是 http://localhost:5000/hello。

管理环境变量

在启动flask的时候,通常是和FLASK_APP,FLASK_DEBUG这两个环境变量打交道,程序名为app.py,所以暂时可以不去动FLASK_APP
FLASK_DEBUG,老演员了,不知道见过多少次了,什么什么计算pin码,进去任意python代码执行,都和这个有关

这里安装用来自动导入系统环境变量的python-dotenv
主要是读取.env文件还有.flaskenv文件,这里我直接touch创建,然后.env不管它,直接在.flaskenv文件中写入

1
FLASK_DEBUG=1
1
2
3
4
5
6
7
8
9
10
from flask import Flask

app=Flask(__name__)

@app.route('/')
def Hello():
return '<h1>Hello Totoro!</h1><img src="http://helloflask.com/totoro.gif">'
@app.route('/home')
def home():
return 'zhubi'

自己随意添加处理函数和它的修饰器,这个也很简单,就不多说什么

==url中自定义变量部分==
例如:

1
2
3
@app.route('/user/<name>')
def user_page(name):
return 'User page'

这里的name就作为了变量,user_page这个函数会处理所有类似”/user/name”的操作
例如/user/eminem,/user/kwansh,都会触发这个函数。
那就很自然的有一个想法,把这个变量自然的放在了操作函数中
接下来会学习有关escape操作,没进行这个操作的话恐怕是会被rce哩

1
2
3
4
5
from markupsafe import escape

@app.route('/user/<name>')
def user_page(name):
return f'User: {escape(name)}'

用户输入的数据会包含恶意代码,所以不能直接作为响应返回,需要使用 MarkupSafe(Flask 的依赖之一)提供的 escape() 函数对 name 变量进行转义处理,比如把 < 转换成 <。这样在返回响应时浏览器就不会把它们当做代码执行。
那我接下来打打看
确实打通了,可以执行rce,大家要是有兴趣的话可以自己试试
本地起的flask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask
from markupsafe import escape
import os
app=Flask(__name__)

@app.route('/')
def Hello():
return '<h1>Hello Totoro!</h1><img src="http://helloflask.com/totoro.gif">'
@app.route('/home')
def home():
return 'zhubi'
@app.route('/user/<path:name>')
def user_page(name):
return eval(f"f'{name}'")

攻击payload

1
http://localhost:5000/user/%7B__import__('os').popen('cat%20app.py').read()%7D

结果图
alt text
当然这里是我用了eval的原因,这里eval后面要是跟着escape的话,我打这个payload上去就直接给我干出debug了

1
2
3
@app.route('/user/<path:name>')
def user_page(name):
return eval(f"User: {escape(name)}")

大家也可以去试试,也可以练练计算pin码

修改视图函数名?

视图函数的名字是自由定义的,和 URL 规则无关。和定义其他函数或变量一样,只需要让它表达出所要处理页面的含义即可。

然后就是一个flask的url_for函数来生成url(有空我去看看这个库,指不定有点0day?),然后它接受的第一个参数就是端点值,默认为操作函数的名称

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask import url_for
from markupsafe import escape

# ...

@app.route('/')
def hello():
return 'Hello'

@app.route('/user/<name>')
def user_page(name):
return f'User: {escape(name)}'

@app.route('/test')
def test_url_for():
# 下面是一些调用示例(请访问 http://localhost:5000/test 后在命令行窗口查看输出的 URL):
print(url_for('hello')) # 生成 hello 视图函数对应的 URL,将会输出:/
# 注意下面两个调用是如何生成包含 URL 变量的 URL 的
print(url_for('user_page', name='greyli')) # 输出:/user/greyli
print(url_for('user_page', name='peter')) # 输出:/user/peter
print(url_for('test_url_for')) # 输出:/test
# 下面这个调用传入了多余的关键字参数,它们会被作为查询字符串附加到 URL 后面。
print(url_for('test_url_for', num=2)) # 输出:/test?num=2
return 'Test page'

大概测试一下这个exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1 - - [20/Jun/2025 13:50:24] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [20/Jun/2025 13:50:24] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [20/Jun/2025 13:50:36] "GET /user/eminem HTTP/1.1" 200 -
127.0.0.1 - - [20/Jun/2025 13:50:48] "GET /user/greyli HTTP/1.1" 200 -
127.0.0.1 - - [20/Jun/2025 13:51:11] "GET /user/hello HTTP/1.1" 200 -
/
/user/greyli
/user/peter
/test
/test?num=2
127.0.0.1 - - [20/Jun/2025 13:51:23] "GET /test HTTP/1.1" 200 -
/
/user/greyli
/user/peter
/test
/test?num=2
127.0.0.1 - - [20/Jun/2025 13:51:41] "GET /test?num=2 HTTP/1.1" 200 -
/
/user/greyli
/user/peter
/test
/test?num=2
127.0.0.1 - - [20/Jun/2025 13:51:44] "GET /test?num=2 HTTP/1.1" 200 -

你应该就能知道是什么意思了
第一课结束

模板

这次学习的主要是Jinja2模板(fenjing狂喜)
基于flask的SSTI就爱打jinja2

我们把包含变量和运算逻辑的 HTML 或其他格式的文本叫做模板,执行这些变量替换和逻辑计算工作的过程被称为渲染,这个工作由我们这一章要学习使用的模板渲染引擎——Jinja2 来完成。
按照默认的设置,Flask 会从程序实例所在模块同级目录的 templates 文件夹中寻找模板,我们的程序目前存储在项目根目录的 app.py 文件里,所以我们要在项目根目录创建这个文件夹

1
mkdir templates

模板基本语法

教程教的

基本老三样

1
2
3
"{{...}}"用来标记变量,也可直接输出运算的结果
"{%...%}"用来标记语句,例如if else
"{#...#}"用来写注释

exp

1
2
3
4
5
6
<h1>{{ username }}的个人主页</h1>
{% if bio %}
<p>{{ bio }}</p> {# 这里的缩进只是为了可读性,不是必须的 #}
{% else %}
<p>自我介绍为空。</p>
{% endif %} {# 大部分 Jinja 语句都需要声明关闭 #}

=={{...}}== 有它自己的用处,可以直接判断有没有 SSTI 注入点

1
2
3
4
{{ 1 + 2 }}           {# 输出 3 #}
{{ user.age * 2 }} {# 输出用户年龄乘以 2 #}
{{ 'Hello ' ~ user }} {# 字符串拼接,用 ~ 运算符 #}
{{ list|length }} {# 使用 filter(见下文)求长度 #}
我自学的

做安全的话,仅仅只了解上面的东西是绝对不够用的

  1. 过滤器

    过滤器(Filter)是对变量进行后处理的函数,以管道符 | 连接在变量或表达式之后

1
{{ variable | filter1(arg1, arg2) | filter2 }}

过滤器可以用内置的,也可以自己定义
exp

1
2
3
4
5
6
7
8
9
10
11
{# 用户列表:先过滤已启用,再按注册时间排序,最后取前 5 名的用户名 #}
<ul>
{% for name in users
|selectattr('enabled')
|sort(attribute='joined_at', reverse=True)
|map('username')
|list # 强制转为列表以便 slice
|slice(0,5) %}
<li>{{ name }}</li>
{% endfor %}
</ul>

和php://filter的那个过滤器有异曲同工之妙,都差不多

  1. 测试
    测试用于布尔判断,语法是 value is test 或 value is not test。常用测试:
    测试 功能
    defined 是否已定义
    undefined 是否未定义
    none 是否为 None
    even/odd 是否为偶数 / 奇数
    in 是否在序列或映射中
    exp
1
2
3
4
5
6
7
{% if user.age is even %}
年龄是偶数
{% endif %}

{% if item is not defined %}
该变量不存在
{% endif %}

编写主页模板

接下来像做项目一样一步步来,学习flask的应用逻辑和底层原理

要在templates/index.html做为主页模板,罗列一些信息,这里就直接借鉴教程的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ name }}'s Watchlist</title>
</head>
<body>
<h2>{{ name }}'s Watchlist</h2>
{# 使用 length 过滤器获取 movies 变量的长度 #}
<p>{{ movies|length }} Titles</p>
<ul>
{% for movie in movies %} {# 迭代 movies 变量 #}
<li>{{ movie.title }} - {{ movie.year }}</li> {# 等同于 movie['title'] #}
{% endfor %} {# 使用 endfor 标签结束 for 语句 #}
</ul>
<footer>
<small>&copy; 2018 <a href="http://helloflask.com/book/3">HelloFlask</a></small>
</footer>
</body>
</html>

这里也直接介绍了过滤器
介绍的也比较简单,要是上面的看不懂的话可以看下面的这个

1
{{ 变量|过滤器 }}

https://jinja.palletsprojects.com/en/3.0.x/templates/#builtin-filters
访问可查看所有的可用的过滤器

准备虚拟数据

这个很简单,在app.py中定义一下就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
name = 'Grey Li'
movies = [
{'title': 'My Neighbor Totoro', 'year': '1988'},
{'title': 'Dead Poets Society', 'year': '1989'},
{'title': 'A Perfect World', 'year': '1993'},
{'title': 'Leon', 'year': '1994'},
{'title': 'Mahjong', 'year': '1996'},
{'title': 'Swallowtail Butterfly', 'year': '1996'},
{'title': 'King of Comedy', 'year': '1999'},
{'title': 'Devils on the Doorstep', 'year': '1999'},
{'title': 'WALL-E', 'year': '2008'},
{'title': 'The Pork of Music', 'year': '2012'},
]

渲染主页模板

模板渲染函数之 render_template()

使用 render_template() 函数可以把模板渲染出来,必须传入的参数为模板文件名(相对于 templates 根目录的文件路径),这里即 ‘index.html’。为了让模板正确渲染,我们还要把模板内部使用的变量通过关键字参数传入这个函数(想想你index.html都定义了什么变量)
exp

1
2
3
@app.route('/')
def index():
return render_template('index.html', name=name, movies=movies)

render_template() 函数在调用时会识别并执行 index.html 里所有的 Jinja2 语句,返回渲染好的模板内容。在返回的页面中,变量会被替换为实际的值(包括定界符),语句(及定界符)则会在执行后被移除(注释也会一并移除)。

然后直接跑跑看
效果很成功,把定义的键值对的值都输出了(这个也是实现在index模板中写好的,只输出值)

第二课结束

静态文件

这个类比hexo博客搭建过程中的图片添加,会更加容易理解

静态文件(static files)和我们的模板概念相反,指的是内容不需要动态生成的文件。比如图片、CSS 文件和 JavaScript 脚本等。
在 Flask 中,我们需要创建一个 static 文件夹来保存静态文件,它应该和程序模块、templates 文件夹在同一目录层级,所以我们在项目根目录创建它:

1
$ mkdir static

你类比成在source目录下创建image文件夹来放置图片即可

生成静态文件url

这个我之前似乎在godyu神的博客之中见过,但是今天才知道原来是这么生成的

在HTML文件之中,要引入这些静态文件需要给出资源所在的url,为了更加的灵活,这些文件的url可以通过Flask提供的url_for()函数来生成

之前在第二章的时候就有说过这个函数了,对于静态文件来说 需要传入的端点值是static,同时使用filename参数来传入相对于static文件夹的文件路径

例如我们丢一个1.jpg进static文件夹的根目录
exp

1
<img src="{{ url_for('static', filename='1.jpg') }}">

他就会自己回调,花括号部分的调用会返回/static/1.jpg

原作者小提示

提示: 在 Python 脚本里,url_for() 函数需要从 flask 包中导入,而在模板中则可以直接使用,因为 Flask 把一些常用的函数和对象添加到了模板上下文(环境)里。

添加Favicon

1
2
3
Favicon(favourite icon) 是显示在标签页和书签栏的网站头像。你需要准备一个 ICO、PNG 或 GIF 格式的图片,大小一般为 16×16、32×32、48×48 或 64×64 像素。把这个图片放到 static 目录下,然后像下面这样在 HTML 模板里引入它:

templates/index.html:引入 Favicon

exp

1
2
3
4
<head>
...
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>

保存后刷新页面,即可在浏览器标签页上看到这个图片。

添加图片

在static下创建一个用于放图片的子文件夹
exp

1
2
$ cd static
$ mkdir images

exp

1
2
3
4
5
6
<h2>
<img alt="Avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ name }}'s Watchlist
</h2>
...
<img alt="Walking Totoro" src="{{ url_for('static', filename='images/totoro.gif') }}">

添加css

在static目录下创建一个CSS文件style.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* 页面整体 */
body {
margin: auto;
max-width: 580px;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
}

/* 页脚 */
footer {
color: #888;
margin-top: 15px;
text-align: center;
padding: 10px;
}

/* 头像 */
.avatar {
width: 40px;
}

/* 电影列表 */
.movie-list {
list-style-type: none;
padding: 0;
margin-bottom: 10px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}

.movie-list li {
padding: 12px 24px;
border-bottom: 1px solid #ddd;
}

.movie-list li:last-child {
border-bottom:none;
}

.movie-list li:hover {
background-color: #f8f9fa;
}

/* 龙猫图片 */
.totoro {
display: block;
margin: 0 auto;
height: 100px;
}

然后在页面的head标签内引入这个CSS文件

1
2
3
4
<head>
...
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
</head>

提示
当你把 CSS 写到单独的文件后,浏览器获取到这个文件后会对其进行缓存(其他静态文件同理,比如 JavaScript 文件)。Flask 从 2.0 版本开始支持自动重载静态文件的变化,如果你使用的仍然是旧版本的 Flask,那么每当你对 CSS 文件的内容进行更新后,都需要使用下面的快捷键清除缓存:

Google Chrome(Mac):Command + Shift + R
Google Chrome(Windows & Linux):Ctrl + F5
Firefox(Mac):Command + Shift + R
Firefox(Windows & Linux):Ctrl + F5
Safari:Command + Option + R

最后在html文件中设置class属性值,以便和对应的CSS定义关联起来

1
2
3
4
5
6
7
8
9
<h2>
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ name }}'s Watchlist
</h2>
...
<ul class="movie-list">
...
</ul>
<img alt="Walking Totoro" class="totoro" src="{{ url_for('static', filename='images/totoro.gif') }}">

第三课结束哈哈

数据库

使用 SQLAlchemy,一个python数据库,需要安装相对应的库,这里需要安装两个版本

1
(env) $ pip install flask-sqlalchemy==2.5.1 sqlalchemy==1.4.47

提示: Flask-SQLAlchemy 3.x / SQLAlchemy 2.x 版本有一些大的变化

导入扩展类然后实例化,传入flask程序实力

1
2
3
from flask_sqlalchemy import SQLAlchemy  # 导入扩展类
app = Flask(__name__)
db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app

设置数据库url

flask提供了一个统一的接口来获取这些配置变量,即Flask.config,配置变量的名称必须得用大写,写入的配置语句一般要放到扩展类实例化语句之前

这里就直接写个例子
下面写入了一个 SQLALCHEMY_DATABASE_URI 变量来告诉 SQLAlchemy 数据库连接地址:

1
2
3
import os
# ...
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(app.root_path, 'data.db')

注意这个配置变量的最后一个单词是uri,而不是url
对于这个变量值,不同的DBMS都有不同的格式,对于SQLite来说,这个值的格式如下
sqlite:////数据库文件的绝对地址

数据库的文件一般直接放项目根目录即可,app.root_path 返回程序实例所在模块的路径(目前来说,即项目根目录),我们使用它来构建文件路径。数据库文件的名称和后缀你可以自由定义,一般会使用 .db、.sqlite 和 .sqlite3 作为后缀

要是数据库是基于windows系统的,就只需要

sqlite:///
三道斜线即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import sys

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

WIN = sys.platform.startswith('win')
if WIN: # 如果是 Windows 系统,使用三个斜线
prefix = 'sqlite:///'
else: # 否则使用四个斜线
prefix = 'sqlite:////'

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭对模型修改的监控
# 在扩展类实例化前加载配置
db = SQLAlchemy(app)

当然也可以这么写,会更具有适应性

模板优化

假设我们现在要写一个404错误响应的页面
404.html
手搓一个,然后记得在app.py加个装饰器来处理错误的响应,然后当错误发生的时候就触发这个函数,然后响应404.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ user.name }}'s Watchlist</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
</head>
<body>
<h2>
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ user.name }}'s Watchlist
</h2>
<ul class="movie-list">
<li>
Page Not Found - 404
<span class="float-right">
<a href="{{ url_for('index') }}">Go Back</a>
</span>
</li>
</ul>
<footer>
<small>&copy; 2018 <a href="http://helloflask.com/book/3">HelloFlask</a></small>
</footer>
</body>
</html>
1
2
3
4
@app.errorhandler(404)  # 传入要处理的错误代码
def page_not_found(e): # 接受异常对象作为参数
user = User.query.first()
return render_template('404.html', user=user), 404 # 返回模板和状态码

和我们前面编写的视图函数相比,这个函数返回了状态码作为第二个参数,普通的视图函数之所以不用写出状态码,是因为默认会使用 200 状态码,表示成功。

使用模板继承组织模板

对于模板内容重复的问题,Jinja2 提供了模板继承的支持。这个机制和 Python 类继承非常类似:我们可以定义一个父模板,一般会称之为基模板(base template)。基模板中包含完整的 HTML 结构和导航栏、页首、页脚等通用部分。在子模板里,我们可以使用 extends 标签来声明继承自某个基模板。

基模板中需要在实际的子模板中追加或重写的部分则可以定义成块(block)。块使用 block 标签创建,

1
{% block 块名称 %}

作为开始标记,

1
{% endblock %} 或 {% endblock 块名称 %}

作为结束标记。通过在子模板里定义一个同样名称的块,你可以向基模板的对应块位置追加或重写内容。

编写基模板
templates/base.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ user.name }}'s Watchlist</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
{% endblock %}
</head>
<body>
<h2>
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ user.name }}'s Watchlist
</h2>
<nav>
<ul>
<li><a href="{{ url_for('index') }}">Home</a></li>
</ul>
</nav>
{% block content %}{% endblock %}
<footer>
<small>&copy; 2018 <a href="http://helloflask.com/book/3">HelloFlask</a></small>
</footer>
</body>
</html>

在基模板里,我们添加了两个块,一个是包含 head /head 内容的 head 块,另一个是用来在子模板中插入页面主体内容的 content 块。在复杂的项目里,你可以定义更多的块,方便在子模板中对基模板的各个部分插入内容。另外,块的名字没有特定要求,你可以自由修改。

在编写子模板之前,我们先来看一下基模板中的两处新变化。

第一处,我们添加了一个新的 meta 元素,这个元素会设置页面的视口,让页面根据设备的宽度来自动缩放页面,这样会让移动设备拥有更好的浏览体验:

1
<meta name="viewport" content="width=device-width, initial-scale=1.0">

第二处,新的页面添加了一个导航栏:

1
2
3
4
5
<nav>
<ul>
<li><a href="{{ url_for('index') }}">Home</a></li>
</ul>
</nav>

有了基模板,子模版的编写会变得很简单
templates/index.html:继承基模板的主页模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% extends 'base.html' %}

{% block content %}
<p>{{ movies|length }} Titles</p>
<ul class="movie-list">
{% for movie in movies %}
<li>{{ movie.title }} - {{ movie.year }}
<span class="float-right">
<a class="imdb" href="https://www.imdb.com/find?q={{ movie.title }}" target="_blank" title="Find this movie on IMDb">IMDb</a>
</span>
</li>
{% endfor %}
</ul>
<img alt="Walking Totoro" class="totoro" src="{{ url_for('static', filename='images/totoro.gif') }}" title="to~to~ro~">
{% endblock %}

第一行使用 extends 标签声明扩展自模板 base.html,可以理解成“这个模板继承自 base.html“。接着我们定义了 content 块,这里的内容会插入到基模板中 content 块的位置。

默认的块重写行为是覆盖,如果你想向父块里追加内容,可以在子块中使用 super() 声明,即

1
{{ super() }}。

404 错误页面的模板类似,如下所示:

templates/404.html:继承基模板的 404 错误页面模板

1
2
3
4
5
6
7
8
9
10
11
12
{% extends 'base.html' %}

{% block content %}
<ul class="movie-list">
<li>
Page Not Found - 404
<span class="float-right">
<a href="{{ url_for('index') }}">Go Back</a>
</span>
</li>
</ul>
{% endblock %}

结束,参考文献

https://tutorial.helloflask.com/
最适合入门的flask教程