FlaskWeb开发(1)Flask介绍

《FlaskWeb开发》阅读笔记

第一部分 Flask简介

程序基本结构

  • 完整的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # test.py
    from flask import Flask
    app = Flask(__name__)
    # 程序实例(Flask类的对象),Web服务器使用WSGI协议,把自客户端的所有请求都转交给这个对象处理
    # __name__参数即程序主模块或包的名字,决定程序的根目录

    @app.route('/') # URL到函数的映射关系
    def index(): # /对应的视图函数
    return '<h1>Hello World!</h1>' # 返回值称为响应

    @app.route('/user/<name>') # 指定路径,<>表示动态参数,默认使用字符串,也可定义类型: /user/<int:id>,匹配动态片段id 为整数的URL
    # Flask支持在路由中使用int、float和path类型(也是字符串,但不把斜线视作分隔符)
    def user(name): # /user/name对应的视图函数
    return '<h1>Hello, %s!</h1>' % name

    if __name__ == '__main__':
    app.run(debug=True) # run函数的参数用于设置服务器的操作模式,但Flask提供的Web服务器不适合在生产环境中使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ python test.py
    * Serving Flask app "test" (lazy loading)
    * Environment: production
    WARNING: This is a development server. Do not use it in a production deployment.
    Use a production WSGI server instead.
    * Debug mode: on
    * Restarting with stat
    * Debugger is active!
    * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

    Flask 没有内置的 manage 管理工具,一般目录结构都要自己创建,以下为一个项目的样例,该目录与后文无关。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ tree
    ├── test
    │ ├── static
    │ ├── templates
    │ ├── __init__.py
    │ ├── models.py
    │ ├── views.py
    ├── config.py
    ├── run.py
    └── tmp

    请求-响应循环

  • Flask 使用上下文临时把某些请求对象变为全局可访问——特定的变量在一个线程中全局可访问

    1
    2
    3
    4
    5
    6
    from flask import request 

    @app.route('/')
    def index():
    user_agent = request.headers.get('User-Agent')
    return '<p>Your browser is %s</p>' % user_agent

    image-20201231095726156

    • 程序上下文被推送(激活)后,才可以在线程中使用 current_app 和 g 变量

    • 同样的,请求上下文被推送(激活)后,才可以使用 request 和 session 变量

      1
      2
      3
      4
      5
      6
      7
      8
      # 在另一个newtest.py中测试以下代码
      >>> from test import app # app为之前test.py中Flask类的实例
      >>> from flask import current_app
      >>> app_ctx = app.app_context() # 获得一个程序的上下文
      >>> app_ctx.push()
      >>> current_app.name
      'test'
      >>> app_ctx.pop()
  • 检查 hello.py 的 URL 映射:app.url_map​

  • 请求钩子:用于在处理请求之前执行一些操作,如建立数据库链接或认证用户;通过注册为通用函数,在请求分发到视图函数之前或之后调用,以修饰器形式实现

    • 修饰器类型:
      • before_first_request:注册一个函数,在处理第一个请求之前运行
      • before_request:注册一个函数,在每次请求之前运行
      • after_request:注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行
      • teardown_request:注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行
    • 请求钩子函数和视图函数之间共享数据一般使用上下文全局变量 g,如 before_request 从数据库中加载已登录用户并保存到g.user,之后的视图函数从g.user获取用户
  • 视图函数的返回称为响应,一般响应就是一个字符串,作为HTML页面返回

    • 响应状态码:Flask 默认为200,表明请求处理成功;可将数字代码作为第二个返回值

      1
      2
      3
      @app.route('/') 
      def index():
      return '<h1>Bad Request</h1>', 400 # 返回一个400状态码,请求无效
    • 还可接收一个由 header 组成的字典作为第三个返回值

    • 还可创建 response 对象,以进一步设置响应:make_response()的参数同上,返回 response 对象

      1
      2
      3
      4
      5
      from flask import make_response
      ...
      response = make_response('<h1>This document carries a cookie!</h1>'python)
      response.set_cookie('answer', '42') # 设置cookie
      return response
    • 重定向响应:响应无页面文档,返回一个新的地址用于加载新的页面,常用于 Web 表单

      • 使用状态码302,指向地址由 Location 首部提供

      • 可使用第一种返回值生成,也可在 response 对象设定,也可用辅助函数 redirect()

        1
        2
        3
        from flask import redirect 
        ...
        return redirect('http://www.example.com')
    • abort() 函数生成特殊响应,用于处理错误,并将控制权交给 Web 服务器

      1
      2
      3
      4
      5
      6
      7
      8
      from flask import abort 

      @app.route('/user/<id>')
      def get_user(id):
      user = load_user(id)
      if not user:
      abort(404) # 动态参数id对应的用户不存在,返回状态码404
      return '<h1>Hello, %s</h1>' % user.name

