购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.2 URL路由配置

本节主要介绍Django框架视图层中关于URL路由配置方面的内容,包括URL路由基础、如何处理请求、PATH转换器、正则表达式匹配等方面。它们是基于Django框架进行视图层开发的基础。

4.2.1 URL路由基础

对于高质量的Web应用来讲,使用简洁、优雅的URL设计模式非常有必要。Django框架允许设计人员自由地设计URL模式,而不用受到框架本身的约束。对于URL路由来讲,其主要实现了Web服务的入口。用户通过浏览器发送过来的任何请求,都会解析到一个指定的URL地址上去,进而得到服务器端的响应,这是一个基本流程。

在Django项目中,配置URL路由通过目录中的urls.py文件来完成。虽然在一个Django项目中可以配置有多个urls.py文件(因为一个项目可以包含若干个App),但这些urls.py文件绝对不能放在同一目录下。一般情况下,在Django项目根目录下需要配置一个urls.py(根路由)文件,然后在每个App下分别定义一个自己的urls.py,这样就相当于是一种比较先进的解耦模式。

归根结底,URL路由就是相当于路径和视图函数之间的一个对应关系,起到了一个中间媒介的作用,URL路由原理如图4.1所示。

图4.1 URL路由原理图

在图4.1中,客户端用户发来的HTTP请求经过URL路由映射处理后,会发送到相应的View视图处理函数进行处理,View视图函数处理完成后,再通过HttpResponse对象返回具体信息到客户端进行显示。

一个urls.py文件的通用基本格式可参考下面的代码。

【代码4-1】

【代码分析】

在第01行代码中,通过调用django.contrib模块导入了admin(管理员)对象,这是一个Django框架自带的管理员模块。

在第02行代码中,通过调用django.urls模块导入了path(路径)对象,这是一个负责URL路由配置的模块。

在第04~08行代码中,通过urlpatterns对象定义了一个数组。其中,第05、06行代码通过path对象定义了具体的路径配置信息。通常,用户自定义的路由配置代码都是在这里完成的。

4.2.2 Django如何处理请求

在Django框架中,当客户端用户发出一个页面请求时,URL路由基本会按照下面的逻辑(算法)执行操作。

(1)决定要使用的根URLconf模块。通常情况下,这是由ROOT_URLCONF所设置的值。但是,如果传入的HttpRequest对象具有urlconf属性(由中间件设置),则其值将被用于代替ROOT_URLCONF参数所设置的值。这也就是说,设计人员可以自行指定自定义项目的入口文件urls.py。

(2)加载这个URLconf模块并寻找可用的urlpatterns路由模式,它是django.urls.path()实例或django.urls.re_path()实例的一个列表。

(3)依次匹配每个URL模式,在找到与请求的URL模式相匹配的第一个模式上停止。这也就是说,URL模式匹配是从上往下的短路操作,因此每个URL在列表中的位置是比较关键的。

(4)继续导入并调用匹配行中给定的视图,该视图是一个简单的Python函数(被称为视图函数)或者是一个基于类的视图。另外,该视图将获得如下3类参数:

· 一个HttpRequest对象实例。

· 如果匹配的表达式返回了未命名的组,那么匹配的内容将作为位置参数提供给视图。

· 关键字参数由表达式匹配的命名组所组成,但是可以被django.urls.path()实例或django.urls.re_path()实例的可选参数kwargs覆盖。

(5)如果没有匹配到任何表达式,或者过程中抛出异常,将调用一个适当的错误处理视图。

一个关于URLconf模块的代码实例如下所示。

【代码4-2】

【代码分析】

在第05~11行代码中,定义的就是urlpatterns数组列表,每一个列表项都是path()或re_path()的实例。具体说明如下:

· 在第06行代码中,将路径'articles/2023/'解析为视图views.special_case_2023,且路径'articles/2023/'的年份(2023)为固定的。

· 在第07行代码中,将路径'articles/<int:year>/'解析为视图views.year_archive,且路径中的年份(<int:year>)为任意的。

· 在第08行代码中,将路径'articles/<int:year>/<int:month>/'解析为视图views.month_archive,且路径中的年份(<int:year>)和月份(<int:month>)均为任意的。

