Python / 23 TemplateView 类视图详解

Django 的 TemplateView 类视图详解

接下来的两个小节,主要介绍 Django 中的几个常用的视图类,我们统一按照这样的方式进行讲解:首先使用该视图类,完成一个简单的例子,熟悉该类的使用;接下来深入源码分析,完整梳理该视图类的实现过程,顺带梳理 Django 中和该类相关的源码 。

1. TemplateView 类介绍和使用

TemplateView 视图类是用来渲染给定的模板文件,其上下文字典包含从 URL 中捕获的参数。首先来看看最简单的模板渲染示例:

准备模板文件,放到 template 目录,在 settings.py 中需要配置好模板 (TEMPLATES) 相关的参数:

[root@server first_django_app]# cat templates/test.html 
<p>{{ content }}</p>
<div>{{ spyinx.age }}</div>

准备好类视图,处理相关 HTTP 请求,这里使用今天要学习的 TemplateView 类:

class TestTemplateView(TemplateView):
    template_name = 'test.html'
    
    @csrf_exempt
    def dispatch(self, request, *args, **kwargs):
        return super(TestTemplateView, self).dispatch(request, *args, **kwargs)

配置相应的 URLConf:

context_data = {'content':'正文1', 'spyinx':{'age': 29}}

urlpatterns = [
    path('test_template_view/',
         views.TestTemplateView.as_view(extra_context=context_data), 
         name='test_template_View')
]

启动 first_django_app 工程,然后使用 curl 命令简单测试,发送 GET 请求:

# 启动first_django_app工程,监听8888端口
(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 16, 2020 - 12:27:49
Django version 2.2.11, using settings 'first_django_app.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C.

# 打开xshell另一个窗口,使用curl发送get请求
[root@server ~]# curl http://127.0.0.1:8888/hello/test_template_view/
<p>正文1</p>
<div>29</div>
# 这个报405错误,不支持POST请求
[root@server ~]# curl -XPOST http://127.0.0.1:8888/hello/test_template_view/
[root@server ~]#

可以看到,我们只是指定了 template_name 属性,连对应的请求函数都没写 (写的 dispatch() 函数只是为了能执行POST请求,避免 csrf 报错),只是继承一下这个视图类 (TemplateView) 能处理 GET 请求,并能返回渲染的模板,对一些情况来说是很方便的,节省了一些代码。注意的是,其他请求方式不行,因为 TemplateView 内部只实现了 get() 方法,所以只能处理 GET 请求。

上面的 context_data 是自定义的,现在我们来从数据库中获取数据,动态填充模板内容。同样是涉及到模板视图,所以可以通过 TemplateView 类来实现。

首先准备新的模板文件 test.html。这个模板页面使用表格分页显示会员数据,查询的数据表是我们前面多次实验的 member 表。

<html>
<head>
<style type="text/css">
    .page{
       margin-top: 10px;
       font-size: 14px;
    }

    .member-table {
       width: 50%;
       text-align: center;
    }
</style>
</head>
<body>

<p>会员信息-第{{ current_page }}页, 每页{{ page_size }}条, 总共{{ sum }}条</p>
<div>
<table border="1" class="member-table">
  <thead>
  <tr>
    <th>姓名</th>
    <th>年龄</th>
    <th>性别</th>
    <th>职业</th>
    <th>所在城市</th>
  </tr>
  </thead>
  <tbody>
  {% for member in members %}
  <tr>
    <td>member.name</td>
    <td>member.age</td>
    {% if member.sex == 0 %}
    <td></td>
    {% else %} 
    <td></td>
    {% endif %}
    <td>member.occupation</td>
    <td>member.city</td>
  </tr>
  {% endfor %}
   </tbody>
</table>
<div >
<div class="page">
</div>
</div>
</div>

</body>

</html>

修改上面的视图函数,分页查询 Member 表中的数据:

class TestTemplateView(TemplateView):
    template_name = 'test.html'
    def get(self, request, *args, **kwargs):
        params = request.GET
        page = int(params.get('page', 1))
        size = int(params.get('size', 5))
        data = {}
        data['sum'] = Member.objects.all().count()
        members = Member.objects.all()[(page - 1) * size:page * size]
        data['current_page'] = page
        data['page_size'] = size
        data['members'] = members
        return self.render_to_response(context=data)

这里我们使用了前面学习的 Django ORM 模型,获取 member 表中的数据,然后使用 TemplateView 中的模板渲染方法 render_to_response() 返回渲染后的 HTML 文本给到客户端。

测试结果如下,以下两张图片是设置了不同的查询参数(当前页码和页大小),可以看到查询参数是起了效果的:

图片描述

图片描述

2. TemplateView 类深入分析

2.1 TemplateResponse 和 SimpleTemplateResponse

很早之前,我们介绍过 HttpResponse,它用于生成 HTTP 请求的相应,返回的内容由 content 属性确定,主要是用于提供静态内容显示。TemplateResponse 对象则不同,它允许装饰器或中间件在通过视图构造响应之后修改响应内容。TemplateResponse 对象保留视图提供的、用于计算响应内容的模板和上下文数据,直到最后需要时才计算相应内容并返回响应

SimpleTemplateResponse 对象是 TemplateResponse 的父类,两者功能和使用基本类似,几乎是一致的。

# 源码位置: django/template/response.py
class TemplateResponse(SimpleTemplateResponse):
    rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request']

    def __init__(self, request, template, context=None, content_type=None,
                 status=None, charset=None, using=None):
        super().__init__(template, context, content_type, status, charset, using)
        self._request = request

SimpleTemplateResponse 是继承自 HttpResponse 对象,并做了诸多扩展。它的重要属性和方法如下:

重要属性

  • template_name:模板文件名;
  • context_data : 上下文字典数据;
  • rendered_content:指使用当前的模板和上下文字段数据已渲染的响应内容。一个作为属性的方法,调用该属性时启动渲染过程;
  • is_rendered: 布尔类型,判断响应内容是否已经被渲染。

重要方法

  • __init__(template, context=None, content_type=None, status=None, charset=None, using=None):类初始化函数,各参数的含义与 HttpResponse 相同;

  • resolve_context(context):预处理会被用于模板渲染的上下文数据 ;

  • resolve_template(template):接收(如由 get_template() 返回的) backend-dependent 的模板对象、模板名字、或者多个模板名字组成的列表。返回 backend-dependent 的模板对象实例,后面用于渲染;

  • add_post_render_callback():添加渲染完成后的回调函数,如果该方法运行时渲染已完成,回调函数会被立即调用;

  • render():设置 response.content 的结果为 SimpleTemplateResponse.rendered_content 的值,执行所有渲染后的回调函数,返回所有响应对象。render() 只会在第一次调用时起作用。在随后的调用中,它将返回从第一个调用获得的结果。

实验部分:

我们来使用 TemplateResponse 来完成一个简单的案例。同样是在 first_django_app 工程中,准备的代码内容参考如下,分别是模板文件、视图文件以及 URLConf 配置文件。

# 模板文件: template/test.html
<p>{{ content }}</p>
<div>{{ spyinx.age }}</div>

# URLConf配置文件: hello_app/urls.py
urlpatterns = [
    path('test-cbv/', views.TestView.as_view(), name="test-cbv")
]

# 视图文件: hello_app/views.py
def my_render_callback(response):
    # Do content-sensitive processing
    print('执行渲染完成后的回调函数,渲染内容:\n{}\n是否完成渲染:{}'.format(response.rendered_content, response.is_rendered))

class TestView(View):

    def get(self, request, *args, **kwargs):
        response = TemplateResponse(request, 'test.html', context={'content': '正文1', 'spyinx':{'age': 29}})
        response.add_post_render_callback(my_render_callback)
        return response

我们在云主机上使用 curl 命令发送 HTTP 请求,观察结果:

# 使用runserver命令启动first_django_app工程
...

# 打开另一个xshell窗口,使用curl命令发送请求结果
[root@server ~]# curl http://127.0.0.1:8888/hello/test-cbv/
<p>正文1</p>
<div>29</div>

# 回到上一个窗口,查看打印结果
(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 16, 2020 - 07:10:18
Django version 2.2.11, using settings 'first_django_app.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C.
执行渲染完成后的回调函数,渲染内容:
<p>正文1</p>
<div>29</div>

是否完成渲染:True
[16/Apr/2020 07:10:38] "GET /hello/test-cbv/ HTTP/1.1" 200 29

2.2 ContextMixin 和 TemplateResponseMixin

接下来,我们查看下 Django 提供的两个 mixin:ContextMixin 和 TemplateResponseMixin。其内容比较简单,源码如下:

# 源码位置:django/views/generic/base.py

class ContextMixin:
    """
    A default context mixin that passes the keyword arguments received by
    get_context_data() as the template context.
    """
    extra_context = None

    def get_context_data(self, **kwargs):
        kwargs.setdefault('view', self)
        if self.extra_context is not None:
            kwargs.update(self.extra_context)
        return kwargs
    
class TemplateResponseMixin:
    """A mixin that can be used to render a template."""
    template_name = None
    template_engine = None
    response_class = TemplateResponse
    content_type = None

    def render_to_response(self, context, **response_kwargs):
        """
        Return a response, using the `response_class` for this view, with a
        template rendered with the given context.

        Pass response_kwargs to the constructor of the response class.
        """
        response_kwargs.setdefault('content_type', self.content_type)
        return self.response_class(
            request=self.request,
            template=self.get_template_names(),
            context=context,
            using=self.template_engine,
            **response_kwargs
        )

    def get_template_names(self):
        """
        Return a list of template names to be used for the request. Must return
        a list. May not be called if render_to_response() is overridden.
        """
        if self.template_name is None:
            raise ImproperlyConfigured(
                "TemplateResponseMixin requires either a definition of "
                "'template_name' or an implementation of 'get_template_names()'")
        else:
            return [self.template_name]

ContextMixin 比较简单,只提供了一个属性 extra_context 和一个方法 get_context_data(),它主要的功能是根据额外提供的参数,组成新的上下文字典,调用 get_context_data() 方法即可实现。

TemplateResponseMixin 是用来渲染模板的,它的属性与方法如下:

属性

  • template_name:模板文件名;
  • template_engine: 模板引擎;
  • response_class: 返回的 Response,默认是 TemplateResponse;
  • content_type:返回客户端的数据类型。

方法

  • render_to_response():默认直接调用 TemplateResponse(),完成模板渲染后返回响应给客户端;
  • get_template_names():获取模板名称,返回列表的形式;如果没有设置 template_name 属性值,则会报错。因此对于继承该 mixin 对象的子类,必须要设置 template_name 属性值。

Mixin 的特点就是功能简单,它们可以混合加到其他类中,那么其他类就具备这些 Mixin 的功能,组合得到一个更高级的类。同样我们在前一小节实验的基础上改造下 views.py 中的内容,如下:

class TestView(TemplateResponseMixin, View):
    template_name = 'test.html'
    def get(self, request, *args, **kwargs):
        return self.render_to_response(context={'content': '正文1', 'spyinx': {'age': 29}})

这里我们额外继承 TemplateResponseMixin 类。首先需要添加 template_name 属性,指定要渲染的模板文件,然后调用从 TemplateResponseMixin 中继承过来的 render_to_response() 方法并返回。从源码角度来看,这个视图实现的功能和上一个小节中直接返回 TemplateResponse 实例是一样的。

# 启动 first_django_app 工程
...

# 打开xshell另一个窗口,发送http请求
[root@server first_django_app]# curl http://127.0.0.1:8888/hello/test-cbv/
<p>正文1</p>
<div>29</div>

2.3 TemplateView 类

介绍完上面那些基础的类以及 Mixin 后,终于可以看 TemplateView 的代码了,我们发现它其实就是 View 类中混入了 ContextMixin 和 TemplateResponseMixin,此外还多了一个 get 函数,对应着 get 请求。

# 源码路径 django/views/generic/base.py

class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    Render a template. Pass keyword arguments from the URLconf to the context.
    """
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

就是这个 get 请求方法让我们前面第一个实验中,只需要写对应的模板文件名,另外传递 extra_context 就可以渲染模板并返回。注意,这个 extra_context 属性是继承自 ContextMixin 对象,它的赋值过程是走的 View 类。现在可以通过代码分析一下这个复制过程,加深印象。

首先看 as_view() 函数传参部分:

context_data = {'content':'正文1', 'spyinx':{'age': 29}}

urlpatterns = [
    path('test_template_view/',
         views.TestTemplateView.as_view(extra_context=context_data), 
         name='test_template_View')
]

as_view() 方法的源码如下。传进来的参数在 initkwargs 中,最后在 View 类中使用该字典参数初始化实例。

# 源码位置: django/views/generic/base.py

    @classonlymethod
    def as_view(cls, **initkwargs):
        # ...
        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            # ...
            
        # ...
        return view

最后来看 View 的初始化函数。可以看到这些初始化的参数会作为类的属性进赋值,使用的是 setattr() 方法,这样我们在 as_view() 方法中传递的 extra_context 参数就被成功赋值到 extra_context 属性上了。

class View:
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    # ...

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in kwargs.items():
            setattr(self, key, value)

其实通过了解这些源代码后,我们能很好得理解前面使用 TemplateView 类做的一些测试结果。目前来看,我们接触到的这部分源代码还是比较简单易懂的,官方网站对 Django 中出现的许多常用类都做了详细的介绍,我们只需要不停的学习源代码、参考官方的文档、坚持不懈,一定能达到精通 Django 的目的。

3. 小结

本小节中,我们先介绍了 TemplateView 的几种常见用法。接下来开始深入 Django 源码,分析 TemplateView 继承的两个 mixin,最后通过整个源码分析和测试,对于前面 TemplateView 的用法会理解的更加透彻。