URL dispatcher簡單點理解就是根據(jù)URL,將請求分發(fā)到相應(yīng)的方法中去處理,它是對URL和View的一個映射,它的實現(xiàn)其實也很簡單,就是一個正則匹配的過程,事先定義好正則表達(dá)式和該正則表達(dá)式對應(yīng)的view方法,如果請求的URL符合這個正則表達(dá)式,那么就分發(fā)這個請求到這個view方法中。
有了這個base,我們先拋出幾個問題,提前思考一下:
這個映射定義在哪里?當(dāng)映射很多時,如果有效的組織?
URL中的參數(shù)怎么獲取,怎么傳給view方法?
如何在view或者是template中反解出URL?
好,先來看一個簡單的例子:
- from django.conf.urls import patterns, url, include
- urlpatterns = patterns('',
- url(r'^articles/2003/$', 'news.views.special_case_2003'),
- url(r'^articles/(/d{4})/$', 'news.views.year_archive'),
- )
- urlpatterns += patterns('',
- url(r'^articles/(?P<year>/d{4})/(?P<month>/d{2})/$', 'news.views.month_archive'),
- url(r'model/', include('model_test.urls')),
- )
這段代碼就是一個URL Dispatcher的例子,它在一個單獨的python模塊定義,Django中管這個模塊叫做URLconf,其實,就是通過python代碼方式實現(xiàn)的配置文件,在這個配置中定義了URL路徑和對應(yīng)的處理方法之間的映射。在Django中,是通過“樹”的結(jié)構(gòu)來管理URLconf之間的關(guān)系的,在Django中的主配置文件中,有一個叫做ROOT_URL_CONF的配置項,就是用來指定根URLconf,從根URLconf開始,逐條進行匹配,直到找到匹配項為止,這就是我們上面提到的第一個問題的答案,下面還會再仔細(xì)剖析。
在上面例子中,我們可以看到有3個方法:patterns, url, include。url方法構(gòu)建了一個URL到View方法的映射關(guān)系對象,patterns將這些映射關(guān)系對象組織成為一個python的列表,那include是做什么的呢?它就是我們上面說到的“樹”結(jié)構(gòu)關(guān)系的聯(lián)系者,include會關(guān)聯(lián)其他的URLconf到本URLconf,也就是說include關(guān)聯(lián)的是孩子節(jié)點。整個URL dispatcher體系,就是由這三個方法構(gòu)建起來的,下面我們重點來介紹這三個方法,了解了這三個方法,整個URL映射機制就會非常清楚了。
- def patterns(prefix, *args):
- pass
- def url(regex, view, kwargs=None, name=None, prefix=''):
- pass
- def include(arg, namespace=None, app_name=None):
- pass
- url()
先來看下最重要的url()方法。第一個參數(shù)regex是代表URL的正則表達(dá)式,第二個參數(shù)指定了和該正則表達(dá)式映射的View,此外,還可以通過kwargs參數(shù),給view方法指定默認(rèn)的kwargs參數(shù),還有name參數(shù),用來命名該URL,主要用在URL反解中,至于prefix用處不大,不解釋。
url()方法最終構(gòu)造了一個對象,我們姑且叫它URL映射對象,當(dāng)?shù)谝淮卧L問這個對象去匹配URL時,它會把這個對象中的正則表達(dá)式編譯一次,然后保存在該對象中,所以以后再次匹配時,就會很快,不會重復(fù)編譯該正則表達(dá)式了。在這里正則匹配其實就是用就是python的re模塊,使用過程大致如下:
- # 第一次訪問時,編譯,然后保存在url對象中
- regex = re.compile(regex_str, re.UNICODE)
- # 每次URL訪問時,進行正則匹配
- match = regex.search(path)
- kwargs = match.groupdict()
- args = match.groups()
注意,這里涉及到了上面提到的第二個問題,即URL中的參數(shù)是如何獲取,如何傳遞給view方法的。從URL中獲取參數(shù),其實是通過re模塊中named groups和non-named groups的概念來獲取的,通過match.groupdict()得到的是named groups,其實就是一個字典,字典的key是在URL中指定的,該字典會作為kwargs參數(shù)傳遞給view,而通過match.groups()得到的是non-named groups,是一個元組,即tuple,該元組會作為args參數(shù)傳遞給view。不過,這里的args和kwargs是不能夠同時存在的,當(dāng)有kwargs不為空時,args就會被置空,當(dāng)kwargs為空時,args才會被用到,而傳遞給view的kwargs就只有url()方法中指定的默認(rèn)kwargs。也就是說,如果你在URL中使用了named groups,那么non-named groups就會被忽略,如果只使用了non-named groups,它才會被作為args參數(shù),傳遞給view方法。
好,糾結(jié)了一大堆,還是來解析一下我們上面提到的例子,假如我們有url和view:
urls:
- url(r'^articles/(/d{4})/$', 'news.views.year_archive')
- url(r'^articles/(?P<year>/d{4})/(?P<month>/d{2})/$', 'news.views.month_archive')
views:
- def year_archive(request, *args, **kwargs):
- pass
- def month_archive(request, *args, **kwargs):
- pass
當(dāng)我們訪問”articles/2014/”這個路徑的時候,解析的過程如下:
- >>> import re
- >>> regex = re.compile(r'^articles/(/d{4})/$', re.UNICODE)
- >>> match = regex.search("articles/2014/")
- >>> match.groupdict()
- {}
- >>> match.groups()
- ('2014',)
所以最終傳遞給year_archive()方法中的參數(shù)應(yīng)該是這樣的:
- (Pdb) pp args
- (u'2014',)
- (Pdb) pp kwargs
- {}
當(dāng)我們訪問”articles/2014/11”這個路徑時,解析的過程如下:
- >>> import re
- >>> regex = re.compile(r'^articles/(?P<year>/d{4})/(?P<month>/d{2})/$', re.UNICODE)
- >>> match = regex.search("articles/2014/11/")
- >>> match.groupdict()
- {'year': '2014', 'month': '11'}
- >>> match.groups()
- ('2014', '11')
所以最終傳遞給month_archive()方法中的參數(shù)應(yīng)該是這樣的:
- (Pdb) pp args
- ()
- (Pdb) pp kwargs
- {'month': u'11', 'year': u'2014'}
再羅嗦一句,因為url()可以指定一個kwargs參數(shù),它是該url關(guān)聯(lián)的view()方法的默認(rèn)kwargs參數(shù),也就是說如果在url()方法中指定了kwargs,那么會將這個參數(shù)的內(nèi)容,也傳遞到view方法中的kwargs參數(shù)中。
好,至此,url()方法基本上就清楚了,第二個問題也解決了,至于name參數(shù),到下面講到URL反解的時候再詳細(xì)解釋。
patterns()
接下來,我們來看patterns()方法,這個其實比較簡單,它就是返回一個由url()方法構(gòu)造的URL映射對象組成的列表。它有一個必填參數(shù)是prefix,這個prefix是它所包含的view的公共前綴,這么做是為了避免代碼重復(fù),比如:
- urlpatterns = patterns('',
- url(r'^articles/(/d{4})/$', 'news.views.year_archive'),
- url(r'^articles/(/d{4})/(/d{2})/$', 'news.views.month_archive'),
- url(r'^articles/(/d{4})/(/d{2})/(/d+)/$', 'news.views.article_detail'),
- )
可以寫成:
- urlpatterns = patterns('news.views',
- url(r'^articles/(/d{4})/$', 'year_archive'),
- url(r'^articles/(/d{4})/(/d{2})/$', 'month_archive'),
- url(r'^articles/(/d{4})/(/d{2})/(/d+)/$', 'article_detail'),
- )
注意,由patterns()生成的列表,被賦值給urlpatterns這個變量,這個變量是不能隨便定義的,必須是約定好的,默認(rèn)django會去URLconf中查找這個變量,也許你可以在某個地方設(shè)定一個參數(shù),來換個約定,改變一下這個變量名。
在2.0版本的Django中,會舍棄這個方法,而是直接賦值給urlpatterns一個列表,不做過多討論。
include()
我們來說include(),這其實是個難點,關(guān)鍵在于URL反解那里,Django的文檔也沒有說清楚,而且關(guān)系也比較亂,所以,必須得實際的測試一下,才會明白。
上面說過,include()是“樹”結(jié)構(gòu)關(guān)系的聯(lián)系者,include會關(guān)聯(lián)其他的URLconf到本URLconf,靠include()才能夠讓Django的URL設(shè)計變得非常的靈活和簡潔。include()有三個參數(shù),第一個參數(shù)不必多說,它指定了要包含的其它URLconf的路徑,關(guān)鍵是剩下的兩個參數(shù),一個是namespace, 一個是app_name,有什么用呢?其實,這兩個參數(shù)再加上url()方法中的name參數(shù),共同構(gòu)成了Django中URL的命名空間,而命名空間主要是為了URL反解的,那什么是URL反解呢?我們現(xiàn)在能根據(jù)請求的一個URL路徑,找到對應(yīng)的view處理方法,那么反過來,我們在view方法中,或者是template中,根據(jù)傳遞過來的參數(shù),能夠解析出對應(yīng)的URL,這就是URL反解。為什么需要URL反解呢?主要是為了不要把程序?qū)懰懒耍绻覀冊趆tml中直接把路徑寫死了,那么以后改起來就會非常的麻煩,所以常常會把這些可變的東西放到一個變量中,在程序中引用的是這個變量名,這是寫程序的一個常識吧。所以,我們能從這個“樹”中,從上到下,也得能夠從下到上。
在template中進行反解使用的是{%url%}這個tag,在view中,進行反解,使用的是`django.core.urlresolvers.reverse()這個方法。
好,先來看一個最簡單的例子:
- mydjango/urls.py:
- urlpatterns = patterns('',
- url(r'model/', include('model_test.urls')),
- )
- model_test/urls.py:
- urlpatterns = patterns('',
- url(r'^$', views.index, name='index'),
- )
mydjango/urls.py是根URLconf,它包含了model_test的URLconf,modul_test中的urlpatterns中有一個命名為index的url映射對象。
如果我們想在template中得到這個view對應(yīng)的url的真實路徑,那么用template的url tag就行了:
{% url 'index' %}
這樣得到的結(jié)果就是: /model/。
同理,如果在view方法中,那么使用reverve()方法:
from django.core.urlresolvers import reverse
reverse("index")
得到的也是: /model/
在這個例子中,我們只使用到了url()方法中的name參數(shù),并沒有用到命名空間,因為這種簡單的情況,沒有產(chǎn)生混淆,還沒有必要用到命名空間,使用命名空間的主要有以下兩種情況:
當(dāng)在一個項目中,有多個應(yīng)用,應(yīng)用中定義的url映射對象的name有可能有重復(fù)的,這樣當(dāng)進行反解時,Django就不能確定到底是哪個應(yīng)用了
當(dāng)在一個項目中,同一個應(yīng)用,被部署多個實例時,這多個實例之間是共享定義的name url的,所以在進行反解時,也不能確定,到底是哪個實例
第一種情況,其實是比較好解決的,在每一個應(yīng)用的include()中,指定不同的namespace參數(shù)就可以了,如:
mydjango/urls.py:
- urlpatterns = patterns('',
- url(r'model/', include('model_test.urls', namespace='model')),
- )
這樣,在template中或者是reverse中,在name前需要加上namepace進行反解:
- {% 'model:index' %}
- or
- reverse("model:index")
這樣就可以準(zhǔn)確的反解到model_test這個應(yīng)用中。
第二種情況,什么叫“一個應(yīng)用,被部署多個實例”呢?其實就是這種情況:
- urlpatterns = patterns('',
- url(r'model1/', include('model_test.urls')),
- url(r'model2/', include('model_test.urls')),
- )
不同的路徑下,引用的是相同的應(yīng)用,同一個應(yīng)用,被實例化了兩次,這種情況,怎么進行區(qū)分呢?我能像第一種情況一樣,在include中指定不同的namespace來解決問題嗎?答案是可行的,但是不推薦。要知道,他們引用的是同一個應(yīng)用,同一個應(yīng)用意味著什么,意味著代碼是一樣的,你在同一份代碼中,通過if/else來判斷該反解到哪個namespace中,這個做法是非常不優(yōu)雅的,嚴(yán)重違背了Django的DRY原則。
那Django通過什么辦法來解決這個問題呢?它通過app_name + namespace + current_app的方式來解決。namespace, app_name分別為include()的第二個和第三個參數(shù),app_name指定這個應(yīng)用的名稱,namespace指定這個應(yīng)用某個實例的url的命名空間,current_app則是根據(jù)請求的路徑,解析出的該url的命名空間,也就是namespace,在進行反解時,動態(tài)的將該current_app傳遞給反解的函數(shù)中,反解的函數(shù)就可以根據(jù)這個namespace,來確定應(yīng)該反解到哪個實例中了。同一個應(yīng)用的多個實例的app_name應(yīng)該是相同的,而namespace應(yīng)該是不同的。可能有點亂了,我們再來舉個例子:
mydjango/urls.py:
- urlpatterns = patterns('',
- url(r'model1/', include('model_test.urls', namespace='model_1', app_name="app")),
- url(r'model2/', include('model_test.urls', namespace='model_2', app_name="app")),
- )
model_test/urls.py:
- urlpatterns = patterns('',
- url(r'^$', views.index, name='index'),
- )
model_test/views.py:
- from django.shortcuts import render
- from django.core.urlresolvers import reverse
- def index(request):
- current_app = request.resolver_match.namespace
- print reverse("app:index", current_app=current_app)
- return render(request, 'model/index.html', current_app=current_app)
model_test/templates/model/index.html:
{% url 'app:index' %}
在view中,首先獲得當(dāng)前的namespace,然后通過current_app傳遞給reverse(),reverse就可以知道應(yīng)該解析到哪個實例中了。同理,在templace中,也需要將current_app傳遞過去。這樣,我們就可以動態(tài)的反解URL了:
當(dāng)我們請求的路徑是/model1/時,current_app就是model_1,再根據(jù)app_name,就可以準(zhǔn)確的反解出該URL為:/model1/,如果請求的路徑是/model2/,那么current_app就是model_2,反解的路徑就是/model2/。
雖然有點復(fù)雜,但是這種情況用的比較少,google了很久,才把這種情況大概弄清楚,也許理解的有不對的地方,待以后實踐去檢驗,現(xiàn)在關(guān)鍵在于理解這種機制思想。
新聞熱點
疑難解答