Flask 总结笔记

0. 概述

一心只为能用,只总结要点
主要目的是一些简单的展示功能,更深的内容以后用到了再看(反正到时候也差不多都忘了)
细致的笔记可以直接看第一个参考资料

参考资料:
Flask 入门笔记
Flask 快速入门
Flask 教程

1. 项目结构

假设我们编辑完了代码,在项目里执行flask run即可运行调试服务器
flask 默认app.py为代码入口,默认为普通模式,这两个设置与两个环境变量有关:FLASK_APPFLASK_ENV

1.1 管理环境变量

可以通过一个小工具管理环境变量:python-dotenv

1
pip install python-dotenv

python-dotenv 会自动读取两个文件中的设置

  • .env:存放系统相关的敏感数据,一般写进.gitignore文件
  • .flaskenv:存放与 flask 相关的环境变量
1
2
3
# .flaskenv 文件
# FLASK_APP = app.py
FLASK_ENV=development

1.2 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 导入 flask 并程序对象
from flask import Flask
app = Flask(__name__)

# 声明一个视图函数(view funciton)
# 然后对其进行注册(可以绑定多个)
@app.route('/')
@app.route('/hello')
def hello():
return 'Welcome to My Watchlist!'

# 特殊的错误处理视图函数
@app.errorhandler(404) # 传入要处理的错误代码
def page_not_found(e): # 接受异常对象作为参数
user = User.query.first()
return render_template('404.html', user=user), 404 # 返回模板和状态码

2. 模板

可以通过渲染模板获得最终的 HTML 文件,模板文件通常放在项目根目录中的templates文件夹中

2.1 模板语法

语法 作用
{{变量}} 标记变量
{%语句%} 标记语句,如 if,for
{#注释#} 注释
{{var | filter}} 过滤器

各种内置过滤器

.操作符可以读取元素的子属性,过滤器则相当于一个函数

主页模板例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!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/tutori al">HelloFlask</a></small
>
</footer>
</body>
</html>

2.2 渲染模板

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
from flask import Flask, render_template
app = Flask(__name__)

# 用来渲染的数据
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'},
]


@app.route('/')
def index():
# 渲染模板
return render_template('index.html', name=name, movies=movies)


# 也可以使用上下文管理器给渲染注入变量
# 写明这个后再渲染模板就不用传入user变量了
@app.context_processor
def inject_user(): # 函数名可以随意修改
user = User.query.first()
return dict(user=user) # 需要返回字典,等同于 return {'user': user}

2.3 模板继承机制

父模板

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
<!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/tutorial">HelloFlask</a></small
>
</footer>
</body>
</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
28
29
30
<!-- 继承父类模板  -->
{% extends 'base.html' %}
<!-- 填充父类模板留出的位置 -->
<!-- 默认行为为覆盖 -->
<!-- 可以使用特殊变量把父类的内容填充进去:{{ super() }} -->
{% 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 %}

3. 静态文件

在模板文件中可以通过 url_for 函数生成静态文件的 url:

1
2
<!-- 相当于<img src="/static/foo.jpg" /> -->
<img src="{{ url_for('static', filename='foo.jpg') }}" />

静态文件主要是 css 文件,图片文件,js 文件之类的
可以通过一些前端框架搞这些东西:
bootstrap
Semantic-UI
Foundation

Bootstrap-Flask可以简化 Bootstrap 的使用

4. 数据库

参考资料:
Flask 入门教程:数据库
Flask-SQLAlchemy 用户指南

4.1 sqlalchemy

首先安装 flask-sqlalchemy 包以在 flask 中使用 sqlalchemy

1
pip install flask-sqlalchemy

然后在程序入口根据程序实例初始化数据库

1
2
3
4
5
6
from flask_sqlalchemy import SQLAlchemy  # 导入扩展类

app = Flask(__name__)

# 初始化扩展,传入程序实例 app
db = SQLAlchemy(app)

然后对数据库进行配置

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)

4.2 数据模型

1
2
3
4
5
6
7
8
class User(db.Model):  # 表名将会是 user(自动生成,小写处理)
id = db.Column(db.Integer, primary_key=True) # 主键
name = db.Column(db.String(20)) # 名字

class Movie(db.Model): # 表名将会是 movie
id = db.Column(db.Integer, primary_key=True) # 主键
title = db.Column(db.String(60)) # 电影标题
year = db.Column(db.String(4)) # 电影年份

4.3 命令行操作

可以使用如下方式创建和删除数据库

1
2
3
4
(env) $ flask shell
>>> from app import db
>>> db.create_all()
>>> db.drop_all()

