有个常见需求是这样的:一个Django应用,在开发时,URL
是以/
为根目录的;而部署时,需要给它一个前缀,比如叫/prefix/
。 它的使用场景是,在一个域名里托管多个应用,它们仅以前缀区分。
这个需求可以拆解成两个具体的要求:
- 在外面访问时是有
/prefix/
前缀的,而在应用那一层,不知道有这个前缀,仍然以为是/
。 - 在内部进行相对
URL
的生成时,虽然不知道前缀,但是要有前缀。 比如,从外面的首页/prefix/
,希望点击一个链接后跳转到/prefix/home/
; 而里面的应用在不知道前缀的情况下,生成的链接是/prefix/home/
,而非/home/
。
WSGI协议的相关知识
What is WSGI
WSGI is the Web Server Gateway Interface. It is a specification that describes how a web server communicates with web applications, and how web applications can be chained together to process one request.
WSGI协议通过SCRIPT_NAME
和PATH_INFO
,来实现加前缀的操作。
from urllib import quote
url = environ['wsgi.url_scheme']+'://'
if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
else:
url += environ['SERVER_NAME']
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']
url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']
从上述代码,源于WSGI的v1.01版本的URL Reconstruction描述,是推荐的URL构建算法。
一个URL的通用形式如下:
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
可以看出,SCRIPT_NAME
和PATH_INFO
,共同构成了[/path]
这部分。 一般情况下,SCRIPT_NAME
相当于为空,/path
就等价于PATH_INFO
。 而如果SCRIPT_NAME=/prefix
,就实现了孤前面的第1个要求; 如果PATH_INFO
等于去掉前缀后的部分,就实现了第2个要求。
WSGI协议参考
- 《Web Server Gateway Interface - Wikipedia》
- 《PEP 333 -- Python Web Server Gateway Interface v1.0》
- 《PEP 3333 -- Python Web Server Gateway Interface v1.0.1》
HTTP服务器方案
现在,主要的两个HTTP服务器------Apache httpd(通常简称Apache)和Nginx,都支持WSGI协议。
Apache httpd
Apache httpd通过模块mod_wsgi对WSGI协议进行支持。 它对这个功能的实现与配置,简单而强大。
WSGIScriptAlias /prefix /PATH/TO/DJANGO/wsgi.py
但由于孤目前对Apache httpd并不太熟,也不实际使用,所以就说到这。
Nginx
旧方案
location ~ ^/prefix/ {
...
uwsgi_param SCRIPT_NAME /prefix;
uwsgi_modifier1 30;
}
这是网上流传最多的设置方式。 然而,由于Nginx不支持修改PATH_INFO
,所以需要uwsgi_modifier1 30
这种丑陋的设置。 否则,内部被访问的应用,实际得到的访问链接是/prefix/prefix/...
这种形式的。
uwsgi_modifier1 30
的机制,就是把向内传的PATH_INFO
,先删除开头部分的SCRIPT_NAME
字符串。
Standard WSGI request followed by the HTTP request body. The PATH_INFO is automatically modified, removing the SCRIPT_NAME from it.
虽然还能工作,但是已经不推荐了。
Note: ancient uWSGI versions used to support the so called "uwsgi_modifier1 30" approach. Do not do it. It is a really ugly hack.
新方案
参考《uWSGI 2.0.11》的更新日志,可以在uWSGI的配置里,使用route-run = fixpathinfo:
来替代uwsgi_modifier1
。
配置部分,不再需要uwsgi_modifier1
:
location ~ ^/prefix/ {
...
uwsgi_param SCRIPT_NAME /prefix;
}
而在uWSGI的ini配置文件中,新增一行:
[uwsgi]
...
route-run = fixpathinfo:
uWSGI方案
其实,除了在HTTP服务器以外,在WSGI应用服务器这一层,也是可以实现这个需求的。
[uwsgi]
...
mount = /prefix=/PATH/TO/DJANGO/wsgi.py
manage-script-name = true
这个wsgi.py
的路径,可以是相对路径或绝对路径。 这相当于把应用以wsgi.py
为入口,挂载到/prefix
这个位置,并且自动处理SCRIPT_NAME
和PATH_INFO
。
野路子
再记录两个孤用过的野路子。 它们能工作,只是有点怪怪的。
Nginx与Django直接配合
首先,用Nginx的rewrite
,实现向内传递时去除SCRIPT_NAME
。
location ~ ^/prefix/ {
rewrite /prefix/(.*) /$1 break;
proxy_pass http://127.0.0.1:8000;
...
}
当然,这里是直接使用的HTTP反向代理。 同时,uWSGI也需要用--http
的方式,直接提供HTTP服务。 这样就实现了要求1。
然后,修改Django的settings.py
,添加一行配置。
FORCE_SCRIPT_NAME = '/prefix'
这个配置,支持在Django这一层,覆盖WSGI协议中的SCRIPT_NAME
。 (参考《stackoverflow.com/questions/10806836》。) 这样就实现了要求2。
这个路子,在uWSGI直接提供HTTP服务时,非常有效。 它相当于绕过了WSGI这一层,由HTTP服务器与应用合作实现。
不过缺点还是相当显著的。 两个修改相互依赖,FORCE_SCRIPT_NAME
影响调试。 这是一种比uwsgi_modifier1
还要丑陋的Hack。
直接在Django添加prefix
这个需求之所以如此麻烦,是因为外面需要额外加一个前缀。 如果在开发时就直接把前缀加上,不就没有这么多事了?
幸运的是,在Django中,可以非常简单地添加一个前缀。
urlpatterns = [
url(...),
...
]
urlpatterns = [
url('^prefix/', include(urlpatterns))
]
无论原先的urlpatterns
是什么,直接再用include
包一层,三行代码就能简单解决问题。
这个方案,可能是大多数开发者遇到这个需求时,所采用的方案。 它把一个运维部署的问题,转换成了一个开发问题。 在开发者自己做部署,而知识储备又不足时,就容易使用这个方案。 它的缺点......认真说来,也没什么不可忍受的。
也许只是不够优雅。
总结
本文介绍了几种实现方案。 Apache httpd的方案看上去最简洁,可惜孤不会用它。 Nginx的新方案是最好的方案,uWSGI方案也很不错,都是可选项。
至于Nginx的旧方案,以及两个野路子,还是算了吧。
优雅......优雅......