一.程序包
程序包用来保存程序的所有代码、模板和静态文件。我们可以把这个包直接称为app(应用),如果有需求,也可使用一个程序专用名字。templates 和static 文件夹是程序包的一部分,因此这两个文件夹被移到了app 中。数据库模型和电子邮件支持函数也被移到了这个包中,分别保存为app/models.py 和app/email.py。
使用程序工厂函数
在单个文件中开发程序很方便,但却有个很大的缺点,因为程序在全局作用域中创建,所以无法动态修改配置。运行脚本时,程序实例已经创建,再修改配置为时已晚。这一点对单元测试尤其重要,因为有时为了提高测试覆盖度,必须在不同的配置环境中运行程序。
这个问题的解决方法是延迟创建程序实例,把创建过程移到可显式调用的工厂函数中。这种方法不仅可以给脚本留出配置程序的时间,还能够创建多个程序实例,这些实例有时在测试中非常有用。程序的工厂函数在app 包的构造文件中定义,如下所示:
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 flask_login import LoginManager from flask_pagedown import PageDown from config import config bootstrap = Bootstrap() mail = Mail() moment = Moment() db = SQLAlchemy() pagedown = PageDown() login_manager = LoginManager() login_manager.login_view = 'auth.login' 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) login_manager.init_app(app) pagedown.init_app(app) ... return app
构造文件导入了大多数正在使用的Flask 扩展。由于尚未初始化所需的程序实例,所以没有初始化扩展,创建扩展类时没有向构造函数传入参数。create_app() 函数就是程序的工厂函数,接受一个参数,是程序使用的配置名。配置类在config.py 文件中定义,其中保存
的配置可以使用Flask app.config 配置对象提供的from_object() 方法直接导入程序。至于配置对象,则可以通过名字从config 字典中选择。程序创建并配置好后,就能初始化扩展了。在之前创建的扩展对象上调用init_app() 可以完成初始化过程。
在蓝本中实现程序功能
转换成程序工厂函数的操作让定义路由变复杂了。在单脚本程序中,程序实例存在于全局作用域中,路由可以直接使用app.route 修饰器定义。但现在程序在运行时创建,只有调用create_app() 之后才能使用app.route 修饰器,这时定义路由就太晚了。和路由一样,自定义的错误页面处理程序也面临相同的困难,因为错误页面处理程序使用app.errorhandler 修饰器定义。
幸好Flask 使用蓝本提供了更好的解决方法。蓝本和程序类似,也可以定义路由。不同的是,在蓝本中定义的路由处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序的一部分。使用位于全局作用域中的蓝本时,定义路由的方法几乎和单脚本程序一样。和程序一样,蓝本可以在单个文件中定义,也可使用更结构化的方式在包中的多个模块中创建。为了获得最大的灵活性,程序包中创建了一个子包,用于保存蓝本。下面的例子是这个子包的构造文件,蓝本就创建于此。
app/main/__init__.py:创建蓝本 from flask import Blueprint main = Blueprint('main', __name__) from . import views, errors
通过实例化一个Blueprint 类对象可以创建蓝本。这个构造函数有两个必须指定的参数:蓝本的名字和蓝本所在的包或模块。和程序一样,大多数情况下第二个参数使用Python 的__name__ 变量即可。程序的路由保存在包里的app/main/views.py 模块中,而错误处理程序保存在app/main/errors.py 模块中。导入这两个模块就能把路由和错误处理程序与蓝本关联起来。注意,这些模块在app/main/__init__.py 脚本的末尾导入,这是为了避免循环导入依赖,因为在views.py 和errors.py 中还要导入蓝本main。
蓝本在工厂函数create_app() 中注册到程序上:
def create_app(config_name): from .main import main as main_blueprint app.register_blueprint(main_blueprint) from .auth import auth as auth_blueprint app.register_blueprint(auth_blueprint, url_prefix='/auth') from .api import api as api_blueprint app.register_blueprint(api_blueprint, url_prefix='/api/v1') return app
在蓝本中编写错误处理程序稍有不同,如果使用errorhandler 修饰器,那么只有蓝本中的错误才能触发处理程序。要想注册程序全局的错误处理程序,必须使用app_errorhandler
from flask import render_template, request, jsonify from . import main @main.app_errorhandler(403) def forbidden(e): if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: response = jsonify({'error': 'forbidden'}) response.status_code = 403 return response return render_template('403.html'), 403
在蓝本中编写视图函数主要有两点不同:第一,和前面的错误处理程序一样,路由修饰器由蓝本提供;第二,url_for() 函数的用法不同。你可能还记得,url_for() 函数的第一个参数是路由的端点名,在程序的路由中,默认为视图函数的名字。例如,在单脚本程序
中,index() 视图函数的URL 可使用url_for('index') 获取。在蓝本中就不一样了,Flask 会为蓝本中的全部端点加上一个命名空间,这样就可以在不同的蓝本中使用相同的端点名定义视图函数,而不会产生冲突。命名空间就是蓝本的名字(Blueprint 构造函数的第一个参数),所以视图函数index() 注册的端点名是main.index,其URL 使用url_for('main.index') 获取。
url_for() 函数还支持一种简写的端点形式,在蓝本中可以省略蓝本名,例如url_for('.index')。在这种写法中,命名空间是当前请求所在的蓝本。这意味着同一蓝本中的重定向可以使用简写形式,但跨蓝本的重定向必须使用带有命名空间的端点名。
@main.route('/', methods=['GET', 'POST']) def index(): form = PostForm() if current_user.can(Permission.WRITE) and form.validate_on_submit(): post = Post(body=form.body.data, author=current_user._get_current_object()) db.session.add(post) db.session.commit() return redirect(url_for('.index'))