项目布局
创建一个目录。
简单的flask只有一个文件。
然而,随着项目变得越来越大,将所有代码保存在一个文件中就变得非常困难。Python项目使用包将代码组织成多个模块,这些模块可以在需要时导入。
项目目录将包含:
- flaskr/, 一个包含应用程序代码和文件的python包。
- tests/, 一个包含测试模块的目录。
- venv/,一个python虚拟环境,其中安装了flask和其他以来项目。
- 告诉Python如何安装项目的安装文件。
- 版本控制配置,如git。您应该养成为所有项目使用某种类型的版本控制的习惯,无论大小如何。
- 您将来可能添加的任何其他项目文件。
最后,你的项目看起来就像这样:
/home/user/Projects/flask-tutorial
├── flaskr/
│ ├── __init__.py
│ ├── db.py
│ ├── schema.sql
│ ├── auth.py
│ ├── blog.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── auth/
│ │ │ ├── login.html
│ │ │ └── register.html
│ │ └── blog/
│ │ ├── create.html
│ │ ├── index.html
│ │ └── update.html
│ └── static/
│ └── style.css
├── tests/
│ ├── conftest.py
│ ├── data.sql
│ ├── test_factory.py
│ ├── test_db.py
│ ├── test_auth.py
│ └── test_blog.py
├── venv/
├── setup.py
└── MANIFEST.in
如果使用版本控制,则应忽略在运行项目时生成的下列文件。根据您使用的编辑器,可能还有其他文件。一般来说,忽略您没有编写的文件。例如,使用git:
venv/
*.pyc
__pycache__/
instance/
.pytest_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/
安装应用
Flask应用程序是Flask类的一个实例。关于应用程序的所有内容,如配置和url,都将用这个类注册。
创建Flask应用程序最直接的方法是在代码的顶部直接创建一个全局Flask实例,比如“Hello, World!”例子在前一页做过。虽然这在某些情况下是简单和有用的,但随着项目的增长,它可能会导致一些棘手的问题。
您将在函数中创建一个Flaks实例,而不是全局创建一个Flask实例。这个函数称为应用程序工厂。应用程序需要的任何配置、注册和其他设置都将在函数中进行,然后返回应用程序。
应用工厂
创建flaskr目录并添加_init_ .py文件。py有双重功能:它将包含应用程序工厂,并告诉Python应该将flaskr目录视为一个包。
flaskr/init.py
import os
from flask import Flask
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev',
DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
)
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('config.py', silent=True)
else:
# load the test config if passed in
app.config.from_mapping(test_config)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# a simple page that says hello
@app.route('/hello')
def hello():
return 'Hello, World!'
return app
create_app 是应用工厂函数。
- app = Flask(__name__, instance_relative_config=True) creates the Flask instance.
- __name__是当前Python模块的名称。应用程序需要知道它位于何处来设置一些路径,而用__name__来告诉它这一点很方便。
- instance_relative_config=True告诉应用程序配置文件相对于实例文件夹。实例文件夹位于flaskr包之外,可以保存不应该提交到版本控制的本地数据,比如配置机密和数据库文件。
- from_mapping()设置应用程序将使用的一些默认配置:
- flask和扩展程序使用SECRET_KEY来保证数据安全。它被设置为’dev’,以便在开发期间提供一个方便的值,但是在部署时应该用一个随机值覆盖它。
- DATABASE是存储SQLite数据库文件的路径。它在app.instance_path下,这是flask为实例文件夹选择的路径。
- from_pyfile()用实例文件夹中的config.py文件(如果存在的话)中的值覆盖默认配置。例如,在部署时,可以使用它设置一个真正的SECRET_KEY。
- test_config也可以传递给工厂,并将代替实例配置使用。这样,您将在后面编写的测试就可以独立于您所配置的任何开发值进行配置。
- makedirs()确保app.instance_path存在。flask不会自动创建实例文件夹,但是需要创建它,因为您的项目将在那里创建SQLite数据库文件。
- @app.route()创建了一个简单的路由,因此在学习本教程的其余部分之前,您可以看到应用程序正在工作。它在URL /hello和一个返回响应的函数(字符串“hello, World!“在这种情况下。
配置环境变量/运行
$env:FLASK_APP = "flaskr"
$env:FLASK_ENV = "development"
flask run
定义使用数据库
应用程序将使用SQLite数据库存储用户和推送。Python在sqlite3模块中提供了对SQLite的内置支持。
SQLite很方便,因为它不需要设置单独的数据库服务器,而且是Python内置的。但是,如果并发请求试图同时写入数据库,则每次写入都会按顺序进行,从而降低速度。小型应用程序不会注意到这一点。一旦你变大了,你可能想要切换到一个不同的数据库。
本教程没有详细介绍SQL。如果您不熟悉它,SQLite文档描述了这种语言。
链接到数据库
当使用SQLite数据库(以及大多数其他Python数据库库)时,要做的第一件事是创建到它的连接。使用连接执行任何查询和操作,连接在工作完成后关闭。
在web应用程序中,这种连接通常与请求绑定。它是在处理请求时创建的,并在发送响应之前关闭。
flaskr/db.py
import sqlite3
import click
from flask import current_app, g
from flask.cli import with_appcontext
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
db = g.pop('db', None)
if db is not None:
db.close()
g是一个特殊的对象,对于每个请求都是惟一的。它用于存储请求期间多个函数可能访问的数据。如果在同一个请求中第二次调用get_db,则存储和重用连接,而不是创建新的连接。
current_app是另一个特殊的对象,**它指向处理请求的Flask应用程序。**由于使用了应用程序工厂,所以在编写其余代码时没有应用程序对象。get_db将在创建应用程序并处理请求时调用,因此可以使用current_app。
sqlite3.connect()建立到数据库配置键指向的文件的连接。这个文件还不需要存在,并且在稍后初始化数据库之前不会存在。
sqlite3.Row告诉连接返回的行,行为像字典一样,这允许按名称访问列。
close_db通过检查g.db是否设置好来检查是否创建了连接。如果连接存在,则关闭连接。接下来,您将告诉应用程序关于应用程序工厂中的close_db函数的信息,以便在每次请求之后调用它。
创建表格
在SQLite中,数据存储在表和列中。在存储和检索数据之前,需要先创建这些属性。Flaskr将用户存储在用户表中,post存储在post表中。用创建空表所需的SQL命令创建一个文件:
flaskr/schema.sql:
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS post;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
title TEXT NOT NULL,
body TEXT NOT NULL,
FOREIGN KEY (author_id) REFERENCES user (id)
);
使用db.py去运行上面的SQL命令。
def init_db():
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear the existing data and create new tables."""
init_db()
click.echo('Initialized the database.')
**open_resource()**打开一个相对于flaskr包的文件,这很有用,因为在稍后部署应用程序时,您不一定知道该位置在哪里。get_db返回一个数据库连接,用于执行从文件中读取的命令。
**command()**定义一个名为init-db的命令行命令,该命令调用init_db函数并向用户显示一条成功消息。您可以阅读命令行界面来了解有关编写命令的更多信息。
注册我们的应用
close_db和init_db_command函数需要在应用程序实例中注册,否则应用程序不会使用它们。但是,由于使用的是工厂函数,所以在编写函数时不能使用该实例。相反,编写一个接受应用程序并进行注册的函数。
flaskr/db.py
def init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)
teardown_appcontext()告诉Flask在返回响应后进行清理时调用该函数。
add_command()添加了一个可以用flask命令调用的新命令。
从工厂导入并调用这个函数。在返回应用程序之前,将新代码放在工厂函数的末尾。
flaskr/init.py
def create_app():
app = ...
# existing code omitted
from . import db
db.init_app(app)
return app
初始化数据库文件
既然init-db已经在应用程序中注册,就可以使用flask命令调用它,类似于前一页中的run命令。
注意
如果您仍然从上一页运行服务器,则可以停止服务器,或者在新终端中运行此命令。如果使用新终端,请记住切换到项目目录并激活env,如activate the environment中所述。您还需要设置FLASK_APP和FLASK_ENV,如前一页所示。
运行init-db命令:
flask init-db
Initialized the database.
现在将会有一个flaskr。项目实例文件夹中的sqlite文件。
蓝图和视图
视图函数是用来响应应用程序请求的代码。Flask使用模式将传入的请求URL匹配到应该处理它的视图。视图返回Flask转换为输出响应的数据。Flask还可以采取另一种方法,根据视图的名称和参数生成视图的URL。
创建一个蓝图
蓝图是组织一组相关视图和其他代码的方法。视图和其他代码不是直接注册到应用程序中,而是注册到蓝图中。然后,当蓝图在工厂函数中可用时,将其注册到应用程序中。
Flaskr将有两个蓝图,一个用于身份验证功能,另一个用于博客文章功能。每个蓝图的代码将放在一个单独的模块中。由于博客需要了解身份验证,您将首先编写身份验证。
flaskr/auth.py:
import functools
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
from flaskr.db import get_db
bp = Blueprint('auth', __name__, url_prefix='/auth')
这将创建一个名为“auth”的蓝图。与应用程序对象一样,蓝图需要知道它在何处定义,因此将作为第二个参数传递给_name__。url_prefix将前缀到与蓝图关联的所有url。
使用app.register_blueprint()从工厂导入并注册蓝图。在返回应用程序之前,将新代码放在工厂函数的末尾。
flaskr/init.py:
def create_app():
app = ...
# existing code omitted
from . import auth
app.register_blueprint(auth.bp)
return app
身份验证蓝图将具有注册新用户、登录和注销的视图。
第一个视图:注册器
当用户访问/auth/register URL时,register视图将返回HTML和一个表单,供用户填写。当他们提交表单时,它将验证他们的输入,或者再次显示带有错误消息的表单,或者创建新用户并转到登录页面。
现在只需要编写视图代码。在后面,您将编写模板来生成HTML表单。
flaskr/auth.py:
@bp.route('/register', methods=('GET', 'POST'))
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
if not username:
error = 'Username is required.'
elif not password:
error = 'Password is required.'
elif db.execute(
'SELECT id FROM user WHERE username = ?', (username,)
).fetchone() is not None:
error = 'User {} is already registered.'.format(username)
if error is None:
db.execute(
'INSERT INTO user (username, password) VALUES (?, ?)',
(username, generate_password_hash(password))
)
db.commit()
return redirect(url_for('auth.login'))
flash(error)
return render_template('auth/register.html')
寄存器视图函数的作用如下:
- @bp。route将URL /register与register视图函数关联起来。当Flask接收到/auth/register的请求时,它将调用register视图并使用返回值作为响应。
- 如果用户提交了表单,则请求。方法为“POST”。在本例中,开始验证输入。
- request.form是提交的表单键和值的一种特殊类型的dict映射。用户将输入他们的用户名和密码。
- 验证用户名和密码是否为空。
- 通过查询数据库并检查是否返回结果来验证用户名尚未注册。db。执行需要一个SQL查询**?任何用户输入的占位符**,以及用于替换占位符的值元组。数据库库将负责转义这些值,这样您就不会受到SQL注入攻击的攻击。
fetchone()从查询中返回一行。如果查询没有返回任何结果,则返回None。稍后,使用fetchall(),它返回所有结果的列表。 - 如果验证成功,则将新用户数据插入数据库。为了安全起见,密码不应该直接存储在数据库中。**相反,generate_password_hash()用于安全地对密码进行散列,并存储该散列。**由于此查询修改数据,因此需要在稍后调用db.commit()来保存更改。
- 在存储用户之后,它们被重定向到登录页面。url_for()根据login视图的名称为其生成URL。这比直接编写URL更好,因为它允许您稍后更改URL,而无需更改所有链接到URL的代码。redirect()生成对生成URL的重定向响应。
- 如果验证失败,将向用户显示错误。flash()存储可以在呈现模板时检索的消息。??
- 当用户最初导航到auth/register时,或者出现验证错误时,应该显示带有注册表单的HTML页面。render_template()将呈现一个包含HTML的模板,您将在本教程的下一步中编写该模板。
登陆
flaskr/auth.py:
@bp.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
user = db.execute(
'SELECT * FROM user WHERE username = ?', (username,)
).fetchone()
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user['password'], password):
error = 'Incorrect password.'
if error is None:
session.clear()
session['user_id'] = user['id']
return redirect(url_for('index'))
flash(error)
return render_template('auth/login.html')
与注册表的观点有一些不同之处:
- 首先查询用户,并将其存储在变量中供以后使用。
- check_password_hash()以与存储的散列相同的方式散列提交的密码,并对它们进行安全比较。如果匹配,则密码有效。
- session是一个dict,**它跨请求存储数据。**当验证成功时,用户的id将存储在一个新会话中。数据存储在发送到浏览器的cookie中,然后浏览器将其与后续请求一起发回。Flask对数据进行了安全的签名,这样就不会被篡改了。
既然用户的id存储在会话中,**它将在后续请求中可用。**在每个请求开始时,如果用户已登录,则应加载其信息并将其提供给其他视图。
flaskr/auth.py
@bp.before_app_request
def load_logged_in_user():
us