Django的视图可以分为:
def index(request):
class AboutView(TemplateView):
早期,人们在视图开发中发现了一些常见的习语和句式,也就是重复性代码和工作。于是引入了基于函数的视图来抽象这些模式,便于一般情况下的视图开发。
基于函数的视图的问题在于,尽管它们覆盖了简单的情况,但是除了一些简单的配置选项之外,没有办法扩展或定制它们,限制了它们在许多现实应用中的实用性。
基于类的视图与基于函数的视图的目标相同,都是想让视图开发更容易。但由于类视图可以使用Mixin等一些面向对象的方法和工具包,使得基于类的视图比基于函数的视图更具扩展性和灵活性。
类视图的优点:
两种视图可以实现同样的功能,本质上是一个东西,没有谁好谁坏之分,只是适用场景不同而已:
Django 提供了很多适用于各种应用场景的基本视图类,我们一般不从头自己写起,这些类视图都继承django.views.generic.base.View
类。比如RedirectView
用于 HTTP 重定向,TemplateView
用于渲染模板。
类视图有很多简单的用法,甚至不需要去views.py中编写代码,比如下面的例子:
from django.urls import path from django.views.generic import TemplateView urlpatterns = [ path('about/', TemplateView.as_view(template_name="about.html")), ]
重点关注:
TemplateView
类视图as_view()
方法template_name
参数更通用的使用方法是继承Django提供的各种视图类,所以上面的例子的一般性写法如下:
# some_app/views.py from django.views.generic import TemplateView class AboutView(TemplateView): template_name = "about.html"
由于类不是函数,需要在URLconf
中使用as_view()
这个类方法将类视图转换成函数形式的接口,后面还有详细介绍。
# urls.py from django.urls import path from some_app.views import AboutView urlpatterns = [ path('about/', AboutView.as_view()), ]
上面的AboutView视图不涉及模型的访问,比较简单。让我们看一个书籍列表视图的例子:
首先是路由:
from django.urls import path from books.views import BookListView urlpatterns = [ path('books/', BookListView.as_view()), ]
BookListView
视图:
from django.http import HttpResponse from django.views.generic import ListView from books.models import Book class BookListView(ListView): model = Book # 指定模型 def head(self, request, *args, **kwargs): last_book = self.get_queryset().latest('publication_date') response = HttpResponse() # RFC 1123 date format response['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT') return response
这个例子中,如果用户通过GET请求数据,那么将正常的返回响应数据(此处省略)。而如果通过HEAD请求,将使用我们写的head方法中的业务逻辑。
基于类的视图允许你使用不同的实例方法响应不同的HTTP 请求,而不是在单个视图函数里使用if/else
代码。
在函数视图里处理 GET
请求的代码像下面这样:
from django.http import HttpResponse def my_view(request): if request.method == 'GET': # <view logic> return HttpResponse('result')
而在类视图中,则通过不同过的实例方法来处理:
from django.http import HttpResponse from django.views import View class MyView(View): def get(self, request): # <view logic> return HttpResponse('result')
上面的继承关系非常重要,不能从头写一个新类,或者随便自己瞎继承一个类,否则你无法和Django系统勾连。
每个类视图都有一个as_view()
方法,用于在urlconf
中进行dispatch。这个方法会创建一个类视图的实例,并调用它的dispatch()
方法。dispatch方法会在类中查找类似get\post之类的方法,然后与请求request中的HTTP方法匹配。匹配上了就调用对应方法内的代码,匹配不上就弹出异常HttpResponseNotAllowed
,也就是不允许当前的请求类型。
# urls.py from django.urls import path from myapp.views import MyView urlpatterns = [ path('about/', MyView.as_view()), # 注意as_view要加括号进行调用 ]
as_view()
方法可以传递参数,例如:
urlpatterns = [ path('view/', MyView.as_view(size=42)), ]
每个MyView视图的实例都可以使用
self.size
接收传入的参数值42,但该参数必须已经在类中定义了。传递给视图的参数会在视图的每个实例之间共享。这意味着你不应使用列表、字典或任何其他可变对象作为视图的参数。如果你这样做并且共享对象被修改,则会导致一个请求对另外一个请求产生影响,这显然存在极大的风险,完全不可接受。
基于类的视图可以有类属性,当需要的时候,有两种方法来配置或设置它们。
第一种是继承父类,在子类中重写父类的属性,如下所示:
父类:
from django.http import HttpResponse from django.views import View class GreetingView(View): greeting = "Good Day" def get(self, request): return HttpResponse(self.greeting)
子类:
class MorningGreetingView(GreetingView): greeting = "Morning to ya"
注意其中的greeting类属性。
另一种就是简单粗暴的在URLconf路由条目中修改as_view()方法的参数。当然,参数名必须是存在的类属性,你不能随便瞎写!如下所示:
urlpatterns = [ path('about/', GreetingView.as_view(greeting="G'day")), ]
但是这种方式有很大的弊端,那就是虽然每次匹配上了url,你的类视图都会被实例化一次,但是你的URLs却是在被导入的时候才配置一次,即as_view()方法的参数的配置只有一次。也就是说这么做,就不能再改了!所以,不要偷懒,使用子类吧。
混入是一种多父类继承的形式,其基础理论知识请参考我的Python教程中多继承相关章节。
在编写子类的时候,一个父类负责类的主体结构、主要行为和主要属性,其它的功能都通过各种mixin父类提供。
mixin是跨多个类重用代码的一个很好的方法,但是它们会带来一些代价。你的代码散布在mixin中越多,阅读子类就越难,很难知道它到底在做什么。如果你在对具有多级继承树的子类进行分类,就更难以判断子类的方法到底继承的是哪个先祖,俗称‘家谱乱了’。
需要注意的是你的父类视图中只有一个类可以继承最顶级的View类,其它的必须以mixin方法混入。
一个用来处理表单的函数视图通常是下面这样的:
from django.http import HttpResponseRedirect from django.shortcuts import render from .forms import MyForm def myview(request): if request.method == "POST": form = MyForm(request.POST) if form.is_valid(): # <process form cleaned data> return HttpResponseRedirect('/success/') else: form = MyForm(initial={'key': 'value'}) return render(request, 'form_template.html', {'form': form})
而如果用类视图来实现,是这样的:
from django.http import HttpResponseRedirect from django.shortcuts import render from django.views import View from .forms import MyForm class MyFormView(View): form_class = MyForm initial = {'key': 'value'} template_name = 'form_template.html' def get(self, request, *args, **kwargs): form = self.form_class(initial=self.initial) return render(request, self.template_name, {'form': form}) def post(self, request, *args, **kwargs): form = self.form_class(request.POST) if form.is_valid(): # <process form cleaned data> return HttpResponseRedirect('/success/') return render(request, self.template_name, {'form': form})
看起来类视图好像比函数视图代码多了很多,复杂了很多,貌似没啥优点啊。但是如果你有多个类似的视图需要编写,那么你就可以发挥子类的继承和复写神操作了,分分钟整出个新的视图来。或者直接在URLConf中修改参数!或者两种操作同时使用!
其实,到现在你应该理解,类视图适用于大量重复性的视图编写工作,在简单的场景下,没几个视图需要编写,或者各个视图差别很大的情况时,还是函数视图更有效!所以,不要认为类视图是多么高大上的东西,人云亦云!
除了mixin,还可以使用装饰器扩展类视图。装饰器的工作方式取决于你是在创建子类还是使用as_view()。
用法一,在URLConf中直接装饰:
from django.contrib.auth.decorators import login_required, permission_required from django.views.generic import TemplateView from .views import VoteView urlpatterns = [ path('about/', login_required(TemplateView.as_view(template_name="secret.html"))), path('vote/', permission_required('polls.can_vote')(VoteView.as_view())), ]
用法二,在类视图中装饰指定的方法:
from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from django.views.generic import TemplateView class ProtectedView(TemplateView): template_name = 'secret.html' @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs)
注意:
method_decorator
这个方法将装饰器运用在类方法上。感觉很绕?其实就是说,我们有很多很多的装饰器,但其中有一些不能直接装饰dispatch这种类方法。那怎么办呢?套层壳!用method_decorator
装饰器包裹起来,假装成一个能用的。有时候,简单地用一下,可以写成下面的精简版:
@method_decorator(login_required, name='dispatch') class ProtectedView(TemplateView): template_name = 'secret.html'
有时候,可能你需要对一个对象应用多个装饰器,正常做法是:
@method_decorator(never_cache, name='dispatch') @method_decorator(login_required, name='dispatch') class ProtectedView(TemplateView): template_name = 'secret.html'
为了偷懒,我们可以这么做:
decorators = [never_cache, login_required] @method_decorator(decorators, name='dispatch') class ProtectedView(TemplateView): template_name = 'secret.html'
唯一需要注意地是装饰器是有先后顺序的。上面的例子中,never_cache
就要先于login_required
被调用。
最后,使用method_decorator
有时会导致TypeError
异常,因为参数传递的原因。
和同步视图类似,可以在类视图中,定义异步处理函数,也就是使用async def
和 await
:
import asyncio from django.http import HttpResponse from django.views import View class AsyncView(View): async def get(self, request, *args, **kwargs): # Perform io-blocking view logic using await, sleep for example. await asyncio.sleep(1) return HttpResponse("Hello async world!")
需要强调的是,在一个类视图中,要么全部是def
同步方法,要么全部是async def
异步方法。如果两者混用的花,会抛出ImproperlyConfigured
异常。
Django会自动探测是否为异步方法,并在异步上下文环境中运行它们。
位于django.views.generic.base
中的类View是Django所有视图类的基础和祖先。
其源码如下所示,阅读其中的as_view
方法可以帮助你理解类视图是如何工作的:
class View: """ 所有视图类的父类。仅实现了方法分发dispatch功能和简单的健全性检查。 """ # HTTP协议中所有的请求类型小写 http_method_names = [ "get", "post", "put", "patch", "delete", "head", "options", "trace", ] 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) @classproperty def view_is_async(cls): handlers = [ getattr(cls, method) for method in cls.http_method_names if (method != "options" and hasattr(cls, method)) ] if not handlers: return False is_async = iscoroutinefunction(handlers[0]) if not all(iscoroutinefunction(h) == is_async for h in handlers[1:]): raise ImproperlyConfigured( f"{cls.__qualname__} HTTP handlers must either be all sync or all " "async." ) return is_async @classonlymethod def as_view(cls, **initkwargs): """请求和响应过程的进入点""" # 首先处理一些额外的初始化关键字参数 for key in initkwargs: # 首先,不允许这些参数中出现HTTP请求的名字 if key in cls.http_method_names: raise TypeError( "The method name %s is not accepted as a keyword argument " "to %s()." % (key, cls.__name__) ) # 其次,只能接收类本身具有的属性参数 if not hasattr(cls, key): raise TypeError( "%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key) ) # 上面是参数的健康检查 # 核心则是定义了这个内部函数view,它负责调用dispatch方法,分发请求 def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *args, **kwargs) # 必须提供request作为第一位置参数 if not hasattr(self, "request"): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) return self.dispatch(request, *args, **kwargs) # 调用dispatch方法 # 下面是做一些信息的保存 view.view_class = cls view.view_initkwargs = initkwargs # __name__ and __qualname__ are intentionally left unchanged as # view_class should be used to robustly determine the name of the view # instead. view.__doc__ = cls.__doc__ view.__module__ = cls.__module__ view.__annotations__ = cls.dispatch.__annotations__ # Copy possible attributes set by decorators, e.g. @csrf_exempt, from # the dispatch method. view.__dict__.update(cls.dispatch.__dict__) # Mark the callback if the view class is async. if cls.view_is_async: markcoroutinefunction(view) return view # 核心是这个return。当调用as_view()的时候,实际上得到了一个view def setup(self, request, *args, **kwargs): """初始化所有视图方法的共享属性""" # 通常情况下,将head请求看作get请求 if hasattr(self, "get") and not hasattr(self, "head"): self.head = self.get # 保存参数 self.request = request self.args = args self.kwargs = kwargs def dispatch(self, request, *args, **kwargs): # 这个方法试图将请求分发到对应的处理函数。比如GET请求,分发到get函数。 # 如果HTTP的请求类型不合法,或者不在允许的请求类型中, # 会调用http_method_not_allowed方法返回一个HttpResponseNotAllowed响应。 # 首先将请求的类型小写,然后和前面定义的合法方法列表对比 if request.method.lower() in self.http_method_names: # 利用字符串映射内置方法getattr,找到对应的处理函数并赋值给handler handler = getattr( self, request.method.lower(), self.http_method_not_allowed ) else: # 如果是个不合法的请求类型,比如"ABC",拒绝请求 handler = self.http_method_not_allowed return handler(request, *args, **kwargs) # 通常情况下,调用handler并返回响应结果 def http_method_not_allowed(self, request, *args, **kwargs): logger.warning( "Method Not Allowed (%s): %s", request.method, request.path, extra={"status_code": 405, "request": request}, ) response = HttpResponseNotAllowed(self._allowed_methods()) if self.view_is_async: async def func(): return response return func() else: return response def options(self, request, *args, **kwargs): """Handle responding to requests for the OPTIONS HTTP verb.""" response = HttpResponse() response.headers["Allow"] = ", ".join(self._allowed_methods()) response.headers["Content-Length"] = "0" if self.view_is_async: async def func(): return response return func() else: return response def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)]
看了,反正都记不住。。。估计最后就学会课程中的那两个项目,就可以面试去了
打卡