十五、flask博客项目实战-之优化应用结构

一、概述

我将使用适用于大型应用的风格重构本应用。

Microblog已经是一个初具规模的应用了,所以我认为这是讨论Flask应用如何在持续增长中不会变得混乱和难以管理的好时机。 Flask是一个框架,旨在让你选择以任何方式来组织项目,基于该理念,在应用日益庞大或者技能水平变化的时候,才有可能更改和调整其结构。

在本章中,我将讨论适用于大型应用的一些模式,并且为了演示他们,我将对Microblog项目的结构进行一些更改,目标是使代码更易于维护和组织。 当然,在真正的Flask精神中,我鼓励你在尝试决定组织自己的项目的方式时仅仅将这些更改作为参考。

二、目前的限制

应用程序在目前为止存在两个基本问题。看一下应用程序的结构,会注意到有一些不同的子系统可识别,但支持它们的代码是混合的,没有任何明确的界限。回顾一下这些子系统是什么:

用户身份验证子系统,包括app/routes.py中的一些视图功能,app/forms.py中的一些表单,app/templates中的一些模板,以及app/email.py中的电子邮件支持。
错误子系统,app/errors.py中定义的错误处理 和app/templates中的模板。
核心应用程序功能,包括显示和编写博客帖子,用户个人资料和关注,以及博客帖子的实时翻译,这些功能通过大多数应用程序的模块 和模板进行传播。
考虑已确定的这个三个子系统以及它们的结构,可能会注意到一种模式。到目前为止,一直遵循的组织逻辑是基于具有专用于不同应用程序功能的模块。有一个视图函数模块、一个用于Web表单、一个用于错误处理、一个用于电子邮件,一个用于HTML模板,等等。虽然这是一个对于小项目有意义的结构,但是一旦项目开始增长,它往往会使这些模块中的一些变得非常庞大和混乱。

清楚地看到问题的一种方法是考虑如何通过尽可能多地重用这个项目来启动第二个项目。例如,用户身份验证部分 应该可以在其他应用程序中正常运行,但如果想要按原样使用这个代码,则必须进入多个模块,并将相关部分复制、粘贴到新项目的新文件中。这将是极其不方便的。如果这个项目将所有与身份验证相关的文件与应用程序的其余部分分开,这就非常好了。Flask蓝图功能有助于实现更实用的组织方式,使重用代码变得更容易。

第二个问题不明显。Flask应用程序实例 在app/init.py中以一个全局变量被创建,然后被很多应用程序模块所导入。虽然这本身不是一个问题,但将应用程序作为全局变量可能会使得某些场景复杂化,特别是与测试相关的场景中。想象一下,若想在不同配置下测试这个应用程序。由于应用程序被定义为全局变量,因此实际上无法实例化使用不同配置变量的两个应用程序。另一种不理想的情况是 所有测试都使用相同的应用程序,因此测试可能会对应用程序进行更改,从而影响以后运行的另一个测试。理想情况下,希望所有测试都在一个质朴的应用程序实例上运行。

实际上在tests.py模块中可看到,在应用程序中设置后要求修改配置,以指示测试使用内存数据库而不是基于磁盘的默认SQLite数据库。我们真的没有其他办法去更改已配置的数据库,因为在测试开始时,已经创建并配置了应用程序。对于这种特殊情况,在应用程序运用于应用程序后去更改配置似乎工作正常,但在其他情况下可能不会,并且在任何情况下,这都是一种不好的做法,可能导致模糊和难以发现的错误。

更好的解决方案是 不让应用程序使用全局变量,而是使用一个应用程序工厂函数在运行时去创建函数。这将是一个接受配置对象作为参数的函数,并返回一个设置了这些配置的Flask应用程序实例。如果我可以修改应用程序以使用应用程序工厂函数,那么编写需要特殊配置的测试讲变得容易,因为每个测试都可以创建自己的应用程序。

在本章中,我将重构应用程序,为上面提到的三个子系统和应用程序工厂函数引入蓝图。向大家显示更改的详细列表是不切实际的,因为属于应用程序的一部分的每个文件几乎没有变化,所以我将讨论重构所采取的的步骤。可在源码文件中查看这些更改。

