离线下载
PDF版 ePub版

Flask · 更新于 2018-11-28 11:00:43

教程

想要用 Python 和 Flask 开发应用吗?让我们来边看例子边学习。本教程中我们将会创建 一个微博应用。这个应用只支持单一用户,只能创建文本条目,也没有不能订阅和评论, 但是已经具备一个初学者需要掌握的功能。这个应用只需要使用 Flask 和 SQLite , SQLite 是 Python 自带的。

如果你想要事先下载完整的源代码或者用于比较,请查看示例源代码

Flaskr 介绍

我们把教程中的博客应用称为 flaskr ,当然你也可以随便取一个没有 Web-2.0 气息的名字 ;) 它的基本功能如下:

  1. 让用户可以根据配置文件中的信息登录和注销。只支持一个用户。
  2. 用户登录以后可以添加新的博客条目。条目由文本标题和支持 HTML 代码的内容组成。 因为我们信任用户,所以不对内容中的 HTML 进行净化处理。
  3. 页面以倒序(新的在上面)显示所有条目。并且用户登录以后可以在这个页面添加新的条目。

我们直接在应用中使用 SQLite3 ,因为在这种规模的应用中 SQlite3 已经够用了。如果 是大型应用,那么就有必要使用能够好的处理数据库连接的 SQLAlchemy 了,它可以 同时对应多种数据库,并做其他更多的事情。如果你的数据更适合使用 NoSQL 数据库, 那么也可以考虑使用某种流行的 NoSQL 数据库。

这是教程应用完工后的截图:

步骤 0 :创建文件夹

在开始之前需要为应用创建下列文件夹:

/flaskr
    /static
    /templates

flaskr 文件夹不是一个 Python 包,只是一个用来存放我们文件的地方。我们将把以后要用到的数据库模式和主模块放在这个文件夹中。 static 文件夹中的文件是用于供应用用户通过 HTTP 访问的文件,主要是 CSS 和 javascript 文件。 Flask 将会在 templates 文件夹中搜索 Jinja2 模板,所有在教程中的模板都放在 templates 文件夹中。

步骤 1 :数据库模式

首先我们要创建数据库模式。本应用只需要使用一张表,并且由于我们使用 SQLite , 所以这一步非常简单。把以下内容保存为 schema.sql 文件并放在我们上一步创建的 flaskr 文件夹中就行了:

drop table if exists entries;
create table entries (
  id integer primary key autoincrement,
  title text not null,
  text text not null
);

这个模式只有一张名为 entries 的表,表中的字段为 id 、 title 和 text 。 id 是主键,是自增整数型字段,另外两个字段是非空的字符串型字段。

步骤 2 :应用构建代码

现在我们已经准备好了数据库模式了,下面来创建应用模块。我们把模块命名为 flaskr.py ,并放在 flaskr 文件夹中。为了方便初学者学习,我们把库的导入与相关配置放在了一起。对于小型应用来说,可以把配置直接放在模块中。但是更加清晰的 方案是把配置放在一个独立的 .ini 或 .py 文件中,并在模块中导入配置的值。

在 flaskr.py 文件中:

# all the imports
import sqlite3
from flask import Flask, request, session, g, redirect, url_for, \
     abort, render_template, flash

# configuration
DATABASE = '/tmp/flaskr.db'
DEBUG = True
SECRET_KEY = 'development key'
USERNAME = 'admin'
PASSWORD = 'default'

接着创建真正的应用,并用同一文件中的配置来初始化,在 flaskr.py 文件中:

# create our little application :)
app = Flask(__name__)
app.config.from_object(__name__)

from_object() 会查看给定的对象(如果该对象是一个字符串就会直接导入它),搜索对象中所有变量名均为大字字母的变量。在我们的应用中,已经将配 置写在前面了。你可以把这些配置放到一个独立的文件中。

通常,从一个配置文件中导入配置是比较好的做法,我们使用 from_envvar() 来完成这个工作,把上面的 from_object() 一行替换为:

app.config.from_envvar('FLASKR_SETTINGS', silent=True)

