Django--视图和URL配置

565 阅读9分钟

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」。

1.视图和URL配置

打开一个网页,需要有网页内容和打开这个网页的url。在Django中,页面内容是靠view function(视图函数)来定义,URL定义在URLconf中。

1.1 第一份视图

在views.py中创建一个hello world视图:

from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello world")

首先,需要导入HttpResponse这个类,接下来,定义视图函数hello:每个视图函数至少要有一个参数,通常被叫做request。这是一个触发这个视图、包含当前Web请求信息的对象,是类django.http.HttpResponse的一个实例。

综上:一个视图就是Python的一个函数,这个函数的第一个参数的类型是HttpRequest,它返回一个HttpResponse实例。

1.2 第一个URLconf

写完了视图函数,我们需要通过一个详细描述的URL来显式地告诉它并且激活这个函数,为了绑定视图函数和url,我们需要使用URLconf。URLconf就像是Django所支撑网站的目录,它的本质是URL模式以及要为该URL模式调用的视图函数之间的映射表,它就是以这种方式告诉django,对于这个URL调用这段代码,对于那个URL调用那段代码。

URLconf配置代码在urls.py文件中:

from django.conf.urls.defaults import *

urlpatterns = patterns('',
)

说明:

  • 第一行导入django.conf.urls.defaluts下的所有模块,它们是Django URLconf的基本构造,这包含了一个patterns函数;
  • 第二行调用patterns函数并将返回结果保存到urlpatterns变量,patterns函数当前只有一个参数--一个空的字符串,这个字符串可以被用来表示一个视图函数的通用前缀。

如果想在URLconf中加入URL和view,只需增加映射URL模式和view功能的Python tuple即可:

from django.conf.urls.defaults import *
from mysite.views import hello

urlpatterns = patterns('',
    ('^hello/$', hello),
)
  • 首先:引入views中的hello视图;
  • 接下来:在urlpatterns加上一行:('^hello/$', hello),这行被称作URLpattern,它是一个Python元组,元组的第一个元素是模式匹配字符串(正则表达式);第二个元素是那个模式将使用的视图函数。

Django在检查URL模式前,移除每一个申请的URL开头的斜杠/,这意味着我们为/hello/写URL模式开头不用包含斜杠。对于尾部的斜杠,有人可能会问,想申请/hello也就是尾部没有斜杠怎么办,因为我们的URL模式要求尾部有一个斜杠。事实上,任何不匹配或者尾部没有斜杠的申请URL,将被重定向至尾部包含斜杠的相同字眼的URL,这是受配置文件setting中的APPEND_SPLASH项控制的。如果你是一个不习惯在尾部加斜杠的人,那么需要设置"APPEND_SPLASH"为"FALSE"。另外,这里我们把hello函数作为一个对象传递,而不是调用它,这也是动态语言的一个特性:函数是一级对象(first-class objects),也就是说可以像传递其它变量一样传递它们。

1.3 Django是怎么处理请求的

当我们在浏览器里面敲入url地址后,一切均开始于setting文件,当我们运行python manage.py runserver,脚本将在manage.py同目录下查找名为setting.py的文件,这个文件包含了所有有关这个Django项目的配置信息,均大写:TEMPLATE_DIRS,DATABASE_NAME等。最重要的设置是ROOT_URLCONF,它将作为URLconf告诉Django在这个站点中哪些python的模块将被用到。

打开setting.py文件,将会看到:

ROOT_URLCONF = 'mysite.urls'

相对应的文件就是工程目录中的urls.py文件。

当访问URL/hello/时,Django根据ROOT_URLCONF的设置装载URLconf,然后按顺序逐个匹配URLconf里的URLpatterns,直到找到一个匹配的,当找到这个匹配的URLpatterns就调用相关联的view函数,并把HttpRequest对象作为第一个参数。

总结如下:

  1. 进来的请求转入/hello/;
  2. Django通过在ROOT_CONF配置来决定根URLconf;
  3. Django在URLconf中的所有URL模式中,查找第一个匹配/hello/的条目;
  4. 如果找到匹配,将调用相应的视图函数;
  5. 视图函数返回一个HttpResponse;
  6. Django转换HttpResponse为一个适合的HTTP response,以web page显示出来。