命令行参数扩展

  • Flask-Script 是一个 Flask 扩展,为 Flask 添加一个命令行解析器(pip install flask-script

  • 使用例子:

    1
    2
    3
    4
    5
    6
    from flask.ext.script import Manager  # 扩展都在flask.ext命名空间下
    manager = Manager(app) # 程序实例作为参数

    # ...
    if __name__ == '__main__':
    manager.run()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    $ python hello.py 
    usage: hello.py [-h] {shell,runserver} ...

    positional arguments:
    {shell,runserver}
    shell 在 Flask 应用上下文中运行 Python shell
    runserver 运行 Flask 开发服务器:app.run()

    optional arguments:
    -h, --help 显示帮助信息并退出
    $ python hello.py runserver --help
    usage: hello.py runserver [-h] [-t HOST] [-p PORT] [--threaded]
    [--processes PROCESSES] [--passthrough-errors] [-d]
    [-r]
    ...

    $ python hello.py runserver --host 0.0.0.0 # 设置监听客户端连接的网络接口
    * Running on http://0.0.0.0:5000/
    * Restarting with reloader

模板

  • 模板是一个包含响应文本的文件,包含用占位变量表示的动态部分,在具体的上下文确定;真实值替换变量,再返回最终的响应,此过程称为渲染
  • Flask 使用了一个 Jinja2 模板引擎渲染模板

Jinja2

  • Flask 在程序文件夹中的 templates 子文件夹中寻找模板(类似Django)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from flask import Flask, render_template 
    # render_template集成了Jinja2模板引擎
    # ...

    @app.route('/')
    def index():
    return render_template('index.html')

    @app.route('/user/<name>')
    def user(name):
    return render_template('user.html', name=name) # 第一个参数是模板文件名,后面参数为键值对,对应模板变量的真实值
  • 模板中使用的{{ name }}结构表示一个变量,其他变量示例如下:

    1
    2
    3
    4
    <p>A value from a dictionary: {{ mydict['key'] }}.</p> 
    <p>A value from a list: {{ mylist[3] }}.</p>
    <p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
    <p>A value from an object's method: {{ myobj.somemethod() }}.</p>
    • 过滤器修改变量:{{ name|capitalize }}

      image-20201231111347862

      • 如果要显示变量中存储的HTML代码,则需要用 safe 过滤器
      • 不可信的值不能使用 safe 过滤器,如表单的输入文本
  • 控制结构

    • 条件控制

      1
      2
      3
      4
      5
      {% if user %} 
      Hello, {{ user }}!
      {% else %}
      Hello, Stranger!
      {% endif %}
    • 循环

      1
      2
      3
      {% for comment in comments %} 
      <li>{{ comment }}</li>
      {% endfor %}
    • 宏(类似于函数)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      {% macro render_comment(comment) %} 
      <li>{{ comment }}</li>
      {% endmacro %}
      <!--{% import 'macros.html' as macros %} 导入保存在单独文件的宏-->
      <ul>
      {% for comment in comments %}
      {{ macros.render_comment(comment) }}
      {% endfor %}
      </ul>
    • 模板 include 与模板继承

      1
      {% include 'common.html' %}
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <!--base.html-->
      <!--定义名为head、title和body的block,其元素可在衍生模板中修改-->
      <html>
      <head>
      {% block head %}
      <title>{% block title %}{% endblock %} - My Application</title>
      {% endblock %}
      </head>
      <body>
      {% block body %}
      {% endblock %}
      </body>
      </html>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <!--衍生模板-->
      {% extends "base.html" %}
      <!--基模板中的 3 个块被重新定义,引擎将其插入基模板对应位置-->
      {% block title %}Index{% endblock %}
      {% block head %}
      {{ super() }} <!--基模板中head的内容不是空的,因此需要super()获取原来的内容-->
      <style>
      </style>
      {% endblock %}
      {% block body %}
      <h1>Hello, World!</h1>
      {% endblock %}

Flask-Bootstrap

pip install flask-bootstrap

Bootstrap 是客户端框架,提供的用户界面组件可用于创建网页。服务器只需要提供引用了 Bootstrap 层叠样式表(CSS)和 JavaScript 文件的HTML响 应, 并 在 HTML、CSS 和 JavaScript 代码中实例化所需组件。

Flask 扩展一般都在创建程序实例时初始化,之后在程序中使用一个包含所有 Bootstrap 文件的基模板。

1
2
3
from flask.ext.bootstrap import Bootstrap  
# ...
bootstrap = Bootstrap(app)
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
{% extends "bootstrap/base.html" %} 

{% block title %}Flasky{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}

{% block content %}
<div class="container">
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
</div>
{% endblock %}

image-20210102150153353

很多 Block 都是 Flask-Bootstrap 自用的,因此如果程序需要向已经有内容的块
中添加新内容,必须使用 Jinja2 提供的 super() 函数

1
2
3
4
{% block scripts %} 
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}

