ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ## **一、概述** Django 中的视图的概念是一类具有相同功能和模板的网页的集合。一个视图通常对应一个页面,提供特定的功能,使用特定的模板。例如:在一个博客应用中,你可能会看到下列视图: * 博客主页:显示最新发布的一些内容 * 每篇博客的详细页面:显示博客的详细内容 * 基于年的博客页面:显示指定年内的所有博客文章 * 基于月的博客页面:显示指定月内的所有博客文章 * 基于天的博客页面:显示指定日内的所有博客文章 * 发布评论:处理针对某篇博客发布的评论 在我们的投票应用中,我们将建立下面的视图: * 问卷“index”页:显示最新的一些问卷 * 问卷“detail”页面:显示一个问卷的详细文本内容,没有调查结果但是有一个投票或调查表单。 * 问卷“results”页面:显示某个问卷的投票或调查结果。 * 投票动作页面:处理针对某个问卷的某个选项的投票动作。 <br /> 在Django中,网页和其它的一些内容都是通过视图来处理的。视图其实就是一个简单的Python函数(在基于类的视图中称为方法)。Django通过对比请求的URL地址来选择对应的视图,也就是路由。 <br /> 在你上网的过程中,很可能看见过像这样的丑陋的URL: "ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B" 。别担心,Django 里的 URL规则要比这优雅的多!比如:`/newsarchive/<year>/<month>/`。 为了将 URL 和视图关联起来,Django 使用`URLconfs`来配置路由。 <br /> ## **二、编写视图** 下面,打开`polls/views.py`文件,输入下列代码: ~~~ # 注意函数的参数 def detail(request, question_id): return HttpResponse("You're looking at question %s." % question_id) def results(request, question_id): response = "You're looking at the results of question %s." return HttpResponse(response % question_id) def vote(request, question_id): return HttpResponse("You're voting on question %s." % question_id) ~~~ 然后,在`polls/urls.py`文件中加入下面的路由,将其映射到我们上面新增的视图。 ~~~ from django.urls import path from . import views urlpatterns = [ # 例如: /polls/ path('', views.index, name='index'), # 例如: /polls/5/ path('<int:question_id>/', views.detail, name='detail'), # 例如: /polls/5/results/ path('<int:question_id>/results/', views.results, name='results'), # 例如: /polls/5/vote/ path('<int:question_id>/vote/', views.vote, name='vote'), ] ~~~ 现在去浏览器中访问`/polls/34/`(注意:这里省略了域名。另外,使用了二级路由后,url中都要添加字符串`polls`前缀,参考前面的章节),它将调用`detail()`函数,然后在页面中显示你在url里提供的ID。访问`/polls/34/results/`和`/polls/34/vote/`,将分别显示预定义的伪结果和投票页面。(PS:这里就不贴图了,请大家务必自己动手测试,多实践。切记不要输入错误!) <br /> 上面访问的路由过程如下:当有人访问`/polls/34/`地址时,Django将首先加载`mysite.urls`模块,因为它是settings文件里设置的根URL配置文件。在该文件里,Django发现了`urlpatterns`变量,于是在其内按顺序进行匹配。当它匹配上了`polls/`,就裁去url中匹配的文本`polls/`,然后将剩下的文本“`34/`”,传递给`polls.urls`进行下一步的处理。在`polls.urls`中,又匹配到了`<int:question_id>/`,最终结果就是调用该模式对应的detail视图,也就是下面的函数: ~~~ detail(request=<HttpRequest object>, question_id=34) ~~~ 函数中的`question_id=’34’`参数,是由`<int:question_id>/`而来。使用尖括号“捕获”这部分 URL,且以关键字参数的形式发送给视图函数。上述字符串的`question_id`部分定义了将被用于区分匹配模式的变量名,而`int`则是一个转换器决定了应该以什么变量类型匹配这部分的 URL 路径。 不要书写类似下面的较为愚蠢的包含`.html`的模式,它显然是没必要,不够简练的: ~~~ path('polls/latest.html', views.index), ~~~ <br /> ## **三、编写能实际干点活的视图** 每个视图至少做两件事之一:返回一个包含请求页面的HttpResponse对象或者弹出一个类似Http404的异常。其它的则随你便,你爱干嘛干嘛。 <br /> 下面是一个新的index视图,用于替代先前无用的index,它会根据发布日期显示最近的5个投票问卷。 ~~~ from django.http import HttpResponse from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] output = ', '.join([q.question_text for q in latest_question_list]) return HttpResponse(output) # 省略了那些没改动过的视图(detail, results, vote) ~~~ **这里有个非常重要的问题:在当前视图中的HTML页面是硬编码的。** 如果你想改变页面的显示内容,就必须修改这里的Python代码。为了解决这个问题,需要使用Django提供的模板系统,解耦视图和模板之间的硬连接。 <br /> 首先,在`polls`目录下创建一个新的`templates`目录,Django会在它里面查找模板文件。 <br /> 项目`settings.py`文件中的`TEMPLATES`配置项描述了 Django 如何载入和渲染模板。默认的设置文件设置了`DjangoTemplates`后端作为模板引擎,并将`APP_DIRS`设置成了 True。这一选项将会让`DjangoTemplates`在每个`INSTALLED_APPS`文件夹中寻找 "`templates`" 子目录。 <br /> 在刚才创建的`templates`目录中,再创建一个新的子目录名叫`polls`,进入该子目录,创建一个新的HTML文件`index.html`。换句话说,你的模板文件应该是`polls/templates/polls/index.html`。因为 Django 会寻找到对应的`app_directories`,所以你只需要使用`polls/index.html`就可以引用到这一模板了。 <br /> **模板命名空间:** 你也许会想,为什么不把模板文件直接放在`polls/templates`目录下,而是费劲的再建个子目录polls呢?设想这么个情况,有另外一个app,它也有一个名叫`index.html`的文件,当Django在搜索模板时,有可能就找到它,然后退出搜索,这就命中了错误的目标,不是我们想要的结果。解决这个问题的最好办法就是在templates目录下再建立一个与app同名的子目录,将自己所属的模板都放到里面,从而达到独立命名空间的作用,不会再出现引用错误。 现在,将下列代码写入文件`polls/templates/polls/index.html`: ~~~ {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} ~~~ 为了让教程看起来不那么长,所有的模板文件都只写出了核心代码。在你自己创建的项目中,你应该使用 完整的 HTML 文档,比如: ~~~ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> </head> <body> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} </body> </html> ~~~ 同时,修改视图文件`polls/views.py`,让新的`index.html`文件生效: ~~~ from django.http import HttpResponse from django.template import loader from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] template = loader.get_template('polls/index.html') context = { 'latest_question_list': latest_question_list, } return HttpResponse(template.render(context, request)) ~~~ 上面的代码会加载`polls/index.html`文件,并传递给它一个参数。这个参数是一个字典,包含了模板变量名和Python对象之间的映射关系。 <br /> 在浏览器中通过访问`/polls/`,你可以看到一个列表,包含`“What’s up”`的问卷,以及连接到其对应详细内容页面的链接点。 <br /> 注意:如果你显示的是`No polls are available.`说明你前面没有添加Questions对象。前面的大量手动API操作你没有做。没关系,我们在admin中追加对象就可以。 <br /> **快捷方式:render()** <br /> 在实际运用中,加载模板、传递参数,返回HttpResponse对象是一整套再常见不过的操作了,为了节省力气,Django提供了一个快捷方式:render函数,一步到位! 将`polls/views.py`中的index修改成下面的代码: ~~~ from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) ~~~ 注意,我们不再需要导入`loader`,而是从`django.shortcuts`导入了render。 <br /> render()函数的第一个位置参数是请求对象(就是view函数的第一个参数),这个参数是固定写法,不需要变动。第二个位置参数是模板文件。还可以有一个可选的第三参数,一个字典,包含需要传递给模板的数据。最后render函数返回一个经过字典数据渲染过的模板封装而成的HttpResponse对象。 <br /> ## **四、返回404错误** 现在让我们来编写返回具体问卷文本内容的视图: ~~~ # polls/views.py from django.http import Http404 from django.shortcuts import render from .models import Question # ... def detail(request, question_id): try: question = Question.objects.get(pk=question_id) except Question.DoesNotExist: raise Http404("Question does not exist") return render(request, 'polls/detail.html', {'question': question}) ~~~ 这里有个新知识点,如果请求的问卷ID不存在,那么会弹出一个Http404错误。 如果你想试试上面这段代码是否正常工作的话,你可以新建`polls/detail.html`文件,暂时写入下面的代码: ~~~ {{ question }} ~~~ <br /> **快捷方式:get_object_or_404()** 就像render函数一样,Django同样为你提供了一个偷懒的方式,替代上面的多行代码,那就是`get_object_or_404()`方法,参考下面的代码: polls/views.py ~~~ from django.shortcuts import get_object_or_404, render from .models import Question # ... def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) ~~~ 别说我没提醒你,和render一样,也需要从Django内置的快捷方式模块中导出`get_object_or_404()`! <br /> `get_object_or_404()`方法将一个Django模型作为第一个位置参数,后面可以跟上任意数量的关键字参数,如果对象不存在则弹出Http404错误。 <br /> 同样,还有一个`get_list_or_404()`方法,和上面的`get_object_or_404()`类似,只不过是用来替代`filter()`函数,当查询列表为空时弹出404错误。(filter是模型API中用来过滤查询结果的函数,它的结果是一个列表集。而get则是查询一个结果的方法,和filter是一个和多个的区别!) <br /> 为什么我们使用辅助函数`get_object_or_404()`而不是自己捕获`ObjectDoesNotExist`异常呢?还有,为什么模型 API 不直接抛出`ObjectDoesNotExist`而是抛出`Http404`呢?因为这样做会增加模型层和视图层的耦合性。指导 Django 设计的最重要的思想之一就是要保证松散耦合。一些受控的耦合将会被包含在`django.shortcuts`模块中。 <br /> ## **五、 使用模板系统** 回过头去看看我们的 detail视图。它向模板传递了上下文变量 question 。下面是 polls/detail.html 模板里正式的代码: ~~~ <h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }}</li> {% endfor %} </ul> ~~~ <br /> 在模板系统中圆点`.`是万能的魔法师,你可以用它访问对象的属性。在例子`{{ question.question_text }}`中,Django首先会在question对象中尝试查找一个字典,如果失败,则尝试查找属性,如果再失败,则尝试作为列表的索引进行查询。 <br /> 在`{% for %}`循环中的方法调用——`question.choice_set.all`其实就是Python的代码`question.choice_set.all()`,它将返回一组可迭代的`Choice`对象,并用在`{% for %}`标签中。 <br /> 这里我们对Django模板语言有个简单的印象就好,更深入的介绍放在后面。 <br /> ## **六、删除模板中硬编码的URLs** 在`polls/index.html`文件中,还有一部分硬编码存在,也就是`href`里的“/polls/”部分: ~~~ <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> ~~~ 它对于代码修改非常不利。设想如果你在urls.py文件里修改了路由表达式,那么你所有的模板中对这个url的引用都需要修改,这是无法接受的! 我们前面给urls定义了一个name别名,可以用它来解决这个问题。具体代码如下: ~~~ <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> ~~~ Django会在`polls.urls`文件中查找`name='detail'`的路由,具体的就是下面这行: ~~~ path('<int:question_id>/', views.detail, name='detail'), ~~~ 举个栗子,如果你想将polls的detail视图的URL更换为`polls/specifics/12/`,那么你不需要在模板中重新修改url地址了,仅仅只需要在`polls/urls.py`文件中,将对应的正则表达式改成下面这样的就行了,所有模板中对它的引用都会自动修改成新的链接: ~~~ # 添加新的单词'specifics' path('specifics/<int:question_id>/', views.detail, name='detail'), ~~~ <br /> ## **七、URL names的命名空间** 本教程例子中,只有一个app,也就是polls,但是在现实中很显然会有5个、10个、更多的app同时存在一个项目中。Django是如何区分这些app之间的URL name呢?举个例子,`polls`应用有`detail`视图,可能另一个博客应用也有同名的视图。Django 如何知道`{% url %}`标签到底对应哪一个应用的 URL 呢? <br /> **答案是使用URLconf的命名空间。** 可以在polls/urls.py文件的开头部分,添加一个`app_name`的变量来指定该应用的命名空间: ~~~ from django.urls import path from . import views app_name = 'polls' # 重点是这一行 urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] ~~~ 现在,让我们将代码修改得更严谨一点,将`polls/templates/polls/index.html`中的 ~~~ <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> ~~~ 修改为: ~~~ <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> ~~~ **注意引用方法是冒号,不是圆点也不是斜杠!**