🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 第三章:视图和URL配置 # 第三章:视图和URL配置 前一章中,我们解释了如何建立一个 Django 项目并启动 Django 开发服务器。 在这一章,你将会学到用Django创建动态网页的基本知识。 ## 你的第一个基于Django的页面: Hello World 正如我们的第一个目标,创建一个网页,用来输出这个著名的示例信息: Hello world. 如果你曾经发布过Hello world页面,但是没有使用网页框架,只是简单的在`hello.html`文本文件中输入Hello World,然后上传到任意的一个网页服务器上。 注意,在这个过程中,你已经说明了两个关于这个网页的关键信息: 它包括(字符串 `"Hello world"`)和它的URL( `http://www.example.com/hello.html` , 如果你把文件放在子目录,也可能是 `http://www.example.com/files/hello.html`)。 使用Django,你会用不同的方法来说明这两件事 页面的内容是靠*view function(视图函数)* 来产生,URL定义在 *URLconf* 中。首先,我们先写一个Hello World视图函数。 ### 第一份视图: 在上一章使用`django-admin.py startproject`制作的`mysite`文件夹中,创建一个叫做`views.py`的空文件。这个Python模块将包含这一章的视图。 请留意,Django对于view.py的文件命名没有特别的要求,它不在乎这个文件叫什么。但是根据约定,把它命名成view.py是个好主意,这样有利于其他开发者读懂你的代码,正如你很容易的往下读懂本文。 我们的Hello world视图非常简单。 这些是完整的函数和导入声明,你需要输入到`views.py`文件: ``` <pre class="calibre9">``` from django.http import HttpResponse def hello(request): return HttpResponse("Hello world") ``` ``` 我们逐行逐句地分析一遍这段代码: > 首先,我们从 `django.http` 模块导入(import) `HttpResponse` 类。参阅附录 H 了解更多关于 `HttpRequest` 和 `HttpResponse` 的细节。 我们需要导入这些类,因为我们会在后面用到。 > > 接下来,我们定义一个叫做`hello` 的视图函数。 > > 每个视图函数至少要有一个参数,通常被叫作`request`。 这是一个触发这个视图、包含当前Web请求信息的对象,是类`django.http.HttpRequest`的一个实例。在这个示例中,我们虽然不用`request`做任何事情,然而它仍必须是这个视图的第一个参数。 > > 注意视图函数的名称并不重要;并不一定非得以某种特定的方式命名才能让 Django 识别它。 在这里我们把它命名为:hello,是因为这个名称清晰的显示了视图的用意。同样地,你可以用诸如:hello\_wonderful\_beautiful\_world,这样难看的短句来给它命名。 在下一小节(Your First URLconf),将告诉你Django是如何找到这个函数的。 > > 这个函数只有简单的一行代码: 它仅仅返回一个HttpResponse对象,这个对象包含了文本“Hello world”。 这里主要讲的是: 一个视图就是Python的一个函数。这个函数第一个参数的类型是HttpRequest;它返回一个HttpResponse实例。为了使一个Python的函数成为一个Django可识别的视图,它必须满足这两个条件。 (也有例外,但是我们稍后才会接触到。 ### 你的第一个URLconf 现在,如果你再运行:python manage.py runserver,你还将看到Django的欢迎页面,而看不到我们刚才写的Hello world显示页面。 那是因为我们的mysite项目还对hello视图一无所知。我们需要通过一个详细描述的URL来显式的告诉它并且激活这个视图。 (继续我们刚才类似发布静态HTML文件的例子。现在我们已经创建了HTML文件,但还没有把它上传至服务器的目录。)为了绑定视图函数和URL,我们使用URLconf。 *URLconf* 就像是 Django 所支撑网站的目录。 它的本质是 URL 模式以及要为该 URL 模式调用的视图函数之间的映射表。 你就是以这种方式告诉 Django,对于这个 URL 调用这段代码,对于那个 URL 调用那段代码。 例如,当用户访问/foo/时,调用视图函数foo\_view(),这个视图函数存在于Python模块文件view.py中。 前一章中执行 `django-admin.py startproject` 时,该脚本会自动为你建了一份 URLconf(即 `urls.py` 文件)。 默认的urls.py会像下面这个样子: ``` <pre class="calibre9">``` from django.conf.urls.defaults import * # Uncomment the next two lines to enable the admin: # from django.contrib import admin # admin.autodiscover() urlpatterns = patterns('', # Example: # (r'^mysite/', include('mysite.foo.urls')), # Uncomment the admin/doc line below and add 'django.contrib.admindocs' # to INSTALLED_APPS to enable admin documentation: # (r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: # (r'^admin/', include(admin.site.urls)), ) ``` ``` 默认的URLconf包含了一些被注释起来的Django中常用的功能,仅仅只需去掉这些注释就可以开启这些功能. 下面是URLconf中忽略被注释的行后的实际内容 ``` <pre class="calibre9">``` from django.conf.urls.defaults import * urlpatterns = patterns('', ) ``` ``` 让我们逐行解释一下代码: - 第一行导入django.conf.urls.defaults下的所有模块,它们是Django URLconf的基本构造。 这包含了一个patterns函数。 - 第二行调用 `patterns()` 函数并将返回结果保存到 `urlpatterns` 变量。patterns函数当前只有一个参数—一个空的字符串。 (这个字符串可以被用来表示一个视图函数的通用前缀。具体我们将在第八章里面介绍。) 当前应该注意是 `urlpatterns` 变量, Django 期望能从 `ROOT_URLCONF` 模块中找到它。 该变量定义了 URL 以及用于处理这些 URL 的代码之间的映射关系。 默认情况下,URLconf 所有内容都被注释起来了——Django 应用程序还是白版一块。 (注:那是上一节中Django怎么知道显示欢迎页面的原因。 如果 URLconf 为空,Django 会认定你才创建好新项目,因此也就显示那种信息。 如果想在URLconf中加入URL和view,只需增加映射URL模式和view功能的Python tuple即可. 这里演示如何添加view中hello功能. ``` <pre class="calibre9">``` from django.conf.urls.defaults import * from mysite.views import hello urlpatterns = patterns('', ('^hello/$', hello), ) ``` ``` 请留意:为了简洁,我们移除了注释代码。 如果你喜欢的话,你可以保留那些行。) 我们做了两处修改。 - 首先,我们从模块 (在 Python 的 import 语法中, `mysite/views.py` 转译为 `mysite.views` ) 中引入了 `hello` 视图。 (这假设mysite/views.py在你的Python搜索路径上。关于搜索路径的解释,请参照下文。) - 接下来,我们为urlpatterns加上一行: (‘^hello/$’, hello), 这行被称作URLpattern,它是一个Python的元组。元组中第一个元素是模式匹配字符串(正则表达式);第二个元素是那个模式将使用的视图函数。 简单来说,我们只是告诉 Django,所有指向 URL `/hello/` 的请求都应由 `hello` 这个视图函数来处理。 Python 搜索路径 *Python 搜索路径* 就是使用 `import` 语句时,Python 所查找的系统目录清单。 举例来说,假定你将 Python 路径设置为 `['','/usr/lib/python2.4/site-packages','/home/username/djcode/']` 。如果执行代码 `from foo import bar` ,Python 将会首先在当前目录查找 `foo.py` 模块( Python 路径第一项的空字符串表示当前目录)。 如果文件不存在,Python将查找 `/usr/lib/python2.4/site-packages/foo.py` 文件。 如果你想看Python搜索路径的值,运行Python交互解释器,然后输入: ``` <pre class="calibre9">``` >>> import sys >>> print sys.path ``` ``` 通常,你不必关心 Python 搜索路径的设置。 Python 和 Django 会在后台自动帮你处理好。 讨论一下URLpattern的语法是值得的,因为它不是显而易见的。 虽然我们想匹配地址/hello/,但是模式看上去与这有点差别。 这就是为什么: > Django在检查URL模式前,移除每一个申请的URL开头的斜杠(/)。 这意味着我们为/hello/写URL模式不用包含斜杠(/)。(刚开始,这样可能看起来不直观,但这样的要求简化了许多工作,如URL模式内嵌,我们将在第八章谈及。) > > 模式包含了一个尖号(^)和一个美元符号($)。这些都是正则表达式符号,并且有特定的含义: 上箭头要求表达式对字符串的头部进行匹配,美元符号则要求表达式对字符串的尾部进行匹配。 > > 最好还是用范例来说明一下这个概念。 如果我们用尾部不是$的模式’^hello/’,那么任何以/hello/开头的URL将会匹配,例如:/hello/foo 和/hello/bar,而不仅仅是/hello/。类似地,如果我们忽略了尖号(^),即’hello/$’,那么任何以hello/结尾的URL将会匹配,例如:/foo/bar/hello/。如果我们简单使用hello/,即没有^开头和$结尾,那么任何包含hello/的URL将会匹配,如:/foo/hello/bar。因此,我们使用这两个符号以确保只有/hello/匹配,不多也不少。 > > 你大多数的URL模式会以^开始、以$结束,但是拥有复杂匹配的灵活性会更好。 > > 你可能会问:如果有人申请访问/hello(尾部没有斜杠/)会怎样。 因为我们的URL模式要求尾部有一个斜杠(/),那个申请URL将不匹配。 然而,默认地,任何不匹配或尾部没有斜杠(/)的申请URL,将被重定向至尾部包含斜杠的相同字眼的URL。 (这是受配置文件setting中APPEND\_SLASH项控制的,参见附件D。) > > 如果你是喜欢所有URL都以’/’结尾的人(Django开发者的偏爱),那么你只需要在每个URL后添加斜杠,并且设置”APPEND\_SLASH”为”True”. 如果不喜欢URL以斜杠结尾或者根据每个URL来决定,那么需要设置”APPEND\_SLASH”为”False”,并且根据你自己的意愿来添加结尾斜杠/在URL模式后. 另外需要注意的是,我们把hello视图函数作为一个对象传递,而不是调用它。 这是 Python (及其它动态语言的) 的一个重要特性: 函数是一级对象(first-class objects), 也就是说你可以像传递其它变量一样传递它们。 很酷吧? 启动Django开发服务器来测试修改好的 URLconf, 运行命令行 `python manage.py runserver` 。 (如果你让它一直运行也可以,开发服务器会自动监测代码改动并自动重新载入,所以不需要手工重启) 开发服务器的地址是 <http://127.0.0.1:8000/> ,打开你的浏览器访问 <http://127.0.0.1:8000/hello/> 。 你就可以看到输出结果了。 开发服务器将自动检测Python代码的更改来做必要的重新加载, 所以你不需要重启Server在代码更改之后。服务器运行地址`http://127.0.0.1:8000/` ,所以打开浏览器直接输入`http://127.0.0.1:8000/hello/` ,你将看到由你的Django视图输出的Hello world。 万岁! 你已经创建了第一个Django的web页面。 正则表达式 *正则表达式* (或 *regexes* ) 是通用的文本模式匹配的方法。 Django URLconfs 允许你 使用任意的正则表达式来做强有力的URL映射,不过通常你实际上可能只需要使用很少的一 部分功能。 这里是一些基本的语法。 符号匹配`.` (dot)任意单一字符`\\d`任意一位数字`\[A-Z\]``A` 到 `Z`中任意一个字符(大写)`\[a-z\]``a` 到 `z`中任意一个字符(小写)`\[A-Za-z\]``a` 到 `z`中任意一个字符(不区分大小写)`+`匹配一个或更多 (例如, `\\d+` 匹配一个或 多个数字字符)`\[^/\]+`一个或多个不为‘/’的字符`\*`零个或一个之前的表达式(例如:`\\d?` 匹配零个或一个数字)`\*`匹配0个或更多 (例如, `\\d\*` 匹配0个 或更多数字字符)`{1,3}`介于一个和三个(包含)之前的表达式(例如,`\\d{1,3}`匹配一个或两个或三个数字)有关正则表达式的更多内容,请访问 <http://www.djangoproject.com/r/python/re-module/>. ### 关于“404错误”的快速参考 目前,我们的URLconf只定义了一个单独的URL模式: 处理URL `/hello/` 。 当请求其他URL会怎么样呢? 让我们试试看,运行Django开发服务器并访问类似 `http://127.0.0.1:8000/goodbye/` 或者 `http://127.0.0.1:8000/hello/subdirectory/` ,甚至 `http://127.0.0.1:8000/` (网站根目录)。 你将会看到一个 “Page not found” 页面(图 3-2)。 因为你的URL申请在URLconf中没有定义,所以Django显示这条信息。 ![](https://img.kancloud.cn/c4/3c/c43cd3d6d431c3693cebd8775a280b8a_857x609.png) 图3-1: Django的404 Error页 这个页面比原始的404错误信息更加实用。 它同时精确的告诉你Django调用哪个URLconf及其包含的每个模式。 这样,你应该能了解到为什么这个请求会抛出404错误。 当然,这些敏感的信息应该只呈现给你-开发者。 如果是部署到了因特网上的站点就不应该暴露 这些信息。 出于这个考虑,这个“Page not found”页面只会在 *调试模式(debug mode)* 下 显示。 我们将在以后说明怎么关闭调试模式。 ### 关于网站根目录的快速参考。 在最后一节,如果你想通过[http://127.0.0.1:8000/看网站根目录你将看到一个404错误消息。Django不会增加任何东西在网站根目录,在任何情况下这个URL都不是特殊的](http://127.0.0.1:8000/%E7%9C%8B%E7%BD%91%E7%AB%99%E6%A0%B9%E7%9B%AE%E5%BD%95%E4%BD%A0%E5%B0%86%E7%9C%8B%E5%88%B0%E4%B8%80%E4%B8%AA404%E9%94%99%E8%AF%AF%E6%B6%88%E6%81%AF%E3%80%82Django%E4%B8%8D%E4%BC%9A%E5%A2%9E%E5%8A%A0%E4%BB%BB%E4%BD%95%E4%B8%9C%E8%A5%BF%E5%9C%A8%E7%BD%91%E7%AB%99%E6%A0%B9%E7%9B%AE%E5%BD%95%EF%BC%8C%E5%9C%A8%E4%BB%BB%E4%BD%95%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%99%E4%B8%AAURL%E9%83%BD%E4%B8%8D%E6%98%AF%E7%89%B9%E6%AE%8A%E7%9A%84) 就像在URLconf中的其他条目一样,它也依赖于指定给它的URL模式. 尽管匹配网站根目录的URL模式不能想象,但是还是值得提一下的. 当为网站根目录实现一个视图,你需要使用URL模式`‘^$’` , 它代表一个空字符串。 例如: ``` <pre class="calibre9">``` from mysite.views import hello, my_homepage_view urlpatterns = patterns('', ('^$', my_homepage_view), # ... ) ``` ``` ## Django是怎么处理请求的 在继续我们的第二个视图功能之前,让我们暂停一下去了解更多一些有关Django是怎么工作的知识. 具体地说,当你通过在浏览器里敲[http://127.0.0.1:8000/hello/来访问Hello](http://127.0.0.1:8000/hello/%E6%9D%A5%E8%AE%BF%E9%97%AEHello) world消息得时候,Django在后台有些什么动作呢? 所有均开始于setting文件。当你运行python manage.py runserver,脚本将在于manage.py同一个目录下查找名为setting.py的文件。这个文件包含了所有有关这个Django项目的配置信息,均大写: TEMPLATE\_DIRS , DATABASE\_NAME , 等. 最重要的设置时ROOT\_URLCONF,它将作为URLconf告诉Django在这个站点中那些Python的模块将被用到 还记得什么时候django-admin.py startproject创建文件settings.py和urls.py吗?自动创建的settings.py包含一个ROOT\_URLCONF配置用来指向自动产生的urls.py. 打开文件settings.py你将看到如下: ``` <pre class="calibre9">``` ROOT_URLCONF = 'mysite.urls' ``` ``` 相对应的文件是mysite/urls.py 当访问 URL `/hello/` 时,Django 根据 `ROOT_URLCONF` 的设置装载 URLconf 。 然后按顺序逐个匹配URLconf里的URLpatterns,直到找到一个匹配的。 当找到这个匹配 的URLpatterns就调用相关联的view函数,并把 `HttpRequest` 对象作为第一个参数。 (稍后再给出 `HttpRequest` 的更多信息) (我们将在后面看到HttpRequest的标准) 正如我们在第一个视图例子里面看到的,一个视图功能必须返回一个HttpResponse。 一旦做完,Django将完成剩余的转换Python的对象到一个合适的带有HTTP头和body的Web Response,(例如,网页内容)。 总结一下: 1. 进来的请求转入/hello/. 2. Django通过在ROOT\_URLCONF配置来决定根URLconf. 3. Django在URLconf中的所有URL模式中,查找第一个匹配/hello/的条目。 4. 如果找到匹配,将调用相应的视图函数 5. 视图函数返回一个HttpResponse 6. Django转换HttpResponse为一个适合的HTTP response, 以Web page显示出来 你现在知道了怎么做一个 Django-powered 页面了,真的很简单,只需要写视图函数并用 URLconfs把它们和URLs对应起来。 你可能会认为用一系列正则表达式将URLs映射到函数也许会比较慢,但事实却会让你惊讶。 ## 第二个视图: 动态内容 我们的Hello world视图是用来演示基本的Django是如何工作的,但是它不是一个动态网页的例子,因为网页的内容一直是一样的. 每次去查看/hello/,你将会看到相同的内容,它类似一个静态HTML文件。 我们的第二个视图,将更多的放些动态的东西例如当前日期和时间显示在网页上 这将非常好,简单的下一步,因为它不引入了数据库或者任何用户的输入,仅仅是输出显示你的服务器的内部时钟. 它仅仅有限度的比Helloworld刺激一些,但是它将演示一些新的概念 这个视图需要做两件事情: 计算当前日期和时间,并返回包含这些值的HttpResponse 如果你对python很有经验,那肯定知道在python中需要利用datetime模块去计算时间 下面演示如何去使用它: ``` <pre class="calibre9">``` >>> import datetime >>> now = datetime.datetime.now() >>> now datetime.datetime(2008, 12, 13, 14, 9, 39, 2731) >>> print now 2008-12-13 14:09:39.002731 ``` ``` 以上代码很简单,并没有涉及Django。 它仅仅是Python代码。 需要强调的是,你应该意识到哪些是纯Python代码,哪些是Django特性代码。 (见上) 因为你学习了Django,希望你能将Django的知识应用在那些不一定需要使用Django的项目上。 为了让Django视图显示当前日期和时间,我们仅需要把语句:datetime.datetime.now()放入视图函数,然后返回一个HttpResponse对象即可。代码如下: ``` <pre class="calibre9">``` from django.http import HttpResponse import datetime def current_datetime(request): now = datetime.datetime.now() html = "<html><body>It is now %s.</body></html>" % now return HttpResponse(html) ``` ``` 正如我们的hello函数一样,这个函数也保存在view.py中。为了简洁,上面我们隐藏了hello函数。下面是完整的view.py文件内容: ``` <pre class="calibre9">``` from django.http import HttpResponse import datetime def hello(request): return HttpResponse("Hello world") def current_datetime(request): now = datetime.datetime.now() html = "<html><body>It is now %s.</body></html>" % now return HttpResponse(html) ``` ``` (从现在开始,如非必要,本文不再重复列出先前的代码。 你应该懂得识别哪些是新代码,哪些是先前的。) (见上) 让我们分析一下改动后的views.py: > 在文件顶端,我们添加了一条语句:import datetime。这样就可以计算日期了。 > > 函数中的第一行代码计算当前日期和时间,并以 `datetime.datetime` 对象的形式保存为局部变量 `now` 。 > > 函数的第二行代码用 Python 的格式化字符串(format-string)功能构造了一段 HTML 响应。 字符串中的%s是占位符,字符串后面的百分号表示用它后面的变量now的值来代替%s。变量%s是一个datetime.datetime对象。它虽然不是一个字符串,但是%s(格式化字符串)会把它转换成字符串,如:2008-12-13 14:09:39.002731。这将导致HTML的输出字符串为:It is now 2008-12-13 14:09:39.002731。 > > (目前HTML是有错误的,但我们这样做是为了保持例子的简短。) > > 最后,正如我们刚才写的hello函数一样,视图返回一个HttpResponse对象,它包含生成的响应。 添加上述代码之后,还要在urls.py中添加URL模式,以告诉Django由哪一个URL来处理这个视图。 用/time/之类的字眼易于理解: ``` <pre class="calibre9">``` from django.conf.urls.defaults import * from mysite.views import hello, current_datetime urlpatterns = patterns('', ('^hello/$', hello), ('^time/$', current_datetime), ) ``` ``` 这里,我们修改了两个地方。 首先,在顶部导入current\_datetime函数; 其次,也是比较重要的:添加URL模式来映射URL中的/time/和新视图。 理解了么? 写好视图并且更新URLconf之后,运行命令python manage.py runserver以启动服务,在浏览器中输入[http://127.0.0.1:8000/time/。](http://127.0.0.1:8000/time/%E3%80%82) 你将看到当前的日期和时间。 Django时区 视乎你的机器,显示的日期与时间可能和实际的相差几个小时。 这是因为Django是有时区意识的,并且默认时区为America/Chicago。 (它必须有个值,它的默认值是Django的诞生地:美国/芝加哥)如果你处在别的时区,你需要在settings.py文件中更改这个值。请参见它里面的注释,以获得最新世界时区列表。 ## URL配置和松耦合 现在是好时机来指出Django和URL配置背后的哲学: *松耦合* 原则。 简单的说,松耦合是一个 重要的保证互换性的软件开发方法。 Django的URL配置就是一个很好的例子。 在Django的应用程序中,URL的定义和视图函数之间是松 耦合的,换句话说,决定URL返回哪个视图函数和实现这个视图函数是在两个不同的地方。 这使得 开发人员可以修改一块而不会影响另一块。 例如,考虑一下current\_datetime视图。 如果我们想把它的URL 从原来的 `/time/` 改变到 `/currenttime/` ,我们只需要快速的修改一下URL配置即可, 不用担心这个函数的内部实现。 同样的,如果我们想要修改这个函数的内部实现也不用担心会影响 到对应的URL。 此外,如果我们想要输出这个函数到 *一些* URL, 我们只需要修改URL配置而不用 去改动视图的代码。 在这个例子里,current\_datetime被两个URL使用。 这是一个故弄玄虚的例子,但这个方法迟早会用得上。 ``` <pre class="calibre9">``` urlpatterns = patterns('', ('^hello/$', hello), ('^time/$', current_datetime), ('^another-time-page/$', current_datetime), ) ``` ``` URLconf和视图是松耦合的。 我们将在本书中继续给出这一重要哲学的相关例子。 ## 第三个视图 动态URL 在我们的`current_datetime` 视图范例中,尽管内容是动态的,但是URL ( `/time/` )是静态的。 在 大多数动态web应用程序,URL通常都包含有相关的参数。 举个例子,一家在线书店会为每一本书提供一个URL,如:/books/243/、/books/81196/。 让我们创建第三个视图来显示当前时间和加上时间偏差量的时间,设计是这样的: `/time/plus/1/` 显示当前时间+1个小时的页面 `/time/plus/2/` 显示当前时间+2个小时的页面 `/time/plus/3/` 显示当前时间+3个小时的页面,以此类推。 新手可能会考虑写不同的视图函数来处理每个时间偏差量,URL配置看起来就象这样: ``` <pre class="calibre9">``` 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), ) ``` ``` 很明显,这样处理是不太妥当的。 不但有很多冗余的视图函数,而且整个应用也被限制了只支持 预先定义好的时间段,2小时,3小时,或者4小时。 如果哪天我们要实现 *5* 小时,我们就 不得不再单独创建新的视图函数和配置URL,既重复又混乱。 我们需要在这里做一点抽象,提取 一些共同的东西出来。 关于漂亮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个以上的数字。 ``` <pre class="calibre9">``` urlpatterns = patterns('', # ... (r'^time/plus/\d+/$', hours_ahead), # ... ) ``` ``` 这里使用# …来表示省略了其它可能存在的URL模式定义。 (见上) 这个URL模式将匹配类似 `/time/plus/2/` , `/time/plus/25/` ,甚至 `/time/plus/100000000000/` 的任何URL。 更进一步,让我们把它限制在最大允许99个小时, 这样我们就只允许一个或两个数字,正则表达式的语法就是 `\d{1,2}` : ``` <pre class="calibre9">``` (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}` 包围起来: ``` <pre class="calibre9">``` (r'^time/plus/(\d{1,2})/$', hours_ahead), ``` ``` 如果你熟悉正则表达式,那么你应该已经了解,正则表达式也是用圆括号来从文本里 *提取* 数据的。 最终的URLconf包含上面两个视图,如: ``` <pre class="calibre9">``` from django.conf.urls.defaults import * from mysite.views import hello, current_datetime, hours_ahead urlpatterns = patterns('', (r'^hello/$', hello), (r'^time/$', current_datetime), (r'^time/plus/(\d{1,2})/$', hours_ahead), ) ``` ``` 现在开始写 `hours_ahead` 视图。 编码次序 这个例子中,我们先写了URLpattern ,然后是视图,但是在前面的例子中, 我们先写了视图,然后是URLpattern 。 哪一种方式比较好? 嗯,怎么说呢,每个开发者是不一样的。 如果你是喜欢从总体上来把握事物(注: 或译为“大局观”)类型的人,你应该会想在项目开始 的时候就写下所有的URL配置。 如果你从更像是一个自底向上的开发者,你可能更喜欢先写视图, 然后把它们挂接到URL上。 这同样是可以的。 最后,取决与你喜欢哪种技术,两种方法都是可以的。 (见上) `hours_ahead` 和我们以前写的 `current_datetime` 很象,关键的区别在于: 它多了一个额外参数,时间差。 以下是view代码: ``` <pre class="calibre9">``` 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) ``` ``` 让我们逐行分析一下代码: > 视图函数, `hours_ahead` , 有 *两个* 参数: `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错误。 ## Django 漂亮的出错页面 花几分钟时间欣赏一下我们写好的Web应用程序,然后我们再来搞点小破坏。 我们故意在 `views.py` 文件中引入一项 Python 错误,注释掉 `hours_ahead` 视图中的 `offset = int(offset)` 一行。 ``` <pre class="calibre9">``` 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并与其他人分享你的追溯信息。](http://www.dpaste.com/%EF%BC%8C%E5%9C%A8%E9%82%A3%E9%87%8C%E4%BD%A0%E5%8F%AF%E4%BB%A5%E5%BE%97%E5%88%B0%E4%B8%80%E4%B8%AA%E5%8D%95%E7%8B%AC%E7%9A%84URL%E5%B9%B6%E4%B8%8E%E5%85%B6%E4%BB%96%E4%BA%BA%E5%88%86%E4%BA%AB%E4%BD%A0%E7%9A%84%E8%BF%BD%E6%BA%AF%E4%BF%A1%E6%81%AF%E3%80%82) > > 接下来的“Request information”部分包含了有关产生错误的 Web 请求的大量信息: GET 和 POST、cookie 值、元数据(象 CGI 头)。 在附录H里给出了request的对象的 完整参考。 > > Request信息的下面,“Settings”列出了 Django 使用的具体配置信息。 (我们已经提及过ROOT\_URLCONF,接下来我们将向你展示各式的Django设置。 附录D覆盖了所有可用的设置。) Django 的出错页某些情况下有能力显示更多的信息,比如模板语法错误。 我们讨论 Django 模板系统时再说它们。 现在,取消 `offset = int(offset)` 这行的注释,让它重新正常 工作。 不知道你是不是那种使用小心放置的 `print` 语句来帮助调试的程序员? 你其实可以用 Django 出错页来做这些,而不用 `print` 语句。 在你视图的任何位置,临时插入一个 `assert False` 来触发出错页。 然后,你就可以看到局部变量和程序语句了。 这里有个使用hours\_ahead视图的例子: ``` <pre class="calibre9">``` 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内建有一个简单有强大的模板处理引擎来让你分离两种工作: 下一章,我们将学习模板引擎。