Flask Web DEvelopment翻译6

第七章 大型程序架构

虽然在一个脚本里完成一个web应用很便利,但是这也意味着它很难扩展。当程序不断增长,越来越复杂,在一个巨大的源码文件中工作就会问题多多。不像其他一些web框架,Flask并没有规定大型程序应该以某种特定形式进行组织,程序结构完全由开发者自己决定。
  本章介绍了一种可能的大型程序的组织方式:包和模块。这一结构将在下面书中的例子中应用。

工程结构

例子7-1显示了一个Flask程序的基本布局。

|--flasky/
    requirements.txt
    config.txt
    manage.py   
    |-app/
        __init__.py
        email.py
        modes.py        
        |-templates/
        |-static/
        |-main/
            __init__.py
            errors.py
            forms.py
            views.py
    |-migrations/
    |-tests/
    |-venv/

这个结构有四个顶层文件夹:

  • 整个应用程序被包含在一个包里面,该包一般名为app;
  • migrations文件夹包含了数据库迁移脚本,前面提到过;
  • 单元测试包含在tests包里;
  • venv文件夹包含了Python 虚拟环境,如前所述;

这里面还有一些新增文件:

  • requirements.txt 列出了依赖的包,这样当在别的机器上运行时可以方便、准确地创建虚拟环境。
  • config.py 保存了全部的配置
  • manage.py 启动程序和其他一些程序任务
    为了帮你充分理解,下列章节展示了把hello.py转换到这一结构的步骤。

配置选项

程序通常需要多个配置项。最好的例子就是在开发、测试、投产不同阶段,需要使用不同的数据库,并确保这些数据库不相互干扰。
  我们使用一个可继承的配置类取代了原来hello.py中简单地类似字典的配置结构。请看例子7-2,

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()类方法,以程序实例为参数。这样就可以执行特定配置的初始化。现在,基础的Config类只是实现了一个空方法。
  在配置脚本的最后,不同的配置被注册为一个config字典。同时,其中的一个配置项(此处是开发模式)被指定为默认值。

译注:这里启动程序时,监视窗口中SQLAlchemy也会有一个提醒信息,要求你把
SQLALCHEMY_TRACK_MODIFICATIONS 设置为True。这个与新版(0.11.1)Flask的提醒一样,都是鉴于将来的版本变动准备的。我们需要在配置文件中添加一行就可以了。
Flasky/config.py文件中: SQLALCHEMY_TRACK_MODIFICATIONS=True

程序包

程序包是包含了所有程序代码、模板、和静态文件的集合。一般简单的称为 app,当然你也可以给它指定一个名字。templates和statics两个文件夹是app包的一部分,所以被移动到app文件夹里。数据模型和email函数也被移动到这个包里,每个都是独立的一个模块:app/models.py和app/email.py

使用应用工厂

在一个文件里创建应用程序非常方便,但也有缺点。因为程序是全局范围的,无法动态更改一些配置:在脚本运行时,程序实例已经被创建了,所以此时要更改配置就太晚了。而这一点在进行单元测试时尤其重要,因为我们必须在不同配置下运行程序才能有更好的测试覆盖率。
  这一问题的解决方案就是延迟创建程序:把程序创建过程转到工厂函数中,从而可以在脚本中明确调用。这不但给了脚本设置配置的时间 ,也具备了创建多个程序实例的能力——在进行测试的时候非常重要。如例子7-3,应用程序工厂函数被定义在app包的构造函数里。
  这个app包构造函数导入了当前使用的大部分Flask扩展,但因为此处还没有初始化程序实例,所以这些扩展的构造函数中都是空参数。create_app()函数就是应用工厂,它以程序使用的配置名作为参数。使用app.config对象的from_object()方法,可以直接把config.py的配置类中保存的配置导入到程序中,并从配置字典中通过名字选择配置对象。一旦程序创建并配置完成,扩展就可以初始化了。调用已创建的各扩展的init_app()完成各自的初始化。
例子7-3

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

工厂函数返回了创建的程序实例,但注意,工厂函数当前状态下创建的程序并没有完成,比如就没有路由、没有自定义错误处理页面。这是下一节的内容。

通过蓝图实现应用程序的功能

在应用程序转换到工厂模式过程中,路由配置上会存在问题。在单脚本程序中,程序实例是全局作用域可用的,所以路由可以使用app.route装饰器很轻松搞定。但是现在由于程序实例是在运行时创建的,而app.route装饰器只能在create_app()调用结束之后才开始作用——这就太晚了。错误处理的问题跟路由类似,因为它也是通过装饰器app.errorhandler来定义的。
  幸运的是,Flask提供了名为“蓝图(blueprint)”的解决方案。蓝图类似于一个应用程序,可以在其中定义路由。不同之处是,与蓝图关联的路由处于休眠状态,直到蓝图被注册到程序,这时路由就变成了该程序的一部分。借助在全局作用域中定义的蓝图,就可以像在单脚本程序中那样定义程序的路由。
  类似于单脚本程序,我们可以把全部蓝图定义在一个文件中,也可以以在一个包里以模块的形式定义多个蓝图。出于最大灵活性的考虑,我们在应用程序包(译注:app包)中创建一个子包来管理这些蓝图。例子7-4显示了创建蓝图的包的结构。

Example 7-4. app/main/__init__.py: Blueprint creation
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors

蓝图是创建为 bulueprint类的一个实例化对象。这个类的构建函数有两个必备参数:蓝图名和模块或包(译注:蓝图所处的)的名字。就像程序中那样,python的__name__变量在大部分情况下都适合做第二个参数。
  程序路由存在app/main/views.py模块里,错误处理则存放在app/main/error.py中。导入这些模块后,路由和错误处理就被关联到蓝图。有很重要的一点要注意:为了防止循环依赖,我们在app/__init__.py脚本的底部才导入模块,因为views.py和errors.py也需要导入main蓝图。
  蓝图在create_app()工厂函数里被注册到程序,如例子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

例子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

在蓝图中编写错误处理时,需要注意有一点不同:如果像以前一样使用errorhandler装饰器,那么只有蓝图内发生的错误才会调用该错误处理器。要设置全程序范围的错误处理器,必须使用app_errorhandler装饰器。
例子7-7展示了更新为蓝图版的程序路由设计

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())

在蓝图中创建视图函数主要有两点不同。首先,就像前面错误处理器那样(@main.app_errorhandler),路由装饰器来自于蓝图(使用@main.route)。其二,url_for()用法不同了。
  你可能记得,url_for()的第一个参数是路由的端点名(endpoint name),对于基于程序的路由默认就是对应的视图函数名。例如,在单脚本程序中,index()的视图函数的url可以通过url_for('index')获得。
  蓝图的不同之处是,Flask为所有蓝图的端点提供了一个命名空间,这样多个蓝图可以使用同样的端点名来定义视图函数而不冲突。命名空间就是蓝图的名字(如main,蓝图构造函数的第一个参数),那么index()视图函数的端点名就被注册为main.index,其url就可以通过url_for('main.index')来取得。
  url_for()函数也支持蓝图的短格式端点名(省略了蓝图名),如url_for('.index'),使用点号('.'),就可以使用蓝图函数处理当前请求。这样一来,在同一个蓝图内的重定向就可以使用短格式,而如果是跨蓝图的话,重定向还是必须加上命名空间名称(蓝图名)的。
  为了完成更改程序页面,表单对象也同样被保存在蓝图下app/main/forms.py模块当中

启动脚本

在最顶层文件夹里的manage.py是用来启动程序的。这个脚本内容如例子7-8所示:

Example 7-8. manage.py: Launch script
#!/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,Flas-Migrate和自定义shell上下文陆续初始化。
  为了方便,添加了一个位置行(#!/usr/bin/env python指定了python解释器位置),这样在Unix类操作系统下,这个脚本可以通过./manage.py启动,而不需要使用python manage.py 命令。

Requirements文件

程序必须包含一个requirements.txt文件,来记录所有的依赖包和确切版本。这可以帮你在别的机器上快速还原虚拟环境,比如当你在部署程序投入生产的时候,可以利用这个文件快速生成虚拟环境。这个文件可以通过pip命令自动生成:

(venv) $ pip freeze >requirements.txt

当你的包升级或新加之后,应该及时更新这个文件。下面是一个示例:

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

此处你看到的文档里各包的版本可能已经过期了。你可以尝试使用比较新的版本。要是出现问题,那就回退到这个文档里标注的版本——它们肯定是兼容的。

译注:2016-7-20,现在全新安装虚拟环境的话,Flask已经升级到0.11.1了,会出现一些提醒信息,比如导入扩展的语法将要从form flask.ext.moment import Moment变更为from flask_moment import Moment

单元测试

程序现在很小,没有什么可测试的。但是,为了演示一下,我们还是在例子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()方法试图为接近运行程序的测试创建环境。首先使用test配置创建程序,并激活其上下文。这一步是确保测试能够像普通请求一样访问current_app。然后,需要的话它就创建一个新的测试数据库。数据库和程序上下文将在tearDown()方法中被移除。
  第一个测试是确定程序示例已经存在。第二个测试则确认程序以测试配置在运行。给tests文件夹添加一个文件tests/__init__.py,把这个文件夹变成一个包。这个文件只是一个空白文件,其作用就是供unittests库在扫描所有模块时用以确定测试文件的位置。
  为了运行单元测试,你需要向manage.py脚本添加一个自定义命令。例子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装饰器就能方便得自定义命令。装饰器函数的名字就是被用作命令的名字,函数的文本字符("""Run the unit tests.""")将被显示在帮助消息里。test()函数的实现则调用了unittest包的test runner。
  可以如下执行单元测试,并出现类似回显:

(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数据库作备胎。环境变量名和SQLite数据库的文件名在三种环境配置下各自不同。例如,开发环境下,URL取自环境变量DEV_DATABASE_URL。如果没有定义的话,就使用data-dev.sqlite这一SQLite数据库备胎。
  不论数据库URL是什么,只要是新数据库,都需要创建数据表。当你使用Flask-Migrate来管理迁移时,只需使用如下命令即可创建新数据表或更新其到最新版本:

(venv) $ python manage.py db upgrade

可能难以置信,现在你已经完成了本书第一部分了!目前,你已经学习了适用Flask开发web应用所需的各种扩展部件等——可能你还不太确定如何在真实程序中把它们组合在一起。下面本书第二部分的目标就是带领你开发一个完整的程序。
<<第六章 EMail 第八章 用户验证>>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容