回到顶部

阅读目录

Django url 反向解析(reverse) 和 命令空间(namespace)

关键的三个参数

  • appname ( 应用命名空间 )
  • namespace ( 应用的实例命名空间 )
  • name ( 视图的名字或者标识 )

文章 django 版本

  • 1.8

URL路由

对于高质量的Web 应用来说,使用简洁、优雅的URL 模式是一个非常值得重视的细节。Django 让你随心所欲设计你的URL,不受框架束缚。

不要求有.php 或.cgi,更不会要求类似0,2097,1-1-1928,00 这样无意义的东西。

参见万维网的发明者Berners-Lee 的Cool URIs don’t change,里面有关于为什么URL 应该保持整洁和有意义的卓越论证。

概览

为了给一个应用设计URL,你需要创建一个Python模块,通常称为路由选择模块或路由解析模块URLconf(URL configuration)。该模块是一个纯粹的Python模块,是URL模式(简单的正则表达式)到Python 函数(你的视图)之间的简单映射。

根据你的需要,这个映射可短可长。它也可以引用其它的映射。而且,由于它是纯粹的Python 代码,所以可以被动态地构造。

Django 还提供了一种方式来根据当前活跃的语言对URL进行翻译。更多信息参见国际化文档

Django 如何处理一个请求

当用户请求Django 站点上的某个页面时,django系统用一个算法来决定执行哪段Python代码:

  1. 首先,Django会使用路由解析根模块(root URLconf)来解析路由。通常,该路由解析根模块的位置由settings中的ROOT_URLCONF 变量指定(该模块的默认位置在BASE_DIR所指定的目录下的主app目录下的urls.py模块)。但是,如果进来的HttpRequest 对象有urlconf 属性(该属性由中间件request processing 设置),那么由ROOT_URLCONF所设置的路由解析根模块的路径则被HttpRequest对象的urlconf属性的值所替换。
  2. Django 加载该路由解析模块,并寻找可用的urlpatterns。这个urlpattens是一个Python列表,该列表的每个元素都是django.conf.urls.url()的一个实例。
  3. Django 依次匹配该列表中的每个URL模式,在遇到第一个与请求的URL相匹配的模式时停下来。
  4. 一旦某个正则表达式与请求的URL相匹配,则Django 导入并调用给定的视图,该视图仅为一个单纯的Python 函数(或者是一个基于类的视图)。同时,如下参数被传递给该视图:
    • 一个HttpRequest 实例。
    • 如果所匹配的正则表达式返回的是若干个无名组,那么该正则表达式所匹配的内容将被作为位置参数提供给该视图。
    • 关键字参数由与正则表达式相匹配的命名组组成,并且这些关键字参数可以被django.conf.urls.url()的可选参数kwargs覆盖。
  5. 如果请求的URL没有匹配到任何一个正则表达式,或者在匹配过程的任何时刻抛出了一个异常,那么Django 将调用适当的错误处理视图进行处理。请参见下面的错误处理

例子

下面是一个简单的路由选择模块(URLconf)的示例:

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003),
    url(r'^articles/([0-9]{4})/$', views.year_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),
]

注:

  • 若要从URL 中捕获某个值,只需要在它周围放置一对圆括号。
  • 在正则表达式中不需要添加一个前导的反斜杠,因为每个URL默认都带有该符号。例如,应该写成^articles 而不是 ^/articles
  • 每个正则表达式前面的'r' 是可选的,但是建议加上。它告诉Python 这个字符串是“原始的” —— 字符串中任何字符都不应该转义。参见Dive Into Python 中的解释

一些请求的例子:

  • /articles/2005/03/ 的请求将匹配列表中的第三个模式。Django 将调用函数views.month_archive(request, '2005', '03')
  • /articles/2005/3/ 不匹配任何URL 模式,因为列表中的第三个模式要求月份应该是两个数字。
  • /articles/2003/ 将匹配列表中的第一个模式不是第二个,因为模式按顺序匹配,第一个会首先测试是否匹配。请像这样自由插入一些特殊的情况来探测匹配的次序。
  • /articles/2003 不匹配任何一个模式,因为每个模式要求URL 以一个斜线结尾。
  • /articles/2003/03/03/ 将匹配最后一个模式。Django 将调用函数views.article_detail(request, '2003', '03', '03')