1.4 第二个视图--动态内容

具体步骤跟上述输出hello类似。

Django里面的默认时区是美国芝加哥时间,如果调用Datetime模块生成的时间会比北京时间晚8个小时,解决办法:修改站点文件setting.py,修改USE_TZ为False,修改TIME_ZONE为"Asia/Shanghai"即可。

URL配置和松耦合

简单来说,松耦合是一个重要的保证互换性的软件开发方法,在Django中,URL的定义和视图函数之间是松耦合的,换句话说,决定URL返回哪个视图函数和实现这个视图函数是在两个不同的地方,这使得开发人员可以修改这一块而不影响另一块。

1.5 第三个视图--动态URL

以上例中的打印当前时间为例,现在提出新的需求,创建一个视图来显示当前时间和加上事件偏差量的时间,设计是这样的:/time/plus/1/显示当前时间+1个小时的页面,/time/plus/2/显示+2小时的页面,/time/plus/3/显示+3小时的页面,普通的url配置会像下面这样:

urlpatterns = patterns('',
    ('^time/$', current_datetime),
    ('^time/plus/1/$', one_hour_ahead),
    ('^time/plus/2/$', two_hours_ahead),
    ('^time/plus/3/$', three_hours_ahead),
    ('^time/plus/4/$', four_hours_ahead),
)

毫无疑问,这样定义,不仅代码冗余,而且灵活性差,为了避免这样的写法,我们可以使用通配符来设计url。因此这里可以使用d+来匹配1个以上的数字。另外一个重点,正则表达式字符串的开头字母"r",它告诉python这是个原始字符串,不需要处理里面的转义字符。

(r'^time/plus/(\d{1,2})+/$', hours_ahead)

注意这里,用括号把\d{1,2}括起来,正则表达式就是使用圆括号来从文本里面提取数据的,有多组数据就用多个括号提取,然后按位置传参,命名组提取的数据后面需要按同名传参,位置不限定。

现在开始编写视图函数hours_ahead:

def hours_ahead(request, offset):
    try:
        offset = int(offset)
    except ValueError:
        raise Http404()
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "<h1>In %s hour(s), it will be %s.</h1>" % (offset, dt)
    return HttpResponse(html)

这里面就用offset接收到了刚才正则表达式提取出来的整数,不过提取到的是字符串类型的,处理之前需要转换成int型。

1.6 Django的出错页面

编写程序出现逻辑代码错误,会导致django在浏览器展示一个出错页面,我们需要好好利用这个出错页面帮助我们解决bug:

  • 在页面顶部,我们可以得到关键的异常信息:异常数据类型、异常的参数、在哪个文件中引发了异常、出错的行号等;
  • 在关键异常的下方,该页面显示了对该异常的完整Python追踪信息。这类似于你在Python命令行解释器中获得的追溯信息,只不过后者更具交互性。对栈中的每一帧,Django均显示了其文件名、函数或方法名、行号及源代码;
  • 点击该行代码(以深灰色显示),你可以看到出错行的前后几行,从而得知上下文情况;
  • 点击栈中任何一帧的"Local vars"可以看到一个所有局部变量的列表,以及在出错那一帧它们的值;
  • "Request information"部分包含了有关产生错误的web请求的大量信息:GET和POST、cookie值、元数据;
  • Request信息的下面,"settings"列出了Django使用的具体配置信息;
  • 在代码中插入assert False可以触发出错页;
  • Django出错信息仅在debug模式下显现。

1.7 一些常用的HttpResponse子类

  • HttpResponseRedirect:返回status302,用于url重定向,需要将重定向的目标地址作为参数传给该类(HttpResponseRedirect的参数经常使用URL反响映射函数reverse()获得,这样可以避免在更改网站urls.py内容的时候维护视图函数中的代码);
  • HttpResponseNotModified:返回status304,用于指示浏览器用其上次请求时的缓存结果作为页面内容显示;
  • HtppResponsePermanrntRedirect:返回status301,与HttpResponseRedirect类似,但是告诉浏览器这是一个永久重定向;
  • HttpResponseBadRequest:返回status400,请求内容错误;
  • HttpResponseForbidden:返回status403,禁止访问错误;
  • HttpResponseNotAllowed:返回status405,用不允许的方法(Get、Post、Head等)访问本页面;
  • HttpResponseServerError:返回status500,服务器内部错误,比如无法处理的异常等。

