[Python自学] Flask框架 (3) (路由、CBV、自定义正则动态路由、请求处理流程、蓝图)

一、路由系统

1.浅析@app.route的源码

我们使用@app.route("/index")可以给视图函数加上路由映射。我们分析一下@app.route装饰器的实现源码:

def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop("endpoint", None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator

可以看到,装饰器的核心就是add_url_rule()函数。这里的self就是Flask的实例app。因为是app调用的route。

也就是说,我们不使用装饰器,也可以直接调用该函数实现路由映射:

def test():
    return 'test'


# 使用app.add_url_rule代替@app.route
# 第一个参数就是url,第二个参数是endpoint(即路由name),第三个参数为视图函数引用
app.add_url_rule('/test', None, test)

执行结果:

 可以看到,这种方式实现的路由,也可以正常访问。

2.分析add_url_rule函数

@setupmethod
def add_url_rule(
    self,
    rule,
    endpoint=None,
    view_func=None,
    provide_automatic_options=None,
    **options
):
    if endpoint is None:  # 如果传入的endpoint为None,则使用视图函数的__name__作为endpoint
        endpoint = _endpoint_from_view_func(view_func)
    options["endpoint"] = endpoint  # 将endpoint设置到options中
    methods = options.pop("methods", None)  # 从参数中获取methods,如果没有,则为None
    # if the methods are not given and the view_func object knows its
    # methods we can use that instead.  If neither exists, we go with
    # a tuple of only ``GET`` as default.
    if methods is None:  # 如果methods为None,就去view_func中找methods,如果找不到,则默认为GET
        methods = getattr(view_func, "methods", None) or ("GET",)
    if isinstance(methods, string_types):  # 如果methods是str,则报错,必须是列表
        raise TypeError(
            "Allowed methods have to be iterables of strings, "
            'for example: @app.route(..., methods=["POST"])'
        )
    methods = set(item.upper() for item in methods)  # methods中元素全部转换为大写
    # Methods that should always be added
    required_methods = set(getattr(view_func, "required_methods", ()))  # 获取required_methods
    # starting with Flask 0.8 the view_func object can disable and
    # force-enable the automatic options handling.
    if provide_automatic_options is None:  # 获取provide_automatic_options
        provide_automatic_options = getattr(
            view_func, "provide_automatic_options", None
        )
    if provide_automatic_options is None:  # 如果还为None
        if "OPTIONS" not in methods:  # 如果methods中没有OPTIONS
            provide_automatic_options = True  # provide_automatic_options置为True
            required_methods.add("OPTIONS")
        else:
            provide_automatic_options = False  # 如果methods中有,则provide_automatic_options置为False
    # Add the required methods now.
    methods |= required_methods  # 并集
    # 将我们传入的url,endpoint,func等封装起来,成为一个Rule对象
    rule = self.url_rule_class(rule, methods=methods, **options)
    rule.provide_automatic_options = provide_automatic_options
    # 将封装好的Rule对象添加到Map类的对象中
    self.url_map.add(rule)
    if view_func is not None:  # 传入的视图函数是否为空,这里不为空
        old_func = self.view_functions.get(endpoint)  # 去view_functions字典中看看有没有同名的视图函数
        if old_func is not None and old_func != view_func:  # 如果有同名视图函数,且函数不是我们当前传入的函数,则报错
            raise AssertionError(
                "View function mapping is overwriting an "
                "existing endpoint function: %s" % endpoint  # 报错:存在同名的视图函数
            )
        # 如果endpoint没有冲突,则将视图函数加入view_functions字典中
        self.view_functions[endpoint] = view_fun

这段代码主要的功能就是,将我们传入的url、endpoint、methods等一系列路由参数,封装成一个Rule对象,然后添加到Map对象中。然后判断是否存在endpoint冲突的视图函数,如果没有,则将 endpoint:视图函数引用 键值对存放在app.view_functions字典中,该字典主要就是用来检查endpoint的冲突问题。

所以从这里可以看出,我们在写路由的时候,尽量不要让endpoint重名,如果一定要重名,则函数必须是相同的(例如两个url对应一个视图函数的场景)。例如:

@app.route('/test2', endpoint='t1')
@app.route('/test', endpoint='t1')
def test():
    return 'test'

3.@app.route装饰器的参数

@app.route和app.add_url_rule参数:

    rule,   # URL规则
    view_func,   # 视图函数名称
    defaults = None,   # 默认值, 当URL中无参数,函数需要参数时,使用defaults = {'k': 'v'}为函数提供参数
    endpoint = None,   # 名称,用于反向生成URL,即: url_for('名称')
    methods = None,   # 允许的请求方式,如:["GET", "POST"]

    strict_slashes = None,   # 对URL最后的 / 符号是否严格要求,如:
        例如:
            @app.route('/index', strict_slashes=False)  #访问 http:// www.xx.com/index/ 或http://www.xx.com/index 均可
            @app.route('/index', strict_slashes=True)  #仅访问 http://www.xx.com/index

    indexredirect_to = None,  # 重定向到指定地址 如:
        例如:
            @app.route('/index/<int:nid>', redirect_to='/home/<nid>')
            或
            def func(adapter, nid):
                return "/home/888"
            @app.route('/index/<int:nid>', redirect_to=func)


    subdomain = None,  # 子域名访问,什么是子域名:主干域名是www.leeoo.com  admin.leeoo.com就是admin子域名
        例如:
            from flask import Flask, views, url_for

            app = Flask(import_name=__name__)
            # 配置服务器地址和端口
            app.config['SERVER_NAME'] = 'leeoo.com:5000'

            # 当访问admin.leeoo.com/时才会走这个路由
            @app.route("/", subdomain="admin")
            def static_index():
                """Flask supports static subdomains
                This is available at static.your-domain.tld"""
                return "static.your-domain.tld"

            # 动态子域名,访问user1.leeoo.com/dynamic,则相当于将'user1'作为参数传入username_index()视图函数
            @app.route("/dynamic", subdomain="<username>")
            def username_index(username):
                """Dynamic subdomains are also supported
                Try going to user1.your-domain.tld/dynamic"""
                return username + ".your-domain.tld"

            if __name__ == '__main__':
                app.run()

二、CBV

1.Flask中的CBV

在Flask中也可以使用类似Django的CBV。

from flask import views


class UserView(views.MethodView):
    def get(self, *args, **kwargs):
        return 'GET'

    def post(self, *args, **kwargs):
        return 'POST'


# CBV不能使用装饰器添加路由,只能使用app.add_url_rule(),注意as_view()的参数会被传递给view_func.__name__,然后会赋值给endpoint
app.add_url_rule('/user', None, UserView.as_view("userview"))

Flask的CBV和django的很类似。当用户的请求到达时,通过MethodView类的dispatch_request()方法,来反射到对应的get或post等视图函数。如下源码所示:

def dispatch_request(self, *args, **kwargs):
    # 用户请求类型request.method先转化为小写,然后看视图类中是否存在对应的方法
    meth = getattr(self, request.method.lower(), None)
    # If the request method is HEAD and we don't have a handler for it
    # retry with GET.
    if meth is None and request.method == "HEAD":
        meth = getattr(self, "get", None)
    # 如果meth为None,则说明用户请求类型没有对应的处理函数,报错
    assert meth is not None, "Unimplemented method %r" % request.method
    # 否则调用对应视图函数
    return meth(*args, **kwargs)

2.视图类的静态属性

from flask import views


# 实现一个自定义装饰器
def wrapper(func):
    def inner(*args, **kwargs):
        return func(*args, **kwargs)

    return inner


class UserView(views.MethodView):
    methods = ['GET']  # 限制支持的请求类型
    decorators = [wrapper, ]  # 在这里使用自定义装饰器,会自动批量添加到各个视图函数

    def get(self, *args, **kwargs):
        return 'GET'

    def post(self, *args, **kwargs):
        return 'POST'


app.add_url_rule('/user', None, UserView.as_view("userview"))

我们可以定义静态属性methods来限制该视图类接收的请求类型。可以定义decorators来批量的对类中的视图函数(get、post...函数)添加自定义装饰器(当然也可以自己手动给需要的视图函数添加)。

三、自定义支持正则的动态路由

我们在使用Flask的动态路由时,Flask默认为我们提供了几种数据类型。参考:[Python自学] Flask框架 (1) (Flask介绍、配置、Session、路由、请求和响应、Jinjia2模板语言、视图装饰器)

1.Flask默认支持的动态参数数据类型

我们可以在app.url_map.converters中看到Flask默认支持的数据类型:

#: the default converter mapping for the map.
DEFAULT_CONVERTERS = {
    "default": UnicodeConverter,
    "string": UnicodeConverter,
    "any": AnyConverter,
    "path": PathConverter,
    "int": IntegerConverter,
    "float": FloatConverter,
    "uuid": UUIDConverter,
}

该字典中,key为支持的类型名,value即为提供转换功能的转换器。

如果我们想要Flask的动态路由支持正则表达式,则需要自己定义一个正则转换器,并添加到app.url_map.converters中。

2.自定义正则转换器

from werkzeug.routing import BaseConverter


class RegexConverter(BaseConverter):
    """
    自定义URL匹配正则表达式
    """

    def __init__(self, map, regex):
        super(RegexConverter, self).__init__(map)
        self.regex = regex

    def to_python(self, value):
        """
        路由匹配时,匹配成功后传递给视图函数中参数的值
        :param value:
        :return:
        """
        return value

    def to_url(self, value):
        """
        使用url_for反向生成URL时,传递的参数经过该方法处理,返回的值用于生成URL中的参数
        :param value:
        :return:
        """
        val = super(RegexConverter, self).to_url(value)
        return val


# 添加到flask中
app.url_map.converters['regex'] = RegexConverter


@app.route('/index/<regex("\d+-\d+"):nid>')
def index(nid):
    print(url_for('index', nid='888-999'))
    return 'Index'

这样,我们就可以在动态路由中,使用正则表达式了。但是注意,这里传递进来的nid是字符串格式(我们也可以在RegexConverter类的to_python中对其进行处理)。

四、Flask请求处理流程

1.启动服务器

我们知道,最简单的Flask代码如下:

from flask import Flask

app = Flask(__name__)


if __name__ == '__main__':
    app.run()

Flask是建立在 werkzeug 这个WSGI服务器上的。

当app.run()运行Flask的时候,底层的werkzeug会开始监听指定的端口,准备接受用户请求。

我们可以在Flask类中的run方法找到如下代码:

try:
     run_simple(host, port, self, **options)

这个run_simple的第三个参数就是满足WSGI协议调用的方法。这里传入了self,这个self就是代指app对象自己。所以当服务器接收到请求时,会调用app(),其实就是调用app中的__call__()方法。

2.接收请求

我们看Flask类中__call__的源代码:

def __call__(self, environ, start_response):
    """The WSGI server calls the Flask application object as the
    WSGI application. This calls :meth:`wsgi_app` which can be
    wrapped to applying middleware."""
    return self.wsgi_app(environ, start_response)

这个__call__方法是在请求到达的时候才会被调用。而被调用时参数是由werkzeug服务器传入的,其中environ是请求相关的信息,而start_response是服务器提供给Flask框架用来封装响应头的函数引用。

可以参考:[Python之路] 实现简易HTTP服务器与MINI WEB框架(利用WSGI实现服务器与框架解耦)中WSGI原理。

3.处理请求和Session

所有Flask框架的源码都是从wsgi_app()这个函数开始的:

def wsgi_app(self, environ, start_response):
    # 1.ctx = RequestContext(self, environ)
    #   ctx.request = Request(environ)
    #   ctx.session = None
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            # 2.将ctx对象加入上下文管理,
            # 3.执行 SecureCookieSessioninterface.open_session,去cookie中获取session值,并给ctx.session重新赋值
            ctx.push()
            # 4.这里调用视图函数
            #   app.dispatch_request()调用视图函数
            # 5.视图函数执行完毕后,调用app.finalize_request(),进行善后工作
            #   在finalize_request中调用process_response,将用户新设置的session加密序列化后写入response中,这里调用的是SecureCookieSessioninterface.save_session
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        except:  # noqa: B001
            error = sys.exc_info()[1]
            raise
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        # 5.视图函数处理完请求,返回了响应之后,清空该次请求在上下文中的数据
        ctx.auto_pop(error)

ctx是app.request_context(environ)中返回的RequestContext实例,并将self和environ传递进去:

def request_context(self, environ):
    # 实例化RequestContext,传入app和environ
    return RequestContext(self, environ)

再看RequestContext类的构造函数:

def __init__(self, app, environ, request=None, session=None):
    # Flask实例app
    self.app = app
    # 这里request我们没有传入,一定为空
    if request is None:
        # request_class是Request类,所以request是Request类的一个实例,并封装了environ(得到我们使用的request)
        request = app.request_class(environ)
    self.request = request
    self.url_adapter = None
    try:
        self.url_adapter = app.create_url_adapter(self.request)
    except HTTPException as e:
        self.request.routing_exception = e
    # 闪现初始化为None
    self.flashes = None
    # session初始化为None
    self.session = session
    self._implicit_app_ctx_stack = []
    self.preserved = False
    self._preserved_exc = None
    self._after_request_functions = []

4.请求处理流程图

五、蓝图

1.修改Flask项目目录结构

在划分目录之前,我们的static目录、templates目录以及写视图函数的app.py文件都位于项目根目录下。

我们对目录进行以下修改:

 

 

1)项目根目录下创建项目同名目录my_flask目录,以及manage.py文件。以后我们的Flask项目就从manage.py启动,而不是以前的app.py

2)在创建好的my_flask目录下,创建static、templates、views目录,以及__init__.py文件。其中static存放静态文件,templates存放模板、view存放视图函数py文件,__init__.py中会创建app实例(Flask对象)。

2.实现my_flask目录下的__init__.py

from flask import Flask


# 封装一个函数,用来生成Flask实例,并返回
def create_app():
    app = Flask(__name__)
    return app

3.实现根目录下的manage.py

# 从my_flask包中导入create_app函数
from my_flask import create_app

# 创建app实例
app = create_app()

if __name__ == '__main__':
    # 运行Flask
    app.run()

manage.py作为整个Flask项目的入口。

4.视图函数分类

我们的视图函数都应该放在views目录下,可以对其进行分门别类,例如登录类的视图函数在login.py中实现,用户信息类的视图函数在user.py中实现。

 

 

login.py和user.py的实现:

# login.py文件

# 导入蓝图模块
from flask import Blueprint, render_template

# 定义一个蓝图对象
lg = Blueprint('lg', __name__)


# 使用蓝图来调用装饰器(而不是使用app)
@lg.route('/login')
def login():
    return render_template('login.html', msg="这是Login页面")
# user.py文件

from flask import Blueprint, render_template

us = Blueprint('us', __name__, template_folder='./templates')


@us.route('/user_list')
def user_list():
    return render_template('user_list.html', msg="这里是USER LIST")

我们通过在每个视图实现文件中都定义一个蓝图实例,然后利用蓝图实例来调用route装饰器给视图函数添加路由映射。

 

但是,使用了蓝图实例后,还需要将蓝图和app实例建立关系

# my_flask/__init__.py文件

from flask import Flask
from .views.login import lg
from .views.user import us


def create_app():
    app = Flask(__name__)
    app.register_blueprint(lg)
    app.register_blueprint(us)
    return app

这样,我们就成功的利用蓝图实现了视图函数的分类,让我们的项目更清晰明了。

5.蓝图指定模板目录

我们看到,在4.节的user.py中,蓝图的参数多了一个template_folder='./templates'。

这个参数的意思是,该蓝图可以单独指定自己的视图函数中的render_template使用的模板在哪里查找

但是,需要注意的是,即使设置了template_folder参数,render_template也会先去全局的templates目录中查找,如果没有对应的模板,才会去蓝图中指定的目录中寻找

我们调整目录结构:

位于项目同名目录my_flask下的templates为全局模板目录。这个目录是所有视图函数默认优先查找的目录

而views中的templates目录,是我们另外任意创建的一个目录(目录名不一定叫templates)。这个目录可以被views中py文件中定义的蓝图所指定。

如下代码所示:

# user.py

from flask import Blueprint, render_template

us = Blueprint('us', __name__, template_folder='./templates')


@us.route('/user_list')
def user_list():
    return render_template('user_list.html', msg="这里是USER LIST")

user.py中定义蓝图的时候,指定了views/templates目录。

当我们访问/user_list页面的时候,render_template函数会先去全局的templates目录中查找user_list.html模板。结果为:

 而当我们删除全局templates中的user_list.html文件,只留下views/templates中的user_list.html文件,结果变为:

 

优先级总结:全局模板目录 > 蓝图实例化指定的模板目录

6.蓝图路由前缀

在创建蓝图实例时,可以为其相关的所有路由设置一个前缀:

us = Blueprint('us', __name__, template_folder='./templates',url_prefix='/user')


@us.route('/user_list')
def user_list():
    return render_template('user_list.html', msg="这里是USER LIST")

这里我们使用url_prefix参数设置了一个前缀"/user"。

以后我们要访问/user_list页面的时候,就需要在url中多加一层"/user":

# 原本的访问URL
http://127.0.0.1:5000/user_list

# 加了前缀后的访问URL
http://127.0.0.1:5000/user/user_list

只要使用该蓝图实例来调用装饰器的路由映射,都需要加上该前缀。这种功能有点类似于django中的路由分发,但是又更加的灵活。

 

##

posted @ 2020-02-27 15:41  风间悠香  阅读(723)  评论(0编辑  收藏  举报