在我们的 current_datetime
视图范例中,尽管内容是动态的,但是 URL( /time/
)是静态的。在大多数动态 web 应用程序,URL
通常都包含有相关的参数。举个例子,一家在线书店会为每一本书提供一个
URL,如:/books/243/、/books/81196/。
让我们创建第三个视图来显示当前时间和加上时间偏差量的时间,设计是这样的:
/time/plus/1/
显示当前时间 +1 个小时的页面 /time/plus/2/
显示当前时间 +2 个小时的页面 /time/plus/3/
显示当前时间 +3 个小时的页面,以此类推。
新手可能会考虑写不同的视图函数来处理每个时间偏差量,URL 配置看起来就象这样:
urlpatterns = [ path('admin/', admin.site.urls), re_path('^time/$', current_datetime), re_path('^time/plus/1/$', one_hour_ahead), re_path('^time/plus/2/$', two_hours_ahead), re_path('^time/plus/3/$', three_hours_ahead), re_path('^time/plus/4/$', four_hours_ahead), ]
很明显,这样处理是不太妥当的。不但有很多冗余的视图函数,而且整个应用也被限制了只支持预先定义好的时间段,2 小时,3 小时,或者 4 小时。如果哪天我们要实现 5 小时,我们就不得不再单独创建新的视图函数和配置URL,既重复又混乱。我们需要在这里做一点抽象,提取一些共同的东西出来。
如果你有其它 web 平台的开发经验(如PHP或Java),你可能会想:嘿!让我们用查询字符串参数吧!就像 /time/plus?hours=3 里面的小时应该在查询字符串中被参数 hours 指定(问号后面的是参数)。
你 可以 在 Django 里也这样做 (如果你真的想要这样做,我们稍后会告诉你怎么做),但是
Django 的一个核心理念就是 URL 必须看起来漂亮。URL /time/plus/3/
更加清晰,更简单,也更有可读性,可以很容易的大声念出来,因为它是纯文本,没有查询字符串那么复杂。 漂亮的URL就像是高质量的Web应用的一个标志。
Django 的 URL 配置系统可以使你很容易的设置漂亮的 URL,而尽量不要考虑它的 反面 。
那么,我们如何设计程序来处理任意数量的时差?答案是:使用通配符(wildcard URLpatterns)。正如我们之前提到过,一个 URL 模式就是一个正则表达式。因此,这里可以使用 d+ 来匹配 1 个以上的数字。
urlpatterns = [ # ... re_path(r'^time/plus/\d+/$', hours_ahead), # ... ]
这里使用 # …
来表示省略了其它可能存在的 URL 模式定义。(见上)
这个 URL 模式将匹配类似 /time/plus/2/
, /time/plus/25/
,甚至
/time/plus/100000000000/
的任何 URL。更进一步,让我们把它限制在最大允许 99
个小时,这样我们就只允许一个或两个数字,正则表达式的语法就是 \d{1,2}
:
re_path(r'^time/plus/\d{1,2}/$', hours_ahead),
在建造 Web 应用的时候,尽可能多考虑可能的数据输入是很重要的,然后决定哪些我们可以接受。在这里我们就设置了 99 个小时的时间段限制。
另外一个重点,正则表达式字符串的开头字母 “r”。它告诉 Python 这是个原始字符串,不需要处理里面的反斜杠(转义字符)。在普通 Python 字符串中,反斜杠用于特殊字符的转义。比如 n 转义成一个换行符。当你用 r 把它标示为一个原始字符串后,Python 不再视其中的反斜杠为转义字符。也就是说,“n” 是两个字符串:“” 和 “n”。由于反斜杠在 Python 代码和正则表达式中有冲突,因此建议你在 Python 定义正则表达式时都使用原始字符串。从现在开始,本教程所有 URL 模式都用原始字符串。
现在我们已经设计了一个带通配符的
URL,我们需要一个方法把它传递到视图函数里去,这样 我们只用一个视图函数就可以处理所有的时间段了。我们使用圆括号把参数在
URL 模式里标识出来。在这个例子中,我们想要把这些数字作为参数,用圆括号把 \d{1,2}
包围起来:
re_path(r'^time/plus/(\d{1,2})/$', hours_ahead),
如果你熟悉正则表达式,那么你应该已经了解,正则表达式也是用圆括号来从文本里 提取 数据的。
最终的 URLconf 包含上面两个视图,如:
from django.contrib import admin from django.urls import path from django.urls import re_path from mysite.views import hours_ahead urlpatterns = [ path('admin/', admin.site.urls), re_path(r'^time/plus/(\d{1,2})/$', hours_ahead), ]
现在开始写 hours_ahead
视图。
这个例子中,我们先写了 URLpattern ,然后是视图,但是在前面的例子中,我们先写了视图,然后是 URLpattern。哪一种方式比较好?
嗯,怎么说呢,每个开发者是不一样的。
如果你是喜欢从总体上来把握事物(注: 或译为“大局观”)类型的人,你应该会想在项目开始的时候就写下所有的URL配置。
如果你从更像是一个自底向上的开发者,你可能更喜欢先写视图,然后把它们挂接到 URL 上。这同样是可以的。
最后,取决与你喜欢哪种技术,两种方法都是可以的。(见上)
hours_ahead
和我们以前写的 current_datetime
很象,关键的区别在于:它多了一个额外参数,时间差。以下是 view 代码:
from django.http import Http404, HttpResponse import datetime def hours_ahead(request, offset): try: offset = int(offset) except ValueError: raise Http404() dt = datetime.datetime.now() + datetime.timedelta(hours=offset) html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt) return HttpResponse(html)
让我们逐行分析一下代码:
视图函数,hoursahead
, 有 两个 参数:request
和offset
. (见上)我们在这个函数中要做的第一件事情就是在request
是一个HttpRequest
对象, 就像在current_datetime
中一样. 再说一次好了: 每一个视图 总是_ 以一个HttpRequest
对象作为 它的第一个参数。 (见上)offset
是从匹配的URL里提取出来的。 例如:如果请求URL是/time/plus/3/,那么offset将会是3;如果请求URL是/time/plus/21/,那么 offset 将会是 21。请注意:捕获值永远都是字符串(string)类型,而不会是整数(integer)类型,即使这个字符串全由数字构成(如:“21”)。(从技术上来说,捕获值总是 Unicode objects,而不是简单的 Python 字节串,但目前不需要担心这些差别。)在这里我们命名变量为offset
,你也可以任意命名它,只要符合 Python 的语法。 变量名是无关紧要的,重要的是它的位置,它是这个函数的第二个参数 (在request
的后面)。你还可以使用关键字来定义它,而不是用位置。offset
上调用int()
。 这会把这个字符串值转换为整数。请留意:如果你在一个不能转换成整数类型的值上调用 int(),Python 将抛出一个 ValueError 异常。如:int(‘foo’)。在这个例子中,如果我们遇到 ValueError 异常,我们将转为抛出 django.http.Http404 异常——正如你想象的那样:最终显示 404 页面(提示信息:页面不存在)。机灵的读者可能会问:我们在 URL 模式中用正则表达式 (d{1,2}) 约束它,仅接受数字怎么样?这样无论如何,offset 都是由数字构成的。答案是:我们不会这么做,因为 URLpattern 提供的是“适度但有用”级别的输入校验。万一这个视图函数被其它方式调用,我们仍需自行检查 ValueError。实践证明,在实现视图函数时,不臆测参数值的做法是比较好的。松散耦合,还记得么?下一行,计算当前日期/时间,然后加上适当的小时数。在 current_datetime 视图中,我们已经见过 datetime.datetime.now()。这里新的概念是执行日期/时间的算术操作。我们需要创建一个 datetime.timedelta 对象和增加一个 datetime.datetime 对象。结果保存在变量 dt 中。这一行还说明了,我们为什么在 offset 上调用 int()——datetime.timedelta 函数要求 hours 参数必须为整数类型。这行和前面的那行的的一个微小差别就是,它使用带有两个值的 Python 的格式化字符串功能,而不仅仅是一个值。因此,在字符串中有两个%s
符号和一个以进行插入的值的元组:(offset, dt)
。最终,返回一个 HTML 的 HttpResponse。如今,这种方式已经过时了。
在完成视图函数和 URL 配置编写后,启动 Django 开发服务器,用浏览器访问 http://127.0.0.1:8000/time/plus/3/
来确认它工作正常。然后是 http://127.0.0.1:8000/time/plus/5/
。再然后是 http://127.0.0.1:8000/time/plus/24/
。最后,访问
http://127.0.0.1:8000/time/plus/100/
,如下是我一个案例图:
来检验 URL 配置里设置的模式是否只接受一个或两个数字;Django 会显示一个 Page not found error 页面, 和以前看到的 404 错误一样。 访问URL http://127.0.0.1:8000/time/plus/
(没有 定义时间差)
也会抛出 404 错误。
花几分钟时间欣赏一下我们写好的 Web 应用程序,然后我们再来搞点小破坏。我们故意在 views.py
文件中引入一项 Python 错误,注释掉 hours_ahead
视图中的 offset = int(offset)
一行。
def hours_ahead(request, offset): # try: # offset = int(offset) # except ValueError: # raise Http404() dt = datetime.datetime.now() + datetime.timedelta(hours=offset) html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt) return HttpResponse(html)
启动开发服务器,然后访问 /time/plus/3/
。你会看到一个包含大量信息的出错页,最上面 的一条 TypeError
信息是: "unsupported
type for timedelta hours component: unicode"
。
怎么回事呢?是的,datetime.timedelta
函数要求 hours
参数必须为整型,而我们注释掉了将 offset
转为整型的代码。这样导致
datetime.timedelta
弹出 TypeError
异常。
这个例子是为了展示 Django 的出错页面。我们来花些时间看一看这个出错页,了解一下其中给出了哪些信息。
以下是值得注意的一些要点:
在页面顶部,你可以得到关键的异常信息:异常数据类型、异常的参数 (如本例中的"unsupported type"
)、在哪个文件中引发了异常、出错的行号等等。在关键异常信息下方,该页面显示了对该异常的完整 Python 追踪信息。这类似于你在 Python 命令行解释器中获得的追溯信息,只不过后者更具交互性。 对栈中的每一帧,Django 均显示了其文件名、函数或方法名、行号及该行源代码。点击该行代码 (以深灰色显示),你可以看到出错行的前后几行,从而得知相关上下文情况。点击栈中的任何一帧的“Local vars”可以看到一个所有局部变量的列表,以及在出错那一帧时它们的值。这些调试信息相当有用。注意“Traceback”下面的“Switch to copy-and-paste view”文字。 点击这些字,追溯会 切换另一个视图,它让你很容易地复制和粘贴这些内容。当你想同其他人分享这些异常 追溯以获得技术支持时(比如在 Django 的 IRC 聊天室或邮件列表中),可以使用它。你按一下下面的“Share this traceback on a public Web site”按钮,它将会完成这项工作。点击它以传回追溯信息至 http://www.dpaste.com/,在那里你可以得到一个单独的 URL 并与其他人分享你的追溯信息。接下来的“Request information”部分包含了有关产生错误的 Web 请求的大量信息: GET 和 POST、cookie 值、元数据(象 CGI 头)。在后面章节里给出了 request的对象的完整参考。Request 信息的下面,“Settings”列出了 Django 使用的具体配置信息。(我们已经提及过 ROOT_URLCONF,接下来我们将向你展示各式的 Django 设置。)
Django 的出错页某些情况下有能力显示更多的信息,比如模板语法错误。我们讨论 Django
模板系统时再说它们。现在,取消 offset = int(offset)
这行的注释,让它重新正常工作。
不知道你是不是那种使用小心放置的 print
语句来帮助调试的程序员?你其实可以用
Django 出错页来做这些,而不用 print
语句。在你视图的任何位置,临时插入一个
assert False
来触发出错页。然后,你就可以看到局部变量和程序语句了。这里有个使用 hours_ahead 视图的例子:
def hours_ahead(request, offset): try: offset = int(offset) except ValueError: raise Http404() dt = datetime.datetime.now() + datetime.timedelta(hours=offset) assert False html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt) return HttpResponse(html)
最后,很显然这些信息很多是敏感的,它暴露了你 Python 代码的内部结构以及 Django 配置,在 Internet 上公开这信息是很愚蠢的。不怀好意的人会尝试使用它攻击你的 Web 应用程序,做些下流之事。因此,Django 出错信息仅在 debug 模式下才会显现。我们稍后说明如何禁用 debug 模式。现在,你只要知道 Django 服务器在你开启它时默认运行在 debug 模式就行了。(听起来很熟悉?页面没有发现错误,如前所述,工作正常。)
目前为止,我们已经写好了视图函数和硬编码的 HTML。在演示核心概念时,我们所作的是为了保持简单。但是在现实世界中,这差不多总是个坏主意。
幸运的是,Django 内建有一个简单有强大的模板处理引擎来让你分离两种工作:下一章,我们将学习模板引擎。