Loading... 有个常见需求是这样的:一个[Django](https://www.djangoproject.com/)应用,在开发时,`URL`是以`/`为根目录的;而部署时,需要给它一个前缀,比如叫`/prefix/`。 它的使用场景是,在一个域名里托管多个应用,它们仅以前缀区分。 这个需求可以拆解成两个具体的要求: 1. 在外面访问时是有`/prefix/`前缀的,而在应用那一层,不知道有这个前缀,仍然以为是`/`。 2. 在内部进行相对`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](https://wsgi.readthedocs.io/en/latest/)协议通过`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](https://www.python.org/dev/peps/pep-3333/#id39)描述,是推荐的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](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface)》 - 《[PEP 333 -- Python Web Server Gateway Interface v1.0](https://www.python.org/dev/peps/pep-0333/)》 - 《[PEP 3333 -- Python Web Server Gateway Interface v1.0.1](https://www.python.org/dev/peps/pep-3333/)》 HTTP服务器方案 -------------- 现在,主要的两个HTTP服务器------[Apache httpd](https://httpd.apache.org/)(通常简称Apache)和[Nginx](http://nginx.org/),都支持[WSGI](https://wsgi.readthedocs.io/en/latest/)协议。 ### Apache httpd [Apache httpd](https://httpd.apache.org/)通过模块[mod_wsgi](http://modwsgi.readthedocs.io/en/develop/index.html)对[WSGI](https://wsgi.readthedocs.io/en/latest/)协议进行支持。 它对这个功能的实现与配置,简单而强大。 ``` WSGIScriptAlias /prefix /PATH/TO/DJANGO/wsgi.py ``` 但由于孤目前对[Apache httpd](https://httpd.apache.org/)并不太熟,也不实际使用,所以就说到这。 ### Nginx #### 旧方案 ``` location ~ ^/prefix/ { ... uwsgi_param SCRIPT_NAME /prefix; uwsgi_modifier1 30; } ``` 这是网上流传最多的设置方式。 然而,由于[Nginx](http://nginx.org/)不支持修改`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](http://uwsgi-docs.readthedocs.io/en/latest/Changelog-2.0.11.html?highlight=uwsgi_modifier1)》的更新日志,可以在[uWSGI](https://github.com/unbit/uwsgi)的配置里,使用`route-run = fixpathinfo:`来替代`uwsgi_modifier1`。 配置部分,不再需要`uwsgi_modifier1`: ``` location ~ ^/prefix/ { ... uwsgi_param SCRIPT_NAME /prefix; } ``` 而在[uWSGI](https://github.com/unbit/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](http://nginx.org/)的`rewrite`,实现向内传递时去除`SCRIPT_NAME`。 ``` location ~ ^/prefix/ { rewrite /prefix/(.*) /$1 break; proxy_pass http://127.0.0.1:8000; ... } ``` 当然,这里是直接使用的HTTP反向代理。 同时,[uWSGI](https://github.com/unbit/uwsgi)也需要用`--http`的方式,直接提供HTTP服务。 这样就实现了要求1。 然后,修改[Django](https://www.djangoproject.com/)的`settings.py`,添加一行配置。 ``` FORCE_SCRIPT_NAME = '/prefix' ``` 这个配置,支持在[Django](https://www.djangoproject.com/)这一层,覆盖WSGI协议中的`SCRIPT_NAME`。 (参考《[stackoverflow.com/questions/10806836](https://stackoverflow.com/questions/10806836/django-prefix-on-all-generated-urls)》。) 这样就实现了要求2。 这个路子,在[uWSGI](https://github.com/unbit/uwsgi)直接提供HTTP服务时,非常有效。 它相当于绕过了WSGI这一层,由HTTP服务器与应用合作实现。 不过缺点还是相当显著的。 两个修改相互依赖,`FORCE_SCRIPT_NAME`影响调试。 这是一种比`uwsgi_modifier1`还要丑陋的Hack。 ### 直接在Django添加prefix 这个需求之所以如此麻烦,是因为外面需要额外加一个前缀。 如果在开发时就直接把前缀加上,不就没有这么多事了? 幸运的是,在[Django](https://www.djangoproject.com/)中,可以非常简单地添加一个前缀。 ``` urlpatterns = [ url(...), ... ] urlpatterns = [ url('^prefix/', include(urlpatterns)) ] ``` 无论原先的`urlpatterns`是什么,直接再用`include`包一层,三行代码就能简单解决问题。 这个方案,可能是大多数开发者遇到这个需求时,所采用的方案。 它把一个运维部署的问题,转换成了一个开发问题。 在开发者自己做部署,而知识储备又不足时,就容易使用这个方案。 它的缺点......认真说来,也没什么不可忍受的。 也许只是不够优雅。 总结 ---- 本文介绍了几种实现方案。 [Apache httpd](https://httpd.apache.org/)的方案看上去最简洁,可惜孤不会用它。 [Nginx](http://nginx.org/)的新方案是最好的方案,[uWSGI](https://github.com/unbit/uwsgi)方案也很不错,都是可选项。 至于[Nginx](http://nginx.org/)的旧方案,以及两个野路子,还是算了吧。 优雅......优雅...... 最后修改:2020 年 10 月 11 日 © 转载自他站 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,请随意赞赏