· 在第09行代码中,将路径'articles/<int:year>/<int:month>/<slug:slug>/'解析为视图views.article_detail,且路径中新增了slug类型转换器。

在这段路径解析代码中,有以下5点要重点说明:

· 要捕获一段URL中的值,需要使用尖括号(< >)。

· 可以将捕获到的值转换为指定类型,比如上面代码中的int类型(整型)。

· 默认情况下,捕获到的结果保存为字符串类型,但是不包含“/”这个特殊字符的。

· 匹配模式的最开头不需要添加特殊字符“/”,因为默认情况下的每个URL地址的最前面都会带有这个特殊字符的。

· 每个匹配模式都建议以特殊字符“/”结尾。

下面,基于【代码4-2】讲解几个典型的、针对URL地址进行模式匹配的示例。具体内容如下:

(1)“/articles/2023/”:匹配第06行代码,并调用views.special_case_2023(request)视图。

(2)“/articles/2023”:无匹配结果,因为最后少了一个斜杠(/),而列表中的所有模式都以斜杠(/)结尾。

(3)“/articles/2050/”:匹配第07行代码,并调用views.year_archive(request)视图。

(4)“/articles/2023/12/”:匹配第08行代码,并调用views.month_archive(request, year=2023,month=12)视图。

(5)“/articles/2023/12/django-url-pattern/”:匹配第09行代码,并调用views.article_detail(request,year=2023, month=12, slug="django-url-pattern"视图。

4.2.3 PATH路径转换器

在Django框架中,默认内置了一组PATH路径转换器,具体介绍如下:

· str类型转换器:匹配任何非空字符串,但是不包含特殊字符“/”;如果设计人员没有指定专门的转换器,默认就是使用该转换器。

· int类型转换器:匹配0和正整数,返回一个int类型。

· slug类型转换器:可理解为注释、后缀、附属等概念,主要是URL链接中置于最后一部分的解释性字符。该转换器匹配任何ASCII字符以及连接符和下画线。

· uuid类型转换器:匹配一个UUID格式的对象。为了防止冲突,规定必须使用中画线(-),并且所有字母必须小写,例如,下面这个UUID字符串01234567-8900-aacc-a8a8-987654321000将返回一个uuid对象。

· path类型转换器:匹配任何非空字符串,重点是可以包含路径分隔符(/);这个转换器适用于匹配整个URL链接,而不是一段一段的URL字符串。同时,要注意区分path转换器和path()方法二者之间的区别。对于更复杂的匹配需求,设计人员可能就需要自定义path转换器了。其实,path转换器就是一个类,主要包含下面的成员和属性:

◆ 类属性regex:一个字符串形式的正则表达式属性。

◆ to_python(self, value)方法:一个用来将匹配到的字符串转换为目标数据类型并传递给视图函数的方法。注意,如果转换失败,则该方法必须弹出ValueError异常。

◆ to_url(self, value)方法:一个将Python数据类型转换为一段URL地址的方法,为to_python(self, value)方法的反向操作。注意,如果转换失败,则该方法也会弹出ValueError异常。

下面介绍一个关于PATH路径转换器的代码实例。

首先,新建一个用于PATH路径转换的Python文件,定义一个用于转换4位正整数年份数值的类FourDigitYearConverter,具体代码如下:

【代码4-3】(详见本书配套下载资源中的源代码中的urlconverter.py)

【代码分析】

在第02行代码定义了类属性regex,格式为4位整数的正则表达式。

在第04、05行代码定义了类方法to_python(),用于将4位整数(value)转换为Python数据类型。

在第07、08行代码定义了类方法to_url(),用于将Python数据类型转换为URL地址,并进行了格式化操作(使用数字“0”从左填充的4位整数)。

然后,基于【代码4-3】进行修改,在URLconf模块中使用register_converter()方法进行注册,具体代码如下:

【代码4-4】(urlconf.py)

【代码分析】

在第05行代码中,通过register_converter()方法注册了一个“yyyy”类型。

第07~13行代码定义了urlpatterns数组列表。其中,第09行、第10行和第11行中关于年份的类型使用了“yyyy”进行定义。

4.2.4 使用正则表达式

在Django框架的新版本(v2.0 +)中,URLconf模块虽然更改了配置方式,但它依然可以对老版本进行兼容,兼容的办法就是使用re_path()方法。

这个re_path()方法本质上就是以前的url()方法,只不过导入的位置变了。另外,re_path()方法与path()方法有以下两个不同点:

· re_path()方法捕获URL地址中的参数使用的是正则表达式方式,语法是(?P<name>pattern)格式,其中的< name>是组名,pattern是要匹配的模式。

· re_path()方法传递给视图的所有参数都是字符串类型,而不像path()方法那样可以指定转换成某种类型,因此在视图中接收参数时一定要小心。

使用正则表达式进行匹配的示例代码如下:

【代码4-5】

【代码分析】

在第07~09行、第10~12行和第13~15行代码中,re_path()方法使用的就是正则表达式方式。

在第08行、第11行和第14行代码中,<year>组名严格匹配4位整数(如12345这样的整数是无法匹配的),这是由正则表达式?P<year>[0-9]{4})/$的规则所决定的。