三、蓝图

在Flask中,蓝图是表示应用程序子集的逻辑结构。蓝图可包括路由、视图函数、表单、模板、静态文件等等元素。如果我们在单独的Python包中编写蓝图,那么我们将拥有一个组件,这个组件封装了与应用程序的特定功能相关的元素。

一个蓝图的内容最初是处于休眠状态。要关联这些元素,需要在应用程序中注册蓝图。在注册期间,添加到蓝图的所有元素都会传递给应用程序。因此,可将蓝图视为应用程序功能的临时存储,以帮助组织代码。

3.1错误处理蓝图

创建的第一个蓝图是封装对错误处理的支持的蓝图。这个蓝图结构如下:

app/
    errors/                             <-- blueprint package
        __init__.py                     <-- blueprint creation
        handlers.py                     <-- error handlers
    templates/
        errors/                         <-- error templates
            404.html
            500.html
    __init__.py                         <-- blueprint registration

app/auth/init.py

from flask import Blueprint

bp = Blueprint('auth', __name__)

from app.auth import routes

app/auth/email.py

from flask import render_template, current_app
from flask_babel import _
from app.email import send_email

def send_password_reset_email(user):
    token = user.get_reset_password_token()
    send_email(_('[Microblog] Reset Your Password'),
               sender=current_app.config['ADMINS'][0],
               recipients=[user.email],
               text_body=render_template('email/reset_password.txt',
                                         user=user, token=token),
               html_body=render_template('email/reset_password.html',
                                         user=user, token=token))

app/auth/forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from flask_babel import _, lazy_gettext as _l
from app.models import User

class LoginForm(FlaskForm):
    username = StringField(_l('Username'), validators=[DataRequired()])
    password = PasswordField(_l('Password'), validators=[DataRequired()])
    remember_me = BooleanField(_l('Remember Me'))
    submit = SubmitField(_l('Sign In'))

class RegistrationForm(FlaskForm):
    username = StringField(_l('Username'), validators=[DataRequired()])
    email = StringField(_l('Email'), validators=[DataRequired(), Email()])
    password = PasswordField(_l('Password'), validators=[DataRequired()])
    password2 = PasswordField(
        _l('Repeat Password'), validators=[DataRequired(),
                                           EqualTo('password')])
    submit = SubmitField(_l('Register'))

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError(_('Please use a different username.'))

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError(_('Please use a different email address.'))

class ResetPasswordRequestForm(FlaskForm):
    email = StringField(_l('Email'), validators=[DataRequired(), Email()])
    submit = SubmitField(_l('Request Password Reset'))

class ResetPasswordForm(FlaskForm):
    password = PasswordField(_l('Password'), validators=[DataRequired()])
    password2 = PasswordField(
        _l('Repeat Password'), validators=[DataRequired(),
                                           EqualTo('password')])
    submit = SubmitField(_l('Request Password Reset'))

app/auth/routes.py

from flask import render_template, redirect, url_for, flash, request
from werkzeug.urls import url_parse
from flask_login import login_user, logout_user, current_user
from flask_babel import _
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm, \
    ResetPasswordRequestForm, ResetPasswordForm
from app.models import User
from app.auth.email import send_password_reset_email

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash(_('Invalid username or password'))
            return redirect(url_for('auth.login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('main.index')
        return redirect(next_page)
    return render_template('auth/login.html', title=_('Sign In'), form=form)

@bp.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('main.index'))

@bp.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash(_('Congratulations, you are now a registered user!'))
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', title=_('Register'),
                           form=form)

@bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = ResetPasswordRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            send_password_reset_email(user)
        flash(
            _('Check your email for the instructions to reset your password'))
        return redirect(url_for('auth.login'))
    return render_template('auth/reset_password_request.html',
                           title=_('Reset Password'), form=form)

@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    user = User.verify_reset_password_token(token)
    if not user:
        return redirect(url_for('main.index'))
    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash(_('Your password has been reset.'))
        return redirect(url_for('auth.login'))
    return render_template('auth/reset_password.html', form=form)

