首页 慕课教程 Django 入门教程 Django 入门教程 10 Django 中传递参数给视图函数

Django 中传递参数给视图函数

Django 框架中推荐使用一个单独的 python 模块配置 URL 和视图函数或者视图类的映射关系,通常称这个配置模块为 URLconf,该 Python 模块通常命名为 urls.py。一般而言,每个应用目录下都会有一个 urls.py 文件,总的映射关系入口在项目目录下的 urls.py 中,而这个位置又是在 settings.py 文件中指定的。

本小节中将会学习 Django 中的路由系统、URLconf 的配置,以及如何将请求参数,如表单数据、文件等传递给视图函数。

1. 测试环境准备

这里的实验环境会采用前面创建的第一个 Django 工程(first_django_app) 来进行测试。在 first_django_app 中,我们创建了第一个 django app 应用:hello_app。现在按照如下步骤准备实验环境:

在 hello_app 应用目录下的 views.py 中添加一个视图函数:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def hello_view(request, *args, **kwargs):
    return HttpResponse('hello, world!')

在 hello_app 应用目录下新建 urls.py 文件,里面内容如下:

from django.urls import path

from . import views

urlpatterns = [
    # 资产查询接口,根据我们自己的机器命名
    path('world/', views.hello_view, name='hello_view'),
]

settings.py 文件中注册应用:

# first_django_app/settings.py
...

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 注册应用
    'hello_app'
]

...

在 URLconf 的总入口位置使用 django 提供的 include 方法将应用中的 urls.py 中的 urlpatterns 添加进来:

from django.contrib import admin
from django.conf.urls import include, url

# 所有url入口
urlpatterns = [
    url('admin/', admin.site.urls),
    url('hello/', include('hello_app.urls')),
]

这样之后,我们请求地址 /hello/world/ 时,就能看到页面显示 “hello, world!” 字符了。后面的测试将在应用的 urls.pyviews.py 中进行。

2. Django 中的传参方式

Django 中传递参数给视图函数的方式主要可分为以下两种形式:URL 传参和非 URL 传参两种。第一种基于 Django 中的 URLconf 配置,可以通过 URL 路径将对应匹配的参数传到视图函数中;而另外一种就是属于HTTP 请求携带的参数了,请求参数可以放到 URL 中以 格式加到 URL 的最后面,也可以将参数放到请求 body 中,最后统一由视图函数中的 request 参数保存并传到视图函数中。

2.1 动态 URL 传参

在 url 的路径 (path)部分可以作为动态参数,传递给视图函数,如下面几种写法:

# hello_app/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('articles/<int:year>/', views.year_archive),
    path('articles/<int:year>/<int:month>/', views.month_archive),
    path('articles/<int:year>/<int:month>/<slug:title>/', views.article_title),
]

注意上面的定义了匹配三个动态 URL 的映射,每个动态 URL 会匹配一个至多个参数,每个动态值使用 <> 符号匹配,采用 <type:name> 这样的形式。我们对应的视图函数如下:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def year_archive(request, year, *args, **kwargs):
    return HttpResponse('hello, {} archive\n'.format(year))
    
def month_archive(request, year, month, *args, **kwargs):
    return HttpResponse('hello, month archive, year={}, moth={}!\n'.format(year, month))

def article_title(request, year, month, title, *args, **kwargs):
    return HttpResponse('hello, title archive, year={}, month={}, title={}!\n'.format(year, month, title))

对于动态的 URL 表达式中,匹配到的值,比如上面的 year,month 和 title 可以作为函数的参数放到对应的视图函数中,Django 会帮我们把匹配到的参数对应的放到函数的参数上。这里参数的位置可以任意写,但是名字必须和 URL 表达式中的对应。