也可以自己创建一些命令

1
2
3
4
5
6
7
8
9
10
import click

@app.cli.command() # 注册为命令
@click.option('--drop', is_flag=True, help='Create after drop.') # 设置选项
def initdb(drop):
"""Initialize the database."""
if drop: # 判断是否输入了选项
db.drop_all()
db.create_all()
click.echo('Initialized database.') # 输出提示信息
1
2
flask initdb
flask initdb --drop

4.4 数据库操作

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
from app import User, Movie

# ===============================
# ---------- 增加数据 ------------
# ===============================
user = User(name='Grey Li')
db.session.add(user)
db.session.commit()

# ===============================
# ---------- 查询数据 ------------
# ===============================
# 通用格式:<模型类>.query.<过滤方法(可选)>.<查询方法>
movie = Movie.query.first()
Movie.query.filter_by(title='Mahjong').first()

# ===============================
# ---------- 更新数据 ------------
# ===============================
movie = Movie.query.get(2)
movie.title = 'WALL-E'
db.session.commit()

# ===============================
# ---------- 删除数据 ------------
# ===============================
movie = Movie.query.get(1)
db.session.delete(movie)
db.session.commit()

5. 表单

首先需要在模板中定义表单

1
2
3
4
5
6
7
8
9
10
11
<form method="post">
<!-- 指定提交方法为 POST -->
<label for="name">名字</label>
<input type="text" name="name" id="name" /><br />
<!-- 文本输入框 -->
<label for="occupation">职业</label>
<input type="text" name="occupation" id="occupation" /><br />
<!-- 文本输入框 -->
<input type="submit" name="submit" value="登录" />
<!-- 提交按钮 -->
</form>

然后写处理表单的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import request, url_for, redirect, flash

# 注册视图函数的时候,添加非默认的POST请求方式
@app.route('/', methods=['GET', 'POST'])
def index():
# 在内部根据请求方法的不同进行不同的处理
if request.method == 'POST':
# 根据表单内的name属性获取表单数据
title = request.form.get('title')
year = request.form.get('year')
# 验证数据
if not title or not year or len(year) > 4 or len(title) > 60:
flash('Invalid input.') # 显示错误提示
return redirect(url_for('index')) # 重定向回主页
# 数据库操作
movie = Movie(title=title, year=year) # 创建记录
db.session.add(movie) # 添加到数据库会话
db.session.commit() # 提交数据库会话
flash('Item created.') # 显示成功创建的提示
return redirect(url_for('index')) # 重定向回主页

movies = Movie.query.all()
return render_template('index.html', movies=movies)

接受视图函数中传来的 flash 信息

1
2
3
4
5
6
<!-- 插入到页面标题上方 -->
<!-- flash('Invalid input.') # 显示错误提示 -->
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
<h2>...</h2>

6. 测试

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
import unittest

from module_foo import sayhello


class SayHelloTestCase(unittest.TestCase):

# 测试初始化
def setUp(self):
pass

# 测试完的清理工作
def tearDown(self):
pass

# 测试用例以 test_ 开头
# 测试函数 1
def test_sayhello(self):
# 对目标的调用
rv = sayhello()
# 对结果的判断
self.assertEqual(rv, 'Hello!')

# 测试函数 2
def test_sayhello_to_somebody(self):
rv = sayhello(to='Grey')
self.assertEqual(rv, 'Hello, Grey!')


if __name__ == '__main__':
unittest.main()

常用来判断结果的断言

  • assertEqual(a, b)
  • assertNotEqual(a, b)
  • assertTrue(x)
  • assertFalse(x)
  • assertIs(a, b)
  • assertIsNot(a, b)
  • assertIsNone(x)
  • assertIsNotNone(x)
  • assertIn(a, b)
  • assertNotIn(a, b)

常用的测试方法可以参考:测试方法

7. 组织代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
├── .flaskenv                   # Flask 环境变量
├── test_watchlist.py # 单元测试(也可以做成一个包)
└── watchlist # 主程序包(需要在flaskenv中修改环境变量)
├── __init__.py # 入口程序
├── commands.py # 命令行操作
├── errors.py # 错误处理(404)
├── models.py # 数据库模型
├── views.py # 视图函数
├── static # 静态资源
│ ├── favicon.ico
│ ├── images
│ │ ├── avatar.png
│ │ └── totoro.gif
│ └── style.css
└── templates # 模板文件
├── base.html
├── edit.html
├── errors
│ ├── 400.html
│ ├── 404.html
│ └── 500.html
├── index.html
├── login.html
└── settings.html

8. 部署上线

创建虚拟环境
部署上线