Django请求到响应的流程

之前在Flask中一次请求到响应的流程 一文中介绍了Flask的处理流程,其实Django的基本思想和Flask差不多。都是遵从WSGI规范,WSGI server调用Django或者Flask的application,进行后续处理。只是Django返回的是一个可调用的实例,Flask返回一个函数。此外,在URL路由,信号机制等几个方面,两者也有很大的不同。下面简单介绍Django的流程

  1. 用户浏览器一个url,发起一个请求,WSGI server收到请求,调用application。

在Web应用启动后,会生成一个 WSGIHandler 实例(根据setting中的WSGI_APPLICATION = 'dailyblog.wsgi.application' 调用函数),每次请求响应都用这个实例。 settings 里设置了 WSGI application。 返回实例代码:

from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application():
    """
    The public interface to Django's WSGI support. Should return a WSGI
    callable.

    Allows us to avoid making django.core.handlers.WSGIHandler public API, in
    case the internal WSGI implementation changes or moves in the future.
    """
    django.setup()
    return WSGIHandler()

看一下WSGIHandler的源代码:

class WSGIHandler(base.BaseHandler):
    initLock = Lock()
    request_class = WSGIRequest #创建 WSGIRequest

    def __call__(self, environ, start_response):
        # Set up middleware if needed. We couldn't do this earlier, because
        # settings weren't available.
        if self._request_middleware is None:
            with self.initLock:
                try:
                    # Check that middleware is still uninitialized.
                    if self._request_middleware is None:
                        self.load_middleware()
                except:
                    # Unload whatever middleware we got
                    self._request_middleware = None
                    raise

        set_script_prefix(get_script_name(environ))
        signals.request_started.send(sender=self.__class__, environ=environ)
        try:
            request = self.request_class(environ)
        except UnicodeDecodeError:
            logger.warning('Bad Request (UnicodeDecodeError)',
                exc_info=sys.exc_info(),
                extra={
                    'status_code': 400,
                }
            )
            response = http.HttpResponseBadRequest()
        else:
            response = self.get_response(request)

        response._handler_class = self.__class__

        status = '%s %s' % (response.status_code, response.reason_phrase)
        response_headers = [(str(k), str(v)) for k, v in response.items()]
        for c in response.cookies.values():
            response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
        start_response(force_str(status), response_headers)
        if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
            response = environ['wsgi.file_wrapper'](response.file_to_stream)
        return respons

 

2、如果是第一次请求,即加载各类中间件。

WSGIHandler继承自BaseHander。这个处理器会导入项目中的 settings模块、导入 自定义例外类,最后调用自己的 load_middleware 方法,加载所有列在 MIDDLEWARE_CLASSES 中的 middleware 类并且内省它们。

BaseHander部分源代码:

class BaseHandler(object):
    # Changes that are always applied to a response (in this order).
    response_fixes = [
        http.conditional_content_removal,
    ]

    def __init__(self):
        self._request_middleware = None
        self._view_middleware = None
        self._template_response_middleware = None
        self._response_middleware = None
        self._exception_middleware = None

请注意,只有第一次请求时会调用 load_middleware,以后不再调用了。
其中,request_middleware、view_middleware 是顺序,template_response_middleware、response_middleware、exception_middleware 是逆序。

中间件的类是在settings中定义,如下:

MIDDLEWARE_CLASSES = [
    #middleware, a framework of hooks into Django’s request/response processing
    'django.middleware.gzip.GZipMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',

 

每一个 middleware 类可以包括请求响应过程的四个阶段:request,view,response 和 exception。对应的方法:process_request,process_view, process_response 和 process_exception。我们在middleware中间件中定义其中的方法。

我之前也写过一篇文章,专门介绍Django的中间件,以及自定义中间件,具体可见:Django中间件

3、构造WSGIRequest。
WSGIHandler 处理器准备工作已经完成,随后它给调度程序发送一个信号 request_started(这个和Flask中的request_started信号差不多),然后根据入 environ 构造 WSGIRequest 对象,它的父类是HttpRequest。

4、 处理Middleware的request中间件
创建WSGIRequest后,会根据request执行get_response方法,该方法处理 _request_middleware 实例变量并调用其中的每一个方法,传入 HttpRequest 的实例作为参数,即请求到达Request Middlewares,中间件对request做一些预处理,如果中间件返回response,会直接响应请求。所以我们在自定义response时,一般情况下,是不要返回对象。

5、 URLConf通过urls.py文件和请求的URL找到相应的视图函数或视图类

第4步谈到了get_response方法,该方法是通过requests获取响应内容。当用户输入一个URL,服务器如何根据URL来获得对应的视图呢?这里用到了Django的URL 调度器-URL dispatcher。

 def get_response(self, request):
        "Returns an HttpResponse object for the given HttpRequest"

        # Setup default url resolver for this thread, this code is outside
        # the try/except so we don't get a spurious "unbound local
        # variable" exception in the event an exception is raised before
        # resolver is set
        urlconf = settings.ROOT_URLCONF
        urlresolvers.set_urlconf(urlconf)
        resolver = urlresolvers.get_resolver(urlconf)
        # Use a flag to check if the response was rendered to prevent
        # multiple renderings or to force rendering if necessary.
        response_is_rendered = False

        resolver_match = resolver.resolve(request.path_info)
        callback, callback_args, callback_kwargs = resolver_match
        request.resolver_match = resolver_match

看上面的代码:

  1. Django会根据设置的ROOT_URLCONF确定要加在的URL模块;
  2. 根据URL,创建django.core.urlresolvers.RegexURLResolver 的一个实例;
  3. 该实例会根据request的路径信息request.path_info在urlpatterns搜索对应的URL,一旦匹配上。立即停止;
  4. 当URL匹配上,Django会调用基于类的视图或者视图函数。视图函数会获得一个HttpRequest类,参数等等。

上面简述了匹配URL,并映射到对应的视图上的过程。现在再看一下实现原理。

我们一般定义URL,格式如下:

from django.conf.urls import url,include


urlpatterns = [

     url('^/','blog.views.test',name='china'),
     ....

]

url实际上是返回一个类RegexURLPattern,里面参数很重要。此外,我们URL是一个正则表达式,我写的这个比较简单。比如我可以写命名组:

  url(r'^account/(?P<user_id>[0-9])/$', views.year_archive)

上面请注意,虽然正则写了数字,但是传递给视图的参数都是字符串。

 

class RegexURLPattern(LocaleRegexProvider):
    def __init__(self, regex, callback, default_args=None, name=None):
        LocaleRegexProvider.__init__(self, regex)
        # callback is either a string like 'foo.views.news.stories.story_detail'
        # which represents the path to a module and a view function name, or a
        # callable object (view).
        if callable(callback):
            self._callback = callback
        else:
            self._callback = None
            self._callback_str = callback
        self.default_args = default_args or {}
        self.name = name

    def resolve(self, path):
        match = self.regex.search(path)
        if match:
            # If there are any named groups, use those as kwargs, ignoring
            # non-named groups. Otherwise, pass all non-named arguments as
            # positional arguments.
            kwargs = match.groupdict()
            if kwargs:
                args = ()
            else:
                args = match.groups()
            # In both cases, pass any extra_kwargs as **kwargs.
            kwargs.update(self.default_args)

            return ResolverMatch(self.callback, args, kwargs, self.name)

       上述代码已经说了,callback可以是一个视图函数,或者一个可调用的视图类。

  django在匹配URL时候,实际上就是在urlpatterns列表中搜索合适的urlpattern:

     

def resolve(self, path):
       
        match = self.regex.search(path)
        if match:
            new_path = path[match.end():]
            for pattern in self.url_patterns:
                try:
                    sub_match = pattern.resolve(new_path)
              
       
                    if sub_match:
                        # Merge captured arguments in match with submatch
                        sub_match_dict = dict(match.groupdict(), **self.default_kwargs)
                        sub_match_dict.update(sub_match.kwargs)

                        # If there are *any* named groups, ignore all non-named groups.
                        # Otherwise, pass all non-named arguments as positional arguments.
                        sub_match_args = sub_match.args
                        if not sub_match_dict:
                            sub_match_args = match.groups() + sub_match.args

                        return ResolverMatch(
                            sub_match.func,
                            sub_match_args,
                            sub_match_dict,
                            sub_match.url_name,
                            [self.app_name] + sub_match.app_names,
                            [self.namespace] + sub_match.namespaces
                        )

 匹配成功后,我们获得了callback, callback_args, callback_kwargs。

URLresolver 遵循一个相当简单的模式:对于在 URL 配置文件中根据 ROOT_URLCONF 的配置产生的每一个在 urlpatterns 列表中的条目,它会检查请 求的 URL 路径是否与这个条目的正则表达式相匹配,如果是的话,有两种选择:

  1. 如果这个条目有一个可以调用的 include,resolver 截取匹配的 URL,转 到 include 指定的 URL 配置文件并开始遍历其中 urlpatterns 列表中的 每一个条目。根据你 URL 的深度和模块性,这可能重复好几次。

  2. 否则,resolver 返回三个条目:匹配的条目指定的 view function;一个 从 URL 得到的未命名匹配组(被用来作为 view 的位置参数);一个关键 字参数字典,它由从 URL 得到的任意命名匹配组和从 URLConf 中得到的任 意其它关键字参数组合而成。

        注意这一过程会在匹配到第一个指定了 view 的条目时停止,因此最好让你的 URL 配置从复杂的正则过渡到简单的正则,这样能确保 resolver 不会首先匹配 到简单的那一个而返回错误的 view function。

        如果没有找到匹配的条目,resolver 会产生 django.core.urlresolvers.Resolver404 异常,它是 django.http.Http404 例 外的子类。后面我们会知道它是如何处理的。

6、 开始调用View中相应的视图函数

完成第4个步骤开始执行对应视图函数或者基于类的视图。

7、View中的方法可以选择性的通过Models访问底层的数据

8、如果需要,Views可以创建一个特殊的Context,Context被传给Template用来生成页面

9、Template渲染输出

10、渲染后的输出被返回到View

11、HTTPResponse被发送到Response Middlewares

12、Response Middlewares对response进行特定的处理,然后返回一个新的response

13、Response返回呈现给用户

14、一旦 middleware完成了最后环节,处理器将发送一个信号 request_finished,订阅这个信号的事件会清空并释放任何使用中的资源。比如,Django 的 request_finished 监听者会关闭所有数据库连接。

15、所有流程至此已经全部完成。

 

异常情况:

如果 view 函数,或者其中的什么东西,发生了异常,那么 get_response将遍历它的 _exception_middleware 实例变量并呼叫那里的每个方法,传入 HttpResponse 和这个 exception 作为参数。如果顺利,这些方法中的一个会实例化一个 HttpResponse 并返回它。

这时候有可能还是没有得到一个 HttpResponse,这可能有几个原因:

  1. view 可能没有返回值。
  2. view 可能产生了例外但没有一个 middleware 能处理它。
  3. 一个 middleware 方法试图处理一个例外时自己又产生了一个新的例外。

处理器会做些相应的异常处理,如404,403,500等等,具体可以看上面的源代码。

 

 

综述:

     Django遵循了MVC模式,但是,其中的C并不是由开发者来做,而是Django系统自己做,即URLRouter负责。开发者更侧重M,V,和T(Template),因此也称作MTV。中间件是Django很重要的特点,在响应过程中,Django的中间件可以帮我们完成一些重复性工作。此外,Django的信号机制也实现了各个模块间的解耦合,让架构更加清晰,也方便开发。

      在整个响应请求阶段,从开发者的角度,我们应该更加关心M,V,T的编写,URL以及中间件的定义。当然,如果有特殊需要,也可以自定义信号的订阅者。

     Django的ORM框架节省了开发时间,Django的模板我认为也比Jinja2功能更全,也更加友好。

     

--------EOF---------
微信分享/微信扫码阅读