这样做就可以设置一个 FLASKR_SETTINGS 的环境变量来指定一个配置文件,并根据该文件来重载缺省的配置。 silent 开关的作用是告诉 Flask 如果没有这个环境变量 不要报错。

secret_key (密钥)用于保持客户端会话安全,请谨慎地选择密钥,并尽可能的使 复杂而且不容易被猜到。 DEBUG 标志用于开关交互调试器。因为调试模式允许用户执行服务器上的代码,所以永远不要在生产环境中打开调试模式 !

我们还添加了一个方便连接指定数据库的方法。这个方法可以用于在请求时打开连接,也可以用于 Python 交互终端或代码中。以后会派上用场。

def connect_db():
    return sqlite3.connect(app.config['DATABASE'])

最后,在文件末尾添加以单机方式启动服务器的代码:

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

到此为止,我们可以顺利运行应用了。输入以下命令开始运行:

python flaskr.py

你会看到服务器已经运行的信息,其中包含应用访问地址。

因为我们还没创建视图,所以当你在浏览器中访问这个地址时,会得到一个 404 页面未 找到错误。很快我们就会谈到视图,但我们先要弄好数据库。

外部可见的服务器

想让你的服务器被公开访问?详见外部可见的服务器

步骤 3 :创建数据库

如前所述 Flaskr 是一个数据库驱动的应用,更准确地说是一个关系型数据库驱动的 应用。关系型数据库需要一个数据库模式来定义如何储存信息,因此必须在第一次运行 服务器前创建数据库模式。

使用 sqlite3 命令通过管道导入 schema.sql 创建模式:

sqlite3 /tmp/flaskr.db < schema.sql

上述方法的不足之处是需要额外的 sqlite3 命令,但这个命令不是每个系统都有的。而且还必须提供数据库的路径,容易出错。因此更好的方法是在应用中添加一个数据库初始化函数。

添加的方法是:首先从 contextlib 库中导入 contextlib.closing() 函数,即在 flaskr.py 文件的导入部分添加如下内容:

from contextlib import closing

接下来,可以创建一个用来初始化数据库的 init_db 函数,其中我们使用了先前创建的 connect_db 函数。把这个初始化函数放在 flaskr.py 文件中的connect_db 函数 下面:

def init_db():
    with closing(connect_db()) as db:
        with app.open_resource('schema.sql', mode='r') as f:
            db.cursor().executescript(f.read())
        db.commit()

closing() 帮助函数允许我们在 with 代码块保持数据库连接打开。应用对象的 open_resource() 方法支持也支持这个功能, 可以在 with 代码块中直接使用。这个函数打开一个位于来源位置(你的 flaskr 文件夹)的文件并允许你读取文件的内容。这里我们用于在数据库连接上执行代码。

当我们连接到数据库时,我们得到一个提供指针的连接对象(本例中的 db )。这个 指针有一个方法可以执行完整的代码。最后我们提供要做的修改。 SQLite 3 和其他事务型数据库只有在显式提交时才会真正提交。

现在可以创建数据库了。打开 Python shell ,导入,调用函数:

>>> from flaskr import init_db
>>> init_db()

故障处理

如果出现表无法找到的问题,请检查是否写错了函数名称(应该是 init_db ), 是否写错了表名(例如单数复数错误)。

步骤 4 :请求数据库连接

现在我们已经学会如何打开并在代码中使用数据库连接,但是如何优雅地在请求时使用它呢?我们会在每一个函数中用到数据库连接,因此有必要在请求之前初始化连接,并在请求之后关闭连接。

Flask 中可以使用 before_request()after_request()teardown_request() 装饰器达到这个目的:

@app.before_request
def before_request():
    g.db = connect_db()

@app.teardown_request
def teardown_request(exception):
    db = getattr(g, 'db', None)
    if db is not None:
        db.close()
    g.db.close()

使用 before_request() 装饰的函数会在请求之前调用,且不传递 参数。使用 after_request() 装饰的函数会在请求之后调用,且 传递发送给客户端响应对象。它们必须传递响应对象,所以在出错的情况下就不会执行。 因此我们就要用到 teardown_request() 装饰器了。这个装饰器下 的函数在响应对象构建后被调用。它们不允许修改请求,并且它们的返回值被忽略。如果 请求过程中出错,那么这个错误会传递给每个函数;否则传递 None 。