命名组

上面的示例使用简单的、没有命名的正则表达式组(通过圆括号)来捕获URL 中的值并以位置 参数传递给视图。在更高级的用法中,可以使用命名的正则表达式组来捕获URL  中的值并以关键字 参数传递给视图。

在Python 正则表达式中,命名正则表达式组的语法是(?P<name>pattern),其中name 是组的名称,pattern 是要匹配的模式。

下面是以上URLconf  使用命名组的重写:

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003),
    url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
    url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
    url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$', views.article_detail),
]

这个实现与前面的示例完全相同,只有一个细微的差别:捕获的值作为关键字参数而不是位置参数传递给视图函数。例如:

  • /articles/2005/03/ 请求将调用views.month_archive(request, year='2005', month='03')函数,而不是views.month_archive(request, '2005', '03')
  • /articles/2003/03/03/ 请求将调用函数views.article_detail(request, year='2003', month='03', day='03')

在实际应用中,这意味你的URLconf 会更加明晰且不容易产生参数顺序问题的错误 —— 你可以在你的视图函数定义中重新安排参数的顺序。当然,这些好处是以简洁为代价;conf

匹配/分组算法

下面是URLconf 解析器使用的算法,针对正则表达式中的命名组和非命名组:

  1. 如果有命名参数,则使用这些命名参数,忽略非命名参数。
  2. 否则,它将以位置参数传递所有的非命名参数。

根据传递额外的选项给视图函数(下文),这两种情况下,多余的关键字参数也将传递给视图。

URLconf 在什么上查找

请求的URL被看做是一个普通的Python 字符串, URLconf在其上查找并匹配。进行匹配时将不包括GET或POST请求方式的参数以及域名。

例如,http://www.example.com/myapp/请求中,URLconf 将查找myapp/

http://www.example.com/myapp/?page=3 请求中,URLconf 仍将查找myapp/

URLconf 不检查使用了哪种请求方法。换句话讲,所有的请求方法 —— 即,对同一个URL的无论是POST请求、GET请求、或HEAD请求方法等等 —— 都将路由到相同的函数。

捕获的参数都是字符串

每个捕获的参数都作为一个普通的Python 字符串传递给视图,无论正则表达式使用的是什么匹配方式。例如,下面这行URLconf 中:

url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),

... views.year_archive() 的year 参数将是一个字符串,即使[0-9]{4} 值匹配整数字符串。

指定视图参数的默认值

有一个方便的小技巧是指定视图参数的默认值。 下面是一个URLconf 和视图的示例:

# URLconf
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^blog/$', views.page),
    url(r'^blog/page(?P<num>[0-9]+)/$', views.page),
]

# View (in blog/views.py)
def page(request, num="1"):
    # Output the appropriate page of blog entries, according to num.
    ...

在上面的例子中,两个URL模式指向同一个视图views.page —— 但是第一个模式不会从URL 中捕获任何值。如果第一个模式匹配,page() 函数将使用num参数的默认值"1"。如果第二个模式匹配,page() 将使用正则表达式捕获的num 值。

性能

urlpatterns 中的每个正则表达式在第一次访问它们时被编译。这使得系统相当快。

urlpatterns 变量的语法

urlpatterns 应该是一个url() 实例类型的Python 列表。

错误处理

当Django 找不到一个匹配请求的URL 的正则表达式时,或者当抛出一个异常时,Django 将调用一个错误处理视图。

这些情况发生时使用的视图通过4个变量指定。它们的默认值应该满足大部分项目,但是通过赋值给它们以进一步的自定义也是可以的。

