zoukankan      html  css  js  c++  java
  • [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中的路由分发,但是又更加的灵活。

    ##

  • 相关阅读:
    Qt 3D教程(二)初步显示3D的内容
    linux关于ftp查看不到文件列表的问题
    Mahout推荐算法API具体解释【一起学Mahout】
    GBK编码具体解析(附GBK码位分布图)
    HTML5实战与剖析之媒体元素(3、媒体元素的事件及方法)
    CentOS添加swap分区
    Transaction: atomicity, consistency, separability, persistence
    redis ins 调试
    jemalloc/jemalloc.h: No such file or directory
    MySQL表设计基础
  • 原文地址:https://www.cnblogs.com/leokale-zz/p/12372468.html
Copyright © 2011-2022 走看看