我们把数据库连接保存在 Flask 提供的特殊的 g 对象中。这个对象与 每一个请求是一一对应的,并且只在函数内部有效。不要在其它对象中储存类似信息, 因为在多线程环境下无效。这个特殊的 g 对象会在后台神奇的工作,保证系统正常运行。

若想更好地处理这种资源,请参阅在 Flask 中使用 SQLite 3

Hint

我该把这些代码放在哪里?

如果你按教程一步一步读下来,那么可能会疑惑应该把这个步骤和以后的代码放在哪 里?比较有条理的做法是把这些模块级别的函数放在一起,并把新的 before_request 和 teardown_request 函数放在前文的 init_db 函数 下面(即按照教程的顺序放置)。

如果你已经晕头转向了,那么你可以参考一下 示例源代码 。在 Flask 中,你可以把应用的所有代码都放在同一个 Python 模块中。但是你没有必要这样做,尤其是当你的应用变大了的时候,更不应当这样。

步骤 5 :视图函数

现在数据库连接弄好了,接着开始写视图函数。我们共需要四个视图函数:

显示条目

这个视图显示所有数据库中的条目。它绑定应用的根地址,并从数据库中读取 title 和 text 字段。 id 最大的记录(最新的条目)在最上面。从指针返回的记录集是一个包含 select 语句查询结果的元组。对于教程应用这样的小应用,做到这样就已经够好了。但是你可能想要把结果转换为字典,具体做法参见简化查询 中的例子。

这个视图会把条目作为字典传递给 show_entries.html 模板,并返回渲染结果:

@app.route('/')
def show_entries():
    cur = g.db.execute('select title, text from entries order by id desc')
    entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
    return render_template('show_entries.html', entries=entries)

添加一个新条目

这个视图可以让一个登录后的用户添加一个新条目。本视图只响应 POST 请求,真正的表单显示在 show_entries 页面中。如果一切顺利,我们会 flash() 一个消息给下一个请求并重定向回到 show_entries 页面:

@app.route('/add', methods=['POST'])
def add_entry():
    if not session.get('logged_in'):
        abort(401)
    g.db.execute('insert into entries (title, text) values (?, ?)',
                 [request.form['title'], request.form['text']])
    g.db.commit()
    flash('New entry was successfully posted')
    return redirect(url_for('show_entries'))

注意,我们在本视图中检查了用户是否已经登录(即检查会话中是否有 logged_in 键,且对应的值是否为 True )。

安全性建议

请像示例代码一样确保在构建 SQL 语句时使用问号。否则当你使用字符串构建 SQL 时容易遭到 SQL 注入攻击。更多内容参见 在 Flask 中使用 SQLite 3 。

登录和注销

这些函数用于用户登录和注销。登录视图根据配置中的用户名和密码验证用户并在会话中设置 logged_in 键值。如果用户通过验证,键值设为 True ,那么用户会被重定向到 show_entries 页面。另外闪现一个信息,告诉用户已登录成功。如果出现错误,模板会 提示错误信息,并让用户重新登录:

@app.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        if request.form['username'] != app.config['USERNAME']:
            error = 'Invalid username'
        elif request.form['password'] != app.config['PASSWORD']:
            error = 'Invalid password'
        else:
            session['logged_in'] = True
            flash('You were logged in')
            return redirect(url_for('show_entries'))
    return render_template('login.html', error=error)

登出视图则正好相反,把键值从会话中删除。在这里我们使用了一个小技巧:如果你使用字典的 pop() 方法并且传递了第二个参数(键的缺省值),那么当字典中有 这个键时就会删除这个键,否则什么也不做。这样做的好处是我们不用检查用户是否已经登录了。

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))

步骤 6 :模板

现在开始写模板。如果我们现在访问 URL ,那么会得到一个 Flask 无法找到模板文件的 异常。 Flask 使用 Jinja2 模板语法并默认开启自动转义。也就是说除非用 Markup 标记一个值或在模板中使用 |safe 过滤器,否则 Jinja2 会把如 < 或 > 之类的特殊字符转义为与其 XML 等价字符。

