学习Flask之七、大型应用架构
尽管存放在单一脚本的小型网络应用很方便,但是这种应用不能很好的放大。随着应用变得复杂,维护一个大的源文件会出现问题。不像别的网络应用,Flask没有强制的大型项目组织结构。构建应用的方法完全留给开发者。本章,呈现一种组织大型应用到包或模块的方法。
这种结构用于维护本书余下的例子。
项目结构
Example 7-1 展示Flask应用的基础布局
Example 7-1. 基础的多文件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
|-manage.py
这个结构有4个顶层目录:
• Flask应用通常放在名为app的包里。
• migrations目录包含数据库迁移脚本,如前所述。
• Unit tests 放在tests包里。
• venv目录包含Python虚拟环境,如前所述。
也有一些新的文件:
• requirements.txt列出了依赖包以便在不同的计算机产生相同的虚拟环境。
• config.py存贮配置设置。
• manage.py启动应用和其它应用任务。
为了帮助你理解这个结构,下一节描述如何将hello.py应用转换到这个结构。
配置选项
应用通常需要多种配置设置。最好的例子是在开发、测试和生产过程中需要不同的数据库,以免相互干扰。 不像hello.py里用简单的字典结构配置,而是使用一个层级的配置类。
Example 7-2展示了config.py文件。
Example 7-2. config.py: Application configuration
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_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
}
Config基类包含所有配置常用的设置,不同的子类定义特定的配置设置。 额外的配置可以按需添加。要使配置更灵活和安全,有些设置可以可选的从环境变量导入。例如 SECRET_KEY值,由于它的敏感性,可以设置在环境里,但是可以提供黙认值以免环境里没有定义。
SQLALCHEMY_DATABASE_URI三种配置有不同的值。这可以使应用在不同的配置下运行,每个使用不同的数据库。
配置类可以定义一个 init_app()类方法,它取应用实例作为参数。这里配置特定的初始化可以进行。现在基础配置类实施一个空的init_app()方法。在配置脚本下部,不同的配置注册于config字典。一个配置(用于开发环境)也注册为黙认。
应用包
应用包是所有的应用代码,静态文件,模板存放的地方。它简单的命名为app,虽然它可以给应用特定的名字,如果需要。
templates和static 目录是应用包的一部分,所以这两个目录放在app里。数据模型和邮件支持函数也放在这个包里,每个都有它自已的模块如app/models.py 和app/email.py。
使用应用工厂
在一个文件里创建应用非常方便,但是有个大的缺点。因为应用创建在全局范围里,没办法动态的应用配置变化:脚本运行时,应用实例已经创建,进行配置变更太迟了。
这对单元测试特别重要,因为有时它有必要在不同的配置设置下运行以覆盖更好的测试。这个问题的解决方案是延迟应用的创建,通过把它放在工厂 函数里,可以从脚本明文的调用。
这不但可以给脚本时间进行配置也可以创建多个应用实例--也时在测试过程中很有用。例Example 7-3显示的工厂函数,在app包的构造函数里定义。构造函数导入大部分Flask扩展,但是因为没有应用实例初始化他们,它的创建没有初始化,通过传递空参数到构造函数。create_app()函数是应用工厂,取名字作为参数供应用使用。配置设置存贮于一个config.py 定义的类里的配置设置可以直接导入到应用,使用from_object()方法,这个方法在app.config配置对象里。通过config字典的name选择配置对象。一旦应用被创建和配置,扩展就可以被初始化。调用init_app()完成初始化工作。
Example 7-3. app/__init__.py: Application package constructor
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)
# attach routes and custom error pages here
return app
工厂函数返回创建的应用实例,但是注意用工厂函数创建的应用当前未完全,因为它们缺少路由和定制错误处理页。这是下一节的主题。
在Blueprint里实施应用功能
转换到应用工厂会使路由变得复杂。在单一脚本的应用里,应用实例存在于全局范围里,所以路由可以很易容的定义,使用app.route装饰器。但是现在应用在运行时创建,app.route只有在create_app()调用之后才有,这太迟了。像路由,定制错误处理页处理器同样有这个问题,因为它们由app.errorhandler装饰器定义。幸运的是,Flask 提供了更好的解决方案,使用blueprints。blueprint与应用的相似之处是它也可以定义路由。不同之处是与blueprint与关的路由处于支配状态,直到blueprint用一个应用注册,这个时候路由成为应用的一部分。使用全局范围里定义的blueprint,应用的路由可以与单一文件应用的路由的定义方法一样。像应用一样,blueprints也可以定义在一个单一的文件或一个包的多个模块里。为了最大的灵活性,应用包里的子包将创建以存放blueprint。
Example 7-4展示包的构造函数,它创建blueprint。
Example 7-4. app/main/__init__.py: Blueprint creation
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors
Blueprints通过实例化一个Blueprint类对象创建。这个类的构造函数取二个要求的参数:blueprint名和blueprint所在的模块或包。
与应用一样, Python的 __name__ 变量是第二个参数的正确值。应用的路由存放在 包的app/main/views.py模块里。 错误处理器放在 app/main/errors.py。导入变些模块使路由和错误处理页与 blueprint关联。重要的是要注意模块在app/__init__.py脚本的底部导入。以免循环依赖,因为views.py 和errors.py 需要导入主blueprint。blueprint 在 create_app()应用工厂里用应用注册,见Example 7-5.
Example 7-5. app/_init_.py: Blueprint registration
def create_app(config_name):
# ...
from main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
Example 7-6 展示错误处理器。
Example 7-6. app/main/errors.py: Blueprint with error handlers
from flask import render_template
from . import main
@main.app_errorhandler(404)
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
在blueprint里书写错误处理器的不同之处是如果使用错误处理器装饰函数,处理器只会因错误blueprint内的错误而调用。要安装应用范围内的处理器,必须使用app_errorhandler。
Example 7-7展示更新于blueprint内的应用路由
Example 7-7. app/main/views.py: Blueprint with application routes
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'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
current_time=datetime.utcnow())
在blueprint里书写view在两个主要的不同之处。首先,像前面的错误处理器,路由装饰器来自blueprint。
第二个不同之处是 url_for() 函数的使用。你可能记得,这个函数的第一个参数是路由的endpoint名, 在应用的路由里黙认是view名。例如,在一个脚本的应用的index()view函数的URL可以用url_for('index')获得。
blueprints的不同是Flask使用名字空间来调用来自blueprint的endpoints,以便多个blueprints可以定义view函数使用相同的endpoint名而不冲突。名字空间是blueprint的名(blueprint构造器的第一个参数),所以index()view函数用endpoint名main.index注册,它的URL可以用url_for('main.index')获得。blueprints里url_for()函数也支持更短格式的 endpoints,其中blueprint名可以忽略,例如url_for('.index')。使用这种标记,使用当前请求的blueprint。这意味着相同blueprint里的重定向可以用更短格式,而blueprints间的重定向必须使用 namespaced端点名。
要完成应用页的变更, form对象也存贮在app/main/forms.py的blueprint里。
启动脚本
使用在顶层目录里的manage.py文件来启动应用。这个脚本展示于 Example 7-8.
Example 7-8. manage.py: 启动脚本
#!/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')
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()
脚本以创建一个应用开始。使用的配置来自环境变量FLASK_CONFIG, 如果它被定义。如果没有定义,使用黙认的配置。然后Flask-Script, Flask-Migrate, 和Python shell的自制义上下文初始化。
作为习惯,增加一行shebang,以便基于Unix的操作系统里脚本可以执行为./manage.py 而不是python manage.py。
Requirements文件
应用必须包括requirements.txt文件记录所有的依赖包,带上正确的版本号。 这很重要,对于不同机器上产生相同的虚拟环境。例如应用布局于生产环境。这个文件通过如下命令自动产生:
(venv) $ pip freeze >requirements.txt
当包被安装或更新时刷新这个文件是很好的想法。示例的requirements文件如下:
Flask==0.10.1
Flask-Bootstrap==3.0.3.1
Flask-Mail==0.9.0
Flask-Migrate==1.1.0
Flask-Moment==0.2.0
Flask-SQLAlchemy==1.0
Flask-Script==0.6.6
Flask-WTF==0.9.4
Jinja2==2.7.1
Mako==0.9.1
MarkupSafe==0.18
SQLAlchemy==0.8.4
WTForms==1.0.5
Werkzeug==0.9.4
alembic==0.6.2
blinker==1.3
itsdangerous==0.23
当你要构建相同的虚拟环境时,你可以创建新的虚拟环境,然后运行如下命令:
(venv) $ pip install -r requirements.txt
你读这本书时,示例requirements.txt 的版本号可能已经过时了。如果你喜欢,你可以使用最新发行包。如果你遇到问题,你可以返回requirements文件指定的版本,因为这些是已知与应用兼容的。
单元测试
这个应用很小,还没有太多的测试,但作为示例,有两个测试可以按Example 7-9:
Example 7-9. tests/test_basics.py: Unit tests
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'])
这些测试用python标准库的标准的unittest包。 setUp()和tearDown()在测试前后运行,任何有test_开始的名称的方法都按测试执行。如果你想要学习更多用python unittest包写单元测试,请看官方文档。
setUp()方法试图创建一个与运行应用相似的测试环境。它首先创建一个测试的应用配置和激活上下文。这一步确保测试可以像正常的请求一样访问。然后它创建一个新的数据库,必要时测试可以使用它。数据库和应用上下文用tearDown()方法删除。
第一个测试确保应用实全存在。第二个测试确保应用运行于测试本置。要使测试目录成为一个合适的包,需要增加tests/__init__.py,但这可以是空的文件。因为 unittest包可以扫描所有模块并定位测试。要运行单元测试,可以在manage.py脚本增加制定命令。
Example 7-10 展示如何增加test命令。
Example 7-10. manage.py: Unit test launcher command
@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
manager.command装饰器使定制命令变得简单。装饰函数的名称用作命令名,函数的 docstring 显示于帮助信息。test()的实施调用来自unittest包的测试运行器。单元测试可以用如下方法执行。
(venv) $ python manage.py test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
.----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
数据库设置
重构的应用使用一个不同于单一脚本版本的数据库。数据库URL取自环境变量作为第一选择,黙认的SQLite数据库作为备选。环境变量和数据文件对于三种配置是不同的。例如,在开发配置里,URL来自环境变量DEV_DATABASE_URL,如果没有定义则使用名为 data-dev.sqlite的 SQLite 数据库。不管数据库URL的来源,都要首先创建新数据库的数据表。当用Flask-Migrate来跟踪迁移时,可以创建或更新数据库表到最新的版本,使用如下命令:
(venv) $ python manage.py db upgrade
不管你信不信,你已到达 Part I的结尾了。你已学习了用Flask构建网络应用的必要元素,但是你可能还不确定如何用这些元素来形成一个实际的应用。