2.URLconf技巧

2.1 流线型化函数导入

直接导入views模块而不是单独的模块名。在URLconf中为某个特别的模式指定视图函数,你可以传入一个包含模块名和函数名字的字符串,而不是函数对象本身:如(r'^hello/$', 'mysite.views.hello'),注意这里传入的是字符串,需要引号,Django会在第一次需要它的时候根据字符串所描述的视图函数名字及路径,导入合适的视图函数。

2.2 传递额外的参数到视图函数中

URLconf里面的每一个模式都可以包含第三个数据:一个关键字参数的字典。比如,对于两个视图函数,内容一致,只是使用的模板不同,可以这样解决:

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^foo/$', views.foobar_view, {'template_name': 'template1.html'}),
    (r'^bar/$', views.foobar_view, {'template_name': 'template2.html'}),
)

# views.py

from django.shortcuts import render_to_response
from mysite.models import MyModel

def foobar_view(request, template_name):
    m_list = MyModel.objects.filter(is_new=True)
    return render_to_response(template_name, {'m_list': m_list})

在这个例子中,URLconf指定了template_name,而视图函数会把它当成另一个参数。

2.3 伪造捕捉到的URLconf值

比如说你有匹配某个模式的一堆视图,以及一个并不匹配这个模式但视图逻辑是一样的URL,这种情况下,你可以通过向同一个视图函数传递额外URLconf参数来伪造URL值的捕捉。

例如,你可能有一个显示某个特定日子的应用,它的url看起来可能像这样:

/mydata/jan/01/

/mydata/jan/02/

... ... ...

常理可以这样进行捕捉:

urlpatterns = [
    url(r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view)
]

然后视图函数的原型看起来像这样:

def my_view(request, month, day):{... ...}

现在提出一个需求,增加一个URL,/mydata/birthday/,使它等价于/mydata/jan/06。这时你可以利用额外的URL参数:

r'^mydata/birthday/$', views.view, {'month':'jan', 'day':'06'}

2.4 创建一个通用视图

以下面代码为例:

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^events/$', views.event_list),
    (r'^blog/entries/$', views.entry_list),
)

# views.py

from django.shortcuts import render_to_response
from mysite.models import Event, BlogEntry

def event_list(request):
    obj_list = Event.objects.all()
    return render_to_response('mysite/event_list.html', {'event_list': obj_list})

def entry_list(request):
    obj_list = BlogEntry.objects.all()
    return render_to_response('mysite/blogentry_list.html', {'entry_list': obj_list})

这两个视图做的事情实质上是一样的:显示一系列的对象,让我们把它们显示的对象的类型抽象出来:

# urls.py

from django.conf.urls.defaults import *
from mysite import models, views

urlpatterns = patterns('',
    (r'^events/$', views.object_list, {'model': models.Event}),
    (r'^blog/entries/$', views.object_list, {'model': models.BlogEntry}),
)

# views.py

from django.shortcuts import render_to_response

def object_list(request, model):
    obj_list = model.objects.all()
    template_name = 'mysite/%s_list.html' % model.__name__.lower()
    return render_to_response(template_name, {'object_list': obj_list})
  • 上述代码,我们通过model参数直接传递了模型类,额外URLconf参数的字典是可以传递任何类型的对象,而不仅仅只是字符串;
  • 我们使用model.__name__.lower()来决定模板的名字,每个Python的类都有一个__name__属性返回类名。

2.5 捕捉值和额外参数之间的优先级额外的选项

当冲突出现的时候,额外URLconf参数优先于捕捉值,也就是说,如果URLconf捕捉到的一个命名组变量和一个额外URLconf参数包含的变量同名时,额外URLconf参数的值会被使用。

