Flask 基于子域名的蓝图管理
在 Flask 中,蓝图(Blueprint)通常是基于路径进行分派的,因此我们看到典型的注册代码一般类似这样:
app.register_blueprint(home_bp, url_prefix='...')
相对少见的另一种用法是,Blueprint 也可以通过子域名来分派,这涉及到程序结构上会有一些改变,同时也会带来一些新的问题(当然都是可以解决的)。使用子域名是大型网站的常规做法,同时也使得 URL 路径更有针对性,比如提供一个 https://api.mydomain.com/... 比起所有页面都堆到 https://mydomain.com/ 下面,看上去也显得更专业一些。我自己也在尝试通过这种方式重构自己的网站,最开始尝试的是每个域名使用一个单独的 app 去管理,但很快发现如果一些比较小的功能也做成独立的网站,会带来比较多额外的管理负担。因此,把这些功能合并到一个app,对外又能通过子域名公开,是不错的做法。因此,我对这种实现做了一些尝试,并对遇到的问题和解决办法做一个记录,以供自己和其他朋友参考。
域名管理
首先需要注意的是,由于需要区分不同的域名,以前那种在开发环境下统一用 localhost 做域名的做法现在行不通了。大型的网站可能会考虑用自定义域名解析的方式实现统一管理,我们现在的场景没那么复杂,简单在 hosts 里面加几条记录也就足够了。
127.0.0.1 yuhao.space
127.0.0.1 www.yuhao.space
127.0.0.1 blog.yuhao.space
基本应用
添加域名记录以后,我们来写一个简单的测试程序,检查一下域名分派在开发环境下是否正常。这里需要注意的几个点:
- app.config['SERVER_NAME'] 需要指向基本域名,包括端口(Flask 默认为5000);
- 创建 Blueprint 需要添加 subdomain 参数指向子域名(除主域名外)。
我们模拟两个域名来测试:一个主域名和一个博客域名(blog)。
from flask import Flask, Blueprint
home_bp = Blueprint('home', __name__)
blog_bp = Blueprint('blog', __name__)
@home_bp.route('/')
def home_index():
return 'home index'
@blog_bp.route('/')
def blog_index():
return 'blog index'
def create_app():
def register_blueprints(app):
app.register_blueprint(home_bp)
app.register_blueprint(blog_bp, subdomain='blog')
app = Flask(__name__)
app.config['SERVER_NAME'] = 'yuhao.space:5000'
register_blueprints(app)
return app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)
运行程序,然后打开浏览器,分别浏览如下地址:
- http://yuhao.space:5000/
- http://blog.yuhao.space:5000/
很好,一切正常。接下来看看在生产环境下表现如何。
使用 Nginx 作为反向代理
在生产环境下,我们基本上不会把 Flask 应用直接暴露在公网上,而是使用类似 Nginx 这样的服务器作为前端代理。这种部署模式会带来一些额外的复杂性,而且容易出错(特别是配置方面),需要仔细验证。同时,正式环境一般也要在公网上启用 HTTPS,这又要求我们有一个有效的证书。因为我已经用 Let's Encrypt 申请过证书,所以这里就偷懒直接拿来用了,想自行验证的读者请在本地生成一个测试证书,不想搞太麻烦的同学就接着往下看吧。
Nginx 的配置不太相关的部分就略过了,其实和普通网站基本没有太大区别:
server {
listen 443 ssl;
server_name yuhao.space *.yuhao.space;
ssl_certificate ...;
ssl_certificate_key ...;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_pass http://localhost:5000;
}
}
这里我们让所有域名都通过相同的服务端口,为了识别到域名,别忘了设置 HOST。
设置完毕并重新加载 Nginx,然后用生产模式运行应用:
FLASK_ENV=production FLASK_APP=app flask run
然后打开浏览器访问...404?怎么回事?回想一下代码能猜到,通过生产环境访问时,浏览器发来的 Host 已经不带 5000 端口号了,因此我们这里要对应修改一下。为了同时保证开发环境仍然正常工作,需要判断一下环境:
app = Flask(__name__)
server_name = 'yuhao.space'
env = os.getenv('FLASK_ENV', 'development')
if env != 'production':
server_name += ':5000'
app.config['SERVER_NAME'] = server_name
当然,这不是足够灵活的生产代码,但我在这里希望作为示例尽量简单明了。
再次运行程序,这次访问应该正常了。
检查反向地址
地址访问正常只是成功的一半。另一半是从客户请求中生成可访问的地址————也就是我们熟悉的 url_for()
,同样应该检查它们是否工作正常。为此,我们把视图函数稍稍修改一下,让它们返回模板内容:
@home_bp.route('/')
def home_index():
return render_template('home.html')
然后写一个简单的模板:
<a href="{{ url_for('home.home_index') }}">Home Index</a>
<a href="{{ url_for('blog.blog_index') }}">Blog Index</a>
<br/>
<a href="{{ url_for('home.home_index', _external=True) }}">Home Index</a>
<a href="{{ url_for('blog.blog_index', _external=True) }}">Blog Index</a>
好消息是,内部地址是正确的; 坏消息则是外部地址(使用 _external
参数)路径虽然没错,但却返回了 http:// 的地址,这可不是我们想要的结果。
一个简单粗暴的处理办法是, 为 url_for 强制指定协议:
<a href="{{ url_for('home.home_index', _external=True, _scheme='https') }}">Home Index</a>
但可想而知,如果网站有很多链接的话,这样会增加不小的工作量。
从官方文档和代码中的备注来看,应用程序配置中有一项 PREFERED_URL_SCHEME
似乎应当与此有关,但文档对此解释不太明确,我尝试过添加该配置也不起作用。Stackoverflow 和 Github 上也有人提过类似的问题:PREFERRED_URL_SCHEME doesn't seem to work in Jinja2 Flask Template。 官方对此也没有明确的答复。
既然没有正规途径,只要自己解决了。所幸 url_for 已经有参数可以利用,所以这也并不难。我们只要定义一个模板函数来强制指定 scheme 就好了:
def external_url_for(endpoint, **values):
values.setdefault('_external', True)
values.setdefault('_scheme', 'https')
return url_for(endpoint, **values)
...
app.add_template_global(external_url_for, 'external_url_for')
然后在模板中用 external_url_for 代替 url_for 调用即可。
结语
本文讨论了基于子域名的 Flask Blueprint 开发实践和相关问题的处理。要处理多个域名,另一种做法是通过 WSGI 接口分配多个应用,这种做法在 官方文档 Application Dispatching 部分 有所涉及。但我觉个这种方法略“重”,因此还是走了 Blueprint 的路子。如果子应用的规模很大,那么单独分配 app 或许是更灵活的做法。从实践中我们也可以体会到,尽管 Flask 在个别地方功能略显不足,但还是给我们提供了很多灵活性,值得好好去挖掘。