[Flask] 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.1yuhao.space 127.0.0.1www.yuhao.space 127.0.0.1blog.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_nameyuhao.space *.yuhao.space; ssl_certificate...; ssl_certificate_key ...; location / { proxy_set_headerHost$http_host; proxy_set_headerX-Real-IP$remote_addr; proxy_set_headerX-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_headerX-Scheme$scheme; proxy_passhttp://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 上也有人提過類似的問題:ofollow,noindex" target="_blank">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 在個別地方功能略顯不足,但還是給我們提供了很多靈活性,值得好好去挖掘。