错误页面

  • 最常见的错误代码:

    • 404:客户端请求未知页面或路由
    • 500:有未处理的异常
  • 自定义处理程序(类似视图函数)

    1
    2
    3
    4
    5
    6
    7
    @app.errorhandler(404) 
    def page_not_found(e):
    return render_template('404.html'), 404

    @app.errorhandler(500)
    def internal_server_error(e):
    return render_template('500.html'), 500
  • 模板继承机制建立模板

    • templates/base.html 继承 bootstrap/base.html
    • 错误页面继承 templates/base.html

模板中的链接

  • url_for()使用程序 URL 映射中保存的信息生成 URL,用于动态生成地址
  • 以视图函数名(或者app.add_url_route()定义路由时使用的端点名)作为参数,返回对应的 URL
  • url_for('index')得到的结果是/
  • url_for('index', _external=True)返回绝对地址
  • url_for('user', name='john', _external=True) 返回http://localhost:5000/user/john
  • 还可将任何额外参数添加到查询字符串中,例如url_for('index', page=2) 返回/?page=2

静态文件

  • 对静态文件的引用被当成一个特殊的路由,即/static/<filename>

  • 默认设置下,Flask 在程序根目录中名为 static 的子目录中寻找静态文件

  • 调用url_for('static', filename='css/styles.css', _external=True)得到http://localhost:5000/static/css/styles.css

    1
    2
    3
    4
    5
    6
    7
    8
    <!--templates/base.html:定义收藏夹图标-->
    {% block head %}
    {{ super() }}
    <link rel="shortcut icon" href="{{ url_for('static', filename = 'favicon.ico') }}"
    type="image/x-icon">
    <link rel="icon" href="{{ url_for('static', filename = 'favicon.ico') }}"
    type="image/x-icon">
    {% endblock %}

本地化日期和时间

