想要用 Python 和 Flask 开发一个应用?在此,你将有机会通过实例来学习。在本教程中,我们会创建一个简单的微博客应用。它只支持单用户和纯文本条目,并且没有推送或评论功能,但是它仍然有你需要开始的一切。我们将使用 Flask,采用 Python 自带的 SQLite 数据库,所以你需要其它的东西。
如果你想预先拿到完整的源码或是用于对照,请查看示例源码。
1. 介绍 Flaskr在本教程中,我们把我们的这个博客应用称为 falskr,也可以选一个不那么 web2.0 的名字。基本上,我们希望它能做这些事情:
我们将会在应用中直接采用 SQLite3,因为它足以应付这种规模的应用。对于更大型的应用,就有必要使用 SQLAlchemy,它能更加智能地处理数据库连接,允许你一次连接不同的关系数据库等等。如果你的数据更适合 NoSQL,你也可以考虑流行的 NoSQL 数据库。
这是一个应用最终效果的截图:
2. 创建文件夹在我们真正开始之前,让我们创建这个应用所需的文件夹:
/flaskr/static/templates
flaskr 文件夹不是一个 Python 包,只是个我们放置文件的地方。在接下来的步骤中,我们会直接把数据库模式和主模块放在这个目录中。用户可以通过 HTTP 访问 static
文件夹中的文件,也即存放 css 和 Javascript 文件的地方。Flask 会在 templates
文件夹里寻找 Jinja2 模板,之后教程中创建的模板将会放在这个文件夹里。
首先我们要创建数据库模式。对于这个应用来说,一张表就足够了,而且只需支持 SQLite,所以会很简单。只需要把下面的内容放进一个名为 schema.sql 的文件,放在刚才创建的 flaskr 文件夹中:
DROP TABLE IF EXISTS entries
;
CREATE TABLE entries (id INTEGER PRIMARY KEY AUTOINCREMENT, title STRING NOT NULL, text STRING NOT NULL
)
;
这个模式包含一个名为 entries 的表,该表中的每行都包含一个 id、一个 title 和一个 text。id 是一个自增的整数,也是主键;其余的两个是字符串,且不允许为空。
4. 应用设置代码现在我们已经有了数据库模式,我们可以创建应用的模块了。让我们把它叫做 flaskr.py,并放置在 flaskr 目录下。我们从添加所需的导入语句开始和添加配置部分开始。对于小型应用,可以直接配置放在主模块里,正如我们需要要做的一样。但更简单的方案是创建独立的 .ini
或 .py
文件,并载入或导入里面的值。
首先在 flaskr.py 里导入:
# all the imports
import os
import sqlite3
from flask import (Flask, request, session, g, redirecturl_for, abort, render_template, flash)
Config
对象的用法如同字典,所以我们可以用新值更新它。
数据库路径:
操作系统有进程当前工作目录的概念。不幸的是,你在 Web 应用中不能依赖此概念,因为你可能会在相同的进程中运行多个应用。
为此,提供了
app.root_path
属性以获取应用的路径。配合os.path
模块使用,轻松可达任意文件。在本例中,我们把数据库放在根目录下。对于实际生产环境的应用,推荐使用实例文件夹。
通常,加载一个单独的、环境特定的配置文件是个好主意。Flask 允许你导入多份配置文件,并且使用最后的导入中定义的设置。这使得配置设定过程更可靠。from_envvar()
可用于达此目的。
app.config.from_envvar('FLASKR_SETTINGS', slient=True)
只需设置一个名为 FLASKR_SETTINGS
的环境变量,指向要加载的配置文件。启用静默模式告诉 Flask 在没有设置该换将变量的情况下噤声。
此外,你可以配置对象上的 from_object()
方法,并传递一个模块的导入名作为参数。Flask 会从这个模块初始化变量。注意,只有名称全为大写字母的变量才会被采用。
secret_key
是保证客户端会话的要点。正确选择一个尽可能猜测,尽可能复杂的密钥。调试标志关系交互式调试器的开启。永远不要在生产系统中激活调试模式,因为它将允许用户在服务器上执行代码。
我们还添加了一个让连接到指定数据库变得很简单的方法,这个方法用于在请求时开启一个数据库连接,并且在交互式 Python Shell 和脚本中也能使用。这位以后的操作提供了相当的便利。我们创建了一个简单的 SQLite 数据库的连接,并让它用 sqlite3.Row
表示数据库中的行,这使得我们可以通过字典而不是元组的形式访问行:
def connect_db():"""Connects to the specific database."""rv = sqlite3.connect(app.config['DATABASE'])rv.row_factory = sqlite3.Rowreturn rv
最后,如果我们想要把这个文件当做独立应用来运行,我们只需在可启动服务器文件末尾添加这一行:
if __name__ == '__main__':app.run()
如此我们便可以开始顺利运行这个应用,使用如下命令:
→ python flaskr.py
你将会看见有消息告诉你访问服务器的地址:
当你在浏览器中访问服务器遇到一个 404 page not found 错误时,是因为我们还没有任何视图。我们之后再来关注视图。首先我们应该让数据库工作起来。
5. 数据库连接我们已经创建了一个能建立数据库连接的函数 connect_db
,但它本身并不是很有用。总是创建或关闭数据库连接是相当低效的,所以我们会让连接保持更长时间。因为数据库连接封装了事务,我们也需要确保同一时刻只有一个请求使用这个连接。那么,如何用 Flask 优雅地实现呢?
这该是应用环境上场的时候了。那么,让我们开始吧。
Flask 提供了两种环境(Context): 应用环境(Application Context) 和请求环境(Request Context)。暂且你所需了解的是,不同环境有不同的特殊变量。例如 request
变量与请求对象有关,而 g
是与当前应用环境有关的通用变量。我们在之后深入了解它们。
现在你只需要知道可以安全地在 g
对象存储对象。
那么你何时把数据库连接存放在它上面?你可以写一个辅助函数。这个函数首次调用的时候会为当前环境创建一个数据库连接,调用成功后返回已经建立好的连接:
def get_db():"""Opens a new database connection if there is none yetfor the current application context."""if not hasattr(g, 'sqlite_db'):g.sqlite_db = connect_db()return g.sqlite_db
于是现在我们知道如何连接到数据库,但如何妥善断开连接呢?为此,Flask 提供了 teardown_appcontext()
装饰器。它将在每次应用环境销毁时执行:
@app.teardown_appcontext
def close_db(error):"""Closes the database again at the end of the request."""if hasattr(g, 'sqlite_db'):g.sqlite_db.close()
teardown_appcontext()
标记的函数会在每次应用环境销毁是调用。这意味着什么?本质上,应用环境在请求传入前创建,每当请求结束时销毁。销毁有两种原因: 一切正常(错误参数会是 None) 或发生异常,后者情况中,错误会被传递给销毁时函数。
提示: 我该把这些代码放在哪?
如果你一直遵循教程,你应该会问从此以后的步骤产生的代码放在什么地方。逻辑上来讲,应该按照模块来组织函数,即把你新的
get_db()
和close_db()
函数放在之前的connect_db
函数下面(逐行复刻教程)。如果你需要来找准定位,可以看一下示例源码是怎么组织的。在 Flask 中,你可以把你应用中所有的代码放在一个 Python 模块里。但你无需这么做,而且在你的应用规模扩大以后,这显然不妥。
正如之前介绍的,Flaskr 是一个数据库驱动的应用,更准确的说法是,一个由关系数据库系统驱动的应用。关系数据库系统需要一个模式来决定存储信息的方式。所以在第一次开启服务器之前,要点是创建模式。
sqlite3 /tmp/flaskr.db < schema.sql
这种方法的缺点是需要安装 sqlite3命令&#xff0c;而并不是每个系统都有安装。而且你必须提供数据库的路径&#xff0c;否则将报错。用函数将初始化时个不错的想法。
要这么做&#xff0c;我们可以创建一个名为 init_db
的函数来初始化数据库。让我们首先看看代码。只需要把这个函数放在 flaskr.py 里的 connect_db
函数的后面:
def init_db():with app.app_context():db &#61; get_db()with app.open_resource(&#39;schema.sql&#39;, mode&#61;&#39;r&#39;) as f:db,cursor().executescript(f.read())db.commit()
那么&#xff0c;这段代码会发生什么&#xff1f;还记得吗&#xff1f;上个章节中提到&#xff0c;应用环境在每次请求传入时创建。这里我们并没有请求&#xff0c;所以我们需要手动创建一个应用环境。g
在应用环境外无法获知它属于哪个应用&#xff0c;因为可能会有多个应用同时存在。
with app.app_context()
语句为我们建立了应用环境。在 with
语句的内部&#xff0c;g
对象会与 app
关联。在语句的结束处&#xff0c;会释放这个关联并执行所有销毁函数。这意味着数据库连接在提交后断开。
应用对象的 open_resource()
是一个很方便的辅助函数&#xff0c;可以打开应用提供的资源。这个函数从资源所在位置(你的 flaskr 文件夹) 打开文件夹&#xff0c;并允许你读取它。我们在此用它来在数据库连接上执行脚本。
SQLite 的数据库连接对象提供了一个游标对象。游标上有一个方法可以执行完整的脚本。最后我们只需要提交变更。SQLite3 和其他支持事务的数据库只会在你显式提交的时候提交。
现在可以在 Python shell 导入并调用这个函数来创建数据库:
>>> from flaskr import init_db
>>> init_db()
故障排除: 如果你遇到了表无法找到的异常&#xff0c;请检查你是否确实调用过
init_db
函数并且表的名称是正确的(比如弄混了单数和复数)。
现在数据库连接已经正常工作&#xff0c;我们终于可以开始写视图函数了。我们一共需要些四个:
这个视图显示数据库中存储的所有条目。它绑定在应用的根地址&#xff0c;并从数据库查询出文章的标题和正文。id 值最大的条目(最新的条目) 会显示在最上方。从指针返回的行是按 select
语句中声明的列组织的元组。这对像我们这样的小应用已经足够了&#xff0c;但是你可能会想把转换成字典。如果你对这方面有兴趣&#xff0c;请参考简化查询的例子。
视图函数会将条目作为字典传递给 show_entries.html
模板&#xff0c;并返回渲染结果:
&#64;app.route(&#39;/&#39;)
def show_entries():cur &#61; g.db.execute(&#39;select title, text from entries order by id desc&#39;)entries &#61; [dict(title&#61;row[0], text&#61;row[1]) for row in cur.fetchall()]return redirect(url_for(&#39;show entries&#39;))
这个视图允许已登录的用户添加新条目&#xff0c;并只响应 POST 请求&#xff0c;实际的表单显示在 show_entries
页。如果一些工作正常&#xff0c;我们会用 flash()
向下一次请求发送提示消息&#xff0c;并重定向回 show_entries
页:
&#64;app.route(&#39;/add&#39;, methods&#61;[&#39;POST&#39;])
def add_entry():if not session.get(&#39;logged_in&#39;):abort(401)g.db.execute(&#39;insert into entries (title, text) values (?, ?)&#39;,[request.form[&#39;title&#39;], request.form[&#39;text&#39;]])g.db.commit()flash(&#39;New entry was successfully posted&#39;)return redirect(url_for(&#39;show_entries&#39;))
注意这里的用户登录简检查&#xff0c;(logged_in
键在会话中存在&#xff0c;并且为 True)。
安全提示: 确保像上面例子一样&#xff0c;使用问号来构建 SQL 语句。否则&#xff0c;当你使用格式化字符串构建 SQL 语句时&#xff0c;你的应用很容易遭受 SQL 注入。更多请见在 Flask 中使用 SQLite3。
这些函数用来让用户登录和登出。登录通过与配置文件中的数据比较检查用户名和密码&#xff0c;并设定会话中的 logged_in
键值。如果用户登录成功&#xff0c;那么这个键值会被设置为 True&#xff0c;并跳转回 show_entries
页。此外&#xff0c;会有消息闪现来提示用户登录成功。如果发生一个错误&#xff0c;模板会通知&#xff0c;并提示重新登录。
&#64;app.route(&#39;/login&#39;, methods&#61;[&#39;POST&#39;, &#39;GET&#39;])
def login():error &#61; Noneif request.method &#61;&#61; &#39;POST&#39;:if request.form[&#39;username&#39;] !&#61; app.config[&#39;USERNAME&#39;]:error &#61; &#39;Invalid username&#39;elif request.form[&#39;password&#39;] !&#61; app.config[&#39;PASSWORD&#39;]:error &#61; &#39;Invalid password&#39;else:session[&#39;logged_in&#39;] &#61; Trueflash(&#39;You were logged in&#39;)return redirect(url_for(&#39;show_entries&#39;))return render_template(&#39;login.teml&#39;, error&#61;error)
登出函数&#xff0c;做相反的事情。从会话中删除 logged_in
键。我们这里使用了一个简洁的方法: 如果你使用字典的 pop()
方法并传入第二个参数(默认)&#xff0c;这个方法会从字段中删除这个键&#xff0c;如果这个键不存在则什么都不做。这很有用&#xff0c;因为我们不需要检查用户是否已经登录。
&#64;app.route(&#39;/logout&#39;)
def logout():session.pop(&#39;logged_in&#39;, None)flash(&#39;You were logged out&#39;)return redirect(url_for(&#39;show_entries&#39;))
8. 登入登出
接下来我们应该创建模板了。如果我们现在请求 URL&#xff0c;只会得到 Flask 无法找到模板的异常。模板使用 Jinja2 语法并默认开启自动转义。这意味着除非你使用 Markup
标记或在模板中使用 |safe
过滤器&#xff0c;否则 Jinja2 会确保特殊字符&#xff0c;比如 <
或 >
被转义为等价的 XML 实体。
我们也会使用模板继承在网站的所有页面中重置布局。
将下面的模板放在 templates 文件夹里:
这个模板包含 HTML 主体结构、标题和一个登入链接(用户已登录则提供登出)。如果有&#xff0c;它也会显示闪现消息。{\% block body \%}
块可以被子模块中相同名字的块(body
) 替换。
session
字典在模板中也是可用的。你可以用它来检查用户是否登入。注意&#xff0c;在 Jinja 中你可以访问不存在的对象/字典属性或成员。比如下面的代码&#xff0c;即便 logged_in
键不存在&#xff0c;仍然可以正常工作:
<title>Flaskrtitle>
<link rel&#61;"stylesheet" type&#61;"text/css" href&#61;"{{ url_for(&#39;static&#39;, filename&#61;&#39;style.css&#39;) }}">
<div class&#61;"page"><h1>Flaskrh1><div class&#61;"metanav">{% if not session.logged_in %}<a href&#61;"{{ url_for(&#39;login&#39;) }}">log ina>{% else %}<a href&#61;"{{ url_for(&#39;logout&#39;) }}">log outa>{% endif %}div>{% for message in get_flashed_message() %}<div clas&#61;"flash">{{ message }}div>{% endfor %}{% block body %}{% endblock %}
div>
这个模板继承了上面的 logout.html
模板来显示消息。注意 for 循环会遍历并输出所有 render_template()
函数传入的消息。我们还告诉表单的 HTTP 的 POST 方法提交信息到 add_entry
函数:
{% extends "layout.html" %}
{% block body %}
{% if session.logged_in %}<form action&#61;"{{ url_for(&#39;add_entry&#39;) }}" method&#61;"post" class&#61;"add_entry"><dl><dt>Title: <dd><input type&#61;"text" size&#61;30 name&#61;"title">dd><dt>Text: <dd><textarea name&#61;"text" rows&#61;5 cols&#61;40>textarea>dd><dd><input type&#61;"submit" value&#61;"Share">dd>dl>form>
{% endif %}
<ul class&#61;"entries">{% for entry in entries %}<li><h2>{{ entry.title }}h2>{{ entry.text|safe }}li>{% else %}<li><em>Unbelievable. No entries here so farem>li>{% endfor %}
ul>
{% endblock %}
最后是登录模板&#xff0c;只是简单地显示一个允许用户登录的表单:
{% extends "layout.html" %}
{% block body %}<h2>Loginh2>{% if error %}<p class&#61;"error"><strong>Error: strong>{{ error }}p>{% endif %}<form action&#61;"{{ url_for(&#39;login&#39;) }}" method&#61;"post"><dl><dt>Usernname:<dd><input type&#61;"text" name&#61;"usernname">dd><dt>Password:<dd><input type&#61;"password" name&#61;"password">dd><dd><input type&#61;"submit" name&#61;"Login">dl>form>
{% endblock %}
9. 添加样式
现在其他的一切都可以正常工作&#xff0c;是时候给应用添加样式了。只需要在之前创建的 static 文件夹中创建一个名为 style.css 的样式表:
body { font-family: sans-serif; background: #eee; }
a, h1, h2 { color: #377BA8; }
h1, h2 { font-family: &#39;Georgia&#39;, 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 d1 { 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; }
10. 福利: 应用测试
现在你应该完成你的应用&#xff0c;并且一切都按预期运转正常&#xff0c;对于简化未来的修改&#xff0c;添加自动测试不是一个坏主意。上面的应用将作为文档中测试Flask应用节的例子来演示如何进行单元测试。去看看测试 Flask 应用是多么简单的一件事。