36

[Flask] Flask 基于子域名的蓝图管理

 5 years ago
source link: http://yuhao.space/blog/2018/11/flask-blueprint-subdomain/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在 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 在个别地方功能略显不足,但还是给我们提供了很多灵活性,值得好好去挖掘。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK