# Django框架之路由详解 路由的作用是根据用户的请求(主要包含url和请求方法)调用相应的处理模块。 这个模块是纯粹的Python代码,包含URL模式(简单的正则表达式)到Python函数(你的视图)的简单映射。 具体讲,这个模块就是指的是django项目中的urls.py文件。 ## Django如何处理一个请求(有点绕,初学者绕行) 当一个用户请求Django站点的一个页面,下面是Django系统决定执行以下Python代码使用的算法: 1. Django,确定使用根URLconf模块。通常,这是[`ROOT_URLCONF`](https://docs.djangoproject.com/zh-hans/2.2/ref/settings/#std:setting-ROOT_URLCONF)设置的值,但如果放置`HttpRequest`对象拥有[`urlconf`](https://docs.djangoproject.com/zh-hans/2.2/ref/request-response/#django.http.HttpRequest.urlconf)属性(通过中间件设置),则其值将被替换为[`ROOT_URLCONF`](https://docs.djangoproject.com/zh-hans/2.2/ref/settings/#std:setting-ROOT_URLCONF)设置。 2. Django加载该Python模块并寻找可用的`urlpatterns`。它是[`django.urls.path()`](https://docs.djangoproject.com/zh-hans/2.2/ref/urls/#django.urls.path)和(或)[`django.urls.re_path()`](https://docs.djangoproject.com/zh-hans/2.2/ref/urls/#django.urls.re_path)实例的序列([sequence](https://docs.python.org/3/glossary.html#term-sequence))。 3. Django依次匹配每个URL模式,在与请求的URL匹配的第一个模式停下来。 4. 一旦有URL匹配成功,Djagno引入并调用相关的视图,这个视图是一个简单的Python函数 (或基于类的视图基于类的[视图](https://docs.djangoproject.com/zh-hans/2.2/topics/class-based-views/))。 - 一个[`HttpRequest`](https://docs.djangoproject.com/zh-hans/2.2/ref/request-response/#django.http.HttpRequest)实例。 - 如果匹配的URL返回没有命名组,那么来自正则表达式中的匹配项将作为位置参数提供。 - 关键字参数由路径表达式匹配的任何命名部分组成,并由[`django.urls.path()`](https://docs.djangoproject.com/zh-hans/2.2/ref/urls/#django.urls.path)或[`django.urls.re_path()`](https://docs.djangoproject.com/zh-hans/2.2/ref/urls/#django.urls.re_path)的可选`kwargs`参数中指定的任何参数覆盖。 5. 如果没有URL被匹配,或者匹配过程中出现了异常,Django会调用一个适当的错误处理视图。参加下面的错误处理( [错误处理](https://docs.djangoproject.com/zh-hans/2.2/topics/http/urls/#error-handling))。 ## Django框架中的两种路由定义方式 + django.urls.path() + django.urls.re_path() 区别: - path: 是定义字符串为url规则 - re_path: 可以是字符串或者字符串和正则的组合 path('tuling/', views.tuling), re_path(r'^\w{3}/$',views.tuling2), 下面是对re的一个基础解释,具体可参看前面课程: # ^ 在正则表达式中 代表 限制开始 # \w 在正则表达式中 代表 单个的 字母/数字/下划线 # {3} 在正则表达式中 代表匹配的次数 # $ 在正则表达式中 代表 限制结尾 # '^\w{3}/$' 就整体代表 字母/数字/下划线 三位组成的字符串以/结尾 # abb/a12/a_11/ 不符合 字符长度不对,开始和结尾不对。 ## Django框架中路由详解 本实验创建一个django项目v2。 具体创建过程参考上面内容。 ### 路由的访问方式 - 从上往下的逐个匹配,如果匹配到相应的路由,则调用响应处理模块,匹配终止 - 如果最终没有找到相应的匹配,则报错 - 如果由子路由,则匹配到相应的父级路由后,进入子路由重复上面匹配过程 ### 子路由 Django项目同名的文件夹下的urls成为总路由,它负责整个项目的路由分配。 一般使用的时候我们推荐的方式是,项目同名的文件夹下只负责配置,具体业务处理根据不同的业务 设置不同的APP,我们在总路由下,也只是做一个简单的配置就可以,以下代码的含义是, 对所有访问 路由是/tuling/下的所有的访问,都交由tuling的APP的子路由处理: # 总路由下的代码 from django.contrib import admin from django.urls import path, include from tuling import views urlpatterns = [ # url中包含tuling的请求都有子路由tuling.urls处理 path('tuling/', include("tuling.urls")) ... ... ] 相应的子路由的urls的代码如下: from django.contrib import admin from django.urls import path, include from . import views urlpatterns = [ path('admin/', admin.site.urls), # 子路由是sub_tuling的由views下的sub_tuling函数处理 path('sub_tuling/', views.sub_tuling) ] 而我们的访问URL路由如下图所示,主路由从根路由开始查找,当找到tuling/的时候就知道 这个访问应该有tuling APP处理,所以把路由剩下的部分sub_tuling/交给子路由处理,子路由再 根据匹配规则进行处理: ![URLs](./imgs/urls001.png) 子路由的访问方式类似文件夹,一层一层的访问,子路由就相当于子文件夹。 ### django.url.path path路由器采用字符串匹配模式,直接匹配用户的字符串。 #此时匹配的url是tuling1,只有找到这个字符串后才调用相应的试图函数进行处理 path('tuling1/', views.tuling1), ### django.url.re_path re_path路由是根据正则匹配的方式进行匹配,如果匹配到了就进行处理。 除了这个是使用正则进行匹配外,其余的匹配规则跟path基本一致。 #匹配的内容这部分采用的是正则的写法 re_path(r'^\w{3}/$',views.tuling2), ### 关键字参数传递 对服务器进行访问的时候有时候需要传入一些内容,路由会根据传入的内容进行匹配,此时可以 把传入的部分url内容当作参数调用试图处理模块。 典型应用如下: - 麦扣网(www.mycode.wang)是程序员博客网 - 比如我的网址是 www.mycode.wang/blog/liudana - 而相应的我发的博客的网址可能就是 www.mycode.wang/blog/liudana/221234.html - 前面blog表示这个是blog的版块 - liudana是某个做着的子url - 221234是我的某一篇blog的id号码 - 此种情况,如果对每一篇blog都由一个自己的url,进行路由匹配的时候会很多,而且也会很麻烦,所以 可以采用一个办法啊,即把blog的id作为参数传递就可以 使用path可以捕获url中的一部分内容作为视图函数的关键字参数进行传递,也是路由的基本功能。 使用方法为``, 此时向视图处理函数传递一个叫year的int型的参数。 则上述blog可能的路由就是: pyth('/blog/liudana//', views.blog), 本代码案例中tuling2和tuling3使用了带参数的路由; from django.contrib import admin from django.urls import path, include #导入tuling APP下的试图 from tuling import views #导入tuling APP下的路由文件, 作为子路由 from tuling import urls as tu urlpatterns = [ path('admin/', admin.site.urls), path('tuling1/', views.tuling1), path('tuling2/', views.tuling2), path('tuling3/', views.tuling3), path('tuling/', include(tu)) ] ### path转换器 path路由使用过程中,会把传入的内容自动转换成了对应类型的参数,比如`` , 会把 传入的数字自动转换成了整数表示的year参数,其中我们把int叫做path转换器(Path Converter), jango2默认支持以下5个转化器: - str: 匹配除了路径分隔符(/)之外的非空字符串,这是默认的形式 - int: 匹配正整数,包含0。 - slug: 匹配字母、数字以及横杠、下划线组成的字符串。 - uuid: 匹配格式化的uuid,如 075124d3-6485-412e-a2a8-6c931ea72f00 - path: 匹配任何非空字符串,包含了路径分隔符 如果上述五种转换器还不能满足需求,我们可能需要自己定义自己的转换器。 Path转换器是一个特殊的类,需要满足三点要求: - regex: 类属性,字符串类型,是匹配内容时候的正则表达式 - to_python(self, value) 方法: value是由类属性 regex 所匹配到的字符串, 返回具体的Python变量值,以供Django传递到对应的视图函数中 - to_url(self, value) 方法: 和 to_python 相反,value是一个具体的Python变量值, 返回其字符串,通常用于url反向引用 可以理解成,一个转换器就是有上述三个属性或者函数的类。 先看看默认的 IntConverter 和 StringConverter 是怎么实现的: 先来看看int和str的转换器的源代码: class IntConverter: regex = '[0-9]+' def to_python(self, value): return int(value) def to_url(self, value): return str(value) class StringConverter: regex = '[^/]+' def to_python(self, value): return value def to_url(self, value): return value 下面我们模仿转换器的源代码自己写一个自己的转换器(源代码:v2/v2/urls.py)。 我们需要一个四位数的年份转换器,用来把用户的输入作为一个四位数的年份,一个要求 是我们只能转换形如`19xx`的年份,别的不行: 编写自定义的转换器需要两步完成: - 编写代码,一般类命名为xxxConverter: class FourDigitYearConverter: #只能识别19xx这样的年份 regex = '19[0-9]{2}' def to_python(self, value): return int(value) def to_url(self, value): return '%04d' % value - 注册, 即使用register_converter 将其注册到URL配置中 from django.urls import path, include, register_converter # 演示的关系,Converter直接写在了路由文件中 # 实际使用时建议放入convertes文件 class FourDigitYearConverter: # 只能识别19xx这样的年份 regex = '19[0-9]{2}' ... ... # 把FourDigitYearConverter注册成Y4这个变量 register_converter(FourDigitYearConverter, 'Y4') #### 2.re_path 捕获url中的内容作为参数 re_path() 中可以使用 (?P<参数名>参数规则) 来捕获URL中的参数 ```python # re_path 捕获url中的内容作为参数 re_path(r'^name/(?P\w{3,8})/$',views.getname), re_path(r'^(?P\w{3,8})/p/(?P\d{8})/$',views.getnamepost), # 非命名参数捕获,这样定义那么对应的视图函数中只要有这个行参就可以 re_path(r'^(\w{3,8})/p/(\d{8})/$',views.gnamep), ``` ### 传递额外参数 请求的参数不仅仅来自以URL,还可能是我们自己定义的内容。 以下的路由处理,除了把url中的对应年份作为参数year传入视图函数外,还会 额外增加参数name,视图需要同时接收两个参数。 path('tuling6//', views.tuling6, {"name":"北京图灵学院"}), 对应的视图函数为: def tuling6(r, year, name): rst = f"Hello, {name}创始于{year}年" return HttpResponse(rst) ### re_path中的参数处理 re_path兼容django2版本前的处理方式,路由匹配采用正则来完成,同样也可以处理带参数的 url。 在Python正则表达式中,命名式分组语法为 (?Ppattern) , 其中name为传入视图的参数名称, pattern为待匹配的模式。 需要注意的是,采用re_path的时候,传入的参数是str类型的,如果需要别的类型,需要自己转换。 # path自定义的converter path('tuling4//', views.tuling4), # 把上述的自定义的converter转变成正 # 正则中的参数处理 # 把19xx的数字传换成参数yy的值传入视图处理函数 re_path('tuling5/(?P19[0-9]{2})/', views.tuling5),则表达是的参数处理方式 ### URL反向的解析 我们再代码中有时候也会用到相应的路由对应的url,此时如果我们直接把 url写入源代码中,会造成硬编码问题,导致后面代码的维护性通用性都会降低,为了 消除这种情况,我们使用反向解析的url。 对url反向解析本质上是对每一个URL进行命名, 以后再编码代码中使用URL的值, 原则上都应该使用反向解析即可。 我们通过案例来讲解url反向解析。 假如有这样一个URL的规则是解析到显示文章的视图的映射: # 路由 urls.py path('get/post//',views.getpost) # 视图 views.py def getpost(request,id): # 通过文章id获取文章数据 # 把文章内容解析到模版中 return HttpResponse(f'{id}:的文章内容。。。') 那么在需要定义链接地址的地方, 如html的a链接中可以直接硬编码的方式定义链接,例如: Python3编码规范-硬编码 这样就可以在页面中,通过a链接跳转到对应的文章视图函数中,查看文章的内容。 但是这样做有一个致命的缺陷: 一旦url规则发生变化或url规则较多时,非常容易出错。 假如 把url路由的规则改变了,那么对应的链接就会失效,你能想象你需要更改整个网站中的链接的痛苦场景。 因此,Django的路由可以设置url反向解析来一劳永逸的解决这个问题。 具体步骤是: 1. 在定义对应的路由规则时,只需要给定义的路由设置name关键字参数来表示路由名字 # 在定义路由时,可以指定路由的名字,在使用时通过路由名动态解析路由规则 url path('getpost//',views.getpost,name="home_getpost") 2. 在需要使用url的html页面中可以使用 url标签进行动态的反向解析 Python3编码规范-反向解析 3. 不管你路由规则怎么变化,只要路由名称不变,那么通过反响解析的链接也不需要更改 url反向解析的使用: + html模版中 可以使用url标签来反向解析, 如上面所示 + 在其他的python代码中 使用 reverse() 方法进行反向解析。django.url.reverse() 示例代码如下: - 路由需要使用name参数 # v2/v2/urls.py ... ... #反向解析 path('tuling7/', views.tuling7, name="TuLing_7"), ... ... - 视图处理需要引入`django.urls.reverse()` # v2/tuling/views.py ... ... from django.urls import reverse def tuling7(r): rst = "本次请求的url是:{} ".format(reverse("TuLing_7")) return HttpResponse(rst) ... ... - 最终结果如下图: ![反向解析](./imgs/urls002.png)