背景
Plotly Dash 是一個Python web應用框架,它能幫助我們快速建立好看的,響應式的,可互動的,資料視覺化頁面。可惜的是,一個只能展示資料的應用並不總是十分有用。如果我們需要一個完整的web應用,那就不得不想辦法利用它的後端 Flask.
問題
儘管Dash借了Flask的殼,但這個Flask執行在sandbox裡,而且和普通的Flask相比也少了很多功能。比如以下的功能要不沒有要不需要升級到Dash enterprise版本。
- 資料庫整合
- 認證
- 多頁面、多路由支援
- 自定義風格
等等
設計
為了克服以上的所有問題,與其利用Dash自帶的Flask,我們完全可以建立一個基礎的Flask應用,然後將我們的Dash應用置於其上。
原則
- 靈活 -- 能夠用Dash和Flask建立任意型別的應用。
- 全功能 -- Dash和Flask都必須是全功能的,不能有功能上的限制。
- 可定製 -- 能夠自定義應用的樣式。
- 簡潔 -- 能夠簡單快捷的建立一個Dash應用。
解決方案
總的來說,我們可以有兩種方式實現目標。
- __子應用__: 建立一個基礎的Flask應用,用這個Flask作為parent server初始化Dash應用,將Dash應用用自定義路由註冊為子應用。
- __iframe__: 建立一個基礎的Flask應用,將Dash應用放在一個
iframe
中,再用Flask去載入這些iframe
.
哪個更好
和將Dash應用放在iframe
中相比,儘管這種方式看起來最簡單,但是因為iframe
完全隔離的特性,反而會引入其他問題:
- __難於定製__:
iframe
不能通過css/js改變iframe
內的應用。 - __不一致__:
iframe
因為和main frame有著不同的路由系統,點選一個iframe
內部的連結並不會觸發main frame的跳轉。 - __無法擴充套件__:
iframe
方案不支援多頁面的Dash應用。
基於以上理由,我們認為使用子應用是一個更靈活且更普適的方案。
程式碼結構
基礎Flask的程式碼結構如下:
├── app
│ ├── dash_apps -- 所有的Dash應用都在這個目錄
│ │ ├── custom_dash_app.py
│ │ ├── experiment_detail_dash_app.py
│ │ ├── experiment_list_dash_app.py
│ │ └── __init__.py
│ ├── __init__.py
│ ├── static
│ │ └── styles.css -- Custom CSS styles
│ ├── templates
│ │ ├── dash_layout.html -- Dash應用的layout
│ │ ├── header.html -- Header
│ │ ├── index.html -- 主頁面
│ │ └── layout.html -- 主頁面的layout
│ └── views.py -- Flask路由和主頁面導航選單定義
├── config.py
├── poetry.lock
├── poetry.toml
├── pyproject.toml
├── README.md
├── scripts
│ ├── fix.sh
│ ├── run.sh
│ └── setup.sh
├── setup.cfg
└── wsgi.py
完整demo實現請參考github。
實現細節
為了實現子應用,我們需要實現如下幾個關鍵功能。
- 建立基礎應用的blueprint
- 在Flask中註冊基礎應用的blueprint
- 將Dash和基礎應用關聯起來,並定義路由和layout
建立基礎應用的blueprint:
Blueprint是Flask用來實現模組化應用的元件。
app/views.py
from flask import Blueprint, render_template
base_app = Blueprint("base_app", __name__)
@base_app.route("/")
def index():
"""Landing page."""
return render_template("index.html", top_menu_items=get_top_menu_items("/"))
在Flask中註冊基礎應用的blueprint
在Flask中,這叫作application factory模式,建立一個create_app
函式,返回application物件。Flask會呼叫這個函式來處理請求。
app/__init__.py
from flask import Flask
from app.views import base_app
def create_app(test_config=None):
"""Create and configure Flask app"""
app = Flask(__name__)
app.register_blueprint(base_app)
return app
將Dash和基礎應用關聯起來,並定義路由和layout
建立一個使用基礎應用的Dash應用。
app/dash_apps/__init__.py
def customize_index_string(app, url):
"""Custom app's index string"""
app.index_string = env.get_template("dash_layout.html").render(
top_menu_items=get_top_menu_items(url)
)
def add_route(app, url):
"""Add route to the app"""
app.server.add_url_rule(url, endpoint=url, view_func=app.index)
app.routes.append(url)
def create_dash_app(server, url_rule, url_base_pathname):
"""Create a Dash app with customized index layout
:param server: base Flask app
:param url_rule: url rule as endpoint in base Flask app
:param url_base_pathname: url base pathname used as dash internal route prefix
"""
app = dash.Dash(name=__name__, server=server, url_base_pathname=url_base_pathname)
customize_index_string(app, url_rule)
add_route(app, url_rule)
return app
app/dash_apps/custom_dash_app.py
from app.dash_apps import create_dash_app
# endpoint of this page
URL_RULE = "/custom-app"
# dash internal route prefix, must be start and end with "/"
URL_BASE_PATHNAME = "/dash/custom-app/"
def create_dash(server):
"""Create a Dash view"""
app = create_dash_app(server, URL_RULE, URL_BASE_PATHNAME)
# dash app definitions goes here
...
return app.server
如何在基礎應用裡新增更多Dash應用
在基礎應用裡新增Dash總共需要2步。
第一步:建立Dash應用
1-1: 在app/dash_apps
目錄建立一個.py
檔案。
1-2: 按照以下程式碼結構建立Dash應用。
from app.dash_apps import create_dash_app
# endpoint of this page
URL_RULE = "/custom-app"
# dash internal route prefix, must be start and end with "/"
URL_BASE_PATHNAME = "/dash/custom-app/"
def create_dash(server):
"""Create a Dash view"""
app = create_dash_app(server, URL_RULE, URL_BASE_PATHNAME)
# dash app definitions goes here, same as what you would do in normal Dash application
...
return app.server
第二步: 在app/views.py
為Dash應用新增主頁面導航選單(可選)
top_menus = [
{"path": "/", "title": "Home"},
{"path": "/experiments", "title": "Experiments"},
{"path": "/custom-app", "title": "Custom App"},
...
]
如何執行
執行以下指令碼啟動應用,如果設定了FLASK_ENV=development
,應用會以開發模式執行。
#!/bin/bash
source .venv/bin/activate
if [[ "${FLASK_ENV}" == "development" ]]; then
flask run --host=0.0.0.0 --port 8050
else
gunicorn wsgi:app \
--bind 0.0.0.0:8050 \
--log-level debug \
--workers 2 \
--threads 4
fi