Flask-Moment 将 moment.js 集成到 Jinja2 模板,以获取客户端电脑的时区和时区设置。Flask-Moment 实现了 moment.js 中的format()fromNow()fromTime()calendar()valueOf()unix()方法

1
2
3
4
5
6
7
8
9
from flask.ext.moment import Moment
from datetime import datetime

moment = Moment(app)

@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())
1
2
3
4
5
6
7
8
9
<!--templates/base.html:引入 moment.js 库-->
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}

<!--templates/index.html:使用 Flask-Moment 渲染时间戳-->
<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p> <!--'L' 到 'LLLL' 分别对应不同的复杂度-->
<p>That was {{ moment(current_time).fromNow(refresh=True) }}</p> <!--fromNow() 渲染相对时间戳-->

Web表单

pip install flask-wtf

跨站请求伪造保护

  • Flask-WTF 能保护所有表单免受跨站请求伪造(CSRF)攻击;需要设置一个密钥,使用这个密钥生成加密令牌,用令牌验证请求中表单数据

    1
    2
    app = Flask(__name__) 
    app.config['SECRET_KEY'] = 'hard to guess string' # app.config 字典可用来存储框架、扩展和程序本身的配置变量,SECRET_KEY 配置变量是通用密钥(密钥不应该直接写入代码,而要保存在环境变量中)

表单类

  • 每个 Web 表单都由一个继承自 Form 的类表示

  • 每个字段都用对象表示,字段对象可附属一个或多个验证函数用来验证用户提交的输入值是否符合要求

    1
    2
    3
    4
    5
    6
    7
    8
    from flask.ext.wtf import Form 
    from wtforms import StringField, SubmitField
    from wtforms.validators import Required
    # 字段和验证函数直接从 WTForms 包中导入
    class NameForm(Form):
    # 有一个名为 name 的文本字段和一个名为 submit 的提交按钮
    name = StringField('What is your name?', validators=[Required()]) # 表示type="text"的<input>元素;validators 指定一个由验证函数组成的列表, Required()确保提交的字段不为空
    submit = SubmitField('Submit') # 表示属性为type="submit"的<input>元素
    image-20210102160912280
image-20210102160931938

表单渲染

  • 视图函数把上面的一个 NameForm 实例通过参数 form 传入模板

    1
    2
    3
    4
    5
    <form method="POST"> 
    {{ form.hidden_tag() }}
    {{ form.name.label }} {{ form.name() }}
    {{ form.submit() }}
    </form>
  • 利用 Flask-Bootstrap 定义好的样式渲染整个表单

    1
    2
    {% import "bootstrap/wtf.html" as wtf %} 
    {{ wtf.quick_form(form) }}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!--templates/index.html:使用 Flask-WTF 和 Flask-Bootstrap 渲染表单-->
    {% extends "base.html" %}
    {% import "bootstrap/wtf.html" as wtf %}

    {% block title %}Flasky{% endblock %}

    {% block page_content %}
    <div class="page-header">
    <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
    </div>
    {{ wtf.quick_form(form) }}
    {% endblock %}

视图函数处理表单

1
2
3
4
5
6
7
8
@app.route('/', methods=['GET', 'POST'])  # 注册为GET和POST请求的处理程序,否则为注册为GET请求的处理程序
def index():
name = None # 存放表单中输入的有效名字
form = NameForm() # 创建NameForm类实例用于表示表单
if form.validate_on_submit(): # 数据能被所有验证函数接受,返回true
name = form.name.data # 名字赋值给局部变量name
form.name.data = '' # 清空表单字段
return render_template('index.html', form=form, name=name) # 渲染模板, name为表单中输入的名字
  • 提交表单大多作为 POST 请求进行处理

