Flask1.0.2系列(二十) Flask相关的模式

英文原文地址:http://flask.pocoo.org/docs/1.0/patterns/

若有翻译错误或者不尽人意之处,请指出,谢谢~


        某些东西很常见,你可能在大多数Web应用程序中都能发现它们。举个栗子,相当多的应用程序使用了关系型数据库,以及用户验证。在这种情形下,它们很可能在开始请求的时候打开一个数据库连接,并且获取当前登录用户的信息。在请求的最后,数据库连接将被再次关闭。

        在Flask Snippet Archives文章中有更多用户贡献的代码片段和模式。


1. 大型应用程序

        对于大型应用程序来说,最好的设计思路是使用包而不是模块。想象一个小的应用程序时这样的:

/yourapplication
    yourapplication.py
    /static
        style.css
    /templates
        layout.html
        index.html
        login.html
        ...

1.1 简单的包

        为了将其转换到一个大的项目中,仅需要在大的项目中创建一个新的文件夹yourapplication,并将所有东西拷贝到这个文件夹下。然后将yourapplication.py更名为__init__.py。(确保首先删除所有.pyc文件,否则可能会有异常出现)

        你随后建立的目录结构应该像下面这样:

/yourapplication
    /yourapplication
        __init__.py
        /static
            style.css
        /templates
            layout.html
            index.html
            login.html
            ...

        但是你现在该如何运行你的应用程序呢?不要天真地认为

python yourapplication/__init__.py

就能工作。Python不希望包中的模块成为启动文件。但是这不是什么大问题,仅需要添加一个叫做setup.py的新文件,这个文件放在yourapplication文件夹所在的文件夹下,并且这个文件的内容如下:

from setuptools import setup


setup(
    name='yourapplication',
    packages=['yourapplication'],
    include_package_data=True,
    install_requires=[
        'flask',
    ],
)

        为了运行这个应用程序,你需要导出一个环境变量,用来告诉Flask在哪里可以找到应用程序实例:

export FLASK_APP=yourapplication

        为了安装和运行应用程序,你需要发送下面的命令:

pip install -e .
flask run

        我们能从这里知道什么呢?现在,我们可以重构应用程序,将其分解到多个模块中。你只需要记住下面的快速检查清单:

        1. Flask应用程序对象的创建是在__init__.py文件中。这种方式能让每个模块安全地导入它,并且__name__变量将解析为正确的包。

        2. 所有视图方法(即在顶部标记了route()装饰器的方法)必须在__init__.py文件中被导入。不仅是对象本身,而且模块也在里面。在应用程序对象生成之后导入视图模块。

        这里有一个__init__.py文件的示例:

from flask import Flask


app = Flask(__name__)


import yourapplication.views

        并且view.py代码如下:

from yourapplication import app


@app.route('/')
def index():
    return 'Hello World!'

        你随后建立的目录结构应该像下面这样:

/yourapplication
    setup.py
    /yourapplication
        __init__.py
        views.py
        /static
            style.css
        /templates
            layout.html
            index.html
            login.html
            ...

        循环导入:

        所有Python开发者都讨厌这个,实际上我们在这里已经这样做过了:循环导入(表示两个模块互相依赖于对方。在这里view.py依赖于__init__.py)。请注意,通常这是一个不好的想法,但是这里实际上是正确的。因为我们实际上在__init__.py中并没有使用views,并且我们确保了在__init__.py文件的底部我们才导入了这个模块。

        这种方法依然有一些问题,但是如果你想要在这里使用装饰器,那就没有其他办法了。后面的部分,我们会讲解有关如何处理这种情况的灵感。

1.2 使用蓝图

        如果你有一个很大的应用程序,这里推荐将这个程序分解成很多小组,而每个小组的实现都依赖于使用蓝图。更多关于蓝图的介绍,请查阅前面的章节,使用蓝图将应用程序模块化。


2. 应用程序工厂

        如果你已经在你的应用程序中使用了包和蓝图,有一些不错的方法可以进一步改善这种体验。一种常用的模式是,在蓝图被导入的时候创建应用程序对象。但是如果你将创建这个对象的代码移动到一个方法内的话,你可以在这个app对象之后再创建其他你所需要的实例。

        那么,为什么你需要这样做呢?

        1. 为了测试。你可以通过不同的配置来创建这个应用程序实例,这样可以在任务你需要的场景下进行测试。

        2. 多个实例。想象一下你需要运行不同版本的应用程序。当然,你可以在建立你的web服务时,根据不同的配置来创建多个实例。但是,如果你使用了工厂,你可以在同一个应用程序进程中运行同一个应用程序的多个实例,这是很方便的。

        那么我们该如何实现这种方式呢?

2.1 基础工厂

        基础工厂的思想是在一个方法中建立应用程序。就像这样:

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    from yourapplication.model import db
    db.init_app(app)

    from yourapplication.views.admin import admin
    from yourapplication.views.frontend import frontend
    app.register_blueprint(admin)
    app.register_blueprint(frontend)

    return app

        这种方法的缺点是,在导入时,你无法在蓝图中使用应用程序对象。然而,你可以通过一个请求使用它。怎样才能访问应用程序的配置项呢?使用current_app就可以:

from flask import current_app, Blueprint, render_template


admin = Blueprint('admin', __name__, url_prefix='/admin')


@admin.route('/')
def index():
    return render_template(current_app.config['INDEX_TEMPLATE'])

        在这里,我们在配置中寻找指定名称的模板。

2.2 工厂 & 扩展

        最好是创建你的扩展和应用程序工厂,以便扩展对象最初不会被绑定到应用程序。

        举个栗子,使用Flask-SQLAlchemy,你不应该像下面这样使用:

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    db = SQLAlchemy(app)

        你应该在model.py(或者等价的模块)中创建对象:

db = SQLAlchemy()

        然后在你的application.py(或者等价的模块)中初始化:

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    from yourapplication.model import db
    db.init_app(app)

        使用这种设计模式,没有特定的应用程序状态被存储到扩展对象上,因此一个扩展对象可以被用到多个应用程序对象上。更多信息请参考Flask Extension Development

2.3 使用应用程序

        为了运行一个应用程序,你可以使用flask命令:

export FLASK_APP=myapp
flask run

        Flask会自动在myapp中检测工厂(create_app或者make_app)方法。你也可以传递参数到这个工厂方法中:

export FLASK_APP="myapp:create_app('dev')"
flask run

        然后在myapp中的create_app工厂会被调用,并且带有字符串参数‘dev’。

2.4 工厂改进

        上面描述的工厂方法并不是很智能,但是你可以改进它。下面的更改很容易实现:

        1. 可以在单元测试中传递配置值,这样就不必在文件系统上创建配置文件了。

        2. 当应用程序建立时,从蓝图中调用一个方法,这样你就有一个可以修改应用程序属性的地方了(就像在before/after请求处理函数的钩子函数一样)。

        3. 如果有需要,可以在应用程序被创建时,添加到WSGI中间件。


3. 应用程序的调度

        应用程序的调度,即在WSGI层级上组合多个Flask应用程序的进程。你不仅仅可以组合Flask应用程序,还可以组合任意的WSGI应用程序。如果你愿意的话,这里还能允许你在同一个解释器中,运行一个Django应用程序和一个Flask应用程序。这是否有用取决于应用程序内部是如何工作的。

        应用程序的调度与模块方法的根据区别在于,在这种情况下,你运行的是相同或不同的Flask应用程序,它们彼此间是完全隔离的。它们运行在不同的配置,并且在WSGI层进行调度。

3.1 如何使用此文档

        下面所有技术和示例最终会得到一个应用程序对象,这个对象能被运行在任何WSGI服务器上。在生产环境下,请参考Deployment Options。在开发环境下,Werkzeug提供了一个内置的开发服务器,可以使用werkzeug.serving.run_simple()来激活它:

from werkzeug.serving import run_simple


run_simple('localhost', 5000, application, use_reloader=True)

        注意run_simple不能用于生产环境。发布应用时可以使用full-blown WSGI server

        为了使用交互式调试器,必须在应用层序和这个简单服务器上开启调试。下面是一个带有调试功能的“hello world”的示例:

from flask import Flask
from werkzeug.serving import run_simple


app = Flask(__name__)
app.debug = True


@app.route('/')
def hello_world():
    return 'Hello World!'


if __name__ == '__main__':
    run_simple('localhost', 5000, app,
               use_reloader=True, use_debugger=True, use_evalex=True)

3.2 合并应用程序

        如果你拥有一些完全独立的应用程序,并且你希望它们能在同一个的Python解释器中彼此共同工作,你可以利用werkzeug.wsgi.DispatcherMiddleware来实现。在这里,每个Flask应用对象都是一个有效的WSGI应用对象,调度器中间件会将它们合并到一个规模更大的应用中,并通过前缀来实现调度。

        举个栗子,你可以使你的主程序运行在‘/’上,使你的后端接口运行在‘/backend’上:

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend


application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})

3.3 通过子域来调度

        有时候你可能希望通过配置的方式来使用多个同样应用程序的实例。假定在一个方法内创建了应用程序,并且你可以调用这个方法来进行初始化操作,这是非常容易实现的。为了开发你的应用程序来支持在方法中创建新的实例,请查看应用程序工厂模式一节。

        一个非常常见的例子是,为每个子域名创建对应的应用程序对象。举个栗子,你可以通过配置你的web服务的方式,将你所有子域名的请求分发到你的应用程序,并且你可以在稍后使用这子域名的信息来创建用户指定的实例。一旦你的服务是为了监听所有子域名而建立的,你可以使用一个非常简单的WSGI应用程序来实现动态应用程序的创建。

        实现此功能最完美的抽象层是WSGI层。你可以编写你自己的WSGI应用程序,它观察接收到的请求,并将请求委托到你的Flask应用程序。如果这个应用程序不存在,它会自动地创建并记住这个创建的应用程序:

from threading import Lock


class SubdomainDispatcher(object):

    def __init__(self, domain, create_app):
        self.domain = domain
        self.create_app = create_app
        self.lock = Lock()
        self.instances = {}

    def get_application(self, host):
        host = host.split(':')[0]
        assert host.endswith(self.domain), 'Configuration error'
        subdomain = host[:-len(self.domain)].rstrip('.')
        with self.lock:
            app = self.instances.get(subdomain)
            if app is None:
                app = self.create_app(subdomain)
                self.instances[subdomain] = app
            return app

    def __call__(self, environ, start_response):
        app = self.get_application(environ['HTTP_HOST'])
        return app(environ, start_response)

        可以这样实现一个调度方:

from myapplication import create_app, get_user_for_subdomain
from werkzeug.exceptions import NotFound


def make_app(subdomain):
    user = get_user_for_subdomain(subdomain)
    if user is None:
        # if there is no user for that subdomain we still have
        # to return a WSGI application that handles that request.
        # We can then just return the NotFound() exception as
        # application which will render a default 404 page.
        # You might also redirect the user to the main page then
        return NotFound()

    # otherwise create the application for the specific user
    return create_app(user)


application = SubdomainDispatcher('example.com', make_app)

3.4 使用路径来调度

        通过URL路径分发请求,跟之前的方法很相似。不是通过检查用来确定子域名的HOST头信息,而是简单检查请求路径中到第一个斜杠之前的部分:

from threading import Lock
from werkzeug.wsgi import pop_path_info, peek_path_info


class PathDispatcher(object):

    def __init__(self, default_app, create_app):
        self.default_app = default_app
        self.create_app = create_app
        self.lock = Lock()
        self.instances = {}

    def get_application(self, prefix):
        with self.lock:
            app = self.instances.get(prefix)
            if app is None:
                app = self.create_app(prefix)
                if app is not None:
                    self.instances[prefix] = app
            return app

    def __call__(self, environ, start_response):
        app = self.get_application(peek_path_info(environ))
        if app is not None:
            pop_path_info(environ)
        else:
            app = self.default_app
        return app(environ, start_response)

        这种方式与子域名的方式最大的区别在于,如果这里创建应用程序对象的函数放回了None,那么请求就被降级回推到另一个应用当中:

from myapplication import create_app, default_app, get_user_for_prefix


def make_app(prefix):
    user = get_user_for_prefix(prefix)
    if user is not None:
        return create_app(user)


application = PathDispatcher(default_app, make_app)


4 实现API异常

        在Flask顶部实现RESTful API是非常常见的。开发人员首先要做的一件事是,认识到对于API来说,内置异常是表达不充分的,而且它们所发出的test/html类型的内容对于API调用者来说,并不是很有用的。

        较于使用abort来发送一个非法API调用的信号,更好的方法是实现你自己的异常类型并且为这个异常类型设置对应的错误处理程序,而这个错误处理程序中就可以将错误转换成用户期望的格式。

4.1 简单的异常类

        最基本的思想是,引入一个新的异常,这个异常可以获取适当的人类可读的信息、错误的状态码以及一些可选的有效负载,从而为错误提供更多的内容。

        简单示例如下:

from flask import jsonify


class InvalidUsage(Exception):
    status_code = 400

    def __init__(self, message, status_code=None, payload=None):
        Exception.__init__(self)
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

        现在,试图可以抛出这个异常,并伴随一个错误信息。此外,通过payload参数可以提供一些像字典一样的额外的有效负载。

4.2 注册一个错误处理程序

        按照上述内容,虽然视图可以抛出这个异常,但是在实际执行时只能看到一个内部服务错误的信息。这是因为,这里并没有为这个错误类注册对应的处理程序。然而这是很容易添加的:

@app.errorhandler(InvalidUsage)
def handle_invalid_usage(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

4.3 在视图中使用

        这里简单演示了一个视图如何使用上述的异常类功能:

@app.route('/foo')
def get_foo():
    raise InvalidUsage('This view is gone', status_code=410)


5. 使用URL处理器

        (新增于版本0.7。)

        Flask0.7引进了URL处理器的概念。你可能有一堆URL,这些URL有一部分相同的部分,有一部分是不能明确提供的。举个例子,有一堆URL,这些URL中分别有各自的语言代码,但是你不想在每个单独的方法中去处理对应语言代码的URL。

        当于蓝图组合使用的时候,URL处理器特别有用。我们将在这里处理应用程序特定的URL处理器,以及蓝图特定的URL处理器。

5.1 国际化的应用程序URL

        考虑一个应用程序如下:

from flask import Flask, g


app = Flask(__name__)


@app.route('/<lang_code>/')
def index(lang_code):
    g.lang_code = lang_code
    ...


@app.route('/<lang_code>/about')
def about(lang_code):
    g.lang_code = lang_code
    ...

        在每个方法中,你必须将语言代码设置到g对象上,这是一个很糟糕的设计,因为需要大量重复的实现代码。虽然一个装饰器可以用来简化这种方式,但是如果你希望从一个方法到另一个方法生成url,那么你仍然需要显示地提供语言代码,这是很让人厌烦的。

        这里需要使用url_defaults()方法。它们可以自动将值注入到url_for()调用中。下面的代码检查了,语言代码是否还在URL值的字典中,并且端点是否想要一个叫做“lang_code”的值:

@app.url_defaults
def add_language_code(endpoint, values):
    if 'lang_code' in values or not g.lang_code:
        return
    if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'):
        values['lang_code'] = g.lang_code

        url_map对象的is_endpoint_expecting()方法可以用来断定为给定的端点提供语言代码是否有意义。

        与这个方法相对的是url_value_preprocessor()。它们在请求匹配之后执行,并且可以根据URL值来执行代码。其想法是,将信息从值字典中提取出来放到别的地方:

@app.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code', None)

        这样,你就不用再在每个函数中将lang_code赋值到g对象中了。你可以通过编写你自己的装饰器来进一步完善这个功能,即使用语言代码作为URL的前缀,但是更漂亮的解决方案是使用一个蓝图。一旦“lang_code”从值字典中弹出,它将不能再被转发到视图函数中,从而代码简化如下:

from flask import Flask, g


app = Flask(__name__)


@app.url_defaults
def add_language_code(endpoint, values):
    if 'lang_code' in values or not g.lang_code:
        return
    if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'):
        values['lang_code'] = g.lang_code


@app.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code', None)


@app.route('/<lang_code>/')
def index():
    ...


@app.route('/<lang_code>/about')
def about():
    ...

5.2 国际化的蓝图URL

        因为蓝图能自动地使用一个通常的字符串作为所有URL的前缀,因此它能很容易自动地对每个方法实现这个。此外,蓝图可以有每个蓝图的URL处理器,它从url_defaults()方法中删除了大量的逻辑,因为它不需要检查URL是否真正对“lang_code”参数感兴趣:

from flask import Blueprint, g


bp = Blueprint('frontend', __name__, url_prefix='/<lang_code>')


@bp.url_defaults
def add_language_code(endpoint, values):
    values.setdefault('lang_code', g.lang_code)


@bp.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code')


@bp.route('/')
def index():
    ...


@bp.route('/about')
def about():
    ...


6. 使用Setuptools进行部署

        Setuptools是一个扩展库,它通常被用于分发Python库和扩展。它继承于distutiles,一个使用Python附带的基本模块安装系统,也支持各种更复杂的结构,使更大的应用程序更容易分发:

        略,原文地址


7. 使用Fabric进行部署

        略,原文地址


8. Flask中使用SQLite3

        在Flask中,你可以很容易地实现,当需要的时候开启数据库连接,以及当上下文完蛋的时候(通常实在请求的最后)关闭数据库连接。

        这里有一个简单的示例展示了在Flask中如何使用SQLite3:

import sqlite3
from flask import g


DATABASE = '/path/to/database.db'


def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    return db


@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

        现在,为了使用这个数据库,应用程序必须拥有一个活跃的应用程序上下文(如果一个请求正在努力奋斗那么应用程序上下文一直都是活跃的),或者应用程序自己创建一个应用程序上下文。这个时候,get_db方法就可以被用来获取当前数据库连接。无论何时上下文被销毁,这个数据库连接也会被终止。

        注意:如果你使用Flask0.9及其以上版本,你需要使用flask._app_ctx_stack.top,而不是绑定到请求和不是应用程序上下文的flask.g对象。

        示例:

@app.route('/')
def index():
    cur = get_db().cursor()
    ...

        注意:

        请记住,teardown_request和appcontext方法总是会被执行,即使一个before_request处理程序失败了或者从来不被执行。正因如此,我们必须在我们关闭数据库之前确保这个数据库在这里是存在的。

8.1 在有需求的时候才连接

        这种方法的好处(在第一次使用时才进行连接)是,只有在真正需要的时候才会打开连接。如果你希望在一个请求上下文之外使用这个代码,你可以在一个Python shell中通过手动打开应用程序上下文来使用它:

with app.app_context():
    # now you can use get_db()

8.2 简单的查询

        现在,在每个请求处理程序方法中,你可以访问get_db()来获取当前打开的数据库连接。为了简化与SQLite的工作,行工厂(row_factory)方法是很有用的。它将执行每一个从数据库返回的结果,以便能转换结果。举个栗子,为了获取字典类型而不是元组类型,下面的代码可以增加到我们之前创建的get_db方法中:

def make_dicts(cursor, row):
    return dict((cursor.description[idx][0], value)
                for idx, value in enumerate(row))


db.row_factory = make_dicts

        这样可以使sqlite3模块针对这个数据库连接返回字典类型,以便更加容易处理。若要更加简化,我们可以在get_db中使用如下方法:

db.row_factory = sqlite3.Row

        这样将使用Row对象而不是字典来返回查询结果。这些都是namedtuple类型,因此我们可以通过索引或键来访问它们。举个栗子,假设我们有一个叫做r的sqlite3.Row对象,其字段包括id,FirstName,LastName以及MiddleInitial:

>>> # You can get values based on the row's name
>>> r['FirstName']
John
>>> # Or, you can get them based on index
>>> r[1]
John
# Row objects are also iterable:
>>> for value in r:
...     print(value)
1
John
Doe
M

        此外,通过结合获取游标、执行和获取结果的方式来提供一个查询方法,这是一个不错的想法:

def query_db(query, args=(), one=False):
    cur = get_db().execute(query, args)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv

        这个方便的小函数结合了行工厂,使与数据库的工作比使用原始游标和连接对象更加方便。

        这里展示如何使用它:

for user in query_db('select * from users'):
    print user['username'], 'has the id', user['user_id']

        或者,如果你仅仅希望单个结果:

user = query_db('select * from users where username = ?',
                [the_username], one=True)
if user is None:
    print 'No such user'
else:
    print the_username, 'has the id', user['user_id']

        为了给SQL语句传递变量,在语句中使用一个“?”标记,并且将这个参数作为一个列表进行传递。永远不要直接使用字符串格式化的方式来添加它们到SQL语句中,因为这样做有可能会因为SQL注入而让应用程序遭到攻击。

8.3 初始化框架

        关系型数据库需要框架,因此应用程序经常会寻找一个schema.sql文件来创建数据库。基于这个框架文件,在一个方法中进行数据库的创建工作,这是一个不错的想法。下面的方法展示了如何实现这个想法:

def init_db():
    with app.app_context():
        db = get_db()
        with app.open_resource('schema.sql', mode='r') as f:
            db.cursor().executescript(f.read())
        db.commit()

        随后,你就可以在Python shell中创建这样的一个数据库了:

>>> from yourapplication import init_db
>>> init_db()


9. Flask中使用SQLAlchemy

        很多人更喜欢使用SQLAlchemy来访问数据库。在这种情况下,我们鼓励使用包而不是Flask应用程序的模块,并将模型放入到单独的模块(更大型的应用程序)中。虽然这不是必须的,但是这样做很有意义。

        通常使用SQLAlchemy,仅需四个步骤。

9.1 Flask-SQLAlchmy扩展

        因为SQLAlchemy是一个常用的数据库抽象层和对象关系映射器,它需要少量的配置工作,所以这里有一个Flask扩展可以为你处理这些问题。如果你想快速入门,那么这是最推荐的方式。

        你可以在PypI上下载Flask-SQLAlchemy

9.2 声明

        SQLAlchemy中的声明式扩展是使用SQLAlchemy最新的方法。它允许你同时定义表和模型,就像Django的工作方式一样。除了下面要讲的内容外,我还推荐了关于声明扩展的官方文档。

        这里有个示例,假设你的应用程序中有一个database.py模块:

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base


engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()


def init_db():
    # import all modules here that might define models so that
    # they will be registered properly on the metadata.  Otherwise
    # you will have to import them first before calling init_db()
    import yourapplication.models
    Base.metadata.create_all(bind=engine)

        为了定义你的模型,你需要定义一个类,这个类继承于上面创建的Base类。如果你很惊奇为什么我们不需要关心线程(就像我们在上面的SQLite3示例中使用g对象一样):这是因为在调用scoped_session的时候,SQLAlchemy就已经为我们做了这些工作了。

        为了使用在你的应用程序中以一种声明式的方式使用SQLAlchemy,你仅需要将下面的代码放到你的应用程序模块中。Flask会在请求结束或者应用程序关闭的时候自动地移除数据库会话。

from yourapplication.database import db_session


@app.teardown_appcontext
def shutdown_session(exception=None):
    db_session.remove()

        这里有一个模型的示例(比如,放到model.py模块中):

from sqlalchemy import Column, Integer, String
from yourapplication.database import Base


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True)
    email = Column(String(120), unique=True)

    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email

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

        为了创建数据库,你可以使用init_db方法:

>>> from yourapplication.database import init_db
>>> init_db()

        你可以像下面这样给数据库新增数据:

>>> from yourapplication.database import db_session
>>> from yourapplication.models import User
>>> u = User('admin', 'admin@localhost')
>>> db_session.add(u)
>>> db_session.commit()

        查询数据操作如下:

>>> User.query.all()
[<User u'admin'>]
>>> User.query.filter(User.name == 'admin').first()
<User u'admin'>

9.3 手动实现对象关系映射

        手动的对象关系映射与上面的声明式方法相比各有千秋。两者最主要的区别在于,手动的对象关系映射需要你单独地定义表和类,然后将两者映射起来。它很灵活,但是需要键入的内容更多。通常上,它运行方式就像声明式方法,因此一定要将你的应用程序分解为一个包中的多个模块。

        这里有一个关于你应用程序的database.py模块的示例:

from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import scoped_session, sessionmaker


engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
metadata = MetaData()
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))


def init_db():
    metadata.create_all(bind=engine)

        就像在声明式方法中一样,你需要在请求结束后或者应用程序上下文关闭时,关闭这个会话。将下面的代码放到你的应用程序模块中:

from yourapplication.database import db_session


@app.teardown_appcontext
def shutdown_session(exception=None):
    db_session.remove()

        这里展示一个数据表和模型(代码放置到models.py中):

from sqlalchemy import Table, Column, Integer, String
from sqlalchemy.orm import mapper
from yourapplication.database import metadata, db_session


class User(object):
    query = db_session.query_property()

    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email

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


users = Table('users', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(50), unique=True),
    Column('email', String(120), unique=True)
)
mapper(User, users)

        查询和新增工作就跟前一节的示例一样。

9.4 SQL抽象层

        如果你仅仅希望使用数据库系统(以及SQL)抽象层,基本上你仅需要这个引擎即可:

>>> con = engine.connect()
>>> con.execute(users.insert(), name='admin', email='admin@localhost')

from sqlalchemy import create_engine, MetaData, Table


engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
metadata = MetaData(bind=engine)

        然后你既可以像前面的示例那样在你的代码中声明数据表,也可以自动地加载它们:

from sqlalchemy import Table


users = Table('users', metadata, autoload=True)

        为了新增数据,你可以使用insert函数。我们必须首先获取一个连接,然后我们就可以使用一个事务了:

>>> con = engine.connect()
>>> con.execute(users.insert(), name='admin', email='admin@localhost')

        SQLAlchemy会为我们自动提交这个操作的。

        为了查询你的数据库,你可以直接使用这个引擎或者使用一个连接:

>>> users.select(users.c.id == 1).execute().first()
(1, u'admin', u'admin@localhost')

        这些结果是元组类型:

>>> r = users.select(users.c.id == 1).execute().first()
>>> r['name']
u'admin'

        你也可以为execute()函数传递SQL命令的字符串:

>>> engine.execute('select * from users where id = :1', [1]).first()
(1, u'admin', u'admin@localhost')

        关于SQLAlchemy的更多信息,请前往其官网


10. 上传文件

        正如你所知的,上传文件是一个老生常谈的问题。文件上传的基础思想是非常简单的,它的主要工作内容如下:

        1. 一个<form>标签被标记成enctype=multipart/form-data,并且在这个表单中放置了一个<input type=file>。

        2. 应用程序从请求对象上的files字典进行访问文件。

        3. 使用文件的save()函数将文件永久保存在文件系统的某个地方。

10.1 相关介绍

        让我们从一个非常基础的应用程序开始,它将一个文件上传到一个特定的上传文件夹,并向用户显示一个文件。让我们看一下我们应用程序的自引导代码(bootstrapping):

import os
from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename


UPLOAD_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

        首先,我们需要进行一些导入操作。大多数导入内容是可以直接理解的,werkzeug.secure_filename()稍后会进行解释。UPLOAD_FOLDER是我们存储上传文件的地方,ALLOWED_EXTENSIONS是一组允许上传文件的后缀。

        为什么我们要限制上传文件的后缀?如果服务器失直接发送数据到客户端的,那么你可能不希望你的用户能够上传所有东西。这样做,你可以确保用户不能上传导致XSS问题的HTML文件(参见Cross-Site Scripting(XSS))。如果服务能执行.php文件,那么也可以确保不会上传这类文件,但是谁在服务器上安装了PHP呢(意思是,一般python+flask不会用到PHP这个世界上最好的语言偷笑)?

        接下来,编写检查扩展是否有效的函数,以及上传文件并将用户重定向到上传文件的URL:

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        # if user does not select file, browser also
        # submit an empty part without filename
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('uploaded_file',
                                    filename=filename))
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

        那么secure_filename()方法实际的作用是什么呢?这里有一个宗旨叫做,永远不要信任用户输入的东西。在这里,对于上传文件的文件名来说也同样不要去信任它。所有提交的表单都可能被遗忘,并且文件名也可能是不安全的。因此,你需要记住:在直接存储一个文件到文件系统上时,一定要使用这个函数来确保文件名是安全的。

        专业信息:

        所以,你感兴趣的是secure_filename()方法能做什么,并且如果你不使用它的话会带来什么问题?想象一下,如果某人将下面的内容作为文件名发送给你的应用程序:

        filename = "../../../../home/username/.bashrc"

        假设这几个../是正确的,你可以将这个路径添加到UPLOAD_FOLDER中,这将导致用户可能有能力修改服务器文件系统上的文件,但实际上他应该不能修改。这确实是需要了解应用程序才能知道,但是相信我,黑客是很有耐心来破解这个路径的。

        现在,让我们看看这个方法是如何工作的:

>>> secure_filename('../../../../home/username/.bashrc')

'home_username_.bashrc'

        还有最后一件需要做的事:文件上传的服务。在upload_file()中,我们将用户重定向到url_for('uploaded_file', filename=filename),即/uploads/filename。因此,我们要编写uploaded_file()方法来返回上传路径下,上传成功的这个文件。在Flask0.5开始,我们可以使用一个方法来为我们实现这个功能:

from flask import send_from_directory


@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

        你可以为uploaded_file注册build_obly规则,也可以使用SharedDataMiddleware来实现下载服务。这也能在其他旧版的Flask上工作:

from werkzeug import SharedDataMiddleware


app.add_url_rule('/uploads/<filename>', 'uploaded_file', build_only=True)
app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
    'uploads': app.config['UPLOAD_FOLDER']
})

        如果你现在运行应用程序,你会发现所有功能都符合预期。

10.2 改进上传

        (新增于版本0.6。)

        Flask到底是如何处理上传的呢?如果文件很小,它会将文件存储到web服务的内存中,否则会存储到本地临时位置(就如tempfile.gettempdir()返回的内容一样)。但是在上传被中止后,你该人如何指定最大文件的大小呢?默认情况下,Flask会很开心地接受文件上传到一个无限制的内存中,但是实际上我们买不起这么夸张的硬件,因此正确的方法是在配置中设置配置项MAX_CONTENT_LENGTH来限制最大上传文件的大小:

from flask import Flask, Request


app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024

        上面的代码将把允许的最大有效负载限制为16Mb。如果一个更大的文件要进行传输,Flask会抛出一个RequestEntityTooLarge的异常。

        连接重置问题:

        当使用本地开发服务器时,你可能获取一个连接重置错误(connection reset error)而不是一个413响应。在使用生产WSGI服务器运行应用程序时,你可以得到正确的状态响应。

        这个特性是于Flask0.6新增的,但是在以前的版本中通过继承请求对象也可以实现。有关信息可以查看Werkzeug文档的文件处理(file handling)。

10.3 上传进度条

        一段时间以前,许多开发人员都有这样的想法:将传输来的文件一小块一小块地读取,并将上传进度存储在数据库中,然后通过客户端的JavaScript代码来读取进度。简单来说,客户端每5秒就会查询一次当前传输了多少。你感觉到这种讽刺了吗?客户端询问一些它已经知道的事情。

10.4 一个更简单的解决方案

        现在,这里有更好的解决方案,它执行更快,并且更加可靠。有一些像JQuery这样的JavaScript库,它们应用表单插件来简化进度条的构建。

        因为在处理上传的所有应用程序中,文件上传的常见模式几乎没有变化,因此Flask也提供了一个叫做Flask-Uploads的扩展来实现一个完整的上传机制,还提供了包括文件类型白名单、黑名单等多种功能。


11. 缓存

        当你的应用程序运行缓慢的时候,试着为其增加一些缓存。至少这是一种提高速度的最简单方式。那么缓存能做什么呢?比如,你有一个需要一段时间才能完成的方法,但是这个方法的返回结果可能在5分钟内都是有效的,因此你可以将这个结果放到缓存中一段时间,而不是每次需要这个值的时候都去执行一次这个方法。

        Flask本身并不提供缓存功能,但是作为Flask基础的Werkzeug库,有一些非常基础的缓存支持。Werkzeug支持多种缓存后端,其中最常见的是Memcached服务器。

11.1 建立一个缓存

        就像创建Flask对象那样,你先创建一个缓存对象,然后让它一直存在。如果你使用了开发服务器,你可以创建一个SimpleCache对象,这对象是一个简单的缓存,用于将元素缓存到Python解释器的内存中:

from werkzeug.contrib.cache import SimpleCache


cache = SimpleCache()

        如果你想要使用Memcached,请确保你有一个支持Memcache的模块(可以从PyPI上获取),并且有一个可用的Memcached服务器正在运行。然后你就可以像下面这样连接到缓存服务器:

from werkzeug.contrib.cache import MemcachedCache


cache = MemcachedCache(['127.0.0.1:11211'])

        如果你正在使用App Engine,你可以通过下面的代码轻松连接到App Engine的缓存服务器:

from werkzeug.contrib.cache import GAEMemcachedCache


cache = GAEMemcachedCache()

11.2 使用一个缓存

        现在怎样使用一个缓存呢?这里有两个非常重要的操作:get()set()

        为了从缓存获取一项数据,你可以调用get(),并为其传递一个字符串作为键名。如果在这个缓存中存在这个键的话,则返回对应的值,否则这个方法将返回None:

rv = cache.get('my-item')

        为了添加一项数据到缓存中,使用set()函数。这个函数的第一个参数是键,第二个参数是需要被设置的值。同时,也可以提供一个timeout关键字参数,用于在达到这个参数规定的时间后,自动移除此键值对。

        示例如下:

def get_my_item():
    rv = cache.get('my-item')
    if rv is None:
        rv = calculate_value()
        cache.set('my-item', rv, timeout=5 * 60)
    return rv


12. 视图装饰器

        Python有一个很有趣的特性叫做函数装饰器。这个特性允许你使用一些非常简洁的语法来编辑Web应用。因为Flask中的每个视图都是一个函数,装饰器可以被用来将附加的功能注入到一个或多个函数中。你可能已经用过route()装饰器了。但是在某些情况下,你还需要实现自己的装饰器。比如,你有一个仅供登陆后的用户访问的视图,如果未登录的用户尝试访问,则把用户重定向到登录界面。这个示例很好地说明了使用装饰器是很屌的方法。

12.1 登录请求的装饰器

        让我们实现这样的一个装饰器。装饰器是封装和替换另一个方法的方法。由于原始方法被替换了,因此你需要记住将原始方法的信息复制到新的方法中。使用functools.wrap()来处理这些。

        下面的示例假设登录页面的名字是‘login’,当前用户被存储在g.user中,如果用户没有登陆则g.user为None:

from functools import wraps
from flask import g, request, redirect, url_for


def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function

        为了使用装饰器,我们将装饰器放置到内部视图方法上。当应用多个装饰器时,请记住route()装饰器始终在最外层。

@app.route('/secret_page')
@login_required
def secret_page():
    pass

        注意:

        在登录页面的GET请求之后,next值将存在于request.args中。在发送来自登录表单的POST请求时,你必须将next值传递下去。你可以用一个隐藏的输入标签来完成它,然后在用户登录的时候,从request.form中获取它。

<input type="hidden" value="{{ request.args.get('next', '') }}"/>

12.2 缓存装饰器

        假设你有一个视图函数需要花费一定的时间进行计算,正因如此,你想要在一定时间内缓存生成的记过。装饰器可以很好地实现这个功能。我们假设你已经建立了一个缓存。

        这里有一个缓存示例方法。它从一个特定的前缀(实际上是一个格式字符串)和请求的当前路径生成了缓存键。注意,我们创建了这样一个方法,它首先创建装饰器,然后用这个装饰器包装目标函数。听起来很复杂?不幸的是,这确实有些难度,但是看代码确是很容易理解的。

        被装饰器包装的方法,工作流程如下:

        1. 基于当前路径和请求,获取唯一的缓存键。

        2. 从缓存中获取这个键对应的值。如果缓存返回的不为空,则我们就将其返回回去。

        3. 如果缓存中不存在这个键,则原始方法将被调用,并且原始方法的返回值在一定时间内(默认5分钟)将被缓存起来。

        代码如下:

from functools import wraps
from flask import request


def cached(timeout=5 * 60, key='view/%s'):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            cache_key = key % request.path
            rv = cache.get(cache_key)
            if rv is not None:
                return rv
            rv = f(*args, **kwargs)
            cache.set(cache_key, rv, timeout=timeout)
            return rv
        return decorated_function
    return decorator

        注意,这段代码是假设cache对象是可用的。参考上一节内容获取更多信息。

12.3 模板装饰器

        前段时间,TurboGears的小伙伴们发明了一种常用模式,即模板装饰器。这个装饰器的思想是,你返回一个字典,其中包含从视图传递给模板的值,并且模板会被自动渲染。下面三个例子是等价的:

@app.route('/')
def index():
    return render_template('index.html', value=42)


@app.route('/')
@templated('index.html')
def index():
    return dict(value=42)


@app.route('/')
@templated()
def index():
    return dict(value=42)

        正如你所看到的,如果模板名没有被指定,那么这里会使用URL映射的端点名,然后将点转换为反斜杠,最后添加上‘.html’作为模板的名字。否则将使用提供的模板名称。当被装饰器包裹的方法返回时,返回的字典就会被传递给模板渲染函数。如果返回的是None,那么相当于返回一个空字典;如果返回的不是字典,我们将返回未更改过的方法的返回值。这样你就可以继续使用重定向方法或者返回简单的字符串了。

        装饰器源码:

from functools import wraps
from flask import request


def templated(template=None):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            template_name = template
            if template_name is None:
                template_name = request.endpoint \
                    .replace('.', '/') + '.html'
            ctx = f(*args, **kwargs)
            if ctx is None:
                ctx = {}
            elif not isinstance(ctx, dict):
                return ctx
            return render_template(template_name, **ctx)
        return decorated_function
    return decorator

12.4 端点装饰器

        当你希望使用Werkzeug的路由系统以获得更大的灵活性时,你需要将Rule中定义的端点(Endpoint)映射到视图方法中。这可能会使用到这种装饰器。举个栗子:

from flask import Flask
from werkzeug.routing import Rule


app = Flask(__name__)
app.url_map.add(Rule('/', endpoint='index'))


@app.endpoint('index')
def my_index():
    return "Hello world"


13. 使用WTForms进行表单验证

        当你必须使用浏览器视图提交表单数据时,代码很快就会变得难以阅读。这里有一些库,旨在使这个过程更加容易管理。其中有一个WTForms,我们将在这里介绍它。如果你发现自己处于有多种表单的情况下,你可能回想试一试WTForms

        当你使用WTForms时,你首先必须定义你的表单类。我建议将应用程序拆分为多个模块(在大型的应用程序中),并为表单条件一些单独的模块。

        挖掘WTForms的最大潜力:

        Flask-WTF扩展在这个模式的基础上进行了扩展,并且添加了一些搓手可得的帮助函数,这些函数将会使在Flask中使用表单更加有趣,你可以通过PyPI获取它。

13.1 表单

        这是一个典型的注册页面的示例:

from wtforms import Form, BooleanField, StringField, PasswordField,\
    validators


class RegistrationForm(Form):
    username = StringField('Username', [validators.Length(min=4, max=25)])
    email = StringField('Email', [validators.Length(min=6, max=35)])
    password = PasswordField('New Password', [
        validators.DataRequired(),
        validators.EqualTo('confirm', message='Passwords must match')
    ])
    confirm = PasswordField('Repeat Password')
    accept_tos = BooleanField('I accept the TOS', [validators.DataRequired()])

13.2 视图

        在视图方法中,使用这个表单的方式如下:

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm(request.form)
    if request.method == 'POST' and form.validate():
        user = User(form.username.data, form.email.data,
                    form.password.data)
        db_session.add(user)
        flash('Thanks for registering')
        return redirect(url_for('login'))
    return render_template('register.html', form=form)

        注意,在这里我们使用了SQLAlchemy,也就是第9节所创建的数据表,当然这不是必须的。然后调整代码成可以运行的状态。

        记住:

        1. 如果数据是通过HTTP POST方法提交的,则使用请求的form属性的值来创建表单。如果数据是通过GET提交的,则使用args属性创建表单。

        2. 为了验证数据,需调用validate()函数,如果通过验证则返回True,否则返回False。

        3. 为了从表单访问单独的值,访问格式为form.<NAME>.data。

13.3 模板中的视图

        现在该对模板下手了。当你传递表单给模板,你可以在这里轻易渲染它们。下面的示例模板就为你展示了这到底是多么简单的事。WTForms已经为我们生成了一半的表单。为了使表单更加漂亮,我们可以写一个宏,这个宏用来label来渲染一个字段,并且列出所有发生的错误(比如验证长度不符合之类的)。

        下面是一个使用这种宏的_formhelpers.html模板的示例:

{% macro render_field(field) %}
  <dt>{{ field.label }}
  <dd>{{ field(**kwargs)|safe }}
  {% if field.errors %}
    <ul class=errors>
    {% for error in field.errors %}
      <li>{{ error }}</li>
    {% endfor %}
    </ul>
  {% endif %}
  </dd>
{% endmacro %}

        这些宏接受一些关键字参数,这些参数被转发到WTForms的字段函数中,从而为我们渲染这些字段。关键字参数将会作为HTML属性进行插入。举个栗子,你可以调用render_field(form.username, class='username')向输入元素添加一个类。注意,WTForms返回标准的Python编码字符串,因此我们必须使用“|safe”筛选器来告诉Jinja2这个数据已经是HTML转义过的。

        这里是register.html模板,利用了_formhelpers.html模板的宏:

{% from "_formhelpers.html" import render_field %}
<form method=post>
  <dl>
    {{ render_field(form.username) }}
    {{ render_field(form.email) }}
    {{ render_field(form.password) }}
    {{ render_field(form.confirm) }}
    {{ render_field(form.accept_tos) }}
  </dl>
  <p><input type=submit value=Register>
</form>

        获取更多关于WTForms的信息,请前往WTForms website


14. 模板的继承

        Jinja最帅气的部分就是模板的继承了。模板继承允许你创建一个基础“骨架”模板,这个骨架模板包含了你的网站的所有通用元素,并且定义了子模板可以重写的块(block)。

        这听起来超级复杂,但实际上这是非常基础的。还是用栗子来说话吧。

14.1 基础模板

        创建一个layout.html模板,这个模板定义了简单的HTML骨架文档,你可以使用一个简单的两列页面。子模板的工作就是要填满这些空白块的内容:

<!doctype html>
<html>
  <head>
    {% block head %}
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <title>{% block title %}{% endblock %} - My Webpage</title>
    {% endblock %}
  </head>
  <body>
    <div id="content">{% block content %}{% endblock %}</div>
    <div id="footer">
      {% block footer %}
      &copy; Copyright 2010 by <a href="http://domain.invalid/">you</a>.
      {% endblock %}
    </div>
  </body>
</html>

        在这个示例中,{% block %}标签定义了四个块,这四个块就是给子模板填充(或者说是修改)的部分。所有块(block)标签所做的事,就是告诉模板引擎,子模板可以覆盖这个模板的这些部分。

14.2 子模板

        子模板可以如下所示:

{% extends "layout.html" %}
{% block title %}Index{% endblock %}
{% block head %}
  {{ super() }}
  <style type="text/css">
    .important { color: #336699; }
  </style>
{% endblock %}
{% block content %}
  <h1>Index</h1>
  <p class="important">
    Welcome on my awesome homepage.
{% endblock %}

        {% extends %}标签在这里是个关键。它告诉模板引擎,这个模板继承于其他模板。当模板系统对该模板进行评估时,首先它将定位到这个模板的父模板。继承(extends)标签在这个模板中必须是第一个标签。为了渲染一个块的在父模板的内容,可以使用{{ super() }}。


15. 消息闪现(Message Flashing)

        好的应用程序和用户接口都会有不错的反馈信息。如果用户没有得到足够的反馈信息,他们最后可能会抛弃这个应用程序。Flask提供了一个非常简单的方式来提供反馈信息——闪现系统(flashing system)。闪现系统基本上可以在请求的末尾记录一条消息,以及访问它的下一个请求,并且也只能访问下一个请求。这通常与布局模板结合在一起使用。注意,浏览器和一些web服务器会强行限制cookie的大小。这意味着,闪现信息(flashing message)如果比会话cookie还要大的话,会造成消息闪现失败。

15.1 简单的闪现

        这里有一个示例:

from flask import Flask, flash, redirect, render_template, \
    request, url_for


app = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        if request.form['username'] != 'admin' or \
                request.form['password'] != 'secret':
            error = 'Invalid credentials'
        else:
            flash('You were successfully logged in')
            return redirect(url_for('index'))
    return render_template('login.html', error=error)

        layout.html模板,页面总体布局:

<!doctype html>
<title>My Application</title>
{% with messages = get_flashed_messages() %}
    {% if messages %}
        <ul class=flashes>
            {% for message in messages %}
                <li>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}
{% endwith %}
{% block body %}{% endblock %}

        index.html模板,继承于layout.html:

{% extends "layout.html" %}
{% block body %}
    <h1>Overview</h1>
    <p>Do you want to <a href="{{ url_for('login') }}">log in?</a></p>
{% endblock %}

        login.html模板,继承于layout.html:

{% extends "layout.html" %}
{% block body %}
    <h1>Login</h1>
    {% if error %}
        <p class=error><strong>Error:</strong> {{ error }}</p>
    {% endif %}
    <form method="post">
        <dl>
            <dt>Username:</dt>
            <dd><input type="text" name="username" value="{{
                request.form.username }}"></dd>
            <dt>Password:</dt>
            <dd><input type="password" name="password"></dd>
        </dl>
        <p><input type="submit" value="Login"></p>
    </form>
{% endblock %}

15.2 分类闪现

        (新增于版本0.3。)

        当闪现消息时,也可以提供对消息进行分类。如果没有提供任何东西,则默认类型为‘message’。当然,你可以使用其他类别,以便给用户更好的反馈。举个栗子,错误信息在显示的时候,背景色为红色。

        为了根据不同的类别闪现一个消息,可以使用flash()方法的第二个参数类设置:

flash(u'Invalid password provided', 'error')

        在模板中,随后你必须告诉get_flashed_messages()方法,它需要返回这个类别。之前模板的循环就会变成下面这个样子:

{% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
        <ul class=flashes>
            {% for category, message in messages %}
                <li class="{{ category }}">{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}
{% endwith %}

        这仅仅是一个如何渲染这些闪现消息的例子。你也可以使用这个类别来添加一个前缀,比如在信息前面加上<strong>Error:</strong>。

15.3 筛选闪现信息

        (新增于版本0.9。)

        你也可以选择给get_flashed_messages()传递一组类别来筛选其结果。如果你想要在单独的块中渲染每个类别的信息,那么这种方法就很有用。

{% with errors = get_flashed_messages(category_filter=["error"]) %}
    {% if errors %}
        <div class="alert-message block-message error">
            <a class="close" href="#">x</a>
            <ul>
                {%- for msg in errors %}
                <li>{{ msg }}</li>
                {% endfor -%}
            </ul>
        </div>
    {% endif %}
{% endwith %}


16. 用jQuery实现Ajax

        略,原文地址


17. 自定义错误页面

        Flask有一个abort()方法,这个方法可以提前终止一个请求,并返回一个HTTP错误编码。它还会为你提供一个简单的黑白错误页面,并提供基本的描述,除此之外没有其他特别的了。

        根据返回的错误编码,它或多或少能让用户找到实际的错误点。

17.1 常用错误编码

        下面列举出一些时常显示给用户的错误编码,即使应用程序行为是正确的:

        404 Not Found

                这是很常见的信息:“嘿,伙计,你在输入那个URL时犯了一个错误”。即使是网络新手也知道404意味着:该死,我要找的东西不在那里。确保在404页上有一些有用的东西,至少是一个返回索引的链接——这是一个不错的想法。

        403 Forbidden

                在你的网站上,如果有某种访问控制,你将不得不发送一个403编码用于不允许的资源。因此,当用户试图访问一个被禁止的资源时,请确保他们不会丢失。

        410 Gone

                你知道“404 Not Found”有一个叫做“410 Gone”的兄弟吗?很少有人真正去实现这个,但是我的想法是,以前存在的资源被删除了,应该返回410,而不是404。如果你没有从数据库永久地删除文档,而只是将它们标记为已删除,按照用户的喜好来看,你应该发送一个410编码,并且显示一条信息,这条信息告诉用户,他们正在查询的资源将被永久删除。

        500 Internal Server Error

                这个错误编码通常发生在编程错误或者服务器超载的情况下。一个非常好的想法是为这个编码设置一个漂亮的页面,因为你的应用程序迟早会有失败的时候。

17.2 错误处理程序

        错误处理程序是一个方法,这个方法在一个错误类型被抛出时返回一个响应,这些动作与“一个视图时一个方法,并且这个方法在请求URL被匹配的时候返回一个响应”类似。错误处理程序接收一个正在处理的错误实例,这很可能是一个HTTPException。而对于“500 Internal Server Error”的错误处理程序来说,它将接收未捕获的异常以及明确的500错误。

        错误处理程序的注册方式有:通过errorhandler()装饰器或者register_error_handler()函数。处理程序是伴随一个状态码注册的,比如404,或者一个异常类。

        响应的状态码不会被设置为处理程序的代码。当处理程序返回响应时,请确保提供了适当的HTTP状态码。

        当运行在调试模式的时候,关于“500 Internal Server Error”的处理程序不会被使用。相反,交互式调试器会直接显示这个错误。

        这里有一个示例,展示了如何实现“404 Page Not Found”的异常:

from flask import render_template


@app.errorhandler(404)
def page_not_found(e):
    # note that we set the 404 status explicitly
    return render_template('404.html'), 404

        当使用应用程序工厂模式时:

from flask import Flask, render_template


def page_not_found(e):
  return render_template('404.html'), 404


def create_app(config_filename):
    app = Flask(__name__)
    app.register_error_handler(404, page_not_found)
    return app

        模板示例如下:

{% extends "layout.html" %}
{% block title %}Page Not Found{% endblock %}
{% block body %}
  <h1>Page Not Found</h1>
  <p>What you were looking for is just not there.
  <p><a href="{{ url_for('index') }}">go somewhere nice</a>
{% endblock %}


18. 延迟加载视图

        Flask通常是与装饰器一起使用的。装饰器很简单,你可以将URL和这个特定URL的函数放在一起。然而,这种方法有一个缺点:它意味着,所有使用装饰器的代码都必须预先导入,否则Flask永远不会找到你的方法。

        如果你的应用程序必须快速导入,那么这是个问题。这种情况可能出现在类似谷歌的App Engine或者其他系统上。因此,如果你突然注意到你的应用程序超过了这种方法能够处理的能力范围,那么你可以使用一个集中的URL映射来解决这个问题。

18.1 转换为集中式的URL映射

        设想一下,当前应用程序像下面这样:

from flask import Flask


app = Flask(__name__)


@app.route('/')
def index():
    pass


@app.route('/user/<username>')
def user(username):
    pass

        然而,使用集中式URL映射的方式,你就需要一个不包含任何装饰器的文件(view.py),如下所示:

def index():
    pass


def user(username):
    pass

        然后,使用一个文件初始化应用,并将函数映射到URL:

from flask import Flask
from yourapplication import views


app = Flask(__name__)
app.add_url_rule('/', view_func=views.index)
app.add_url_rule('/user/<username>', view_func=views.user)

18.2 延迟加载

        到目前为止,我们仅仅将视图和对应的路由分隔开而已,但是模块仍然是在前面就导入了。下面的技巧使得视图方法是在我们需要的时候才会加载。可以使用一个辅助类来实现,它的行为就像一个方法,但是在第一次使用时,这个类才会在内部导入真正的方法:

from werkzeug.utils import import_string, cached_property


class LazyView(object):
    
    def __init__(self, import_name):
        self.__module__, self.__name__ = import_name.rsplit('.', 1)
        self.import_name = import_name

    @cached_property
    def view(self):
        return import_string(self.import_name)

    def __call__(self, *args, **kwargs):
        return self.view(*args, **kwargs)

        werkzeug.utils.import_string()是用来基于字符串导入一个对象。werkzeug.utils.cached_property将一个函数转换成懒惰属性的装饰器,被包装的方法在第一次检索结果的时候被调用,在随后访问该值的时候均使用已经计算好的结果,因此在使用这个装饰器的时候,所处的类必须有一个__dict__来支持这个属性能正常运作。

        在上面的示例中,最重要的是__module__和__name__要正确设置。这两个变量是在Flask内部使用的,以确定如何命名URL规则,以防你没有手动指定一个URL规则。

        然后你可以像下面这样,在你集中式加载的地方,组合你所需要的视图:

from flask import Flask
from yourapplication.helpers import LazyView


app = Flask(__name__)
app.add_url_rule('/', 
                 view_func=LazyView('yourapplication.views.index'))
app.add_url_rule('/user/<username>',
                 view_func=LazyView('yourapplication.views.user'))

        你还可以进一步改进它。通过编写一个在内部调用add_url_rule()方法的函数,自动将一个包含项目名称以及点符号的字符串添加为前缀,并按需求将view_func封装到LazyView中:

def url(import_name, url_rules=[], **options):
    view = LazyView('yourapplication.' + import_name)
    for url_rule in url_rules:
        app.add_url_rule(url_rule, view_func=view, **options)


# add a single route to the index view
url('views.index', ['/'])

# add two routes to a single function endpoint
url_rules = ['/user/', '/user/<username>']
url('views.user', url_rules)

        请记住,在请求前后激发的处理程序必须在一个文件中,并且在前面进行导入,这样就能在第一个请求到来的时候正常工作。对于其他装饰器来说也应该这样做。


19. 在Flask中使用MongoKit

        现在比较流行文档数据库,有跟关系型数据库一较高下的趋势。这一节将为你展示如何使用MongoKit,一个文档映射库,用于MongoDB数据库。

        这一节需要一个运行的MongoDB服务器以及安装好的MongoKit库。

        这里有两种常用的方式来使用MongoKit。

19.1 声明

        MongoKit默认行为是声明性的,它基于Django或者SQLAlchemy声明扩展的常见思想。

        这里有一个app.py模块示例:

from flask import Flask
from mongokit import Connection, Document


# configuration
MONGODB_HOST = 'localhost'
MONGODB_PORT = 27017

# create the little application object
app = Flask(__name__)
app.config.from_object(__name__)

# connect to the database
connection = Connection(app.config['MONGODB_HOST'],
                        app.config['MONGODB_PORT'])

        要定义你的模块,只需要一个从MongoKit导入的Document类派生的子类。如果你已经看过SQLAlchemy模式,你可能会惊奇,为什么在这里我们不用使用一个会话,甚至都不用定义一个init_db方法。一方面是因为MongoKit没有像是会话的东西,这样做导致有时候会增加代码量,但同时也使得数据库操作非常高效。另一方面,MongoDB是没有架构的,这意味着你可以通过一个插入命令来修改数据结构,而通过另一种数据结构来进行查询。MongoKit本身也是没有架构的,但是它实现了一些确保数据完整的验证工作。

        这里有一个示例文档(在app.py模块中):

from mongokit import ValidationError


def max_length(length):
    def validate(value):
        if len(value) <= length:
            return True
        # must have %s in error format string to have mongokit place key in there
        raise ValidationError('%s must be at most {} characters long'.format(length))
    return validate


class User(Document):
    structure = {
        'name': unicode,
        'email': unicode,
    }
    validators = {
        'name': max_length(50),
        'email': max_length(120),
    }
    use_dot_notation = True

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


# register the User document with our current connection
connection.register([User])

        这个示例向你展示了如何定义你自己的结构(命名structure),一个验证器来检测最大字符长度,并且使用了一个叫做use_dot_notation的MongoKit特性。默认情况下,MongoKit的行为就像Python的字典,但是use_dot_notation设置为True,你可以使用你的文档就像你在其他ORM中使用模型那样,使用点(.)来分隔属性。

        你可以像下面这样为数据库新增数据:

>>> from yourapplication.database import connection
>>> from yourapplication.models import User
>>> collection = connection['test'].users
>>> user = collection.User()
>>> user['name'] = u'admin'
>>> user['email'] = u'admin@localhost'
>>> user.save()

        注意,MongoKit是列类型的使用是有一点严格的,你不能使用常见的str类型来表示name或者email,而是使用unicode。

        查询数据的简单示例如下:

>>> list(collection.User.find())
[<User u'admin'>]
>>> collection.User.find_one({'name': u'admin'})
<User u'admin'>

19.2 PyMongo兼容层

        如果你只想使用PyMongo,MongoKit也能很好地完成这个工作。如果你想要查询数据达到最佳的效率,那么你可以这样用。注意,这个示例并没有展示与Flask结合使用,你可以参考上面MongoKit的示例来补充完整代码:

from MongoKit import Connection


connection = Connection()

        为了插入数据,你可以使用insert函数。首先我们必须要获取一个连接,这在SQL的世界中都是一样的。

>>> collection = connection['test'].users
>>> user = {'name': u'admin', 'email': u'admin@localhost'}
>>> collection.insert(user)

        MongoKit会自动提交插入。

        为了查询你的数据库,你可以直接使用集合:

>>> list(collection.find())
[{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'}]
>>> collection.find_one({'name': u'admin'})
{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'}

        这些结果都是像是字典的对象:

>>> r = collection.find_one({'name': u'admin'})
>>> r['email']
u'admin@localhost'

        更多关于MongoKit的信息,请前往这个网站


20. 添加Favicon

        Favicon是一个在网页浏览器显示的标签页上的或者历史记录里的图标。这个图标能帮助用户将你的网站与其他网站区分开,因此请使用一个独一无二的标志。

        一个常见的问题是,如何添加一个图标到Flask应用程序中。首先,当然,你需要一个图标。这个图标应该是16*16像素的,并且是ICO文件格式。这不是必须的,但是是被所有浏览器所支持的事实标准。将这个图标放置到你的静态文件目录下,文件名为favicon.ico。

        现在,为了让浏览器找到你的图标,正确的方法是在你的HTML中添加一个链接标签:

<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">

        对于大多数浏览器而言,这样做就足够了。然而,一些很老的浏览器不支持这种标准。原来的标准是在网站的根路径下查找favicon文件,并使用它。如果应用程序不是挂在域名的根目录,你要么需要配置web服务器,以便在根目录提供这个图标,要么就无法实现这个功能。然而,如果你的应用程序是在根目录,你就可以简单地配置一条重定向的路由:

app.add_url_rule('/favicon.ico',
                 redirect_to=url_for('static', filename='favicon.ico'))

        如果你想要保存额外的重定向请求,你也可以使用send_from_directory()来写一个视图:

import os
from flask import send_from_directory


@app.route('/favicon.ico')
def favicon():
    return send_from_directory(os.path.join(app.root_path, 'static'),
                               'favicon.ico', mimetype='image/vnd.microsoft.icon')

        我们可以省略显式的mimetype,它也能被猜测出来。但我们这样显式指定它,可以避免额外的猜测,因为它总是相同的。

        以上的代码将会通过你的应用程序来提供图标文件的访问。然而,如果可能的话,配置你的网页服务器来提供访问服务会更好。请参看对应网页服务器的文档。

        参考:Wikipedia上的Favicon文章。


21. 流媒体内容

        有时候,你希望传输一个极大的数据到客户端,这个数据大小已经超过了可以存储在内存中的大小。当你动态生成数据的时候,你如何将其发送回客户端,而不需要往返于文件系统呢?

        答案就是使用生成器(generator)和直接响应(direct response)。

21.1 基本用法

        下面这个基础视图方法,可以实时地生成大量CSV数据。这一技巧使用了一个内部函数,它使用生成器生成数据,然后调用该函数并将其传递给响应对象:

from flask import Response


@app.route('/large.csv')
def generate_large_csv():
    def generate():
        for row in iter_all_rows():
            yield ','.join(row) + '\n'
    return Response(generate(), mimetype='text/csv')

        每一个yield语句都会直接发送给浏览器。请注意,一些WSGI中间件可能打断数据流,所以在调试环境中使用剖析器和其他可能启动的东西时要小心。

21.2 来自模板的数据流

        Jinja2模板引擎也支持逐渐地渲染模板。这个功能不会直接暴露在Flask中,因为它是很少见的,但是你可以轻松地实现它:

from flask import Response


def stream_template(template_name, **context):
    app.update_template_context(context)
    t = app.jinja_env.get_template(template_name)
    rv = t.stream(context)
    rv.enable_buffering(5)
    return rv


@app.route('/my-large-page.html')
def render_large_template():
    rows = iter_all_rows()
    return Response(stream_template('the_template.html', rows=rows))

        这里的诀窍是,从应用程序的Jinja2环境中获取模板对象,并调用stream()方法返回一个流对象(stream object),而不是调用render()方法返回一个字符串。因为我们绕过了Flask模板渲染方法,并且使用模板对象本身,因此我们必须通过手动调用update_template_context()来确保更新了模板的渲染上下文。这一模板随后以流的方式迭代,直到结束。因为每一次你执行一个yield,服务器都会将内容刷新到客户端,你可能想要缓冲模板中的一些条目,因此你可以使用rv.enable_buffering(size)来实现,size较为合理的默认值为5。

21.3 上下文的流

        (新增于版本0.9。)

        注意,当你传输数据的时候,请求上下文在函数执行的那一刻就已经消失了。Flask0.9开始为你提供了一个辅助功能,你可以在生成器执行期间保持请求上下文:

from flask import stream_with_context, request, Response


@app.route('/stream')
def streamed_response():
    def generate():
        yield 'Hello '
        yield request.args['name']
        yield '!'
    return Response(stream_with_context(generate()))

        如果不适用stream_with_context()方法,你会得到一个RuntimeError的异常。


22. 延迟请求回调

        Flask的设计原则之一是,响应对象被创建并传递给一个潜在的回调链,也可以修改和替换这个回调链中的任意一个回调。当请求开始处理的时候,这里并不存在响应。响应是在需要的时候由视图方法或者系统中一些其他的组件生成的。

        如果你希望在响应还不存在的时候修改响应,这会发生什么情况呢?一个常见的例子使,你可能希望在before_request()回调中,在响应对象上设置一个cookie。

        一种方式是避免这种情形,通常这是可能的。举个栗子,你可以尝试将该逻辑移动到after_request()回调中。然而,有时候移动代码,会使它变得更加复杂或笨拙。

        另一种方法,你可以使用after_this_request()来注册回调,这个回调仅会在当前请求之后被执行。通过这种方式,你可以根据当前请求,从应用程序的任何地方延迟代码执行。

        在请求期间的任何时候,我们都可以注册一个在请求结束时调用的方法。例如,你可以在before_request()回调中记录用户当前的语言到cookie中:

from flask import request, after_this_request


@app.before_request
def detect_user_language():
    language = request.cookies.get('user_lang')

    if language is None:
        language = guess_language_from_request()

        # when the response exists, set a cookie with the language
        @after_this_request
        def remember_language(response):
            response.set_cookie('user_lang', language)

    g.language = language


23. 添加HTTP函数的重载

        一些HTTP代理不支持任意的HTTP方法或者新的HTTP方法(比如PATCH)。这种情形下,可以通过另一种完全违背协议的HTTP方法来“代理”HTTP方法。

        这个方法使客户端发出HTTP POST请求并且设置X-HTTP-Method-Override标头的值为我们想要的HTTP方法(比如PATCH)。

        使用一个HTTP中间件就可以轻松达到这个目的:

class HTTPMethodOverrideMiddleware(object):
    allowed_methods = frozenset([
        'GET',
        'HEAD',
        'POST',
        'DELETE',
        'PUT',
        'PATCH',
        'OPTIONS',
    ])
    bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE'])

    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper()
        if method in self.allowed_methods:
            method = method.encode('ascii', 'replace')
            environ['REQUEST_METHOD'] = method
        if method in self.bodyless_methods:
            environ['CONTENT_LENGTH'] = '0'
        return self.app(environ, start_response)

        为了在Flask中使用它,下面的操作时必须的:

from flask import Flask


app = Flask(__name__)
app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app)


24. 请求内容校验和

        不同的代码片段可以消费请求数据并对其进行预处理。举个栗子,JSON数据最终会出现在已经阅读和处理过的请求对象上,表单数据也会在那里结束,但是会经历不同的代码路径。当你想要计算传入请求数据的校验和时,这似乎很不方便,但这对于某些API来说确是很有必要的。

        幸运的是,可以通过包装输入流来达到目的。

        下面的示例,计算了接收数据的SHA1校验和,并将其存储到WSGI环境中:

import hashlib


class ChecksumCalcStream(object):

    def __init__(self, stream):
        self._stream = stream
        self._hash = hashlib.sha1()

    def read(self, bytes):
        rv = self._stream.read(bytes)
        self._hash.update(rv)
        return rv

    def readline(self, size_hint):
        rv = self._stream.readline(size_hint)
        self._hash.update(rv)
        return rv


def generate_checksum(request):
    env = request.environ
    stream = ChecksumCalcStream(env['wsgi.input'])
    env['wsgi.input'] = stream
    return stream._hash

        要使用这段代码,你需要做的是在请求开始消耗数据之前调用计算流。(例如:小心访问request.form或者类似的东西,比如,应该避免访问before_request_handlers。)

        示例代码:

@app.route('/special-api', methods=['POST'])
def special_api():
    hash = generate_checksum(request)
    # Accessing this parses the input stream
    files = request.files
    # At this point the hash is fully constructed.
    checksum = hash.hexdigest()
    return 'Hash was: %s' % checksum


25. 基于Celery的后台任务

        如果你的应用程序有一个需要长时间运行的任务,比如处理一些上传数据或者发送邮件,你不希望在请求期间一直等待直到这些工作结束。使用一个任务队列来发送必要的数据到另外的进程,这样可以在后台运行这个任务,而请求会立即返回。

        Celery是一个强大的任务队列,它可以被用于简单的后台任务,也可以用于复杂的多阶段程序和调度。这里只演示如何在Flask中配置Celery,并假设你已经阅读了First Steps with Celery一文。

25.1 安装

        Celery是一个单独的Python包,安装方式如下:

$ pip install celery

25.2 配置

        首先,你需要一个Celery实例,这被称为celery应用。与Flask中的Flask对象的功能类似,celery应用仅为Celery提供服务支持。因为这个实例被用作你想在Celery中做的任何事情的入口点,比如创建任务和管理工作者,其他模块都有可能导入它。

        举个栗子,你可以将celery应用放到一个tasks模块中。虽然你可以使用Celery而不需要对Flask进行重新配置,但通过子类化任务和添加对Flask应用上下文的支持,与Flask配置关联起来,这样会让程序结构看起来更好一些。

        下面是将Celery和Flask结合起来的必要操作:

from celery import Celery


def make_celery(app):
    celery = Celery(
        app.import_name,
        backend=app.config['CELERY_RESULT_BACKEND'],
        broker=app.config['CELERY_BROKER_URL']
    )
    celery.conf.update(app.config)

    class ContextTask(celery.Task):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return self.run(*args, **kwargs)

    celery.Task = ContextTask
    return celery

        这个方法创建了一个新的Celery对象,从应用程序配置中获取broker代理来配置它,从Flask配置文件中更新剩余Celery的配置文件,然后创建一个在应用程序上下文中封装任务执行的任务子类。

25.3 一个简单的任务

        编写一个任务,这个任务进行两个数值的加法运算并返回其结果。我们配置Celery的broker和backend来使用Redis,通过使用上面的内容来创建一个celery应用程序,并且使用它来定义这个任务。

from flask import Flask


flask_app = Flask(__name__)
flask_app.config.update(
    CELERY_BROKER_URL='redis://localhost:6379',
    CELERY_RESULT_BACKEND='redis://localhost:6379'
)
celery = make_celery(flask_app)


@celery.task()
def add_together(a, b):
    return a + b

        现在,这个任务将会在后台被调用:

result = add_together.delay(23, 42)
result.wait()  # 65

25.4 运行一个工作者

        如果你已经执行了上面的代码,你会失望地发现.wait()永远没有真正地返回。这是因为你还需要运行一个Celery工作者来接收和执行这个任务。

$ celery -A your_application.celery worker

        your_application字符串是创建celery对象的应用程序包或者模块。

        现在,工作者开始运行,wait将返回一次结果,然后这个任务就结束了。


26. 子类化Flask

        Flask类是被设计来用于子类化的。

        举个栗子,你可能希望重写“请求参数如何被处理的,以用来保护这些参数”的需求:

from flask import Flask, Request
from werkzeug.datastructures import ImmutableOrderedMultiDict


class MyRequest(Request):
    """Request subclass to override request parameter storage"""
    parameter_storage_class = ImmutableOrderedMultiDict


class MyFlask(Flask):
    """Flask subclass using the custom request class"""
    request_class = MyRequest

        这是重写或者扩展Flask内部功能的推荐方法。


猜你喜欢

转载自blog.csdn.net/ReganDu/article/details/80270224
今日推荐