另外,正则表达式?P<year>[0-9]{4})/$也可以简写成未命名的形式[0-9]{4},但为了避免歧义,不建议这么做。

4.2.5 URLconf在什么上查找

在Django框架中,客户端请求的URL地址会被当作一个普通的Python字符串来处理,URLconf模块将基于此进行查找并匹配。在查找和匹配时,将不包括域名、GET和POST请求方式或HEAD请求方法等。

举例来讲,在下面的请求URL地址中:

     https://www.example.com/myapp/

URLconf模块将会查找“myapp/”字符串,不会对域名“www.example.com”进行查找。而在下面的请求URL地址中:

     https://www.example.com/myapp/?page=1

URLconf模块仍将会查找“myapp/”字符串,既不会对域名“www.example.com”进行查找,也不会对参数“page=1”进行查找。

URLconf模块不会检查使用了哪种请求方法,即对于同一个URL地址,无论是GET请求、POST请求,还是HEAD请求方法等,均将路由到相同的函数。

4.2.6 指定视图参数的默认值

在Django框架中,对于URLconf有一个方便的小技巧是指定视图参数的默认值。可参看下面这个关于URLconf和视图的代码示例。

【代码4-6】

【代码分析】

在第06~09行代码中,定义了urlpatterns数组列表。其中,第07、08行代码分别定义了两个URL路径模式。

在第12、13行代码中,定义了一个视图函数page。其中,在第12行代码中指定了num参数的默认值为1。

第07行和第08行代码定义的两个URL路径模式均指向同一个视图views.page,但第一个模式(第07行代码)是不会从URL地址中捕获任何值的。如果第一个模式匹配成功,则page()函数将使用num参数的默认值(1);如果第二个模式匹配成功,则page()函数将使用num参数的实际值。

4.2.7 包含其他的URLconf模块

在Django框架中,还可以在一个URLconf模块中包含其他的URLconf模块,这实际上就是将一部分URL放置于其他URL的下面。该方式的具体操作方法是,在自己的urlpatterns数组列表中,通过include语法命令引入另一个URLconf模块,示例代码如下:

【代码4-7】

【代码分析】

在第01行代码中,首先引入了include模块。

在第05~07行代码中,通过include方式分别引入了3个URLconf模块community.urls、contact.urls和about.urls。这样,在该URLconf模块中就包含了另外3个URLconf模块。

通过include方式,还可以消除URLconf模块中的冗余URL路径。例如,当某个模式前缀被重复使用时,就可以使用include语法进行简化。示例代码如下:

【代码4-8】

【代码分析】

在第05~09行代码中,定义的一组(共5个)URL路径模式均包含了相同的前缀<page_slug>-<page_id>。

其实,可以通过include方式改进上面代码的写法,只需要声明一次共同的路径前缀,并将后面的部分进行分组即可。示例代码如下:

【代码4-9】

【代码分析】

在第01行代码中,首先引入了include模块。