我们还使用了模板继承以保存所有页面的布局统一。

请把以下模板放在 templates 文件夹中:

layout.html

这个模板包含 HTML 骨架、头部和一个登录链接(如果用户已登录则变为一个注销链接 )。如果有闪现信息,那么还会显示闪现信息。 {% block body %} 块会被子模板中同名( body )的块替换。

session 字典在模板中也可以使用。你可以使用它来检验用户是否已经 登录。注意,在 Jinja 中可以访问对象或字典的不存在的属性和成员。如例子中的 'logged_in' 键不存在时代码仍然能正常运行:

<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
  <h1>Flaskr</h1>
  <div class=metanav>
  {% if not session.logged_in %}
    <a href="{{ url_for('login') }}">log in</a>
  {% else %}
    <a href="{{ url_for('logout') }}">log out</a>
  {% endif %}
  </div>
  {% for message in get_flashed_messages() %}
    <div class=flash>{{ message }}</div>
  {% endfor %}
  {% block body %}{% endblock %}
</div>

show_entries.html

这个模板扩展了上述的 layout.html 模板,用于显示信息。注意, for 遍历了我们通过 render_template() 函数传递的所有信息。模板还告诉表单使用 POST 作为 HTTP 方法向 add_entry 函数提交数据:

{% extends "layout.html" %}
{% block body %}
  {% if session.logged_in %}
    <form action="{{ url_for('add_entry') }}" method=post class=add-entry>
      <dl>
        <dt>Title:
        <dd><input type=text size=30 name=title>
        <dt>Text:
        <dd><textarea name=text rows=5 cols=40></textarea>
        <dd><input type=submit value=Share>
      </dl>
    </form>
  {% endif %}
  <ul class=entries>
  {% for entry in entries %}
    <li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}
  {% else %}
    <li><em>Unbelievable.  No entries here so far</em>
  {% endfor %}
  </ul>
{% endblock %}

login.html

最后是简单显示用户登录表单的登录模板:

{% extends "layout.html" %}
{% block body %}
  <h2>Login</h2>
  {% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
  <form action="{{ url_for('login') }}" method=post>
    <dl>
      <dt>Username:
      <dd><input type=text name=username>
      <dt>Password:
      <dd><input type=password name=password>
      <dd><input type=submit value=Login>
    </dl>
  </form>
{% endblock %}

步骤 7 :添加样式

现在万事俱备,只剩给应用添加一些样式了。只要把以下内容保存为 static 文件夹中的 style.css 文件就行了:

body            { font-family: sans-serif; background: #eee; }
a, h1, h2       { color: #377ba8; }
h1, h2          { font-family: 'Georgia', serif; margin: 0; }
h1              { border-bottom: 2px solid #eee; }
h2              { font-size: 1.2em; }

.page           { margin: 2em auto; width: 35em; border: 5px solid #ccc;
                  padding: 0.8em; background: white; }
.entries        { list-style: none; margin: 0; padding: 0; }
.entries li     { margin: 0.8em 1.2em; }
.entries li h2  { margin-left: -1em; }
.add-entry      { font-size: 0.9em; border-bottom: 1px solid #ccc; }
.add-entry dl   { font-weight: bold; }
.metanav        { text-align: right; font-size: 0.8em; padding: 0.3em;
                  margin-bottom: 1em; background: #fafafa; }
.flash          { background: #cee5F5; padding: 0.5em;
                  border: 1px solid #aacbe2; }
.error          { background: #f0d6d6; padding: 0.5em; }

额外赠送:测试应用

现在你已经完成了整个应用,一切都运行正常。为了方便以后进行完善修改,添加自动测试不失为一个好主意。本教程中的应用已成为测试 Flask 应用文档中演示如何进行 单元测试的例子,可以去看看测试 Flask 应用是多么容易

© Copyright 2013, Armin Ronacher. Created using Sphinx.

上一篇: 快速上手 下一篇: 模板