完整的细节请参见自定义错误视图

这些值可以在你的根URLconf 中设置。在其它URLconf 中设置这些变量将不会产生效果。

它们的值必须是可调用的或者是表示视图的Python 完整导入路径的字符串,可以方便地调用它们来处理错误情况。

这些值是:

包含其它的URLconfs

在任何时候,你的urlpatterns 都可以包含其它URLconf 模块。这实际上将一部分URL 放置于其它URL 下面。

例如,下面是 Django 网站自己的URLconf 中一个片段。它包含许多其它URLconf:

from django.conf.urls import include, url

urlpatterns = [
    # ... snip ...
    url(r'^community/', include('django_website.aggregator.urls')),
    url(r'^contact/', include('django_website.contact.urls')),
    # ... snip ...
]

注意,这个例子中的正则表达式没有包含$(字符串结束匹配符),但是包含一个末尾的斜杠。每当Django 遇到include()django.conf.urls.include())时,它会去掉URL 中匹配的部分并将剩下的字符串发送给包含的URLconf 做进一步处理。

另外一种包含其它URL 模式的方式是使用一个url() 实例的列表。例如,请看下面的URLconf:

from django.conf.urls import include, url

from apps.main import views as main_views
from credit import views as credit_views

extra_patterns = [
    url(r'^reports/(?P<id>[0-9]+)/$', credit_views.report),
    url(r'^charge/$', credit_views.charge),
]

urlpatterns = [
    url(r'^$', main_views.homepage),
    url(r'^help/', include('apps.help.urls')),
    url(r'^credit/', include(extra_patterns)),
]

在这个例子中,/credit/reports/ URL将被 credit.views.report() 这个Django 视图处理。

这可以用于移除URL配置中重复的部分例如,考虑这个URLconf:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/history/$', views.history),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/edit/$', views.edit),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/discuss/$', views.discuss),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/permissions/$', views.permissions),
]

我们可以改进它,通过只声明共同的路径前缀一次并将后面的部分分组:

from django.conf.urls import include, url
from . import views

urlpatterns = [
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/', include([
        url(r'^history/$', views.history),
        url(r'^edit/$', views.edit),
        url(r'^discuss/$', views.discuss),
        url(r'^permissions/$', views.permissions),
    ])),
]

捕获的参数

被包含的URLconf 会收到来自父URLconf 捕获的任何参数,所以下面的例子是合法的:

# In settings/urls/main.py
from django.conf.urls import include, url

urlpatterns = [
    url(r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
]

# In foo/urls/blog.py
from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.blog.index),
    url(r'^archive/$', views.blog.archive),
]

在上面的例子中,捕获的"username"变量将被如期传递给include()指向的URLconf。

嵌套的参数

正则表达式允许嵌套参数,Django 将解析它们并传递给视图。当反查时,Django 将尝试填满所有外围捕获的参数,并忽略嵌套捕获的参数。考虑下面的URL 模式,它带有一个可选的page 参数:

from django.conf.urls import url

urlpatterns = [
    url(r'blog/(page-(\d+)/)?$', blog_articles),                  # bad
    url(r'comments/(?:page-(?P<page_number>\d+)/)?$', comments),  # good
]

两个模式都使用嵌套的参数,其解析方式是:例如blog/page-2/ 将匹配blog_articles并带有两个位置参数page-2/ 和2。第二个comments 的模式将匹配comments/page-2/ 并带有一个值为2 的关键字参数page_number。这个例子中外围参数是一个不捕获的参数(?:...)

blog_articles 视图需要最外层捕获的参数来反查,在这个例子中是page-2/或者没有参数,而comments可以不带参数或者用一个page_number值来反查。

嵌套捕获的参数使得视图参数和URL 之间存在强耦合,正如blog_articles 所示:视图接收URL的一部分(page-2/),而不只是视图参数所要的值。这种耦合在反查时更加显著,因为反查视图时我们需要传递URL 的一个片段而不只是page 的值。

