Flask学习(6)——重构应用结构

到现在为止,hello.py的完整代码如下:

from flask import Flask, render_template, session, redirect, url_for, flash
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_moment import Moment
import os
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail
from flask_mail import Message
from threading import Thread

basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)

app.config['SECRET_KEY'] = 'I am Lethe'

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
    os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 电子邮件
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <[email protected]>'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')

bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)


class NameForm(FlaskForm):
    name = StringField('What is your name?', validators=[DataRequired()])
    submit = SubmitField('Submit')


class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='role')

    def __repr__(self):
        return '<Role %r>' % self.name


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    message = db.Column(db.Text)

    def __repr__(self):
        return '<User %r>' % self.username


@app.shell_context_processor
def make_shell_context():
    return dict(db=db, User=User, Role=Role)


def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def sned_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr


@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username=form.name.data)
            db.session.add(user)
            db.session.commit()
            session['known'] = False
            # 发送电子邮件
            if app.config['FLASKY_ADMIN']:
                sned_email(app.config['FLASKY_ADMIN'], 'New User',
                           'mail/new_user', user=user)
        else:
            session['known'] = True
        session['name'] = form.name.data
        session['message'] = user.message
        form.name.data = ''
        return redirect(url_for('index'))
    return render_template('index.html',
                           form=form, name=session.get('name'),
                           known=session.get('known', False),
                           message=session.get('message'))


@app.route('/user/<name>')
def user(name):
    return render_template('user.html', name=name)


@app.errorhandler(404)
def pate_not_found(e):
    return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

可以看到随着应用复杂程度增加,将所有部分写在一个脚本里会导致许多问题,而不同于多数其他的 Web 框架,Flask 并不强制要求大型项目使用特定的组织方式,应用结构的组织方式完全由开发者决定。

一、项目结构

多文件 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
  |-flasky.py

这种结构有4个顶级文件夹:

  • Flask 应用一般保存在名为 app 的包中;
  • 数据库迁移脚本在 migrations 文件夹中;
  • 单元测试在 tests 包中编写;
  • Python虚拟环境在 venv 文件夹中。

此外,还多了一些新文件:

  • requirements.txt 列出了所有依赖包,便于在其他计算机中重新生成相同的虚拟环境;
  • config.py 存储配置;
  • flasky.py 定义 Flask 应用实例,同时还有一些辅助管理应用的任务。

下面我们尝试把之前的 hello.py 应用转换成此种结构。


二、配置选项

应用经常需要设定多个配置,如开发、测试和生产环境要使用不同的数据库,这样才不会彼此影响。

除了 hello.py 中类似字典的 app.config 对象之外,还可以使用具有层次结构的配置类。将 hello.py 中的配置项独立在 config.py 中如下:

import os

basedir = os.path.abspath(os.path.dirname(__file__))


class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'I am Lethe'
    MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.qq.com')
    MAIL_PORT = int(os.environ.get('MAIL_PORT', '465'))
    MAIL_USE_TLS = os.environ.get('MAIL_USE_SSL', '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 <[email protected]>'
    FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
    sQLALCHEMY_TRACK_MODIFICATIONS = False

    @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://'


# 生成环境数据库
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包含通用配置,各个子类分别定义专用的配置。如果需要,也可以添加其他配置类。
  • 为了更安全和灵活,多数配置都可以从环境变量中导入。
  • 在 3 个子类中,SQLALCHEMY_DATABASE_URI 变量都被指定了不同的值。这样应用就可以在不同的环境中使用不同的数据库。
  • 开发环境和生产环境都配置了邮件服务器。为了再给应用提供一种定制配置的方式,Config 类及其子类可以定义 init_app() 类方法,其参数为应用实例。现在,基类 Config 中的 init_app() 方法为空。
  • 在这个配置脚本末尾,config 字典中注册了不同的配置环境,而且还注册了一个默认配置(这里注册为开发环境)。

三、应用包

应用包用来存放应用的所有代码、模板和静态文件,通常称为为 app(应用)。templates 和 static 目录需要移动到应用包中,数据库模型和电子邮件支持函数也要移到这个包中,分别保存为 app/models.py 和 app/email.py。

3.1 使用应用工厂函数

单个文件中开发应用是很方便,但却有个很大的缺点:应用在全局作用域中创建,无法动态修改配置。运行脚本时,应用实例已经创建,再修改配置为时已晚。这一点对单元测试尤其重要,因为有时为了提高测试覆盖度,必须在不同的配置下运行应用。

这个问题的解决方法是延迟创建应用实例,把创建过程移到可显式调用的工厂函数中。这种方法不仅可以给脚本留出配置应用的时间,还能够创建多个应用实例,为测试提供便利。

应用的工厂函数在 app 包的构造文件 app/__init__.py 中定义如下:

from flask import Flask, render_template
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])
    config[config_name].init_app(app)

    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)

    # 添加路由和自定义错误页面
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app
  • 构造文件导入了大多数使用的 Flask 扩展,由于此时尚未初始化应用实例,所以这些扩展的实例化并未传参,也就没有真正初始化。
  • create_app() 函数是应用的工厂函数,接收一个参数,即应用使用的配置名(前面在config.py中定义的)。配置可以通过 app.config 配置对象提供的 from_object() 方法直接导入应用,参数 config[config_name] 即从 config 字典中选择一个配置类进行配置。
  • 在之前创建的的扩展对象上调用 init_app() 方法可以将 Flask 扩展完成初始化。