重定向与用户会话

  • 刷新页面时,浏览器会重新发送之前已经发送过的最后一个请求

  • 使用重定向作为 POST 请求的响应,内容是 URL,而不是包含 HTML 代码的字符串,浏览器收到重定向响应时,会向重定向的 URL 发起 GET 请求——原先用form.name.data获取用户输入,但请求结束后数据便丢失,因此需要保存输入数据

  • 把数据存储在用户会话中,在请求之间”记住“数据(session,类似 Python 字典)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from flask import Flask, render_template, session, redirect, url_for 

    @app.route('/', methods=['GET', 'POST'])
    def index():
    form = NameForm()
    if form.validate_on_submit():
    session['name'] = form.name.data
    return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name')) # 对于不存在的键,get()返回None

Flash消息

  • 提交表单后,让用户知道变化——flash()函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from flask import Flask, render_template, session, redirect, url_for, flash 

    @app.route('/', methods=['GET', 'POST'])
    def index():
    form = NameForm()
    if form.validate_on_submit():
    old_name = session.get('name')
    if old_name is not None and old_name != form.name.data: # 每次提交的名字都会和存储在用户会话中的名字进行比较
    flash('Looks like you have changed your name!')
    session['name'] = form.name.data
    return redirect(url_for('index'))
    return render_template('index.html',
    form = form, name = session.get('name'))
  • 仅调用flash()不能把消息显示出来,需要在模板渲染(最好为基模板)——get_flashed_messages()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!--templates/base.html:渲染 Flash 消息-->
    {% block content %}
    <div class="container">
    {% for message in get_flashed_messages() %}
    <div class="alert alert-warning">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{ message }} <!--每次调用flash()都会生成一个消息,获取的消息在下次调用时不会再次返回-->
    </div>
    {% endfor %}

    {% block page_content %}{% endblock %}
    </div>
    {% endblock %}
    image-20210102222657693

数据库

SQL数据库

  • 关系数据库

  • 主键:其值为表中各行的唯一标识符

  • 外键:引用同一个表或不同表中某行的主键

    image-20210102223815533

NoSQL数据库

  • 文档数据库和键值对数据库

  • 使用集合代替表,使用文档代替记录

    image-20210102223729144

Python数据库框架

  • 对象关系映射(Object-Relational Mapper,ORM)与对象文档映射(Object-Document Mapper,ODM)
  • ORM 和 ODM 把对象业务转换成数据库业务,性能上有一定的损耗
  • 数据库抽象层代码包:SQLAlchemy 和 MongoEngine 等,可以直接处理高等级的 Python 对象,而非表、文档、查询语言等数据库实体

Flask-SQLAlchemy

pip install flask-sqlalchemy

  • SQLAlchemy 是一个关系型数据库框架,提供高层 ORM,也提供使用数据库原生 SQL 的功能

  • 数据库使用 URL 指定,保存到 Flask 配置对象的SQLALCHEMY_DATABASE_URI 键; SQLALCHEMY_COMMIT_ON_TEARDOWN 键设置为 true 时,每次请求结束后会自动提交数据库中的变动

    image-20210102230828386
    • hostname:MySQL 服务所在的主机,可以是本地主机(localhost)也可以是远程服务器

    • database:要使用的数据库名

    • username 和 password:数据库用户和口令

    • SQLite 数据库不需要服务器,不用指定 hostname、username 和
      password,database 是硬盘上文件的文件名

      1
      2
      3
      4
      5
      6
      7
      8
      9
      from flask.ext.sqlalchemy import SQLAlchemy 

      basedir = os.path.abspath(os.path.dirname(__file__))

      app = Flask(__name__)
      app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
      app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

      db = SQLAlchemy(app) # 程序使用的数据库,有Flask-SQLAlchemy的所有功能

定义模型

在 ORM 中,模型一般是一个 Python 类,类中的属性对应数据库表中的列;Flask-SQLAlchemy 要求每个模型都要定义主键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Role(db.Model): 
__tablename__ = 'roles' # 定义在数据库中使用的表名,其余变量都是该模型的属性
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)

def __repr__(self): # 返回一个具有可读性的字符串表示模型
return '<Role %r>' % self.name

