到现在为止,hello.py的完整代码如下:
from flask import Flask, render_template, session, redirect, url_for, flash
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_moment import Moment
import os
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail
from flask_mail import Message
from threading import Thread
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'I am Lethe'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 电子邮件
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <xxxxxxx@qq.com>'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role')
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)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
message = db.Column(db.Text)
def __repr__(self):
return '<User %r>' % self.username
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def sned_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
@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)
db.session.commit()
session['known'] = False
# 发送电子邮件
if app.config['FLASKY_ADMIN']:
sned_email(app.config['FLASKY_ADMIN'], 'New User',
'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
session['message'] = user.message
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
message=session.get('message'))
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
@app.errorhandler(404)
def pate_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
可以看到随着应用复杂程度增加,将所有部分写在一个脚本里会导致许多问题,而不同于多数其他的 Web 框架,Flask 并不强制要求大型项目使用特定的组织方式,应用结构的组织方式完全由开发者决定。
一、项目结构
多文件 Flask 应用的基本结构如下:
|-flasky
|-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/
|-requirements.txt
|-config.py
|-flasky.py
这种结构有4个顶级文件夹:
- Flask 应用一般保存在名为 app 的包中;
- 数据库迁移脚本在 migrations 文件夹中;
- 单元测试在 tests 包中编写;
- Python虚拟环境在 venv 文件夹中。
此外,还多了一些新文件:
- requirements.txt 列出了所有依赖包,便于在其他计算机中重新生成相同的虚拟环境;
- config.py 存储配置;
- flasky.py 定义 Flask 应用实例,同时还有一些辅助管理应用的任务。
下面我们尝试把之前的 hello.py 应用转换成此种结构。
二、配置选项
应用经常需要设定多个配置,如开发、测试和生产环境要使用不同的数据库,这样才不会彼此影响。
除了 hello.py 中类似字典的 app.config 对象之外,还可以使用具有层次结构的配置类。将 hello.py 中的配置项独立在 config.py 中如下:
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'I am Lethe'
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.qq.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', '465'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_SSL', 'true').lower() in \
['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <xxxxxxx@qq.com>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
sQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod
def init_app(app):
pass
# 开发环境数据库
class DevelopmentConfig(Config):
DEBUG = True
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://'
# 生成环境数据库
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
}
- 基本Config包含通用配置,各个子类分别定义专用的配置。如果需要,也可以添加其他配置类。
- 为了更安全和灵活,多数配置都可以从环境变量中导入。
- 在 3 个子类中,SQLALCHEMY_DATABASE_URI 变量都被指定了不同的值。这样应用就可以在不同的环境中使用不同的数据库。
- 开发环境和生产环境都配置了邮件服务器。为了再给应用提供一种定制配置的方式,Config 类及其子类可以定义 init_app() 类方法,其参数为应用实例。现在,基类 Config 中的 init_app() 方法为空。
- 在这个配置脚本末尾,config 字典中注册了不同的配置环境,而且还注册了一个默认配置(这里注册为开发环境)。
三、应用包
应用包用来存放应用的所有代码、模板和静态文件,通常称为为 app(应用)。templates 和 static 目录需要移动到应用包中,数据库模型和电子邮件支持函数也要移到这个包中,分别保存为 app/models.py 和 app/email.py。
3.1 使用应用工厂函数
单个文件中开发应用是很方便,但却有个很大的缺点:应用在全局作用域中创建,无法动态修改配置。运行脚本时,应用实例已经创建,再修改配置为时已晚。这一点对单元测试尤其重要,因为有时为了提高测试覆盖度,必须在不同的配置下运行应用。
这个问题的解决方法是延迟创建应用实例,把创建过程移到可显式调用的工厂函数中。这种方法不仅可以给脚本留出配置应用的时间,还能够创建多个应用实例,为测试提供便利。
应用的工厂函数在 app 包的构造文件 app/__init__.py 中定义如下:
from flask import Flask, render_template
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_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
- 构造文件导入了大多数使用的 Flask 扩展,由于此时尚未初始化应用实例,所以这些扩展的实例化并未传参,也就没有真正初始化。
- create_app() 函数是应用的工厂函数,接收一个参数,即应用使用的配置名(前面在config.py中定义的)。配置可以通过 app.config 配置对象提供的 from_object() 方法直接导入应用,参数 config[config_name] 即从 config 字典中选择一个配置类进行配置。
- 在之前创建的的扩展对象上调用 init_app() 方法可以将 Flask 扩展完成初始化。
3.2 在蓝本中实现应用功能
(1)蓝本(blueprint)和应用类似,也可以定义路由和错误处理程序。但是在蓝本中定义的路由和错误处理程序处于休眠状态,直到蓝本注册到应用上之后,才相当于真正定义在了应用中。
蓝本可以在单个文件中定义,也可使用更结构化的方式在
包中的多个模块中创建。我们将在应用包中创建一个子包 main,用于保存应用的第一个蓝本。
此子包的构造文件 app/main/__init__.py 如下,创建主蓝本:
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors
- 蓝本通过实例化一个 Blueprint 类对象创建。这个构造函数有两个必须指定的参数:蓝本的名称和蓝本所在的包或模块。
- 应用的路由保存在包里的 app/main/views.py 模块中,而错误处理程序保存在 app/main/errors.py 模块中,导入这两个模块就能把路由和错误处理程序与蓝本关联起来。
- 这些模块在 app/main/init.py 脚本的末尾导入,这是为了避免循环导入依赖,因为在 app/main/views.py 和app/main/errors.py 中还要导入 main 蓝本,所以除非循环引用出现在定义 main 之后,否则会致使导入出错。
(2)蓝本在工厂函数 create_app() 中注册到应用上,如下注册主蓝本:
# app/__init__.py
def create_app(config_name):
# ...
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
(3)主蓝本中的错误处理程序 app/main/errors.py:
from flask import render_template
from . import main
@main.app_errorhandler(404)
def pate_not_found(e):
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
- 之前我们使用的是 errorhandler 装饰器,但是在蓝本中如果使用他,就只有蓝本中的错误才能触发处理程序。
- 因此我们需要使用 app_errorhandler 装饰器来注册全局的错误处理程序。
(4)主蓝本中定义的应用路由 app/main/views.py:
from datetime import datetime
from flask import render_template, session, redirect, url_for, flash
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():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit()
session['known'] = False
# 发送电子邮件
if app.config['FLASKY_ADMIN']:
sned_email(app.config['FLASKY_ADMIN'], 'New User',
'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
session['message'] = user.message
form.name.data = ''
return redirect(url_for('main.index')) # 在同一蓝本中可简写为 .index
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
message=session.get('message'))
@main.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
- 和错误处理程序一样,这里的路由装饰器使用的是 main.route,而不是 app.route。
- url_for() 函数使用的是 url_for(‘main.index’) ,而不是 url_for(‘index’)。这是因为 Flask 会为蓝本中的全部端点加上一个命名空间,即为蓝本的名称(Blueprint 构造函数的第一个参数)。
- 若请求的端在在蓝本内,则也可以缩写为 url_for(’.index’)
(5)还需要将表单类移到蓝本中,保存在 app/main/forms.py 模块中:
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
四、应用脚本
应用实例在顶级目录中的 flasky.py 模块里定义:
import os
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
- 此主脚本先创建了一个应用实例,配置名可以从环境变量中读取,也可以使用默认值。
- 然后初始化数据库迁移扩展 Flask-Migreate 并为 Python shell 注册上下文。
现在我们要想运行应用,就需要把环境变量 FLASK_APP 设置为 flasky.py ,再执行 flask run 才可以。此外,还可以将 FLASK_DEBUG设置为1,来开启调试模式。
五、需求文件
应用中最好有个 requirements.txt 文件,用于记录所有依赖包及其精确的版本号,以便在另一个环境上重新生成虚拟环境。
在虚拟环境中执行如下命令:
pip freeze >requirements.txt
在安装或升级包后,最好更新一下这个文件。
然后当你想创建这个虚拟环境的副本时,则可以先创建一个新的虚拟环境,然后根据 requirements.txt 安装需要的包和扩展:
pip install -r requirements.txt
六、创建数据库
首选从环境变量中读取数据库的 URL,同时还提供了一个默认的SQLite 数据库作为备用。3 种配置环境中的环境变量名和 SQLite 数据库文件名都不一样。
不管从哪里获取数据库 URL,都要在新数据库中创建数据表,参见“数据库”章节
如果使用 Flask-Migrate 跟踪迁移,可使用下述命令创建数据表或者升级到最新修订版本:
flask db upgrade