通常来说,我们只捕获视图需要的参数;并且当正则需要参数但是视图忽略参数时,请使用非嵌套参数

传递额外的选项给视图函数

URLconfs 具有一个钩子,让你传递一个Python 字典作为额外的参数传递给视图函数。

django.conf.urls.url() 函数可以接收一个可选的第三个参数,它是一个字典,表示想要传递给视图函数的额外关键字参数。

例如:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^blog/(?P<year>[0-9]{4})/$', views.year_archive, {'foo': 'bar'}),
]

在这个例子中,对于/blog/2005/请求,Django 将调用views.year_archive(request, year='2005', foo='bar')

这个技术在Syndication 框架 中使用,来传递元数据和选项给视图。

处理冲突

URL 模式捕获的命名关键字参数和在字典中传递的额外参数有可能具有相同的名称。当这种情况发生时,将使用字典中的参数而不是URL 中捕获的参数。

传递额外的选项给include()

类似地,你可以传递额外的选项给include()。当你传递额外的选项给include() 时,被包含的URLconf 的每一 行将被传递这些额外的选项。

例如,下面两个URLconf 设置功能上完全相同:

设置一:

# main.py
from django.conf.urls import include, url

urlpatterns = [
    url(r'^blog/', include('inner'), {'blogid': 3}),
]

# inner.py
from django.conf.urls import url
from mysite import views

urlpatterns = [
    url(r'^archive/$', views.archive),
    url(r'^about/$', views.about),
]

设置二:

# main.py
from django.conf.urls import include, url
from mysite import views

urlpatterns = [
    url(r'^blog/', include('inner')),
]

# inner.py
from django.conf.urls import url

urlpatterns = [
    url(r'^archive/$', views.archive, {'blogid': 3}),
    url(r'^about/$', views.about, {'blogid': 3}),
]

注意,额外的选项将永远传递给被包含的URLconf 中的每一行,无论该行的视图实际上是否认为这些选项是合法的。由于这个原因,该技术只有当你确定被包含的URLconf 中的每个视图都接收你传递给它们的额外的选项时才有价值。

URL 的反向解析

在创建Django 项目时,一个常见的需求是如何获得URL的最终形式以嵌入到所生成的Content中(即嵌入到视图中、或URL资源中,或者显示给用户的URL等)或者用于处理服务器端的导航流(如:重定向等)。

人们强烈希望不要硬编码这些URL(费力、不可扩展且容易产生错误)或者设计一种与URLconf 毫不相关的专门的URL 生成机制,因为这样容易导致一定程度上产生过期的URL。

换句话讲,需要的是一个DRY 机制。除了其它优点,它还允许设计的URL 可以自动更新而不用遍历项目的源代码来搜索并替换过期的URL。

要获取一个URL,最初拥有的信息是负责处理它的视图的标识(例如名字),与查找正确的URL 的其它必要的信息如视图参数的类型(位置参数、关键字参数)和值。

Django 提供了一个解决方案使得URL 映射是URL 设计唯一的储存库。你用你的URLconf填充它,然后可以双向使用它:

  • 根据用户/浏览器发起的URL 请求,它调用正确的Django 视图,并从URL 中提取它的参数需要的值。
  • 根据Django 视图的标识和将要传递给它的参数的值,获取与之关联的URL。

第一种方式是我们在前面的章节中一直讨论的用法。第二种方式叫做反向解析URL反向URL匹配反向URL查询或者简单的URL反查

在需要URL 的地方,对于不同层级,Django 提供不同的工具用于URL 反查:

例子

考虑下面的URLconf:

from django.conf.urls import url

from . import views

urlpatterns = [
    #...
    url(r'^articles/([0-9]{4})/$', views.year_archive, name='news-year-archive'),
    #...
]

根据这里的设计,某一年nnnn对应的归档的URL是/articles/nnnn/

你可以在模板的代码中使用下面的方法获得它们:

<a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>

<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>

在Python 代码中,这样使用:

from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect

def redirect_to_year(request):
    # ...
    year = 2006
    # ...
    return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

如果出于某种原因决定按年归档文章发布的URL应该调整一下,那么你将只需要修改URLconf 中的内容。

在某些场景中,一个视图是通用的,所以在URL 和视图之间存在多对一的关系。对于这些情况,当反查URL 时,只有视图的名字还不够。请阅读下一节来了解Django 为这个问题提供的解决办法。

URL 模式的命名

为了完成上面例子中的URL 反查,你将需要使用命名的URL 模式。URL 的名称使用的字符串可以包含任何你喜欢的字符。并不仅限于合法的Python 名称。

当命名你的URL 模式时,请确保使用的名称不会与其它应用中名称冲突。如果你的URL 模式叫做comment,而另外一个应用中也有一个同样的名称,当你在模板中使用这个名称的时候不能保证将插入哪个URL。

在URL 名称中加上一个前缀,比如应用的名称,将减少冲突的可能。我们建议使用myapp-comment 而不是comment

URL 命名空间

简介

URL 命名空间允许你反查到唯一的命名URL 模式,即使不同的应用使用相同的URL 名称。第三方应用始终使用带命名空间的URL 是一个很好的实践(我们在教程中也是这么做的)。类似地,它还允许你在一个应用有多个实例部署的情况下反查URL。换句话讲,因为一个应用的多个实例共享相同的命名URL,命名空间提供了一种区分这些命名URL 的方法。

在一个站点上,正确使用URL命名空间的Django 应用可以部署多次。例如,django.contrib.admin 具有一个AdminSite 类,它允许你很容易地部署多个管理站点的实例。在下面的例子中,我们将讨论在两个不同的地方部署教程中的polls应用,这样我们可以为两种不同的用户(作者和发布者)提供相同的功能。

一个URL命名空间有两个部分,它们都是字符串:

应用命名空间

它表示正在部署的应用的名称。一个应用的每个实例具有相同的应用命名空间。例如,可以预见Django 的管理站点的应用命名空间是'admin'

实例命名空间

它表示应用的一个特定的实例。实例的命名空间在你的全部项目中应该是唯一的。但是,一个实例的命名空间可以和应用的命名空间相同。它用于表示一个应用的默认实例。例如,Django 管理站点实例具有一个默认的实例命名空间'admin'

URL 的命名空间使用':' 操作符指定。例如,管理站点应用的主页使用'admin:index'。它表示'admin' 的一个命名空间和'index' 的一个命名URL。

命名空间也可以嵌套。命名URL'sports:polls:index' 将在命名空间'polls'中查找'index',而poll 定义在顶层的命名空间'sports' 中。

反查带命名空间的URL

当解析一个带命名空间的URL(例如'polls:index')时,Django 将切分名称为多个部分,然后按下面的步骤查找:

  1. 首先,Django 查找匹配的应用命名空间(在这个例子中为'polls')。这将得到该应用实例的一个列表。

  2. 如果有一个当前应用被定义,Django 将查找并返回那个实例的URL 解析器。当前应用可以通过请求上的一个属性指定。预期会具有多个部署的应用应该设置正在处理的request 的current_app 属性。

    Changed in Django 1.8:

    在以前版本的Django 中,你必须在用于渲染模板的每个Context 或 RequestContext上设置current_app 属性。

    当前应用还可以通过reverse() 函数的一个参数手工设定。

  3. 如果没有当前应用。Django 将查找一个默认的应用实例。默认的应用实例是实例命名空间 与应用命名空间 一致的那个实例(在这个例子中,polls 的一个叫做'polls' 的实例)。

  4. 如果没有默认的应用实例,Django 将挑选该应用最后部署的实例,不管实例的名称是什么。

  5. 如果提供的命名空间与第1步中的应用命名空间 不匹配,Django 将尝试直接将此命名空间作为一个实例命名空间查找。