本质上,所做的是将app/errors.py模块 移动到app/errors/handlers.py中;将这两个错误模板移动到app/templates/errors中,以便它们与其他模板分开。还必须更改在两个错误处理中的render_template()调用,以使用的新的错误模板子目录。之后,在创建应用程序实例后,将蓝图创建添加到app/errors/init.py模块中,并将蓝图注册添加到app/init.py中。

应该注意到,可将Flask蓝图配置为具有模板或静态文件的单独目录。我已决定将模板移动到应用程序模板目录的子目录中,以便所有模板都在单个层次结构中,但如果希望在蓝图包中具有属于蓝图的模板,那是支持的。例如,如果向Blueprint()构造函数添加一个template_folder='templates'参数,那么可将蓝图的模板存储在app/errors/templates中。

创建蓝图与创建应用程序非常相似。这是在蓝图包的init.py模块中完成的:

app/errors/__init__.py:错误蓝图

from flask import Blueprint

bp = Blueprint('errors', __name__)

from app.errors import handlers

Blueprint类 采取蓝图的名字,基础模块的名字(如同在Flask应用程序实例中通常设置name),以及一些可选参数(在这种情况下,我不需要)。创建蓝图对象后,我导入handlers.py模块,以便其中的错误处理程序注册到蓝图。这个导入位于底部以避免循环依赖。

在handlers.py模块中,我没有使用@app.errorhandler装饰器将错误处理程序附加到应用程序中,而是使用蓝图的@bp.app_errorhandler装饰器。虽然两个装饰器都达到了相同的最终结果,但我们的想法是尝试 使蓝图独立于应用程序,以便它更具可移植性。还需要修改两个错误模板的路径,以便考虑移动它们的新错误子目录。

app/errors/handlers.py:

from flask import render_template
from app import db
from app.errors import bp

@bp.app_errorhandler(404)
def not_found_error(error):
    return render_template('errors/404.html'), 404

@bp.app_errorhandler(505)
def internal_error(error):
    db.session.rollback()
    return render_template('errors/500.html'), 500

完成错误处理程序重构的最后一步是 在应用程序中注册蓝图:

app/__init__.py:在应用程序中注册错误蓝图

app = Flask(__name__)

# ...

from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)

# ...

from app import routes, models  # <-- remove errors from this import!

要注册蓝图,得使用Flask应用程序实例的register_blueprint()方法。注册蓝图时,任何视图函数、模板、静态文件、错误处理模块等都会连接到应用程序。我把蓝图的导入放在app.register_blueprint()的上方,以避免循环依赖。
用户认证Blueprint
将应用的认证功能重构为blueprint的过程与错误处理程序的过程非常相似。 以下是重构为blueprint的目录层次结构:

app/
    auth/                               <-- blueprint package
        __init__.py                     <-- blueprint creation
        email.py                        <-- authentication emails
        forms.py                        <-- authentication forms
        routes.py                       <-- authentication routes
    templates/
        auth/                           <-- blueprint templates
            login.html
            register.html
            reset_password_request.html
            reset_password.html
    __init__.py                         <-- blueprint registration

为了创建这个blueprint,我必须将所有认证相关的功能移到为blueprint创建的新模块中。 这包括一些视图函数,Web表单和支持功能,例如通过电子邮件发送密码重设token的功能。 我还将模板移动到一个子目录中,以将它们与应用的其余部分分开,就像我对错误页面所做的那样。

在blueprint中定义路由时,使用@bp.route装饰器来代替@app.route装饰器。 在url_for()中用于构建URL的语法也需要进行更改。 对于直接附加到应用的常规视图函数,url_for()的第一个参数是视图函数名称。 但当在blueprint中定义路由时,该参数必须包含blueprint名称和视图函数名称,并以句点分隔。 因此,我不得不用诸如url_for('auth.login')的代码替换所有出现的url_for('login')代码,对于其余的视图函数也是如此。

注册auth blueprint到应用时,我使用了些许不同的格式:

app/__init__.py:注册用户认证blueprint到应用。

# ...
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
# ...

