Python / 09 Django 视图函数

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 加上装饰器的方法。