Django 视图函数
Django 中的视图是 MTV 架构模式中的 V 层,主要处理客户端的请求并生成响应数据返回。它其实类似于 MVC 架构模式中的 C 层,用于处理项目的业务逻辑部分。Django 的视图函数就是 Django 项目中专门处理对应 path 的 python 函数,这也是整个项目开发中最核心的业务处理部分。当然,在 Django 中处理 path 路径的不只有视图函数(FBV),还有视图类(CBV)。
1. Django 视图中的 FBV 和 CBV
FBV 全称是 function base views, 即在视图中是使用函数来对应处理请求。如下示例:
# 在django中会对跨域请求做拦截处理,这里使用@csrf\_exempt注解表示不作处理,主要是针对POST请求
@csrf_exempt
def hello\_world(request, \*args, \*\*kwargs):
if request.method == 'GET':
return HttpResponse('Hello, get request', content_type="text/plain")
elif request.method == 'POST':
return HttpResponse('Hello, post request', content_type="text/plain")
return HttpResponse("Hello, world.", content_type="text/plain")
urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', hello_world),
]
注意: 视图函数可以接受 request 参数,用于存放浏览器传递过来的所有数据。它是 WSGIRequest 类的一个实例,而 WSGIRequest 类又继承自 HttpRequest 。我们通过 request 这个实例可以拿到客户端 http 请求的方法,请求路径、传递的参数以及上传的文件等等。
CBV 全称是 class base views 就是在视图中使用类处理请求。在 Django 中加入了 CBV 的模式,让我们用类去处理响应,这样做有以下几个好处:
- 可以使用面向对象的技术,比如 Mixin(多继承),这样可以有效复用增删改查数据库的代码;
- 在视图类中我们定义如 get 方法就能处理对应的 GET 请求,这样无需使用 if 再进行判断,一方面提高了代码的可读性,另一方面也规范了 web api 的接口设计。
示例代码:
from django.http import HttpResponse
from django.views import View
class HelloView(View):
def get(self, request, \*args, \*\*kwargs):
return HttpResponse('get\n')
def post(self, request, \*args, \*\*kwargs):
return HttpResponse('post\n')
def put(self, request, \*args, \*\*kwargs):
return HttpResponse('put\n')
def delete(self, request, \*args, \*\*kwargs):
return HttpResponse('delete\n')
# 注意,给CBV加上@csrf\_exempt注解,需要加到dispatch方法上,后续会详解介绍这个函数的作用
@csrf_exempt
def dispatch(self, request, \*args, \*\*kwargs):
return super(HelloView, self).dispatch(request, \*args, \*\*kwargs)
urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', HelloView.as_view()),
]
将包含这个视图层的 django demo 工程启动后,请求 /hello/
路径:
[root@server ~]# curl http://127.0.0.1:8881/hello/
get
[root@server ~]# curl -XPOST http://127.0.0.1:8881/hello/
post
[root@server ~]# curl -XPUT http://127.0.0.1:8881/hello/
put
[root@server ~]# curl -XDELETE http://127.0.0.1:8881/hello/
delete
可以看到,这样封装的视图类对应的 web api 接口具有良好的 restful 风格,而且代码可读性也非常好。 后面我们会深入学习这种视图模式以及 Django 内部封装的各种 view 类。
2. Django 中的 HttpRequest 类
上面我们初步接触到了 HttpRequest 类,现在来详细介绍下这个类及其相关属性和方法。当 URLconf 文件匹配到客户端的请求路径后,会调用对应的 FBV 或者 CBV,并将 HttpRequest 类的实例作为第一个参数传入对应的处理函数中。那么这个 HttpRequest 类有哪些常用的属性和方法呢?
常用属性:
- HttpRequest.scheme:请求的协议,一般为 http 或者 https;
- HttpRequest.body:请求主体;
- HttpRequest.path: 所请求 URL 的完整路径,即去掉协议,主机地址和端口后的路径;
- HttpRequest.method:客户端 HTTP 请求方法,如 GET、POST、PUT、DELETE等;
- HttpRequest.GET: 返回一个 querydict 对象,该对象包含了所有的 HTTP 请求中 GET 请求的参数;
- HttpRequest.POST: 返回一个 querydict 对象,该对象包含了所有的 HTTP 请求中 POST 请求的参数;
- HttpRequest.COOKIES:返回一个包含了所有 cookies 的字典;
- HttpRequest.FILES:返回一个包含所有文件对象的字典。
常用方法:
- HttpRequest.get_host():返回客户端发起请求的 IP + 端口;
- HttpRequest.get_port():返回客户端请求端口;
- HttpRequest.get_full_path():返回请求的完整路径,包括 “?” 后面所带参数;
- HttpRequest.get_raw_uri():返回完整的 uri 地址,包括了协议、主机和端口以及完整请求路径;
- HttpRequest.build_absolute_uri():通过 request 实例中的地址和变量生成绝对的 uri 地址。
示例代码:
# 省略了import内容
def hello\_world(request, \*args, \*\*kwargs):
request_info = ""
request_info += "request.scheme={}\n".format(request.scheme)
request_info += "request.body={}\n".format(request.body)
request_info += "request.path={}\n".format(request.path)
request_info += "request.method={}\n".format(request.method)
request_info += "request.GET={}\n".format(request.GET)
request_info += "request.FILES={}\n".format(request.FILES)
request_info += "request.get\_host={}\n".format(request.get_host())
request_info += "request.get\_port={}\n".format(request.get_port())
request_info += "request.get\_full\_path={}\n".format(request.get_full_path())
request_info += "request.get\_raw\_uri={}\n".format(request.get_raw_uri())
request_info += "request.build\_absolute\_uri={}\n".format(request.build_absolute_uri())
return HttpResponse(request_info, content_type="text/plain")
urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', hello_world),
]
我们启动 Django 服务后,我们使用 curl
命令发送 HTTP 请求如下:
# 准备一个新的文件
[root@server ~]# cat upload_file.txt
upload file test
[root@server ~]# curl -XPOST "http://127.0.0.1:8881/hello/?a=xxx&a=yyy&b=zzz" -F 'data={"name": "join", "age": 28}' -F "file=@/root/upload_file.txt"
request.scheme=http
request.body=b'------------------------------c28860e155fe\r\nContent-Disposition: form-data; name="data"\r\n\r\n{"name": "join", "age": 28}\r\n------------------------------c28860e155fe\r\nContent-Disposition: form-data; name="file"; filename="upload_file.txt"\r\nContent-Type: text/plain\r\n\r\nupload file test\n\r\n------------------------------c28860e155fe--\r\n'
request.path=/hello/
request.method=POST
request.GET=<QueryDict: {'a': ['xxx', 'yyy'], 'b': ['zzz']}>
request.FILES=<MultiValueDict: {'file': [<InMemoryUploadedFile: upload_file.txt (text/plain)>]}>
request.get_host=127.0.0.1:8881
request.get_port=8881
request.get_full_path=/hello/?a=xxx&a=yyy&b=zzz
request.get_raw_uri=http://127.0.0.1:8881/hello/?a=xxx&a=yyy&b=zzz
request.build_absolute_uri=http://127.0.0.1:8881/hello/?a=xxx&a=yyy&b=zzz
通过测试结果可以更容易理解 HttpRequest 类属性和方法的含义。其中,上述 curl 请求中 -F 表示带表单数据。
3. Django 视图函数的返回值
对于视图函数的返回值,往往有如下几种方式:
3.1 直接返回字符串
直接返回字符串是非常常见的一种方式,不过我们需要将字符串作为参数传到 HttpResponse 类中实例化后返回:
def hello\_world(request, \*args, \*\*kwargs):
return HttpResponse('要返回的字符串')
Tips:HttpResponse:是 Django 中封装的用于返回响应的类 。
3.2 返回 json 类型
视图函数直接返回 json 数据是在微服务架构中常见的套路。这里 Django 程序只提供后端数据并不提供静态资源。针对返回 json 数据,在 Django 中专门定义了一个 JsonResponse 类用来生成 json 数据。它实际上是继承自 HttpResponse 类:
# django/http/response.py
# 忽略其他内容
class JsonResponse(HttpResponse):
"""
忽略注释部分内容
"""
def \_\_init\_\_(self, data, encoder=DjangoJSONEncoder, safe=True,
json_dumps_params=None, \*\*kwargs):
if safe and not isinstance(data, dict):
raise TypeError(
'In order to allow non-dict objects to be serialized set the '
'safe parameter to False.'
)
if json_dumps_params is None:
json_dumps_params = {}
kwargs.setdefault('content\_type', 'application/json')
data = json.dumps(data, cls=encoder, \*\*json_dumps_params)
super().__init__(content=data, \*\*kwargs)
JsonResponse 类的使用和 HttpResponse 类一样简单,我们只需要把字典数据传给 JsonResponse 类进行实例化即可。但是字典数据中存在中文时候,会出现乱码,我们只需要在实例化 JsonResponse 时,多传入一个参数即可:
# 在页面上会出现乱码
def hello\_world(request, \*args, \*\*kwargs):
data = {'code': 0, "content": "返回中文字符串", "err\_msg": ""}
return JsonResponse(data)
# 经过处理后的JsonResponse
def hello\_world(request, \*args, \*\*kwargs):
data = {'code': 0, "content": "返回中文字符串", "err\_msg": ""}
return JsonResponse(data, json_dumps_params={'ensure\_ascii': False})
请求结果:
# 第一个不处理的 JsonResponse 返回
[root@server ~]# curl "http://127.0.0.1:8881/hello/"
{"code": 0, "content": "\u8fd4\u56de\u4e2d\u6587\u5b57\u7b26\u4e32", "err\_msg": ""}
# 使用第二个数据处理后的HttpResponse返回
[root@server ~]# curl "http://127.0.0.1:8881/hello/"
{"code": 0, "content": "返回中文字符串", "err\_msg": ""}
另外一种比较好的方式是,仿照 JsonResponse 类,定义一个支持返回包含中文 json 数据的 Response 类:
# 忽略导入模块
# 将原来支持的json\_dumps\_params参数固定写死成{'ensure\_ascii':False}
class JsonResponseCn(HttpResponse):
"""
忽略注释部分内容
"""
def \_\_init\_\_(self, data, encoder=DjangoJSONEncoder, safe=True, \*\*kwargs):
if safe and not isinstance(data, dict):
raise TypeError(
'In order to allow non-dict objects to be serialized set the '
'safe parameter to False.'
)
kwargs.setdefault('content\_type', 'application/json')
data = json.dumps(data, cls=encoder, \*\*{'ensure\_ascii':False})
super().__init__(content=data, \*\*kwargs)
这样处理后,我们在和原来一样使用 JsonResponseCn 类来返回 json 数据即可,不用考虑返回的字典数据中是否包含中文字符:
def hello\_world(request, \*args, \*\*kwargs):
data = {'code': 0, "content": "返回中文字符串", "err\_msg": ""}
return JsonResponseCn(data)
3.3 调用 render 函数返回
HTML 文本或者模板文件。其实,通过查看 Django 的源代码,可以看到 render 函数会调用 loader.render_to_string()
方法将 html 文件转成 string,然后作为 content 参数传递给 HttpResponse 类进行实例化:
# django/shortcuts.py
def render(request, template_name, context=None, content_type=None, status=None, using=None):
"""
Return a HttpResponse whose content is filled with the result of calling
django.template.loader.render\_to\_string() with the passed arguments.
"""
content = loader.render_to_string(template_name, context, request, using=using)
return HttpResponse(content, content_type, status)
它的用法如下:
# 第一步,在django工程中准备一个静态文件,放到templates目录下
(django-manual) [root@server first_django_app]# cat templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>这是首页</h1>
</body>
</html>
# 第二步,在first\_django\_app/setting.py文件,指定静态资源的目录
(django-manual) [root@server first_django_app]# cat first\_django\_app/settings.py
...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# 指定项目目录下的templates目录
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP\_DIRS': True,
'OPTIONS': {
'context\_processors': [
'django.template.context\_processors.debug',
'django.template.context\_processors.request',
'django.contrib.auth.context\_processors.auth',
'django.contrib.messages.context\_processors.messages',
],
},
},
]
...
# 第三步,添加视图函数以及URLconf,位置first\_django\_app/urls.py
def index(request, \*args, \*\*kwargs):
return render(request, "index.html")
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', index),
]
就这样一个简单的配置,我们请求 /index/ 路径时,会返回 index.html 文件内容:
[root@server ~]# curl "http://127.0.0.1:8881/index/"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>这是首页</h1>
</body>
</html>
另一方面,index.html 还可以是一个模板文件,我们通过 render 函数最后会将该模板文件转成一个完整的 HTML 文件,返回给客户端:
# index.html
(django-manual) [root@server first_django_app]# cat templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</body>
</html>
# 修改 urls.py 中的视图函数
(django-manual) [root@server first_django_app]# cat first_django_app/urls.py
...
def index(request, *args, **kwargs):
return render(request, "index.html", {"title":"首页", "content": "这是正文"})
...
最后请求结果可以看到完整的 HTML 文本:
[root@server ~]# curl "http://127.0.0.1:8881/index/"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p>这是正文</p>
</body>
</html>
3.4 调用 redirect 函数
实现重定向功能,它的常用形式如下:
from django.shortcuts import redirect
# 指定完整的网址
def redirect\_view1(request):
# 忽略其他
return redirect("http://www.baidu.com")
# 指定内部path路径
def redirect\_view2(request):
# 忽略其他
return redirect("/index/")
def redirect\_view3(request):
# 忽略其他
return redirect(reverse('blog:article\_list'))
def redirect\_view4(request):
# 忽略其他
return redirect('some-view-name', foo='bar')
4. 给视图加装饰器
在 Django 工程中,由于视图有两种:FBV 和 CBV,因此视图加装饰器的情况也分为两种:给视图函数加装饰器和给视图类加装饰器。由于视图函数是普通的 Python 函数,因此给视图函数加装饰器和给普通函数加装饰器方式一致。下面代码中我们给视图函数 index 加了一个简单的装饰器,用于在执行视图函数前和后各打印一段字符:
from django.shortcuts import render, HttpResponse, redirect
def wrapper(f):
def innser(\*args, \*\*kwargs):
print('before')
ret = f(\*args, \*\*kwargs)
print('after')
return ret
return innser
@wrapper
def index(request):
return render(request, 'index.html')
由于类中的方法与普通函数不完全相同,因此不能直接将函数装饰器应用于类中的方法 ,我们需要先将其转换为方法装饰器。Django 中提供了 method_decorator
装饰器用于将函数装饰器转换为方法装饰器:
# 定义函数装饰器
def wrapper(f):
def innser(\*args, \*\*kwargs):
print('before')
ret = f(\*args, \*\*kwargs)
print('after')
return ret
return innser
# 另一个代码文件中使用wrapper装饰器
from django.views import View
from django.utils.decorators import method_decorator
@method_decorator(wrapper, name='get')
class HelloView(View):
# @method\_decorator(wrapper)
def get(self, request, \*args, \*\*kwargs):
print('get')
return HttpResponse('get\n')
def post(self, request, \*args, \*\*kwargs):
print('post')
return HttpResponse('post\n')
def put(self, request, \*args, \*\*kwargs):
print('put')
return HttpResponse('put\n')
def delete(self, request, \*args, \*\*kwargs):
print('delete')
return HttpResponse('delete\n')
@csrf_exempt
# @method\_decorator(wrapper)
def dispatch(self, request, \*args, \*\*kwargs):
return super(HelloView, self).dispatch(request, \*args, \*\*kwargs)
对于给 View 类添加装饰器,我们有如下几种性质:
- 将 method_decorator 装饰器直接添加到 View 类上。第一个参数指定需要添加的装饰器,如 wrapper,name 参数可以选择将装饰器 wrapper 作用到 View 类中的哪个函数。给类添加装饰器时,必须要指定 name 参数,不然运行 Django 服务时会报错;
- method_decorator 装饰器可以直接作用于 View 类中的函数上。如果是在 get、post 这类 HTTP 请求方法的函数上,则装饰器只会作用于调用该函数的 http 请求上;
- 如果 method_decorator 装饰器作用于 dispatch 函数上。由于对于视图中所有的 HTTP 请求都会先调用 dispatch 方法找到对应请求方法的函数,然后再执行。所以这样对应视图内的所有 HTTP 请求方式都先经过装饰器函数处理;
5. 小结
本小节中,我们介绍了视图的两种形式 FBV 和 CBV,并进行了简单的说明。接下来我们详细描述了 Django 中的 HttpRequest 和 HttpResponse 对象,并进行了代码说明。最后我们介绍了给 FBV 和 CBV 加上装饰器的方法。