在这种情况下,register_blueprint()调用接收了一个额外的参数,url_prefix。 这完全是可选的,Flask提供了给blueprint的路由添加URL前缀的选项,因此blueprint中定义的任何路由都会在其完整URL中获取此前缀。 在许多情况下,这可以用来当成“命名空间”,它可以将blueprint中的所有路由与应用或其他blueprint中的其他路由分开。 对于用户认证,我认为让所有路由以 /auth 开头很不错,所以我添加了该前缀。 所以现在登录URL将会是 http://localhost:5000/auth/login 。 因为我使用url_for()来生成URL,所有URL都会自动合并前缀。

3.2主应用Blueprint

第三个blueprint包含核心应用逻辑。 重构这个blueprint和前两个blueprint的过程一样。 我给这个blueprint命名为main,因此所有引用视图函数的url_for()调用都必须添加一个main.前缀。 鉴于这是应用的核心功能,我决定将模板留在原来的位置。 这不会有什么问题,因为我已将其他两个blueprint中的模板移动到子目录中了。

3.3应用工厂模式

正如我在本章的介绍中所提到的,将应用设置为全局变量会引入一些复杂性,主要是以某些测试场景的局限性为形式。 在我介绍blueprint之前,应用必须是一个全局变量,因为所有的视图函数和错误处理程序都需要使用来自app的装饰器来修饰,比如@app.route。 但是现在所有的路由和错误处理程序都被转移到了blueprint中,因此保持应用全局性的理由就不够充分了。

所以我要做的是添加一个名为create_app()的函数来构造一个Flask应用实例,并消除全局变量。 转换并非容易,我不得不理清一些复杂的东西,但我们先来看看应用工厂函数:

app/__init__.py:应用工厂函数。

# ...
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = _l('Please log in to access this page.')
mail = Mail()
bootstrap = Bootstrap()
moment = Moment()
babel = Babel()

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)
    mail.init_app(app)
    bootstrap.init_app(app)
    moment.init_app(app)
    babel.init_app(app)

    # ... no changes to blueprint registration

    if not app.debug and not app.testing:
        # ... no changes to logging setup

    return app

你已经看到,大多数Flask插件都是通过创建插件实例并将应用作为参数传递来初始化的。 当应用不再作为全局变量时,有一种替代模式,插件分成两个阶段进行初始化。 插件实例首先像前面一样在全局范围内创建,但没有参数传递给它。 这会创建一个未附加到应用的插件实例。 当应用实例在工厂函数中创建时,必须在插件实例上调用init_app()方法,以将其绑定到现在已知的应用。

在初始化期间执行的其他任务保持不变,但会被移到工厂函数而不是在全局范围内。 这包括blueprint和日志配置的注册。 请注意,我在条件中添加了一个not app.testing子句,用于决定是否启用电子邮件和文件日志,以便在单元测试期间跳过所有这些日志记录。 由于在配置中TESTING变量在单元测试时会被设置为True,因此app.testing标志在运行单元测试时将变为True。

那么谁来调用应用程工厂函数呢? 最明显使用此函数的地方是处于顶级目录的microblog.py脚本,它是唯一会将应用设置为全局变量的模块。 另一个调用该工厂函数的地方是tests.py,我将在下一节中更详细地讨论单元测试。

正如我上面提到的,大多数对app的引用都是随着blueprint的引入而消失的,但是我仍然需要解决代码中的一些问题。 例如,app/models.py、app/translate.py和app/main/routes.py模块都引用了app.config。 幸运的是,Flask开发人员试图使视图函数很容易地访问应用实例,而不必像我一直在做的那样导入它。 Flask提供的current_app变量是一个特殊的“上下文”变量,Flask在分派请求之前使用应用初始化该变量。 你之前已经看到另一个上下文变量,即存储当前语言环境的g变量。 这两个变量,以及Flask-Login的current_user和其他一些你还没有看到的东西,是“魔法”变量,因为它们像全局变量一样工作,但只能在处理请求期间且在处理它的线程中访问。

用Flask的current_app变量替换app就不需要将应用实例作为全局变量导入。 通过简单的搜索和替换,我可以毫无困难地用current_app.config替换对app.config的所有引用。

