背景

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

BetaRabbit
1.4k 声望22 粉丝

全栈,猫奴