[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, 1998 archive
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/
hello, month archive, year=1998, moth=12!
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/test/
hello, title archive, year=1998, month=12, title=test

比如 URL 中有 3 个动态参数,在视图函数中只写上两个参数接收也是没问题的,因为剩下的参数会被传到 kwargs 中以 key-value 的形式保存:

(django-manual) [root@server first_django_app]# cat hello_app/views.py
...

def article_title(request, year, month, *args, **kwargs):
    return HttpResponse('hello, title archive, year={}, month={}, kwargs={}\n'.format(year, month, kwargs))

# 启动服务,再次请求后
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/test/
hello, title archive, year=1998, month=12, kwargs={'title': 'test'}

上述介绍的动态 URL 匹配格式 <type:name> 中,Django 会对捕捉到的 URL 参数进行强制类型装换,然后赋给 name 变量,再传到视图函数中。其中 Django 框架中支持的转换类型有:

  • str:匹配任意非空字符,不能匹配分隔符 “/”;

  • int:匹配任意大于0的整数;

  • slug:匹配任意 slug 字符串, slug 字符串可以包含任意的 ASCII 字符、数字、连字符和下划线等;

  • uuid:匹配 UUID 字符串;

  • path:匹配任意非空字符串,包括 URL 的分隔符 “/”。

2.2 自定义URL参数类型转换器

除了 Django 定义的简单类型,我们还可以自定义参数类型转换器来支持更为复杂的 URL 场景。比如前面的 int 类型并不支持负整数,我希望开发一个能匹配正负数的类型,具体的步骤如下:

hello_app/urls.py 中定义一个 SignInt 类。该类有一个固定属性 regex,用于匹配动态值;两个固定方法:to_python() 方法和 to_url() 方法:

# hello_app/urls.py

class SignInt:
    regex = '-*[0-9]+'

    def to_python(self, value):
        # 将匹配的value转换成我们想要的类型
        return int(value)

    def to_url(self, value):
        # 反向生成url时候回用到
        return value

注册该定义的转换器,并给出一个简短的名字,比如 sint:

# hello_app/urls.py

from django.urls import converters, register_converter

register_converter(SignInt, 'sint')

...

最后,我们就可以在 URLconf 中使用该类型来配置动态的 URL 表达式:

# hello_app/urls.py

urlpatterns = [
    path('articles/<sint:signed_num>/', views.signed_convert),
]

# hello_app/views.py
def signed_convert(request, signed_num, **kwargs):
    return HttpResponse('hello, 自定义类型转换器,获取参数={}\n'.format(signed_num))

启动 Django 服务后,执行相关请求,可以看到 Django 的路由系统能成功匹配到带负数和正数的 URL:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, 自定义类型转换器,获取参数=1998
[root@server ~]# curl http://127.0.0.1:8881/hello/articles/-1998/
hello, 自定义类型转换器,获取参数=-1998

2.3 使用正则表达式

上面是比较简单的 URLconf 配置形式,Django 框架中可以使用正则表达式来进一步扩展动态 URL 的配置,此时 urlpatterns 中的不再使用 path 方法而是支持正则表达式形式的 re_path 方法。此外,在 Python 的正则表达式中支持对匹配结果进行重命名,语法格式为:(?P<name>pattern),其中 name 为该匹配的名称,pattern 为匹配的正则表达式。 这样我们可以有如下的 URLconf 配置:

# hello_app/urls.py
from django.urls import re_path

from . import views

urlpatterns = [
    re_path('articles/(?P<year>[0-9]{4})/', views.year_archive),
    re_path('articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/', views.month_archive),
    re_path('articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/(?P<title>[a-zA-Z0-9-_]+)/', views.article_title),
]

注意:这里使用正则表达式的 URL 匹配和前面的普通的动态 URL 匹配有一个非常重要的区别,基于正则表达式的URL 匹配一旦匹配成功就会直接跳转到视图函数进行处理,而普通的动态 URL 匹配则会找到最长匹配的动态 URL,然后再进入相应的视图函数去处理:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/12/test
hello, 1998 archive

可以看到,这里并没有匹配到第三个 re_path 的 URL 配置,而是直接由第一个 re_path 的视图函数进行了处理。

2.4 URLconf 传递额外参数

在前面的 URLconf 配置中,我们的 re_path 方法中只传递两个参数,分别是设计的路由以及对应的视图函数。我们可以看看 Django-2.2.10 中的 path 和 re_path 方法的源代码:

# django/urls/conf.py
# ...

def _path(route, view, kwargs=None, name=None, Pattern=None):
    if isinstance(view, (list, tuple)):
        # For include(...) processing.
        pattern = Pattern(route, is_endpoint=False)
        urlconf_module, app_name, namespace = view
        return URLResolver(
            pattern,
            urlconf_module,
            kwargs,
            app_name=app_name,
            namespace=namespace,
        )
    elif callable(view):
        # view是函数
        pattern = Pattern(route, name=name, is_endpoint=True)
        return URLPattern(pattern, view, kwargs, name)
    else:
        raise TypeError('view must be a callable or a list/tuple in the case of include().')


path = partial(_path, Pattern=RoutePattern)
re_path = partial(_path, Pattern=RegexPattern)

可以看到,除了route 和 view 外,我们还有 name、kwargs、Pattern 参数(比较少用)。其中 name 参数表示的是 route 匹配到的 URL 的一个别名,而 kwargs 是我们可以额外传给视图函数的参数:

# hello_app/urls.py
...

urlpatterns = [
    re_path('articles/(?P<year>[0-9]{4})/', views.year_archive, {'hello': 'app'}),
]

# hello_app/views.py
def year_archive(request, *args, **kwargs):
    return HttpResponse('hello, year archive, 额外参数={}\n'.format(kwargs))

启动 Django 服务后,我们请求对应的服务,可以看到除了 URL 中匹配的 year 参数外,还有 re_path 中额外传递的参数,最后都被视图函数中的 **kwargs 接收:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, year archive, 额外参数={'year': '1998', 'hello': 'app'}

2.5 从 HttpRequest 中获取参数

从 HttpRequest 中获取参数是我们进行 Web 开发中最常用的一种方式。对于 Django 的视图函数来说,HTTP 请求的数据被 HttpRequest 实例化后传到了视图函数的第一个参数中。为了能观察相关信息,我们修改请求的视图函数:

@csrf_exempt
def hello_view(request, *args, **kwargs):
    # 在第三次使用表单上传包括文件数据时,需要request.GET和request.POST操作,不然会抛异常
    params = "request.GET={}\n".format(request.GET)
    params += "request.POST={}\n".format(request.POST)
    params += "request.body={}\n".format(request.body)
    params += "request.FILES={}\n".format(request.FILES)

    return HttpResponse(params)

我们测试如下 3 种 HTTP 请求,分别为 GET 请求、POST 请求 和带文件参数的请求,结果如下:

[root@server ~]# curl -XGET "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" 
request.GET=<QueryDict: {'a': ['xxxx'], 'b': ['yyyy']}>
request.POST=<QueryDict: {}>
request.body=b''
request.FILES=<MultiValueDict: {}>

[root@server ~]# curl -XPOST -d "username=shen&password=shentong" "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" 
request.GET=<QueryDict: {'a': ['xxxx'], 'b': ['yyyy']}>
request.POST=<QueryDict: {'username': ['shen'], 'password': ['shentong']}>
request.body=b'username=shen&password=shentong'
request.FILES=<MultiValueDict: {}>

# 本次请求中,需要去掉request.GET和request.POST操作语句,不然请求会报错
[root@server ~]# curl -XPOST -F "username=shen&password=shentong" "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" -F "files=@/root/upload_file.txt"
request.body=b'------------------------------68c9ede00e93\r\nContent-Disposition: form-data; name="username"\r\n\r\nshen&password=shentong\r\n------------------------------68c9ede00e93\r\nContent-Disposition: form-data; name="files"; filename="upload_file.txt"\r\nContent-Type: text/plain\r\n\r\nupload file test\n\r\n------------------------------68c9ede00e93--\r\n'
request.FILES=<MultiValueDict: {'files': [<InMemoryUploadedFile: upload_file.txt (text/plain)>]}>

可以看到,跟在 “?” 后的参数数据会保存到 request.GET 中,这也是 GET 请求带参数的方式。对于 POST 请求的传参,数据一般会保存在 request.POSTrequest.body 中。对于最后发送的上传文件请求,可以看到,文件内容的内容数据是保存到了 request.body 中。

3. 小结

本节内容我们主要介绍了如何向视图函数传送参数,包括两种方式:

  1. 通过动态的 URLconf 配置传递参数到视图函数中;

  2. 通过 http 请求带参数传递给视图函数。至此,Django 中的路由系统和视图函数已经介绍完毕。接下来会介绍 Django 中的模板系统。