app/email.py模块提出了一个更大的挑战,所以我必须使用一个小技巧:

app/email.py:将应用实例传递给另一个线程。

from flask import current_app

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

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    Thread(target=send_async_email,
           args=(current_app._get_current_object(), msg)).start()

在send_email()函数中,应用实例作为参数传递给后台线程,后台线程将发送电子邮件而不阻塞主应用程序。在作为后台线程运行的send_async_email()函数中直接使用current_app将不会奏效,因为current_app是一个与处理客户端请求的线程绑定的上下文感知变量。在另一个线程中,current_app没有赋值。直接将current_app作为参数传递给线程对象也不会有效,因为current_app实际上是一个代理对象,它被动态地映射到应用实例。因此,传递代理对象与直接在线程中使用current_app相同。我需要做的是访问存储在代理对象中的实际应用程序实例,并将其作为app参数传递。 current_app._get_current_object()表达式从代理对象中提取实际的应用实例,所以它就是我作为参数传递给线程的。

另一个棘手的模块是app/cli.py,它实现了一些用于管理语言翻译的快捷命令。 在这种情况下,current_app变量不起作用,因为这些命令是在启动时注册的,而不是在处理请求期间(这是唯一可以使用current_app的时间段)注册的。 为了在这个模块中删除对app的引用,我使用了另一个技巧,将这些自定义命令移动到一个将app实例作为参数的register()函数中:

app/cli.py:注册自定义应用命令。

import os
import click

def register(app):
    @app.cli.group()
    def translate():
        """Translation and localization commands."""
        pass

    @translate.command()
    @click.argument('lang')
    def init(lang):
        """Initialize a new language."""
        # ...

    @translate.command()
    def update():
        """Update all languages."""
        # ...

    @translate.command()
    def compile():
        """Compile all languages."""
        # ...

然后我从microblog.py中调用这个register()函数。 以下是完成重构后的microblog.py:

microblog.py:重构后的主应用模块。

from app import create_app, db, cli
from app.models import User, Post

app = create_app()
cli.register(app)

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post' :Post}

3.4单元测试的改进

正如我在本章开头所暗示的,到目前为止,我所做的很多工作都是为了改进单元测试工作流程。 在运行单元测试时,要确保应用的配置方式不会污染开发资源(如数据库)。

tests.py的当前版本采用了应用实例化之后修改配置的技巧,这是一种危险的做法,因为并不是所有类型的更改都会在修改之后才生效。 我想要的是有机会在添加到应用之前指定我想要的测试配置项。

create_app()函数现在接受一个配置类作为参数。 默认情况下,使用在config.py中定义的Config类,但现在我可以通过将新类传递给工厂函数来创建使用不同配置的应用实例。 下面是一个适用于我的单元测试的示例配置类:

tests.py:测试配置。

from config import Config

class TestConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite://'

我在这里做的是创建应用的Config类的子类,并覆盖SQLAlchemy配置以使用内存SQLite数据库。 我还添加了一个TESTING属性,并设置为True,我目前不需要该属性,但如果应用需要确定它是否在单元测试下运行,它就派上用场了。

你一定还记得,我的单元测试依赖于setUp()和tearDown()方法,它们由单元测试框架自动调用,以创建和销毁每次测试运行的环境。 我现在可以使用这两种方法为每个测试创建和销毁一个测试专用的应用:

tests.py:为每次测试创建一个应用。

class UserModelCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app(TestConfig)
        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()

新的应用将存储在self.app中,但光是创建一个应用不足以使所有的工作都成功。 思考创建数据库表的db.create_all()语句。 db实例需要注册到应用实例,因为它需要从app.config获取数据库URI,但是当你使用应用工厂时,应用就不止一个了。 那么db如何关联到我刚刚创建的self.app实例呢?

答案在application context中。 还记得current_app变量吗?当不存在全局应用实例导入时,该变量以代理的形式来引用应用实例。 这个变量在当前线程中查找活跃的应用上下文,如果找到了,它会从中获取应用实例。 如果没有上下文,那么就没有办法知道哪个应用实例处于活跃状态,所以current_app就会引发一个异常。 下面你可以看到它是如何在Python控制台中工作的。 这需要通过运行python启动,因为flask shell命令会自动激活应用程序上下文以方便使用。