在第05~11行代码中,先是声明了一次共同的路径前缀<page_slug>-<page_id>,再通过include方式将路径后面不同的部分进行了分组。这样,就消除了URLconf模块中的冗余URL路径。

4.2.8 传递额外参数给视图函数

在Django框架中,URLconf模块还支持一种传递额外参数给视图函数的方式,该参数为Python字典类型。具体操作方法是,先在path()函数中包含Python字典类型的参数,然后将该参数传递给视图函数。

通过URLconf模块传递额外参数给视图函数的示例代码如下:

【代码4-10】

【代码分析】

在第05行代码中,通过path()函数包含了一个Python字典类型参数{'foo': 'bar'},该参数将会传递给视图函数views.year_archive。

基于这段代码,如果客户端发来一个URL请求/article/2020/,那么Django服务器将会调用视图函数views.year_archive(request, year=2020, foo='bar')。这样,额外参数{'foo': 'bar'}就传递给视图函数了。

另外,通过include方式也可以实现传递额外参数给视图函数的操作,示例代码如下:

【代码4-11】

【代码分析】

在第05行代码中,在path()函数中通过include方式引入了inner模块,同时还包含了一个Python字典类型参数{'article_id': 3}。

在第12~15行代码中,定义了inner模块的URL路径模式。其中,第13行和第14行代码定义的path()函数将会自动获取mian模块传递过来的参数{'article_id': 3},效果等同于下面的【代码4-12】。

【代码4-12】

【代码分析】

本例中第13行和第14代码中定义的path()函数的效果,等价于【代码4-11】传递参数的效果。

4.2.9 反向解析

在Django项目实际开发中,经常需要获取某个具体对象的URL,为生成的内容配置URL链接。

例如下面这样一个很常见的场景,在页面中展示一个文章标题列表,且每个标题都被设计成一个超链接,单击该链接就进入对应文章的详细页面。通常情况下,我们首先会简单地将URLconf模块设计成类似【代码4-13】的形式。

【代码4-13】

     01  from django.urls import path
     02  from . import views
     03
     04  urlpatterns = [
     05      path('article/<int:pk>/', views.article_pk),
     06  ]

【代码分析】

在第05行代码中,通过path()函数包含了一个URL请求('article/<int:pk>/')对应的视图函数(views.article_pk)。

然后在前端HTML页面中,超链接<a>标签的href属性会被定义为类似“http://www.domain.com/article/1/”的值。当然,其中的域名部分(www.domain.com)将会由Django框架负责处理,设计人员只需要关注路径(/article/1/)的部分。

上述这样的设计当然也能够行得通,但存在巨大隐患,即将来既难以维护,又难以修改。因为,当URLconf模块被修改后,设计人员势必将手动修改HTML页面中的每一个超链接<a>标签中硬编码的href属性值,其工作量是可想而知的。

于是,设计人员就需要一种既安全又可靠,还能具有自适应功能的机制。该机制能够实现当修改URLconf模块中的代码后,无须在项目源码中大范围手动修改全部失效的硬编码URL地址。

Django框架恰好提供了一种解决方案——在URL地址中提供一个name参数,并赋值一个自定义的、便于标记的字符串。通过这个name参数,可以反向解析URL链接、反向URL链接匹配或反向URL链接查询。

Django框架在需要解析URL链接地址的地方,对不同层级提供了不同的工具,用于URL链接反查,具体有以下3种方式:

· 写前端HTML网页时,在模板语言中使用url模板标签。

· 写视图函数时,使用Python语法的reverse()函数。

· 写模型(model)实例时,使用get_absolute_url()方法。

上面的3种方式都依赖于首先在path()函数中为URL链接地址添加name属性。

下面,我们继续完善这个在页面中展示一个文章标题列表的实例。首先,需要重新定义URLconf模块,具体代码如下:

【代码4-14】

【代码分析】

在第07行代码中,在path()函数中新增了一个name参数(name='article-year-archive'),该参数主要在模板中使用。

然后,通过在模板(HTML页面)中引用上面的name参数,实现对文章标题列表的获取,具体代码如下:

【代码4-15】

【代码分析】