class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)

def __repr__(self):
return '<User %r>' % self.username
image-20210103123557330 image-20210103123656336

关系

关系型数据库使用关系把不同表中的行联系起来

1
2
3
4
5
6
7
8
9
class Role(db.Model): 
# ...
users = db.relationship('User', backref='role') # User模型中的role_id列被定义为外键,其users属性将返回与角色相关联的用户组成的列表
# backref参数向User模型中添加一个role属性,从而定义反向关系
# 如果无法决定外键,就要为db.relationship()提供额外参数,从而确定所用外键

class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
  • 一对一关系调用db.relationship()时要把参数 uselist 设为 False
  • 多对一关系也可使用一对多表示,对调两个表即可,或者把外键和db.relationship()都放在“多”这一侧

数据库操作

  • 创建表:让 Flask-SQLAlchemy 根据模型类创建数据库

    1
    2
    3
    (venv) $ python hello.py shell 
    >>> from hello import db
    >>> db.create_all()
  • 插入行

    1
    2
    3
    4
    5
    6
    7
    >>> from hello import Role, User 
    >>> admin_role = Role(name='Admin')
    >>> mod_role = Role(name='Moderator')
    >>> user_role = Role(name='User')
    >>> user_john = User(username='john', role=admin_role)
    >>> user_susan = User(username='susan', role=user_role)
    >>> user_david = User(username='david', role=user_role)

    通过数据库会话管理对数据库所做的改动(数据库会话也称为事务)

    1
    2
    >>> db.session.add_all([admin_role, mod_role, user_role, user_john, user_susan, user_david])
    >>> db.session.commit()
  • 修改行

    1
    2
    3
    >>> admin_role.name = 'Administrator' 
    >>> db.session.add(admin_role)
    >>> db.session.commit()
  • 删除行:删除与插入和更新一样,提交数据库会话后才会执行

    1
    2
    >>> db.session.delete(mod_role) 
    >>> db.session.commit()
  • 查询行:Flask-SQLAlchemy 为每个模型类都提供了 query 对象

    • Role.query.all()

    • 使用过滤器可以配置 query 对象:User.query.filter_by(role=user_role).all()

    • 查看 SQL 语句:str(User.query.filter_by(role=user_role))

      image-20210103131011287
    • 查询关系:此处执行 user_role.users 表达式时,隐含的查询会调用 all() 返回一个用户列表;因此最好修改关系的设置,加入 lazy = ‘dynamic’ 参数,从而能指定更精确的查询过滤器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      >>> users = user_role.users 
      >>> users
      [<User u'susan'>, <User u'david'>]
      >>> users[0].role
      <Role u'User'>

      >>> user_role.users.order_by(User.username).all()
      [<User u'david'>, <User u'susan'>]
      >>> user_role.users.count()
      2

结合视图函数

  • 上面的操作可在视图函数中进行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @app.route('/', methods=['GET', 'POST']) 
    def index():
    form = NameForm()
    if form.validate_on_submit():
    user = User.query.filter_by(username=form.name.data).first()
    if user is None:
    user = User(username = form.name.data)
    db.session.add(user)
    session['known'] = False
    else:
    session['known'] = True
    session['name'] = form.name.data
    form.name.data = ''
    return redirect(url_for('index'))
    return render_template('index.html',
    form = form, name = session.get('name'),
    known = session.get('known', False))
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {% extends "base.html" %} 
    {% import "bootstrap/wtf.html" as wtf %}

    {% block title %}Flasky{% endblock %}

    {% block page_content %}
    <div class="page-header">
    <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
    {% if not known %}
    <p>Pleased to meet you!</p>
    {% else %}
    <p>Happy to see you again!</p>
    {% endif %}
    </div>
    {{ wtf.quick_form(form) }}
    {% endblock %}

Python Shell 集成