>>> from flask import current_app
>>> current_app.config['SQLALCHEMY_DATABASE_URI']
Traceback (most recent call last):
    ...
RuntimeError: Working outside of application context.

>>> from app import create_app
>>> app = create_app()
>>> app.app_context().push()
>>> current_app.config['SQLALCHEMY_DATABASE_URI']
'sqlite:////home/miguel/microblog/app.db'

这就是秘密所在! 在调用你的视图函数之前,Flask推送一个应用上下文,它会使current_app和g生效。 当请求完成时,上下文将与这些变量一起被删除。 为了使db.create_all()调用在单元测试setUp()方法中工作,我为刚刚创建的应用程序实例推送了一个应用上下文,这样db.create_all()可以使用 current_app.config知道数据库在哪里。 然后在tearDown()方法中,我弹出上下文以将所有内容重置为干净状态。

你还应该知道,应用上下文是Flask使用的两种上下文之一,还有一个请求上下文,它更具体,因为它适用于请求。 在处理请求之前激活请求上下文时,Flask的request、session以及Flask-Login的current_user变量才会变成可用状态。

3.5环境变量

正如构建此应用时你所看到的,在启动服务器之前,有许多配置选项取决于在环境中设置的变量。 这包括密钥、电子邮件服务器信息、数据库URL和Microsoft Translator API key。 你可能会和我一样觉得,这很不方便,因为每次打开新的终端会话时,都需要重新设置这些变量。

译者注:可以通过将环境变量设置到开机启动中,来保持它们在该计算机中的所有终端中都生效。

应用依赖大量环境变量的常见处理模式是将这些变量存储在应用根目录中的 .env 文件中。 应用在启动时会从此文件中导入变量,这样就不需要你手动设置这些变量了。

有一个支持 .env 文件的Python包,名为python-dotenv。 所以让我们安装这个包:

(venv) #pip3 install python-dotenv

由于config.py模块是我读取所有环境变量的地方,因此我将在创建Config类之前导入 .env 文件,以便在构造类时设置变量:

config.py:导入 .env 文件中的环境变量。

import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))

class Config(object):
    # ...

现在你可以创建一个 .env 文件并在其中写入应用所需的所有环境变量了。不要将 .env 文件加入到源代码版本控制中,这非常重要。否则,一旦你的密码和其他重要信息上传到远程代码库中后,你就会后悔莫及。

.env文件可以用于所有配置变量,但是不能用于Flask命令行的FLASK_APP和FLASK_DEBUG环境变量,因为它们在应用启动的早期(应用实例和配置对象存在之前)就被使用了。

以下示例显示了 .env 文件,该文件定义了一个安全密钥,将电子邮件配置为在本地运行的邮件服务器的25端口上,并且不进行身份验证,设置Microsoft Translator API key,使用数据库配置的默认值:

SECRET_KEY=a-really-long-and-unique-key-that-nobody-knows
MAIL_SERVER=localhost
MAIL_PORT=25
MS_TRANSLATOR_KEY=<your-translator-key-here>

3.6依赖文件

此时我已经在Python虚拟环境中安装了一定数量的软件包。 如果你需要在另一台机器上重新生成你的环境,将无法记住你必须安装哪些软件包,所以一般公认的做法是在项目的根目录中写一个requirements.txt文件,列出所有依赖的包及其版本。 生成这个列表实际上很简单:

(venv) # pip3 freeze > requirements.txt

pip freeze命令将安装在虚拟环境中的所有软件包以正确的格式输入到requirements.txt文件中。 现在,如果你需要在另一台计算机上创建相同的虚拟环境,无需逐个安装软件包,可以直接运行一条命令实现:

(venv) # pip3 install -r requirements.txt
十五、flask博客项目实战-之优化应用结构https://xucg.info/2020/11/06/1586.html
THE END
分享
二维码
< <上一篇
下一篇>>