> 文章列表 > 学习Flask之七、大型应用架构

学习Flask之七、大型应用架构

学习Flask之七、大型应用架构

学习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@example.com>'

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 提供了更好的解决方案,使用blueprintsblueprint与应用的相似之处是它也可以定义路由。不同之处是与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构建网络应用的必要元素,但是你可能还不确定如何用这些元素来形成一个实际的应用。