例如,下面这个URLconf:

r'^mydata/(?P<id>\d+)/$', views.my_view, {'id':3}

这里,正则表达式和额外字典都包含了一个id,硬编码的(额外字典的)id将被采用。

从URL中捕获文本

每个被捕获的参数将作为纯Python字符串来发送,而不管正则表达式中的格式,在写视图的时候,这一点很重要,许多Python内建的方法对于接受对象的类型很讲究,一个典型的错误就是用字符串而不是整数值来创建datetime对象。

3.视图函数的高级概念

对于请求方法的分支,我们可以先写一个视图函数然后由它来具体分派其它的视图,在之前或之后可以执行一些我们自定的程序逻辑,如下例:

# views.py

from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render_to_response

def method_splitter(request, GET=None, POST=None):
    if request.method == 'GET' and GET is not None:
        return GET(request)
    elif request.method == 'POST' and POST is not None:
        return POST(request)
    raise Http404

def some_page_get(request):
    assert request.method == 'GET'
    do_something_for_get()
    return render_to_response('page.html')

def some_page_post(request):
    assert request.method == 'POST'
    do_something_for_post()
    return HttpResponseRedirect('/someurl/')

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = [
    # ...
    url(r'^somepage/$', views.method_splitter, {'GET': views.some_page_get, 'POST': views.some_page_post}),
    # ...
]

现在,我们就拥有了一个不错的,可以通用的视图函数了,里面封装着由"request.method"的返回值来分派不同的视图的程序,当然,对于这个splitter,我们让然可以对它进行改进。如下:

def method_splitter(request, *args, **kwargs):
    get_view = kwargs.pop('GET', None)
    post_view = kwargs.pop('POST', None)
    if request.method == 'GET' and get_view is not None:
        return get_view(request, *args, **kwargs)
    elif request.method == 'POST' and post_view is not None:
        return post_view(request, *args, **kwargs)
    raise Http404

这里,我们重构method_splitter,去掉了GET和POST这两个关键字参数,改而支持使用*args和**kwargs,这是一个Python特性,允许函数接受动态的、可变数量的、参数名只在运行时可知的参数。注意pop函数里面的'None',通过指定pop的缺省值为None,来避免一个或者多个关键字缺失带来的KeyError。

3.1 包装视图函数

如果发现自己多个视图中重复了大量的代码,就如下例:

def my_view1(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/')
    # ...
    return render_to_response('template1.html')

def my_view2(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/')
    # ...
    return render_to_response('template2.html')

def my_view3(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/')
    # ...
    return render_to_response('template3.html')

这里的request.user是描述当前用户是登录的还是匿名的。如果我们能够从每个视图里移除那些重复代码,就完美了。使用视图包装来达到这一目的:

def requires_login(view):
    def new_view(request, *args, **kwargs):
        if not request.user.is_authenticated():
            return HttpResponseRedirect('/accounts/login/')
        return view(request, *args, **kwargs)
    return new_view

函数requires_login传入一个视图函数,然后返回一个新的视图函数new_view,这个新的视图函数new_view在函数requires_login内定义,处理request.user.is_authenticated()这个验证,从而决定是否执行原来的view函数。

现在,可以从views中去掉if not request.user.is_authenticated()验证,我们可以在URLconf中很容易的用requires_login来包装实现:

from django.conf.urls.defaults import *
from mysite.views import requires_login, my_view1, my_view2, my_view3

urlpatterns = [
    url(r'^view1/$', requires_login(my_view1)),
    url(r'^view2/$', requires_login(my_view2)),
    url(r'^view3/$', requires_login(my_view3)),
]

django通过include包含其他的URLconf,那么,**捕获的参数如何和include()协同工作?**被捕获的变量将传递给被包含的URLconf,进而传递给哪个URLconf中的每一个视图函数,注意,这个被捕获的参数总是传递到被包含的URLconf中的每一行,不管那些行对应的视图是否需要这些参数。额外的参数传递给下级URLconf可以通过字典传递,当你这样做的时候,被包含的URLconf的每一行都会受到那些额外的参数。