尽管在单一脚本中编写小型Web程序很方便,但这种方法并不能广泛使用。程序变复杂后,使用单个大型源码文件会导致很多问题。
不同于大多数其他Web框架,Flask并不强制要求大型项目使用特定的组织方式,程序结构的组织方式完全由开发者决定。本章将会介绍一种使用包和模块组织大型程序的方式。本书后续示例都会采用这种结构。
7.1 项目结构
Flask程序的基本结构:
| -flasky
| -app/
| -templates/
| -static/
| -main/
| -__init__.py
| -errors.py
| -forms.py
| -views.py
| -__init__.py
| -email.py
| -models.py
| -migrations.py
| -tests/
| -__init__.py
| -test.py
| -venv
| -requirements.txt
| -config.py
| -manage.py
这种结构有四个顶级文件夹:
- Flask程序一般都保存在名为app的包中
- 和之前一样,migrations文件夹包含数据库迁移脚本
- 单元测试编写在tests包中
- 和之前一样,venv文件夹包含Python虚拟环境
- requirements.txt列出了所有依赖包,便于在其他电脑中重新生成相同的虚拟环境
- config.py存储配置
- manage.py用于启动程序以及其他的程序任务
7.2 配置选项
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中包含通用配置,子类分别定义专用的配置。如果需要,你还可添加其他配置类。
7.3 程序包
7.3.1 使用程序工厂函数
这个问题的解决方法是延迟创建程序实例, 把创建过程移到可显式调用的工厂函数中。这种方法不仅可以给脚本留出配置程序的时间,还能够创建多个程序实例,这些实例有时在测试中非常有用。程序的工厂函数在 app 包的构造文件中定义,如下所示。
from flask import Flask,render_template
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_sqlalchemy import SQLAlchemy
from flask_moment import Moment
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)
#附加路由和自定义的错误页面
return app
构造文件导入了大多数正在使用的 Flask 扩展。由于尚未初始化所需的程序实例(即Flask(__name__)),所以没有初始化扩展,创建扩展类时没有向构造函数传入参数。create_app() 函数就是程序的工厂函数,接受一个参数,是程序使用的配置名。配置类在 config.py 文件中定义,其中保存的配置可以使用 Flask app.config 配置对象提供的 from_object() 方法直接导入程序。至于配置对象,则可以通过名字从 config 字典中选择。程序创建并配置好后,就能初始化扩展了。在之前创建的扩展对象上调用 init_app() 可以完成初始化过程。
7.3.2 在蓝本中实现程序功能
from flask import Blueprint
main = Blueprint('main',__name__)
from . import views,errors
通过实例化一个Blueprint类对象可以创建蓝本。这个构造函数有两个必须指定的参数:蓝本的名字和蓝本所在的包或模块。和程序一样,大多数情况下第二个参数使用 Python 的__name__变量即可。
#...
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
示例 app/main/errors.py:蓝本中的错误处理程序
from flask import render_template
from . import main
@main.app_errorhandler(404)
def page_not_found(e):
return render_template('404.html')
@main.app_errorhandler(500):
def internal_server_error(e):
return render_template('500.html')
在蓝本中编写错误处理程序稍有不同,如果使用errorhandler修饰器,那么只有蓝本中的错误才能触发处理程序。要想注册全局的错误处理程序,必须使用app_errorhandler。
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=datatime.utcnow())
在蓝本中编写视图函数主要有两点不同:第一,和前面的错误处理程序一样,路由修饰器由蓝本提供;第二,url_for()函数的用法不同。我们还记得,url_for()函数的第一个参数是路由的端点名,在程序的路由中,默认为视图函数的名字。例如,在但脚本程序中index视图函数的URL可使用url_for('index')获取。
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')
7.4 启动脚本-----
#!/usr/bin/env python
import os
from app import create_app, db
from app.models import User, Role
from flask_script import Manager, Shell
from flask_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。
7.5 需求文件
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
7.6 单元测试
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_开头的函数都作为测试执行。
第一个测试确保程序实例存在。第二个测试确保程序在测试配置中运行。 若想把 tests 文件夹作为包使用,需要添加 tests/__init__.py 文件,不过这个文件可以为空,因为 unittest包会扫描所有模块并查找测试。
@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
manager.command 修饰器让自定义命令变得简单。修饰函数名就是命令名,函数的文档字符串会显示在帮助消息中。test() 函数的定义体中调用了 unittest 包提供的测试运行函数。单元测试可使用下面的命令运行: (venv) $ python manage.py test
7.7 创建数据库
重组后的程序和单脚本版本使用不同的数据库。
首选从环境变量中读取URL,同时还提供了一个默认的SQLite数据库做备用。3种配置环境中的环境变量名和SQLite数据库文件名都不一样。例如,在开发环境中,数据库URL从环境变量DEV_DATABASE_URL读取,如果没有定义这个环境变量,则使用名为data-dev.sqlite的SQLite数据库。
不管从哪里获取数据库URL,都要在新数据库中创建数据表。如果使用Flask-Migrate跟踪迁移,可使用如下命令创建新数据表或者升级到最新修订版本:
(venv) $ python manage.py db upgrade
至此,第一部分已经结束了。现在我们已经学习了使用Flask开发Web程序的必备基础知识,不过可能还不确定如何把这些知识融贯起来开发一个真正的程序。第二部分将会带着我们一步一步地开发出一个完整的程序。
如果我们的操作每一步都没出问题,那我们应该得到这样的文件夹
而我们从作者的github点击打开链接上下载的是这样的:
如果你担心你的步骤有问题,可以直接在他的GitHub上下载点击打开链接 然后把你的manage.py文件移动到和flasky.py相同的目录即可。
现在我们可以在这个目录下通过manage.py来运行flask程序。
注意:当使用runserver开始启动服务器程序时,如果我们的数据库中没有赋值相关表的数据,在打开网页时会报错,因此我们要先进入程序的shell模式为数据库初始化一些值。
在shell中可以输入我们第五章学的那些添加的指令,将role_admin,role_user,user_john等添加到数据库会话并提交。
这时我们就可以启动我们的程序了,命令行python manage.py runserver
ok,这时我们的程序就正常运行起来了。尝试在name栏输入john、david、susan和123等名字试试,就可以看到运行效果。