前一篇笔记我们介绍了Django中的路由系统,路由系统通过声明式的匹配机制能够将HTTP请求路由到视图函数。这篇笔记我们继续详细介绍视图函数(FBV),包括视图函数中如何获取HTTP请求参数和请求内容以及如何生成HTTP响应。
Django中,定义视图逻辑代码的方式其实有两种:视图函数(FBV)和视图类(CBV)。FBV简单直观且功能强大,是Django视图层的核心基础;而CBV则相对来说有一些争议,CBV是出于提高代码复用性而引入的,它设计的非常复杂和抽象,使用门槛高且可定制性差,实际开发中完全不使用CBV也是可行的,不过合理使用CBV确实可以起到简化代码提高代码复用性的目的。
这篇笔记我们仍以FBV为基础进行介绍,有关CBV的内容我们放到后续章节。
我们知道HTTP协议是一种文本协议,不过Django中具体解析HTTP请求主要是由框架自动完成的,我们只需要在urls.py
定义路由规则并在视图函数中编写取值的代码逻辑即可。Django能很方便的实现传统URL风格参数解析或RESTful的URL风格参数解析,下面我们以例子的形式简单介绍。
下面是一个请求示例,请求中包含一个GET参数blogId
。
GET http://localhost:8000/demo01/blogs/?blogId=2
下面代码中,我们在视图函数里获取了GET请求参数并打印。
from django.shortcuts import render
def blogs(request):
blog_id = request.GET.get('blogId')
print('Blog ID ', type(blog_id), blog_id)
return render(request, 'success.html')
GET参数是通过request.GET
这个QueryDict
对象获取的,我们可以通过get()
方法获取,如果键存在则返回参数字符串值;如果不存在get()
方法返回None
。实际上,通过下标从QueryDict
对象中获取参数也是可以的,写法例如blog_id = request.GET['blogId']
,但不推荐这种方式,因为如果键不存在下标获取会直接抛出异常。
下面是一个RESTful风格的请求示例。
GET http://localhost:8000/demo01/blogs/2/
前面章节我们介绍过,这种风格的URL参数我们可以在路由配置中通过转换器匹配进行设置。
from django.urls import path
from demo01.views import blogs
urlpatterns = [
path('blogs/<int:blog_id>/', blogs)
]
视图函数写法如下,我们直接以blog_id
为参数名接收匹配到的值即可,Django会自动将匹配到的值作为参数传递给视图函数。
from django.shortcuts import render
def blogs(request, blog_id):
print('Blog ID ', type(blog_id), blog_id)
return render(request, 'success.html')
如果用户访问时没有传递blog_id
参数,那么路由匹配就不会成功,未找到合适的路由服务端就会响应404
错误。
请求头信息可以从request.META
对象中获取,下面是一个例子。
from django.shortcuts import render
def blogs(request):
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
print(user_agent)
return render(request, 'success.html')
request.META
对象包含了大量请求相关的基础信息,包括请求头、请求方法、协议、请求路径等,具体可以参考官方文档。我们如果需要获取请求头,可以通过HTTP_
前缀加上请求头名称的方式获取,例如HTTP_USER_AGENT
对应获取USER-AGENT
请求头,注意HTTP协议规定请求头名称是大小写不敏感的。对于自定义请求头也是使用类似的转换规则,例如HTTP_X_MY_CUSTOM_HEADER
将对应于X-MY-CUSTOM-HEADER
请求头。如果想要获取所有请求头,打印request.META
中以HTTP_
为前缀的字段即可。
Cookie信息可以从request.COOKIES
对象中获取,下面是一个例子。
from django.http import HttpResponse
def index(request):
username = request.COOKIES.get('username')
print(username)
return HttpResponse('OK')
代码中,我们获取了名为username
的Cookie值,如果不存在则返回None
。
表单格式实际上有form-data
和x-www-form-urlencoded
两种格式,Django对这两种格式表单进行了统一的支持。
from django.http import HttpResponseNotAllowed
from django.shortcuts import render
def post_blog(request):
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
print(title)
print(content)
return render(request, 'success.html')
else:
return HttpResponseNotAllowed(['POST'])
读取表单请求和读取GET参数类似,只需要通过request.POST
对象调用get()
方法获取请求参数值即可,如果参数不存在,get()
方法返回None
。
注意:Django中默认配置启用了CSRF中间件,POST请求必须带上CSRF_TOKEN
,上述代码如果直接在Postman等接口调试工具中请求其实是无法成功的,CSRF_TOKEN
可以通过在表单模板中使用{% csrf_token %}
获取,并随表单参数提交或使用X-CSRFToken
请求头提交。有关CSRF_TOKEN
的问题后文会详细介绍。
如果使用请求体传递JSON数据(常见于现代的前后端分离工程),我们可以直接获取request.body
并使用Python内置的json
模块解析即可。
import json
from django.http import HttpResponseNotAllowed, HttpResponse
def post_blog(request):
if request.method == 'POST':
data = json.loads(request.body)
print(data.get('title'))
print(data.get('content'))
return HttpResponse('OK')
else:
return HttpResponseNotAllowed(['POST'])
代码中,我们的视图函数解析如下的JSON数据结构。
{
"title": "123",
"content": "abc"
}
我们前面了解到,Django中默认配置启用了CSRF中间件,这要求POST请求必须带一个CSRF_TOKEN
参数,实际上对于PUT、DELETE、PATCH请求也需要该参数,这个参数需要通过模板引擎获取,并通过POST表单键值对的形式提交回Django框架。下面例子代码演示了模板中如何获取CSRF_TOKEN
。
注:有关什么是CSRF攻击可以参考信息安全/Web安全/CSRF攻击
相关章节。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>表单页</title>
</head>
<body>
<form method="post">
{% csrf_token %}
<label for="username">用户名</label>
<input type="text" id="username" name="username">
<input type="submit" value="Submit">
</form>
</body>
</html>
程序运行后,{% csrf_token %}
会被替换为如下形式。此时提交表单,CSRF_TOKEN
参数会被以csrfmiddlewaretoken
作为名字随同表单提交到后端,CSRF中间件会检查该参数值,并决定是否处理请求。
<input type="hidden" name="csrfmiddlewaretoken" value="ae8DOdPL6IyIJFBv5kuG48HfhTabwe2XRZ1b9Fug6WmrWPFh6NpnvJiWnn5aJ2Nw">
对于前后端分离工程,我们提交POST请求时一般都采用JSON格式,且前后端分离工程可能完全不使用模板引擎。对于这类工程,一种可行的方式是编写专门的视图函数,每次提交请求前访问CSRF_TOKEN
生成接口,由后端新生成一个CSRF_TOKEN
。下面代码中,我们用Python代码的形式获取CSRF_TOKEN
,然后通过AJAX方式将其传给前端。
from django.http import JsonResponse
from django.middleware import csrf
def get_token(request):
csrf_token = csrf.get_token(request)
return JsonResponse({
'csrf_token': csrf_token
})
在前端代码中,我们需要先通过AJAX请求获取CSRF_TOKEN
,然后将其作为请求头X-CSRFToken
的值提交给后端,建议每次POST请求前都先获取CSRF_TOKEN
。下面例子使用JQuery编写附带X-CSRFToken
的POST请求。
function postJson() {
$.ajax({
type: "POST",
async: true,
url: '/csrftest/post',
headers: {
'X-CSRFToken': csrf_token
},
data: JSON.stringify({
'username': 'Tom',
'password': 'abc123'
}),
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function (msg) {
},
error: function (err) {
alert('POST请求失败!');
}
});
}
此外,如果你真的确认不需要CSRF校验功能(常规工程中不推荐),可以在项目配置文件settings.py
中禁用CSRF中间件。
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Django中,HTTP响应主要分为以下几种类型:
基本响应:可能是一个简单的字符串或是一个404
状态。
模板响应:返回模板页面,用于传统的非前后端分离工程。
重定向响应:用于服务端返回重定向跳转。
JSON响应:返回JSON数据,用于现代前后端分离工程。
下面代码向页面输出一个OK
字符串,状态码为200
。
from django.http import HttpResponse
def index(request):
return HttpResponse('OK')
返回错误状态的方法也差不多,下面代码向页面输出一个Http 400 Bad Request
字符串,状态码为400
。
from django.http import HttpResponseBadRequest
def index(request):
return HttpResponseBadRequest("Http 400 Bad Request")
其余常用的响应状态码,比如403
、404
、500
等参考文档选择对应的响应封装函数即可。
响应模板页面需要用到render()
函数,下面是一个例子。
from django.shortcuts import render
def index(request):
return render(request, 'index.html', {
'msg': 'hello'
})
其中,三个参数分别是request
对象、模板文件(可以带文件夹路径)和模板包含的数据对象,模板引用数据方式如下。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
{{ msg }}
</body>
</html>
服务端重定向本质上就是返回301
或302
响应,常用于非前后端分离项目,例如提交表单成功后,没什么信息可显示,需要重定向到另一个页面这种场景。
from django.http import HttpResponseRedirect
def index(request):
return HttpResponseRedirect("/app/index")
HttpResponseRedirect()
会返回302
重定向响应,浏览器会根据响应信息中的Location
响应头自动进行跳转到对应路径。
前后端分离项目中,我们经常需要返回JSON数据而非页面,Django对此也进行了封装,下面是一个例子。
from django.http import JsonResponse
def index(request):
return JsonResponse({
'username': 'tom',
'age': 12
})
其中,JsonResponse
的参数就是返回的JSON内容,这个响应Django会自动帮我们加上Content-Type: application/json
的响应头,告知浏览器该响应是JSON格式。
Django中可以通过对HttpResponse
对象进行一些额外设置来添加响应头。
from django.http import HttpResponse
def index(request):
response = HttpResponse('OK')
response['X-MY-CUSTOM-HEADER'] = 'hello'
return response
HttpResponse
对象实现了运算符重载,我们可以直接通过类似操作字典的写法对其设置响应头。
HttpResponse
的set_cookie()
方法能够设置Cookie。
from django.http import HttpResponse
def index(request):
response = HttpResponse('OK')
response.set_cookie('username', 'tom', max_age=3600)
return response
代码中,set_cookie()
方法接收三个参数,分别是Cookie名称、Cookie值和Cookie过期时间(单位为秒)。设置Cookie时有以下几个常用的命名参数。
max_age
:Cookie过期时间,不设置默认浏览器关闭后Cookie即失效。path
:Cookie生效的路径,默认为/
。secure
:安全设置,是否限制仅允许HTTPS传输Cookie,默认为False
。httponly
:安全设置,是否禁止JavaScript访问Cookie,默认为False
。samesite
:安全设置,Cookie的跨站设置,可以设置为Lax
、Strict
或None
,用以防止跨站请求伪造攻击(CSRF)。默认为None
。domain
:Cookie生效的域名,可以用于子域名之间共享Cookie。调用delete_cookie()
方法可以删除Cookie。
from django.http import HttpResponse
def index(request):
response = HttpResponse('OK')
response.delete_cookie('username')
return response
删除Cookie的原理其实就是将Cookie的超时时间设置为0
,即立即失效。
流式响应(EventStream)是一种特殊的HTTP响应类型,它允许我们以流的形式返回数据,而不是一次性返回所有数据。流式响应广泛应用于需要实时更新数据的场景,例如动态展示股票交易信息等。下面例子实现了一个简单的text/event-stream
格式的流式响应,它每秒向客户端发送一个数字。
import time
from django.http import StreamingHttpResponse
def index(request):
def event_stream():
for i in range(0, 100):
yield 'data: %s\n\n' % i
time.sleep(1)
return StreamingHttpResponse(event_stream(), content_type='text/event-stream')
我们可以用Postman测试这个接口,效果如下图。