# 第八章:高級視圖和URL配置
# 第八章:高級視圖和URL配置
在第三章,我們已經對基本的Django視圖和URL配置做了介紹。 在這一章,將進一步說明框架中這兩個部分的高級機能。
## URLconf 技巧
URLconf沒什么特別的,就象 Django 中其它東西一樣,它們只是 Python 代碼。 你可以在幾方面從中得到好處,正如下面所描述的。
### 流線型化(Streamlining)函數導入
看下這個 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),
)
```
```
正如第三章中所解釋的,在 URLconf 中的每一個入口包括了它所關聯的視圖函數,直接傳入了一個函數對象。 這就意味著需要在模塊開始處導入視圖函數。
但隨著 Django 應用變得復雜,它的 URLconf 也在增長,并且維護這些導入可能使得管理變麻煩。 (對每個新的view函數,你不得不記住要導入它,并且采用這種方法會使導入語句將變得相當長。)可以通過導入 views 模塊本身來避免這個麻煩。 下面例子的URLconf與前一個等價:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
**from mysite import views**
urlpatterns = patterns('',
(r'^hello/$', **views.hello** ),
(r'^time/$', **views.current_datetime** ),
(r'^time/plus/(d{1,2})/$', **views.hours_ahead** ),
)
```
```
Django 還提供了另一種方法可以在 URLconf 中為某個特別的模式指定視圖函數: 你可以傳入一個包含模塊名和函數名的字符串,而不是函數對象本身。 繼續示例:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^hello/$', **'mysite.views.hello'** ),
(r'^time/$', **'mysite.views.current_datetime'** ),
(r'^time/plus/(d{1,2})/$', **'mysite.views.hours_ahead'** ),
)
```
```
(注意視圖名前后的引號。 應該使用帶引號的 `'mysite.views.current_datetime'` 而不是 `mysite.views.current_datetime` 。)
使用這個技術,就不必導入視圖函數了;Django 會在第一次需要它時根據字符串所描述的視圖函數的名字和路徑,導入合適的視圖函數。
當使用字符串技術時,你可以采用更簡化的方式:提取出一個公共視圖前綴。 在我們的URLconf例子中,每個視圖字符串的開始部分都是[``](#id1)`\\,造成重復輸入。 我們可以把公共的前綴提取出來,作為第一個參數傳給\\ ```函數:
System Message: WARNING/2 (`<string>`, line 99); *backlink*
Inline literal start-string without end-string.
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
urlpatterns = patterns(**'mysite.views'** ,
(r'^hello/$', **'hello'** ),
(r'^time/$', **'current_datetime'** ),
(r'^time/plus/(d{1,2})/$', **'hours_ahead'** ),
)
```
```
注意既不要在前綴后面跟著一個點號(`"."` ),也不要在視圖字符串前面放一個點號。 Django 會自動處理它們。
牢記這兩種方法,哪種更好一些呢? 這取決于你的個人編碼習慣和需要。
字符串方法的好處如下:
- 更緊湊,因為不需要你導入視圖函數。
- 如果你的視圖函數存在于幾個不同的 Python 模塊的話,它可以使得 URLconf 更易讀和管理。
函數對象方法的好處如下:
- 更容易對視圖函數進行包裝(wrap)。 參見本章后面的《包裝視圖函數》一節。
- 更 Pythonic,就是說,更符合 Python 的傳統,如把函數當成對象傳遞。
兩個方法都是有效的,甚至你可以在同一個 URLconf 中混用它們。 決定權在你。
### 使用多個視圖前綴
在實踐中,如果你使用字符串技術,特別是當你的 URLconf 中沒有一個公共前綴時,你最終可能混合視圖。 然而,你仍然可以利用視圖前綴的簡便方式來減少重復。 只要增加多個 `patterns()` 對象,象這樣:
舊的:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^hello/$', 'mysite.views.hello'),
(r'^time/$', 'mysite.views.current_datetime'),
(r'^time/plus/(\d{1,2})/$', 'mysite.views.hours_ahead'),
(r'^tag/(\w+)/$', 'weblog.views.tag'),
)
```
```
新的:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
urlpatterns = patterns('mysite.views',
(r'^hello/$', 'hello'),
(r'^time/$', 'current_datetime'),
(r'^time/plus/(\d{1,2})/$', 'hours_ahead'),
)
urlpatterns += patterns('weblog.views',
(r'^tag/(\w+)/$', 'tag'),
)
```
```
整個框架關注的是存在一個名為 `urlpatterns` 的模塊級別的變量。如上例,這個變量可以動態生成。 這里我們要特別說明一下,patterns()返回的對象是可相加的,這個特性可能是大家沒有想到的。
### 調試模式中的特例
說到動態構建 `urlpatterns`,你可能想利用這一技術,在 Django 的調試模式下修改 URLconf 的行為。 為了做到這一點,只要在運行時檢查 `DEBUG` 配置項的值即可,如:
```
<pre class="calibre9">```
from django.conf import settings
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^$', views.homepage),
(r'^(\d{4})/([a-z]{3})/$', views.archive_month),
)
if settings.DEBUG:
urlpatterns += patterns('',
(r'^debuginfo/$', views.debug),
)
```
```
在這個例子中,URL鏈接`/debuginfo/` 只在你的 `DEBUG` 配置項設為 `True` 時才有效。
### 使用命名組
在目前為止的所有 URLconf 例子中,我們使用簡單的*無命名* 正則表達式組,即,在我們想要捕獲的URL部分上加上小括號,Django 會將捕獲的文本作為位置參數傳遞給視圖函數。 在更高級的用法中,還可以使用 *命名* 正則表達式組來捕獲URL,并且將其作為 *關鍵字* 參數傳給視圖。
關鍵字參數 對比 位置參數
一個 Python 函數可以使用關鍵字參數或位置參數來調用,在某些情況下,可以同時進行使用。 在關鍵字參數調用中,你要指定參數的名字和傳入的值。 在位置參數調用中,你只需傳入參數,不需要明確指明哪個參數與哪個值對應,它們的對應關系隱含在參數的順序中。
例如,考慮這個簡單的函數:
```
<pre class="calibre9">```
def sell(item, price, quantity):
print "Selling %s unit(s) of %s at %s" % (quantity, item, price)
```
```
為了使用位置參數來調用它,你要按照在函數定義中的順序來指定參數。
```
<pre class="calibre9">```
sell('Socks', '$2.50', 6)
```
```
為了使用關鍵字參數來調用它,你要指定參數名和值。 下面的語句是等價的:
```
<pre class="calibre9">```
sell(item='Socks', price='$2.50', quantity=6)
sell(item='Socks', quantity=6, price='$2.50')
sell(price='$2.50', item='Socks', quantity=6)
sell(price='$2.50', quantity=6, item='Socks')
sell(quantity=6, item='Socks', price='$2.50')
sell(quantity=6, price='$2.50', item='Socks')
```
```
最后,你可以混合關鍵字和位置參數,只要所有的位置參數列在關鍵字參數之前。 下面的語句與前面的例子是等價:
```
<pre class="calibre9">```
sell('Socks', '$2.50', quantity=6)
sell('Socks', price='$2.50', quantity=6)
sell('Socks', quantity=6, price='$2.50')
```
```
在 Python 正則表達式中,命名的正則表達式組的語法是 `(?P<name>pattern)` ,這里 `name` 是組的名字,而 `pattern` 是匹配的某個模式。
下面是一個使用無名組的 URLconf 的例子:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^articles/(\d{4})/$', views.year_archive),
(r'^articles/(\d{4})/(\d{2})/$', views.month_archive),
)
```
```
下面是相同的 URLconf,使用命名組進行了重寫:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^articles/(?P<year>\d{4})/$', views.year_archive),
(r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/$', views.month_archive),
)
```
```
這段代碼和前面的功能完全一樣,只有一個細微的差別: 取的值是以關鍵字參數的方式而不是以位置參數的方式傳遞給視圖函數的。
例如,如果不帶命名組,請求 `/articles/2006/03/` 將會等同于這樣的函數調用:
```
<pre class="calibre9">```
month_archive(request, '2006', '03')
```
```
而帶命名組,同樣的請求就會變成這樣的函數調用:
```
<pre class="calibre9">```
month_archive(request, year='2006', month='03')
```
```
使用命名組可以讓你的URLconfs更加清晰,減少搞混參數次序的潛在BUG,還可以讓你在函數定義中對參數重新排序。 接著上面這個例子,如果我們想修改URL把月份放到 年份的 *前面* ,而不使用命名組的話,我們就不得不去修改視圖 `month_archive` 的參數次序。 如果我們使用命名組的話,修改URL里提取參數的次序對視圖沒有影響。
當然,命名組的代價就是失去了簡潔性: 一些開發者覺得命名組的語法丑陋和顯得冗余。 命名組的另一個好處就是可讀性強。
### 理解匹配/分組算法
需要注意的是如果在URLconf中使用命名組,那么命名組和非命名組是不能同時存在于同一個URLconf的模式中的。 如果你這樣做,Django不會拋出任何錯誤,但你可能會發現你的URL并沒有像你預想的那樣匹配正確。 具體地,以下是URLconf解釋器有關正則表達式中命名組和 非命名組所遵循的算法:
- 如果有任何命名的組,Django會忽略非命名組而直接使用命名組。
- 否則,Django會把所有非命名組以位置參數的形式傳遞。
- 在以上的兩種情況,Django同時會以關鍵字參數的方式傳遞一些額外參數。 更具體的信息可參考下一節。
### 傳遞額外的參數到視圖函數中
有時你會發現你寫的視圖函數是十分類似的,只有一點點的不同。 比如說,你有兩個視圖,它們的內容是一致的,除了它們所用的模板不太一樣:
```
<pre class="calibre9">```
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^foo/$', views.foo_view),
(r'^bar/$', views.bar_view),
)
# views.py
from django.shortcuts import render_to_response
from mysite.models import MyModel
def foo_view(request):
m_list = MyModel.objects.filter(is_new=True)
return render_to_response('template1.html', {'m_list': m_list})
def bar_view(request):
m_list = MyModel.objects.filter(is_new=True)
return render_to_response('template2.html', {'m_list': m_list})
```
```
我們在這代碼里面做了重復的工作,不夠簡練。 起初你可能會想,通過對兩個URL都使用同樣的視圖,在URL中使用括號捕捉請求,然后在視圖中檢查并決定使用哪個模板來去除代碼的冗余,就像這樣:
```
<pre class="calibre9">```
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^(foo)/$', views.foobar_view),
(r'^(bar)/$', views.foobar_view),
)
# views.py
from django.shortcuts import render_to_response
from mysite.models import MyModel
def foobar_view(request, url):
m_list = MyModel.objects.filter(is_new=True)
if url == 'foo':
template_name = 'template1.html'
elif url == 'bar':
template_name = 'template2.html'
return render_to_response(template_name, {'m_list': m_list})
```
```
這種解決方案的問題還是老缺點,就是把你的URL耦合進你的代碼里面了。 如果你打算把 `/foo/` 改成 `/fooey/` 的話,那么你就得記住要去改變視圖里面的代碼。
對一個可選URL配置參數的優雅解決方法: URLconf里面的每一個模式都可以包含第三個數據: 一個關鍵字參數的字典:
有了這個概念以后,我們就可以把我們現在的例子改寫成這樣:
```
<pre class="calibre9">```
# 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` 。 而視圖函數會把它當成另一個參數。
這種使用額外的URLconf參數的技術以最小的代價給你提供了向視圖函數傳遞額外信息的一個好方法。 正因如此,這技術已被很多Django的捆綁應用使用,其中以我們將在第11章討論的通用視圖系統最為明顯。
下面的幾節里面有一些關于你可以怎樣把額外URLconf參數技術應用到你自己的工程的建議。
#### 偽造捕捉到的URLconf值
比如說你有匹配某個模式的一堆視圖,以及一個并不匹配這個模式但視圖邏輯是一樣的URL。 這種情況下,你可以通過向同一個視圖傳遞額外URLconf參數來偽造URL值的捕捉。
例如,你可能有一個顯示某一個特定日子的某些數據的應用,URL類似這樣的:
```
<pre class="calibre9">```
/mydata/jan/01/
/mydata/jan/02/
/mydata/jan/03/
# ...
/mydata/dec/30/
/mydata/dec/31/
```
```
這太簡單了,你可以在一個URLconf中捕捉這些值,像這樣(使用命名組的方法):
```
<pre class="calibre9">```
urlpatterns = patterns('',
(r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)
```
```
然后視圖函數的原型看起來會是:
```
<pre class="calibre9">```
def my_view(request, month, day):
# ....
```
```
這種解決方案很直接,沒有用到什么你沒見過的技術。 當你想添加另外一個使用 `my_view` 視圖但不包含`month`和/或者`day`的URL時,問題就出現了。
比如你可能會想增加這樣一個URL, `/mydata/birthday/` , 這個URL等價于 `/mydata/jan/06/` 。這時你可以這樣利用額外URLconf參數:
```
<pre class="calibre9">```
urlpatterns = patterns('',
(r'^mydata/birthday/$', views.my_view, {'month': 'jan', 'day': '06'}),
(r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)
```
```
在這里最帥的地方莫過于你根本不用改變你的視圖函數。 視圖函數只會關心它 *獲得* 了 參數,它不會去管這些參數到底是捕捉回來的還是被額外提供的。month和day
#### 創建一個通用視圖
抽取出我們代碼中共性的東西是一個很好的編程習慣。 比如,像以下的兩個Python函數:
```
<pre class="calibre9">```
def say_hello(person_name):
print 'Hello, %s' % person_name
def say_goodbye(person_name):
print 'Goodbye, %s' % person_name
```
```
我們可以把問候語提取出來變成一個參數:
```
<pre class="calibre9">```
def greet(person_name, greeting):
print '%s, %s' % (greeting, person_name)
```
```
通過使用額外的URLconf參數,你可以把同樣的思想應用到Django的視圖中。
了解這個以后,你可以開始創作高抽象的視圖。 更具體地說,比如這個視圖顯示一系列的 `Event` 對象,那個視圖顯示一系列的 `BlogEntry` 對象,并意識到它們都是一個用來顯示一系列對象的視圖的特例,而對象的類型其實就是一個變量。
以這段代碼作為例子:
```
<pre class="calibre9">```
# 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})
```
```
這兩個視圖做的事情實質上是一樣的: 顯示一系列的對象。 讓我們把它們顯示的對象的類型抽象出來:
```
<pre class="calibre9">```
# 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})
```
```
就這樣小小的改動,我們突然發現我們有了一個可復用的,模型無關的視圖! 從現在開始,當我們需要一個視圖來顯示一系列的對象時,我們可以簡簡單單的重用這一個 `object_list` 視圖,而無須另外寫視圖代碼了。 以下是我們做過的事情:
- 我們通過 `model` 參數直接傳遞了模型類。 額外URLconf參數的字典是可以傳遞任何類型的對象,而不僅僅只是字符串。
- 這一行: `model.objects.all()` 是 *鴨子界定* (原文:
- 我們使用 `model.__name__.lower()` 來決定模板的名字。 每個Python的類都有一個 `__name__` 屬性返回類名。 這特性在當我們直到運行時刻才知道對象類型的這種情況下很有用。 比如, `BlogEntry` 類的 `__name__` 就是字符串 `'BlogEntry'` 。
- 這個例子與前面的例子稍有不同,我們傳遞了一個通用的變量名給模板。 當然我們可以輕易的把這個變量名改成 `blogentry_list` 或者 `event_list` ,不過我們打算把這當作練習留給讀者。
因為數據庫驅動的網站都有一些通用的模式,Django提供了一個通用視圖的集合,使用它可以節省你的時間。 我們將會在下一章講講Django的內置通用視圖。
#### 提供視圖配置選項
如果你發布一個Django的應用,你的用戶可能會希望配置上能有些自由度。 這種情況下,為你認為用戶可能希望改變的配置選項添加一些鉤子到你的視圖中會是一個很好的主意。 你可以用額外URLconf參數實現。
一個應用中比較常見的可供配置代碼是模板名字:
```
<pre class="calibre9">```
def my_view(request, template_name):
var = do_something()
return render_to_response(template_name, {'var': var})
```
```
#### 了解捕捉值和額外參數之間的優先級 額外的選項
當沖突出現的時候,額外URLconf參數優先于捕捉值。 也就是說,如果URLconf捕捉到的一個命名組變量和一個額外URLconf參數包含的變量同名時,額外URLconf參數的值會被使用。
例如,下面這個URLconf:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^mydata/(?P<id>\d+)/$', views.my_view, {'id': 3}),
)
```
```
這里,正則表達式和額外字典都包含了一個 `id` 。硬編碼的(額外字典的) `id` 將優先使用。 就是說任何請求(比如, `/mydata/2/` 或者 `/mydata/432432/` )都會作 `id` 設置為 `3` 對待,不管URL里面能捕捉到什么樣的值。
聰明的讀者會發現在這種情況下,在正則表達式里面寫上捕捉是浪費時間的,因為 `id` 的值總是會被字典中的值覆蓋。 沒錯,我們說這個的目的只是為了讓你不要犯這樣的錯誤。
### 使用缺省視圖參數
另外一個方便的特性是你可以給一個視圖指定默認的參數。 這樣,當沒有給這個參數賦值的時候將會使用默認的值。
例子:
```
<pre class="calibre9">```
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^blog/$', views.page),
(r'^blog/page(?P<num>\d+)/$', views.page),
)
# views.py
def page(request, num='1'):
# Output the appropriate page of blog entries, according to num.
# ...
```
```
在這里,兩個URL表達式都指向了同一個視圖 `views.page` ,但是第一個表達式沒有傳遞任何參數。 如果匹配到了第一個樣式, `page()` 函數將會對參數 `num` 使用默認值 `"1"` ,如果第二個表達式匹配成功, `page()` 函數將使用正則表達式傳遞過來的num的值。
(注:我們已經注意到設置默認參數值是字符串 `‘1’` ,不是整數`1` 。為了保持一致,因為捕捉給`num` 的值總是字符串。
就像前面解釋的一樣,這種技術與配置選項的聯用是很普遍的。 以下這個例子比提供視圖配置選項一節中的例子有些許的改進。
```
<pre class="calibre9">```
def my_view(request, template_name='mysite/my_view.html'):
var = do_something()
return render_to_response(template_name, {'var': var})
```
```
### 特殊情況下的視圖
有時你有一個模式來處理在你的URLconf中的一系列URL,但是有時候需要特別處理其中的某個URL。 在這種情況下,要使用將URLconf中把特殊情況放在首位的線性處理方式 。
比方說,你可以考慮通過下面這個URLpattern所描述的方式來向Django的管理站點添加一個目標頁面
```
<pre class="calibre9">```
urlpatterns = patterns('',
# ...
('^([^/]+)/([^/]+)/add/$', views.add_stage),
# ...
)
```
```
這將匹配像 `/myblog/entries/add/` 和 `/auth/groups/add/` 這樣的URL 。然而,對于用戶對象的添加頁面( `/auth/user/add/` )是個特殊情況,因為它不會顯示所有的表單域,它顯示兩個密碼域等等。 我們 *可以* 在視圖中特別指出以解決這種情況:
```
<pre class="calibre9">```
def add_stage(request, app_label, model_name):
if app_label == 'auth' and model_name == 'user':
# do special-case code
else:
# do normal code
```
```
不過,就如我們多次在這章提到的,這樣做并不優雅: 因為它把URL邏輯放在了視圖中。 更優雅的解決方法是,我們要利用URLconf從頂向下的解析順序這個特點:
```
<pre class="calibre9">```
urlpatterns = patterns('',
# ...
('^auth/user/add/$', views.user_add_stage),
('^([^/]+)/([^/]+)/add/$', views.add_stage),
# ...
)
```
```
在這種情況下,象 `/auth/user/add/` 的請求將會被 `user_add_stage` 視圖處理。 盡管URL也匹配第二種模式,它會先匹配上面的模式。 (這是短路邏輯。)
### 從URL中捕獲文本
每個被捕獲的參數將被作為純Python字符串來發送,而不管正則表達式中的格式。 舉個例子,在這行URLConf中:
```
<pre class="calibre9">```
(r'^articles/(?P<year>\d{4})/$', views.year_archive),
```
```
盡管 `\d{4}` 將只匹配整數的字符串,但是參數 `year` 是作為字符串傳至 `views.year_archive()` 的,而不是整型。
當你在寫視圖代碼時記住這點很重要,許多Python內建的方法對于接受的對象的類型很講究。 許多內置Python函數是挑剔的(這是理所當然的)只接受特定類型的對象。 一個典型的的錯誤就是用字符串值而不是整數值來創建 `datetime.date` 對象:
```
<pre class="calibre9">```
>>> import datetime
>>> datetime.date('1993', '7', '9')
Traceback (most recent call last):
...
TypeError: an integer is required
>>> datetime.date(1993, 7, 9)
datetime.date(1993, 7, 9)
```
```
回到URLconf和視圖處,錯誤看起來很可能是這樣:
```
<pre class="calibre9">```
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
(r'^articles/(\d{4})/(\d{2})/(\d{2})/$', views.day_archive),
)
# views.py
import datetime
def day_archive(request, year, month, day):
# The following statement raises a TypeError!
date = datetime.date(year, month, day)
```
```
因此, `day_archive()` 應該這樣寫才是正確的:
```
<pre class="calibre9">```
def day_archive(request, year, month, day):
date = datetime.date(int(year), int(month), int(day))
```
```
注意,當你傳遞了一個并不完全包含數字的字符串時, `int()` 會拋出 `ValueError` 的異常,不過我們已經避免了這個錯誤,因為在URLconf的正則表達式中已經確保只有包含數字的字符串才會傳到這個視圖函數中。
### 決定URLconf搜索的東西
當一個請求進來時,Django試著將請求的URL作為一個普通Python字符串進行URLconf模式匹配(而不是作為一個Unicode字符串)。 這并不包括 `GET` 或 `POST` 參數或域名。 它也不包括第一個斜杠,因為每個URL必定有一個斜杠。
例如,在向 `http://www.example.com/myapp/` 的請求中,Django將試著去匹配 `myapp/` 。在向 `http://www.example.com/myapp/?page=3` 的請求中,Django同樣會去匹配 `myapp/` 。
在解析URLconf時,請求方法(例如, `POST` , `GET` , `HEAD` )并 *不會* 被考慮。 換而言之,對于相同的URL的所有請求方法將被導向到相同的函數中。 因此根據請求方法來處理分支是視圖函數的責任。
### 視圖函數的高級概念
說到關于請求方法的分支,讓我們來看一下可以用什么好的方法來實現它。 考慮這個 URLconf/view 設計:
```
<pre class="calibre9">```
# urls.py
from django.conf.urls.defaults import *
from mysite import views
urlpatterns = patterns('',
# ...
(r'^somepage/$', views.some_page),
# ...
)
# views.py
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render_to_response
def some_page(request):
if request.method == 'POST':
do_something_for_post()
return HttpResponseRedirect('/someurl/')
elif request.method == 'GET':
do_something_for_get()
return render_to_response('page.html')
else:
raise Http404()
```
```
在這個示例中,`some_page()` 視圖函數對`POST` 和`GET` 這兩種請求方法的處理完全不同。 它們唯一的共同點是共享一個URL地址: `/somepage/.`正如大家所看到的,在同一個視圖函數中對`POST` 和`GET` 進行處理是一種很初級也很粗糙的做法。 一個比較好的設計習慣應該是,用兩個分開的視圖函數——一個處理`POST` 請求,另一個處理`GET` 請求,然后在相應的地方分別進行調用。
我們可以像這樣做:先寫一個視圖函數然后由它來具體分派其它的視圖,在之前或之后可以執行一些我們自定的程序邏輯。 下邊的示例展示了這個技術是如何幫我們改進前邊那個簡單的`some_page()` 視圖的:
```
<pre class="calibre9">```
# 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 = patterns('',
# ...
(r'^somepage/$', views.method_splitter, {'GET': views.some_page_get, 'POST': views.some_page_post}),
# ...
)
```
```
讓我們從頭看一下代碼是如何工作的:
> 我們寫了一個新的視圖,`method_splitter()` ,它根據`request.method` 返回的值來調用相應的視圖。可以看到它帶有兩個關鍵參數,`GET` 和`POST` ,也許應該是 *視圖函數* 。如果`request.method` 返回`GET` ,那它就會自動調用`GET` 視圖。 如果`request.method` 返回的是`POST` ,那它調用的就是`POST` 視圖。 如果`request.method` 返回的是其它值(如:`HEAD` ),或者是沒有把`GET` 或`POST` 提交給此函數,那它就會拋出一個`Http404` 錯誤。
>
> 在URLconf中,我們把`/somepage/` 指到`method_splitter()` 函數,并把視圖函數額外需要用到的`GET` 和`POST` 參數傳遞給它。
>
> 最終,我們把`some_page()` 視圖分解到兩個視圖函數中`some_page_get()` 和`some_page_post()` 。這比把所有邏輯都擠到一個單一視圖的做法要優雅得多。
>
> 注意,在技術上這些視圖函數就不用再去檢查`request.method` 了,因為`method_splitter()` 已經替它們做了。 (比如,`some_page_post()` 被調用的時候,我們可以確信`request.method` 返回的值是`post` 。)當然,這樣做不止更安全也能更好的將代碼文檔化,這里我們做了一個假定,就是`request.method` 能象我們所期望的那樣工作。
現在我們就擁有了一個不錯的,可以通用的視圖函數了,里邊封裝著由`request.method` 的返回值來分派不同的視圖的程序。關于`method_splitter()` 就不說什么了,當然,我們可以把它們重用到其它項目中。
然而,當我們做到這一步時,我們仍然可以改進`method_splitter` 。從代碼我們可以看到,它假設`Get` 和`POST` 視圖除了`request` 之外不需要任何其他的參數。那么,假如我們想要使用`method_splitter` 與那種會從URL里捕捉字符,或者會接收一些可選參數的視圖一起工作時該怎么辦呢?
為了實現這個,我們可以使用Python中一個優雅的特性 帶星號的可變參數 我們先展示這些例子,接著再進行解釋
```
<pre class="calibre9">```
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特性,允許函數接受動態的、可變數量的、參數名只在運行時可知的參數。 如果你在函數定義時,只在參數前面加一個*號,所有傳遞給函數的參數將會保存為一個元組. 如果你在函數定義時,在參數前面加兩個*號,所有傳遞給函數的關鍵字參數,將會保存為一個字典
例如,對于這個函數
```
<pre class="calibre9">```
def foo(*args, **kwargs):
print "Positional arguments are:"
print args
print "Keyword arguments are:"
print kwargs
```
```
看一下它是怎么工作的
```
<pre class="calibre9">```
>>> foo(1, 2, 3)
Positional arguments are:
(1, 2, 3)
Keyword arguments are:
{}
>>> foo(1, 2, name='Adrian', framework='Django')
Positional arguments are:
(1, 2)
Keyword arguments are:
{'framework': 'Django', 'name': 'Adrian'}
```
```
回過頭來看,你能發現我們用`method_splitter()`和`*args`接受`**kwargs`函數參數并把它們傳遞到正確的視圖。*any* 但是在我們這樣做之前,我們要調用兩次獲得參數`kwargs.pop()``GET``POST`,如果它們合法的話。 (我們通過指定pop的缺省值為None,來避免由于一個或者多個關鍵字缺失帶來的KeyError)
### 包裝視圖函數
我們最終的視圖技巧利用了一個高級python技術。 假設你發現自己在各個不同視圖里重復了大量代碼,就像 這個例子:
```
<pre class="calibre9">```
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`是否是已經認證的,是的話,當前用戶已經成功登陸站點否則就重定向`/accounts/login/` (注意,雖然我們還沒有講到request.user,但是14章將要講到它.就如你所想像的,request.user描述當前用戶是登陸的還是匿名)
如果我們能夠叢每個視圖里移除那些 重復代,并且只在需要認證的時候指明它們,那就完美了。 我們能夠通過使用一個視圖包裝達到目的。 花點時間來看看這個:
```
<pre class="calibre9">```
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,傳入一個視圖函數view,然后返回一個新的視圖函數new\_view.這個新的視圖函數new\_view在函數requires\_login內定義 處理request.user.is\_authenticated()這個驗證,從而決定是否執行原來的view函數
現在,我們可以從views中去掉if not request.user.is\_authenticated()驗證.我們可以在URLconf中很容易的用requires\_login來包裝實現.
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
from mysite.views import requires_login, my_view1, my_view2, my_view3
urlpatterns = patterns('',
(r'^view1/$', requires_login(my_view1)),
(r'^view2/$', requires_login(my_view2)),
(r'^view3/$', requires_login(my_view3)),
)
```
```
優化后的代碼和前面的功能一樣,但是減少了代碼冗余 現在我們建立了一個漂亮,通用的函數requires\_login()來幫助我們修飾所有需要它來驗證的視圖
## 包含其他URLconf
如果你試圖讓你的代碼用在多個基于Django的站點上,你應該考慮將你的URLconf以包含的方式來處理。
在任何時候,你的URLconf都可以包含其他URLconf模塊。 對于根目錄是基于一系列URL的站點來說,這是必要的。 例如下面的,URLconf包含了其他URLConf:
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^weblog/', include('mysite.blog.urls')),
(r'^photos/', include('mysite.photos.urls')),
(r'^about/$', 'mysite.views.about'),
)
```
```
在前面第6章介紹Django的admin模塊時我們曾經見過include. admin模塊有他自己的URLconf,你僅僅只需要在你自己的代碼中加入include就可以了.
這里有個很重要的地方: 例子中的指向 `include()` 的正則表達式并 *不* 包含一個 `$` (字符串結尾匹配符),但是包含了一個斜桿。 每當Django遇到 `include()` 時,它將截斷匹配的URL,并把剩余的字符串發往包含的URLconf作進一步處理。
繼續看這個例子,這里就是被包含的URLconf `mysite.blog.urls` :
```
<pre class="calibre9">```
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^(\d\d\d\d)/$', 'mysite.blog.views.year_detail'),
(r'^(\d\d\d\d)/(\d\d)/$', 'mysite.blog.views.month_detail'),
)
```
```
通過這兩個URLconf,下面是一些處理請求的例子:
- `/weblog/2007/` :在第一個URLconf中,模式 `r'^weblog/'` 被匹配。 因為它是一個 `include()` ,Django將截掉所有匹配的文本,在這里是 `'weblog/'` 。URL剩余的部分是 `2007/` , 將在 `mysite.blog.urls` 這個URLconf的第一行中被匹配到。 URL仍存在的部分為 `2007/` ,與第一行的 `mysite.blog.urls`URL設置相匹配。
- /weblog//2007/(包含兩個斜杠) 在第一個URLconf中,r’^weblog/’匹配 因為它有一個include(),django去掉了匹配的部,在這個例子中匹配的部分是’weblog/’ 剩下的部分是/2007/ (最前面有一個斜杠),不匹配mysite.blog.urls中的任何一行.
- `/about/` : 這個匹配第一個URLconf中的 `mysite.views.about` 視圖。
### 捕獲的參數如何和include()協同工作
一個被包含的URLconf接收任何來自parent URLconfs的被捕獲的參數,比如:
```
<pre class="calibre9">```
# root urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
)
# foo/urls/blog.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^$', 'foo.views.blog_index'),
(r'^archive/$', 'foo.views.blog_archive'),
)
```
```
在這個例子中,被捕獲的 `username` 變量將傳遞給被包含的 URLconf,進而傳遞給那個URLconf中的 *每一個* 視圖函數。
注意,這個被捕獲的參數 *總是* 傳遞到被包含的URLconf中的 *每一* 行,不管那些行對應的視圖是否需要這些參數。 因此,這個技術只有在你確實需要那個被傳遞的參數的時候才顯得有用。
### 額外的URLconf如何和include()協同工作
相似的,你可以傳遞額外的URLconf選項到 `include()` , 就像你可以通過字典傳遞額外的URLconf選項到普通的視圖。 當你這樣做的時候,被包含URLconf的 *每一* 行都會收到那些額外的參數。
比如,下面的兩個URLconf在功能上是相等的。
第一個:
```
<pre class="calibre9">```
# urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^blog/', include('inner'), {'blogid': 3}),
)
# inner.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^archive/$', 'mysite.views.archive'),
(r'^about/$', 'mysite.views.about'),
(r'^rss/$', 'mysite.views.rss'),
)
```
```
第二個
```
<pre class="calibre9">```
# urls.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^blog/', include('inner')),
)
# inner.py
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^archive/$', 'mysite.views.archive', {'blogid': 3}),
(r'^about/$', 'mysite.views.about', {'blogid': 3}),
(r'^rss/$', 'mysite.views.rss', {'blogid': 3}),
)
```
```
這個例子和前面關于被捕獲的參數一樣(在上一節就解釋過這一點),額外的選項將 *總是* 被傳遞到被包含的URLconf中的 *每一* 行,不管那一行對應的視圖是否確實作為有效參數接收這些選項,因此,這個技術只有在你確實需要那個被傳遞的額外參數的時候才顯得有用。 因為這個原因,這種技術僅當你確信在涉及到的接受到額外你給出的選項的每個URLconf時有用的才奏效。
## 下一章
這一章提供了很多高級視圖和URLconfs的小提示和技巧。 接下來,在Chapter 9,我們將會將這個先進的處理方案帶給djangos模板系統。