尽管在单一脚本中编写小型Web 程序很方便,但这种方法并不能广泛使用。程序变复杂后,使用单个大型源码文件会导致很多问题。不同于大多数其他的Web 框架,Flask 并不强制要求大型项目使用特定的组织方式,程序结构的组织方式完全由开发者决定。在本节,我们将介绍一种使用包和模块组织大型程序的方式。
一.项目结构
Flask 程序的基本结构如下所示:
|-blogs |-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文件夹包含数据库迁移脚本
- 单元测试编写在tests包中
- 和之前一样,venv 文件夹包含Python 虚拟环境
同时还创建了一些新文件:
- requirements.txt列出了所有依赖包,便于在其他电脑中重新生成相同的虚拟环境
- config.py 存储配置
- manage.py用于启动程序以及其他的程序任务
为了帮助你完全理解这个结构,下面几节讲解把前面介绍的hello.py 程序转换成这种结构的过程
二.配置选项
程序经常需要设定多个配置。这方面最好的例子就是开发、测试和生产环境要使用不同的数据库,这样才不会彼此影响。我们不再使用hello.py 中简单的字典状结构配置,而使用层次结构的配置类。config.py 文件的内容如下示例所示:
import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com') MAIL_PORT = int(os.environ.get('MAIL_PORT', '587')) MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1'] MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]' FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>' FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN') SSL_REDIRECT = False SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_RECORD_QUERIES = True FLASKY_POSTS_PER_PAGE = 20 FLASKY_FOLLOWERS_PER_PAGE = 50 FLASKY_COMMENTS_PER_PAGE = 30 FLASKY_SLOW_DB_QUERY_TIME = 0.5 @staticmethod def init_app(app): pass class DevelopmentConfig(Config): DEBUG = True 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://' WTF_CSRF_ENABLED = False class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data.sqlite') @classmethod def init_app(cls, app): Config.init_app(app) # email errors to the administrators import logging from logging.handlers import SMTPHandler credentials = None secure = None if getattr(cls, 'MAIL_USERNAME', None) is not None: credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD) if getattr(cls, 'MAIL_USE_TLS', None): secure = () mail_handler = SMTPHandler( mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT), fromaddr=cls.FLASKY_MAIL_SENDER, toaddrs=[cls.FLASKY_ADMIN], subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error', credentials=credentials, secure=secure) mail_handler.setLevel(logging.ERROR) app.logger.addHandler(mail_handler) class HerokuConfig(ProductionConfig): SSL_REDIRECT = True if os.environ.get('DYNO') else False @classmethod def init_app(cls, app): ProductionConfig.init_app(app) # handle reverse proxy server headers from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) # log to stderr import logging from logging import StreamHandler file_handler = StreamHandler() file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) class DockerConfig(ProductionConfig): @classmethod def init_app(cls, app): ProductionConfig.init_app(app) # log to stderr import logging from logging import StreamHandler file_handler = StreamHandler() file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) class UnixConfig(ProductionConfig): @classmethod def init_app(cls, app): ProductionConfig.init_app(app) # log to syslog import logging from logging.handlers import SysLogHandler syslog_handler = SysLogHandler() syslog_handler.setLevel(logging.INFO) app.logger.addHandler(syslog_handler) config = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, 'heroku': HerokuConfig, 'docker': DockerConfig, 'unix': UnixConfig, 'default': DevelopmentConfig }
基类Config 中包含通用配置,子类分别定义专用的配置。如果需要,你还可添加其他配置类。为了让配置方式更灵活且更安全,某些配置可以从环境变量中导入。例如,SECRET_KEY 的值,这是个敏感信息,可以在环境中设定,但系统也提供了一个默认值,以防环境中没有定义。在3 个子类中,SQLALCHEMY_DATABASE_URI 变量都被指定了不同的值。这样程序就可在不同的配置环境中运行,每个环境都使用不同的数据库。配置类可以定义init_app() 类方法,其参数是程序实例。在这个方法中,可以执行对当前环境的配置初始化。现在,基类Config 中的init_app() 方法为空。在这个配置脚本末尾,config 字典中注册了不同的配置环境,而且还注册了一个默认配置。
三.启动脚本
顶级文件夹下的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.txt 文件,用于记录所有依赖包及其精确的版本号。如果要在另一台电脑上重新生成虚拟环境,这个文件的重要性就体现出来了,例如部署程序时使用的电脑。pip 可以使用如下命令自动生成这个文件:
pip freeze >requirements.txt
安装或升级包后,最好更新这个文件。需求文件的内容示例如下:
alembic==0.9.3 bleach==2.0.0 blinker==1.4 click==6.7 dominate==2.3.1 Flask==0.12.2 Flask-Bootstrap==3.3.7.1 Flask-HTTPAuth==3.2.3 Flask-Login==0.4.0 Flask-Mail==0.9.1 Flask-Migrate==2.0.4 Flask-Moment==0.5.1 Flask-PageDown==0.2.2 Flask-SQLAlchemy==2.2 Flask-WTF==0.14.2 html5lib==0.999999999 itsdangerous==0.24 Jinja2==2.9.6 Mako==1.0.7 Markdown==2.6.8 MarkupSafe==1.0 python-dateutil==2.6.1 python-dotenv==0.6.5 python-editor==1.0.3 six==1.10.0 SQLAlchemy==1.1.11 visitor==0.1.3 webencodings==0.5.1 Werkzeug==0.12.2 WTForms==2.1
如果你要创建这个虚拟环境的完全副本,可以创建一个新的虚拟环境,并在其上运行以下命令:
pip install -r requirements.txt
五.创建数据库
不管从哪里获取数据库URL,都要在新数据库中创建数据表。如果使用Flask-Migrate 跟踪迁移,可使用如下命令创建数据表或者升级到最新修订版本
python manage.py db upgrade