Flask-Migrate

  • 需要修改数据库模型

  • pip install flask-migrate

    1
    2
    3
    4
    5
    6
    from flask.ext.migrate import Migrate, MigrateCommand 

    # ...

    migrate = Migrate(app, db)
    manager.add_command('db', MigrateCommand)
  • 创建迁移仓库:创建 migrations 文件夹,所有迁移脚本都存放其中

    1
    (venv) $ python hello.py db init 
  • 数据库迁移用迁移脚本表示。脚本中有两个函数

    • upgrade()函数把迁移中的改动应用到数据库中,downgrade()函数则将改动删除

    • 自动创建的迁移会根据模型定义和数据库当前状态之间的差异,生成upgrade()downgrade()函数的内容

      1
      (venv) $ python hello.py db migrate -m "initial migration" 
  • 更新数据库

    1
    (venv) $ python hello.py db upgrade

电子邮件

pip install flask-mail

  • Flask-Mail 连接到 SMTP 服务器,默认使用 localhost 端口25,不用验证即可发送

    image-20210102225156764
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from flask.ext.mail import Mail 
    import os

    mail = Mail(app)
    # ...
    app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
    app.config['MAIL_PORT'] = 587
    app.config['MAIL_USE_TLS'] = True
    app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
    app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

    需要预先设置环境变量

    1
    2
    (venv) $ set MAIL_USERNAME=<Gmail username> 
    (venv) $ set MAIL_PASSWORD=<Gmail password>
  • 实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from flask.ext.mail import Message 

    app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]' # 定义邮件主题的前缀
    app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>' # 发件人的地址

    def send_email(to, subject, template, **kwargs): # 收件人地址、主题、渲染邮件正文的模板和关键字参数列表
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
    sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs) # 使用两个模板分别渲染纯文本正文和富文本正文
    mail.send(msg) # send()使用current_app,必须先激活程序上下文
  • 每当表单接收新的名字,就给管理员发送一个电子邮件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # ... 
    app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN') # 收件人保存在环境变量 FLASKY_ADMIN 中
    # ...
    @app.route('/', methods=['GET', 'POST'])
    def index():
    form = NameForm()
    if form.validate_on_submit():
    user = User.query.filter_by(username=form.name.data).first()
    if user is None:
    user = User(username=form.name.data)
    db.session.add(user)
    session['known'] = False
    if app.config['FLASKY_ADMIN']:
    send_email(app.config['FLASKY_ADMIN'], 'New User',
    'mail/new_user', user=user) # 模板文件都保存在templates下的mail子文件夹
    else:
    session['known'] = True
    session['name'] = form.name.data
    form.name.data = ''
    return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'),
    known=session.get('known', False))
  • 异步发送

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from threading import Thread 

    def send_async_email(app, msg):
    with app.app_context(): # 创建程序上下文
    mail.send(msg)

    def send_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
    sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|-app/  # Flask程序一般都保存在app包中
|-templates/
|-static/
|-main/
|-__init__.py
|-errors.py
|-forms.py
|-views.py
|-__init__.py
|-email.py
|-models.py
|-migrations/ # 包含数据库迁移脚本
|-tests/ # 单元测试
|-__init__.py
|-test*.py
|-venv/ # 包含 Python 虚拟环境
|-requirements.txt # 列出了所有依赖包
|-config.py # 存储配置
|-manage.py # 用于启动程序以及其他的程序任务

配置选项

使用层次结构的配置类。

基类 Config 中包含通用配置,子类分别定义专用的配置。

某些配置可以从环境变量中导入。

SQLALCHEMY_DATABASE_URI 变量被指定了不同的值,这样可在不同的配置环境中运行,每个环境都使用不同的数据库。

配置类可以定义 init_app() 类方法,其参数是程序实例。

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
import os 
basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')

@staticmethod
def init_app(app):
pass

class DevelopmentConfig(Config):
DEBUG = True
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')

class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')

config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,

'default': DevelopmentConfig
}

程序包(app)