在第07~13行代码中,通过在HTML页面中使用for循环语句,实现了文章标题列表的显示。具体说明如下:

· 在第09行代码中,在模板中通过引用上面定义的name参数(name='article-year-archive'),实现了文章标题链接的反向解析。

最后,在视图函数中编写实现URL链接地址反向解析的Python代码,具体如下:

【代码4-16】

【代码分析】

在第02行代码中,引入了反向解析模块reverse。

在第04~10行代码中,定义了反向解析视图函数redirect_to_year()。其中关键的是第09行代码,通过调用reverse()方法实现了name='article-year-archive'与URLconf模块中的该PATH路径的反向解析操作。

在URL链接地址中使用name参数时,可以包含任何自定义的字符串,但稍不注意可能就会出现重名冲突的问题。于是,为了解决这个问题,引出了“命名空间”的概念。

4.2.10 命名空间

在URLconf模块中定义PATH路径时,通过添加name参数可以实现反向URL地址解析与软编码解耦。不过,在出现下面的情况时又会很麻烦。

· 应用appA定义了一条path路由A,其name参数的值为index。

· 应用appB定义了一条path路由B,其name参数的值为index。

当出现上述这种情况时,如果在某个视图中使用reverse('index', args=(...)),或在模板中使用{%url 'index' ... %},那么最终生成的URL地址到底是路由A还是路由B呢?

造成上述这种情况出现的原因,根本上是各个应用之间没有进行统一的路由管理(实际上也不可能有)。于是,Django框架设计了一个app_name属性来解决上述问题,这就是应用级别的命名空间。具体代码如下:

【代码4-17】

【代码分析】

在第05行代码中,引入的app_name属性就是关键代码,其属性值就是应用的命名空间。

具体添加方法很简单,只需要在app自身的urls.py文件内添加app_name属性即可。

在实际项目中的使用也很简单,具体代码如下:

【代码4-18】

【代码分析】

无论是在视图中还是模板中,都将app_name属性值与对应的name参数值一起使用('your_app_name:index')就可以了。

在Django框架中,要实现对URL链接地址的反向解析,除了应用级别的命名空间方式之外,还支持一种实例命名空间(namespace)方式。

这种实例命名空间方式,是通过在path()函数中添加一个namespace属性实现的,具体代码如下:

【代码4-19】(项目根urls.py文件)

【代码分析】

在第06行和第07行代码中,在ptah()函数中新定义了一个namespace属性,属性值就是实例命名空间。

然后,在应用级别的urls.py文件中定义具体路由,具体代码如下:

【代码4-20】(应用urls.py文件)

【代码分析】

在第07行代码中,还是要添加app_name属性的定义。

在第10行和第11行代码中,在ptah()函数中定义了URL链接对应的视图函数及name参数。

最后,定义具体的视图函数,代码如下:

【代码4-21】(应用views.py文件)

【代码分析】

在第05、06行代码中,定义了视图函数index,它将不同的URL路径解析成不同的内容。例如:

· 当访问URL路径author/index/时,会得到字符串内容“Current namespace is author.”。

· 当访问URL路径article/index/时,会得到字符串内容“Current namespace is article.”。

在第08~14行代码中,定义了视图函数detail,它同样也会将不同的URL路径解析成不同的内容。例如:

· 当访问URL路径author/detail/时,会得到字符串内容“This is author page.”。

· 当访问URL路径article/detail/时,会得到字符串内容“This is article page.”。

· 当URL路径无法匹配时,会得到字符串内容“Hello, Django!”。

另外,在使用实例命名空间时要注意以下4点:

(1)namespace属性参数要定义在include之中。

(2)整个项目中所有应用(app)中的namespace属性不能重名,也就是必须全局唯一。

(3)使用实例命名空间功能的前提是要设置app_name属性,如果不设置则会弹出异常。

(4)如要在视图中获取namespace属性值,则必须通过request.resolver_match.namespace参数。 wxej5UnykX7J1qFp+aC/2oAz/qMI2z4ILcqCkMgbQfFIWTFdJrrC6pBWQQrgP9Yb

点击中间区域
呼出菜单
上一章
目录
下一章
×