Django 子域的实现
Django作为一个典型的MVC三层框架,有着自己一套full-stack的实现。针对开发和运行效率而言,有人想用[SQLAlchemy]来替换Django的ORM;有人会吐槽Django的Template模板渲染系统,转而尝试[Jinjia2];但或许没有人对Django的URL处理机制不满。StackOverflow上说,面对愈发大型的系统,Django的URL Routing可能是唯一剩下的模块。Django使用了强大的正则表达式来Resolve URL,重用了程序员既有的知识;提供了Reverse方法来反向构造URL,使得View与其对应的URL解耦;再加之飞快的处理速度(因为全局的resolver_cache的存在),使得一切看起来是那么美好。然而,美中不足的是,Django官方对subdomain的支持并不完备,譬如URL的Routing只考虑URI,并不考虑host信息(#8896)。
在Django里有两种方式可以实现subdomain:Contrib下的SitesApp&Middleware动态指定request.urlconf。
让我们先来看看第一种方式:Sites,可定义任意的Domain。
我们知道每个项目的配置文件settings.py都一个属性SITE_ID(默认为1), INSTALLED_APPS中也默认包含‘django.contrib.sites’,那么说明了django内部有使用这个APP。
那么,对于我们上层应用而言,Sites适用于什么场合呢?Django文档上说:
“Use it if your single Django installation powers more than one site and you need to differentiate between those sites in some way.”
对于singleDjangoinstallation,我的理解是不同site使用相同的default数据库,即它们使用同一张django_site数据库表。
咋一看,很符合我们的需求嘛:我们本来在同一个数据库里,只要设置不同的site_id即可。但是site_id设置在settings里,这意味着不同的site需要有不同的project。这对于某些不同站点间功能关联不紧密,原有结构划分清晰,有可重用的APP的项目而言可能不是什么大问题,然而,对于subdoamin来言其功能逻辑代码关联紧密,用sites的方式来处理显得过于粗暴了。
来看看第二种方式:middleware指定request.urlconf属性。
Django对于request的转发基于两个部分。1. request.path_info, 请求的URL。2. request.urlconf, 项目处理的全部URL。
在middleware里我们可以指定urlconf到特定的urls.py,如果没有指定的话,默认使用settings里的ROOT_URLCONF。在调用urlresolvers中resolve和reverse方法都可指定request的urlconf属性,但遗憾的是template的url标签并没有将urlconf属性暴露出来。
试想一下,我们可以给不同的subdomain定义自己的一套urlconf(并不麻烦,归结于django灵活的urlpatterns构造),再在middleware里根据domain的信息,将其转发到定义的subdomain上,就可以达到了目的。
那么,这样的实现方式会有什么问题呢?
1. runserver时除ROOT_URLCONF定义的patterns,其他域名下的url不可用。因为我们在runserver时通过IP访问,没有Domain信息。
2. 因为Django的reverse没有domain信息,我们需要手动将reverse的结果指定到对应的subdomain上。
为解决这两个问题,我们使用了一个trick。我们在middleware处理时设法保留了subdomain的信息,并将其反映在path上;在urls.py中构造对应的patterns;最后在reverse时将subdomain的信息取出构造出正确的url。代码如下:
SubdomainMiddleware:
class SubdomainMiddleware(object):
def process_request(self, request):
domain_parts = request.get_host().split('.')
if len(domain_parts) == 3:
# 将subdomain的信息放到URI的第一层级
request.path_info = '/%s%s' % (domain_parts[0], request.path)
return None
将其添加在MIDDLEWARE_CLASSES中,确保一个靠上的位置。在此,我们没有修改request.path的信息,是因为path_info才是Django内部分发的依据,同时我们也可以根据我们的需要灵活引用path或path_info。
Reverse Monkey Patch:
# not in settings.py: will be imported twice
# not in urls.py: too late
# works in models.py
from django.conf import settings
if not settings.DEBUG:
from django.core import urlresolvers
def reverse_subdomain(*args, **kwargs):
path_info = old_reverse(*args, **kwargs)
parts = path_info[1:].split('/', 1)
path_info = 'http://%s%s/%s' % (
parts[0], settings.SESSION_COOKIE_DOMAIN, parts[1])
return path_info
old_reverse = urlresolvers.reverse
urlresolvers.reverse = reverse_subdomain
在此我们有一个假设,假设我们和我们要使用的第三方包并不依赖于reverse的结果,都不会对其做进一步的处理,仅仅是显示在页面上显示或重定向等直接返回的行为。这个假设我们感觉基本上成立。
还有一点需要说明的是,包含re.VERBOSE(\x)或零宽断言的正则表达式在resolver时没有问题,但在reverse时会异常,因为它们是“Non-reversible reg-exp portion”。偶尔,我们可能会为了效率考虑,或许更多的是为了满足自己的好奇心和挑战欲,写出复杂的正则匹配,而往往是没有必要的。
# 来源:九九房博客
最新招聘
- [珠海] 云查杀工程师(Python, Django) - 金山
- [成都] Python 程序开发(技术经理,2年以上Python开发经验) - 热酷
- [武汉] 云存储软件研发工程师(高新急聘) - 赛诺(武汉)技术有限公司
- [梅州] 网站程序员、网页设计、实习生 - 梅州天勤网络科技有限公司
- [广州] python游戏开发工程师 - 广州哈谷特