数据库模型和电子邮件支持函数被移到了这个包中,分别保存为 app/models.py 和 app/email.py

  • __init__.py:程序包的构造文件

    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 flask import Flask, render_template 
    from flask.ext.bootstrap import Bootstrap
    from flask.ext.mail import Mail
    from flask.ext.moment import Moment
    from flask.ext.sqlalchemy import SQLAlchemy
    from config import config

    bootstrap = Bootstrap()
    mail = Mail()
    moment = Moment()
    db = SQLAlchemy()

    def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)

    # 附加路由和自定义的错误页面

    # 注册蓝本
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app
  • 蓝本中实现功能:蓝本和程序类似,也可以定义路由。但在蓝本中定义的路由处于休眠状态,需要蓝本注册到程序上

    1
    2
    3
    4
    5
    6
    # app/main/__init__.py:创建蓝本
    from flask import Blueprint

    main = Blueprint('main', __name__) # 参数:蓝本的名字和蓝本所在的包或模块

    from . import views, errors # 程序的路由保存在包里的app/main/views.py中,错误处理程序保存在app/main/errors.py中。脚本的末尾导入,以避免循环导入依赖(views.py 和 errors.py 中还要导入蓝本 main)
  • 错误处理程序(app/main/errors.py)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from flask import render_template 
    from . import main

    @main.app_errorhandler(404) # 如果使用errorhandler修饰器,只有蓝本中的错误才能触发处理程序。注册程序全局的错误处理程序,必须使用app_errorhandler
    def page_not_found(e):
    return render_template('404.html'), 404

    @main.app_errorhandler(500)
    def internal_server_error(e):
    return render_template('500.html'), 500
  • 程序路由(app/main/views.py)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from datetime import datetime 
    from flask import render_template, session, redirect, url_for

    from . import main
    from .forms import NameForm
    from .. import db
    from ..models import User

    @main.route('/', methods=['GET', 'POST']) # 路由修饰器由蓝本提供
    def index():
    form = NameForm()
    if form.validate_on_submit():
    # ...
    return redirect(url_for('.index')) # 蓝本中Flask会为蓝本中的全部路由端点加上一个命名空间,以在不同的蓝本中使用相同的端点名定义视图函数
    # 命名空间就是蓝本的名字——Blueprint构造函数的第一个参数——index() 注册的端点名是main.index
    # 同一蓝本中的重定向可以使用简写形式,跨蓝本的重定向必须使用带有命名空间的端点名
    return render_template('index.html',
    form=form, name=session.get('name'),
    known=session.get('known', False),
    current_time=datetime.utcnow())

启动脚本

manage.py 文件用于启动程序

加入了 shebang 声明,所以在基于 Unix 的操作系统中可以通过./manage. py执行脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python 
import os
from app import create_app, db
from app.models import User, Role
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default') # 如果已经定义了环境变量 FLASK_CONFIG,则从中读取配置名;否则使用默认配置
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
manager.run()

需求文件

requirements.txt 文件,用于记录所有依赖包及其精确的版本号

生成文件:pip freeze >requirements.txt

创建环境:pip install -r requirements.txt

单元测试

使用 Python 标准库中的 unittest 包编写,setUp()tearDown()方法分别在各测试前后运行,并且名字以 test_ 开头的函数都作为测试执行

setUp()方法尝试创建一个测试环境,并激活上下文,创建一个全新的数据库

数据库和程序上下文在tearDown()中删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import unittest 
from flask import current_app
from app import create_app, db

class BasicsTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()

def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()

def test_app_exists(self): # 确保程序实例存在
self.assertFalse(current_app is None)

def test_app_is_testing(self): # 确保程序在测试配置中运行
self.assertTrue(current_app.config['TESTING'])
1
2
3
4
5
6
7
# manage.py:启动单元测试的命令
@manager.command # 修饰函数名就是命令名
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
1
(venv) $ python manage.py test