7.1 项目结构

多文件Flask程序的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|-flasky
|-app/
|-init__.py
|-templates/ # 模板
|-static/ # 静态文件
|-main/ # 蓝本,名字可更改
|-__init__.py
|-errors.py # 路由,视图函数
|-forms.py # 表单
|-views.py # 路由器,视图函数
|-email.py # 邮件支持
|-models.py # 数据库模型
|-migrations # 数据库迁移
|-tests/ # 单元测试
|-__init__.py
|-test*.py
|-venv/
|-requirements.txt # 依赖包文本
|-config.py # 配置
|-manage.py # 启动程序

7.2 配置config.py

在前面的章节中,我们在hello.py中是使用字典状结构配置的(如app.config['FLASKY_ADMIN'] = 12345678@qq.com),现在我们把相关配置提取出来,在config.py中使用层次结构的配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
FLASK_MAIL-SUBJECT_PREFECT = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin 123456789@qq.com'
FLASK_ADMIN = os.environ.get('FLASKY_ADMIN')
@staticmethod
# init_app()可对当前环境的配置初始化,参数是程序实例
def init_app(app):
pass
# 开发环境配置(继承Config基类)
class DevelopmentConfig(Config):
DEBUG = True
MAIL_SERVER = 'smtp.qq.com'
MAIL_PORT = 465
MAIL_USE_SSL = True
MAIL_USENAME = os.environ.get('MAIL_USENAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
SQLALCHEMY_DATABASE_URI = os.environ,get('DEV_DATABASE_URI') or \'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
# 测试环境配置(继承Config基类)
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ,get('TEST_DATABASE_URI') or \'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
# 生产环境配置(继承Config基类)
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ,get('DATABASE_URI') or \'sqlite:///' + os.path.join(basedir, 'data.sqlite')
# 配置字典
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}

7.3 程序包(app文件夹)

程序包用来保存程序的所有代码、模板和静态文件。

7.3.1 使用程序工厂函数

存在问题:在单脚本中开发程序的缺点是:脚步在运行时,程序实例已经创建,已经不能修改配置,自然也无法动态修改配置。这样也不利于单元测试。

解决思路:延迟创建程序实例。
解决方法:把创建过程移到可显式调用的工厂函数中。
这样不仅可以给脚步流出配置程序时间,还能创建多个程序实例。

程序的工厂函数在app包的构造文件(__init__.py)中定义
app/__inti__.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from flask import Flask
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']
# 使用配置类中的init_app()方法初始化配置
config['config_name'].init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
# 附加路由和自定义的错误页面
return app