如果有嵌套的命名空间,将为命名空间的每个部分重复调用这些步骤直至剩下视图的名称还未解析。然后该视图的名称将被解析到找到的这个命名空间中的一个URL。

例子

为了演示解析的策略,考虑教程中polls 应用的两个实例:'author-polls' 和'publisher-polls'。假设我们已经增强了该应用,在创建和显示投票时考虑了实例命名空间。

urls.py

from django.conf.urls import include, url

urlpatterns = [
    url(r'^author-polls/', include('polls.urls', namespace='author-polls', app_name='polls')),
    url(r'^publisher-polls/', include('polls.urls', namespace='publisher-polls', app_name='polls')),
]

polls/urls.py

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
    ...
]

根据以上设置,可以使用下面的查询:

  • 如果其中一个实例是当前实例 —— 如果我们正在渲染'author-polls' 实例的detail 页面 —— 'polls:index' 将解析成'author-polls' 实例的主页面;例如下面两个都将解析成"/author-polls/"

    在基于类的视图的方法中:

    reverse('polls:index', current_app=self.request.resolver_match.namespace)
    

    和在模板中:

    {% url 'polls:index' %}
    

    注意,在模板中的反查需要添加request 的current_app 属性,像这样:

    def render_to_response(self, context, **response_kwargs):
        self.request.current_app = self.request.resolver_match.namespace
        return super(DetailView, self).render_to_response(context, **response_kwargs)
    
  • 如果没有当前实例 —— 如果我们在站点的其它地方渲染一个页面 —— 'polls:index' 将解析到最后注册的polls的一个实例。因为没有默认的实例(命名空间为'polls'的实例),将使用注册的polls 的最后一个实例。它将是'publisher-polls',因为它是在urlpatterns中最后一个声明的。

  • 'author-polls:index' 将永远解析到 'author-polls' 实例的主页('publisher-polls' 类似)。

如果还有一个默认的实例 —— 例如,一个名为'polls' 的实例 —— 上面例子中唯一的变化是当没有当前实例的情况(上述第二种情况)。在这种情况下 'polls:index' 将解析到默认实例而不是urlpatterns 中最后声明的实例的主页。

URL 命名空间和被包含的URLconf

被包含的URLconf 的命名空间可以通过两种方式指定。

首先,在你构造你的URL 模式时,你可以提供应用实例的命名空间给include() 作为参数。例如:

url(r'^polls/', include('polls.urls', namespace='author-polls', app_name='polls')),

这将包含polls.urls中定义的URL 到应用命名空间 'polls'中,其实例命名空间'author-polls'

其次,你可以include 一个包含嵌套命名空间数据的对象。如果你include() 一个url() 实例的列表,那么该对象中包含的URL 将添加到全局命名空间。然而,你还可以include() 一个3个元素的元组:

(<list of url() instances>, <application namespace>, <instance namespace>)

例如:

from django.conf.urls import include, url

from . import views

polls_patterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
]

url(r'^polls/', include((polls_patterns, 'polls', 'author-polls'))),

这样会包含命名的URL模式进入到给定的应用和实例命名空间中。

例如,Django 的管理站点部署的实例叫AdminSiteAdminSite 对象具有一个urls 属性:一个3元组,包含管理站点中的所有URL 模式和应用的命名空间'admin'以及管理站点实例的名称。你include()到你项目的urlpatterns 中的是这个urls 属性。

请确保传递一个元组给include()。如果你只是传递3个参数:include(polls_patterns, 'polls', 'author-polls'),Django 不会抛出一个错误,但是根据include() 的功能,'polls' 将是实例的命名空间而'author-polls' 将是应用的命名空间,而不是反过来的。

搬运地址:https://yiyibooks.cn/xx/django_182/topics/http/urls.html

^_^
请喝咖啡 ×

文章部分资料可能来源于网络,如有侵权请告知删除。谢谢!

前一篇: html clipboard.js 现代化的拷贝复制粘贴剪切文字
下一篇: django 入门篇