3.2 在蓝本中实现应用功能

(1)蓝本(blueprint)和应用类似,也可以定义路由和错误处理程序。但是在蓝本中定义的路由和错误处理程序处于休眠状态,直到蓝本注册到应用上之后,才相当于真正定义在了应用中。

蓝本可以在单个文件中定义,也可使用更结构化的方式在
包中的多个模块中创建。我们将在应用包中创建一个子包 main,用于保存应用的第一个蓝本。

此子包的构造文件 app/main/__init__.py 如下,创建主蓝本:

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors
  • 蓝本通过实例化一个 Blueprint 类对象创建。这个构造函数有两个必须指定的参数:蓝本的名称和蓝本所在的包或模块。
  • 应用的路由保存在包里的 app/main/views.py 模块中,而错误处理程序保存在 app/main/errors.py 模块中,导入这两个模块就能把路由和错误处理程序与蓝本关联起来。
  • 这些模块在 app/main/init.py 脚本的末尾导入,这是为了避免循环导入依赖,因为在 app/main/views.py 和app/main/errors.py 中还要导入 main 蓝本,所以除非循环引用出现在定义 main 之后,否则会致使导入出错。

(2)蓝本在工厂函数 create_app() 中注册到应用上,如下注册主蓝本:

# app/__init__.py

def create_app(config_name):
# ...
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app

(3)主蓝本中的错误处理程序 app/main/errors.py:

from flask import render_template
from . import main

@main.app_errorhandler(404)
def pate_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 装饰器来注册全局的错误处理程序。

(4)主蓝本中定义的应用路由 app/main/views.py:

from datetime import datetime
from flask import render_template, session, redirect, url_for, flash
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():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username=form.name.data)
            db.session.add(user)
            db.session.commit()
            session['known'] = False
            # 发送电子邮件
            if app.config['FLASKY_ADMIN']:
                sned_email(app.config['FLASKY_ADMIN'], 'New User',
                           'mail/new_user', user=user)
        else:
            session['known'] = True
        session['name'] = form.name.data
        session['message'] = user.message
        form.name.data = ''
        return redirect(url_for('main.index')) # 在同一蓝本中可简写为 .index
    return render_template('index.html',
                           form=form, name=session.get('name'),
                           known=session.get('known', False),
                           message=session.get('message'))


@main.route('/user/<name>')
def user(name):
    return render_template('user.html', name=name)
  • 和错误处理程序一样,这里的路由装饰器使用的是 main.route,而不是 app.route。
  • url_for() 函数使用的是 url_for(‘main.index’) ,而不是 url_for(‘index’)。这是因为 Flask 会为蓝本中的全部端点加上一个命名空间,即为蓝本的名称(Blueprint 构造函数的第一个参数)。
  • 若请求的端在在蓝本内,则也可以缩写为 url_for(’.index’)

(5)还需要将表单类移到蓝本中,保存在 app/main/forms.py 模块中:

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

四、应用脚本

应用实例在顶级目录中的 flasky.py 模块里定义:

import os
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)

@app.shell_context_processor
def make_shell_context():
    return dict(db=db, User=User, Role=Role)
  • 此主脚本先创建了一个应用实例,配置名可以从环境变量中读取,也可以使用默认值。
  • 然后初始化数据库迁移扩展 Flask-Migreate 并为 Python shell 注册上下文。

现在我们要想运行应用,就需要把环境变量 FLASK_APP 设置为 flasky.py ,再执行 flask run 才可以。此外,还可以将 FLASK_DEBUG设置为1,来开启调试模式。


五、需求文件

应用中最好有个 requirements.txt 文件,用于记录所有依赖包及其精确的版本号,以便在另一个环境上重新生成虚拟环境。

在虚拟环境中执行如下命令:

 pip freeze >requirements.txt

在安装或升级包后,最好更新一下这个文件。

然后当你想创建这个虚拟环境的副本时,则可以先创建一个新的虚拟环境,然后根据 requirements.txt 安装需要的包和扩展:

pip install -r requirements.txt

六、创建数据库

首选从环境变量中读取数据库的 URL,同时还提供了一个默认的SQLite 数据库作为备用。3 种配置环境中的环境变量名和 SQLite 数据库文件名都不一样。

不管从哪里获取数据库 URL,都要在新数据库中创建数据表,参见“数据库”章节

如果使用 Flask-Migrate 跟踪迁移,可使用下述命令创建数据表或者升级到最新修订版本:

flask db upgrade
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章