config.py文件中定义的配置类,可以使用app.config配置对象提供的from_object()方法直接导入程序。
程序创建(app=Flask(__name__))并配置好后,就能初始化Flask拓展(在Flask拓展实例对象上调用init_app()方法,如bootstrap.init(app)

工厂函数返回的是程序实例,不过此时工厂函数创建的程序实例还不完整,因为还没有路由和自定义的错误页面处理程序(简单来说就是缺少路由)。

7.3.2 在蓝本中实现程序功能

存在问题:在单脚本程序中,程序实例存在于全局作用域中,路由可以直接使用app.route修饰器定义,但是现在程序实例时在运行时创建的(调用create_app()函数),这时定义路由已经太晚了(因为只有调用create_app()函数创建实例后才能使用app.route修饰器,而路由又要定义在create_app()函数里)

解决方法:使用蓝本。蓝本和程序类似,也可以定义路由,但不同的是:在蓝本中定义的路由会处于休眠状态,直到调用app.register_blueprint()方法把蓝本注册到程序后,路由在真正称为程序的一部分。

理解蓝本:蓝本通常作用于相同的URL前缀,如user/iduser/profile这样的地址,都是以/user开头,它们是一组用户相关的操作,那么就可以放在一个模块中。大多数项目都是把蓝本当做拆分视图用的。

使用位于全局作用域中的蓝本时,定义路由方法和单脚本程序基本一样(不同之处下面会讲到)

  1. 创建蓝本app/main/__init__.py:
    1
    2
    3
    4
    5
    6
    from flask import Blueprint
    main = Blueprint('main', __name__)
    # 导入view和error模块,导入之后就能将路由和错误处理程序与蓝本关联起来
    from . import views, errors

Blueprint()类的第一个参数是蓝本名字,第二个参数是篮本所在的包或模块(一般使用__name__即可)。

注意: 像viewerror这些模块,为了避免循环导入,要在app/main/__init.py__的末尾处导入,因为在view.pyerror.py中还要导入蓝本main

  • app/main/error.py如下:
1
2
3
4
5
6
7
8
9
10
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修饰器。

  • app/main/view.py如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from datetime import datetime
from flask import render_template, session, redirect, url_for, current_app
from . import main
from .froms import NameForm
from .. import db
from ..models import User
@main.route('/', methods=['GET', 'POST'])
def index():
form = NmaeForm()
if form.validate_on_submit():
# ...
return redirect(url_for('.index')
return render_template('index.html', form=form, name=session.get('name'),
know=session.get('know', Flase), current_time=datetime.utcnow())

注意:在蓝本中编写视图函数主要有两点不同:(1)和前面的错误处理程序一样,路由修饰器由蓝本提供(体现在main.route(),而不是app.route())。
(2)url_for()函数的第一个参数。url_for()函数的第一个参数时路由的端点名,在程序中默认为视图函数的名字(所以在单脚本中可以url_for('index')),但是在蓝本中,Flask会为蓝本中的全部端点加上一个命名空间(也就是蓝本的名字),这样做是为了可以在不同的蓝本中使用相同的端点名(函数名)定义视图函数。所以该蓝本中视图函数index()注册的端点名是main.index,因此需要使用url_for('main.index'),也可以简写为url_for('.index')(前提是同一蓝本,跨蓝本的话必须使用带有命名空间的端点名)。

另外:模板中的hrfe属性中的链接,使用url_for()获取的,其中参数也是同样写端点名。

重点注意:如果在其他地方(如这里的app/main/view.pyapp/email.py等)需要用到程序实例app,均需使用from flask import current_app来导入程序上下文

2.在工厂函数create_app()中把蓝本注册到程序上,app/__init__.py如下:

1
2
3
4
5
6
7
8
9
10
# ...
def create_app(config_name):
# ...
# 导入整个包
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app

7.4 启动脚本

manage.py用于启动脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/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()

在脚本中加入了#!/usr/bin/env python声明,因此在Unix系统中可以直接通过./manage.py命令运行脚步,而无需使用python manage.py命令。

7.5 需求文件

requirements.txt文件:用于记录所有依赖包及其精确的版本号。可以使用如下命令自动生成这个文件:
(venv) $ pip freeze > requirements.txt

当要创建同一个环境时,可以使用如下命令:
(venv) $ pip install -r requirements.txt

7.6 单元测试

  1. tests/test_basics.py文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)
# 测试程序的环境是否为TESTING
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])

setUp()tearDown()方法分别在各测试前后运行。函数名字以test_开头的函数都作为测试执行。

  1. 运行单元测试。可在manage.py文件中添加一个自定义命令,用于执行测试:
1
2
3
4
5
6
7
8
9
# ...
@manager.command
def test():
# 字符串内容会显示在帮助消息中
"""Run the unit tests"""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextRunner(verbosity=2).run(tests)

manager.command修饰器修饰的函数名就是命令名。因此可以使用如下命令运行测试:

1
2
3
4
5
6
7
8
(venv) $ python manage.py test
test_app_exits (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
--------------------------------------------------------
Ran 2 tests in 0.001s
OK

7.7 创建数据库

首先需要在环境变量中定义所需要的数据库URI,然后再创建数据库。

不管从哪里获取数据库URI,都要在新数据库中创建数据表,如果使用Flask-Migrate跟踪迁移,可以使用如下命令创建数据表或者更新到最新版本
(venv) $ python manage.py db upgrade