Dash Pages#

Dash Pages 从 Dash 版本 2.5.0 开始提供。它实现了简化创建多页面应用的功能,处理 URL 路由,并提供了一种简单的方法来组织和定义应用中的页面。

使用 Dash Pages 创建多页面应用有三个基本步骤:

  1. 为您应用中的每个页面创建单独的 .py 文件,并将它们放在 /pages 目录中。

  2. 在这些页面文件中需要:

    • 添加 dash.register_page(__name__),告诉 Dash 这是应用中的页面。

    • 在名为 layout 的变量中或名为 layout 的函数中定义页面的内容,该函数返回内容。

  3. 在您的主应用文件,app.py 中:

    • 在声明您的应用时,将 use_pages 设置为 Trueapp = Dash(__name__, use_pages=True)

    • 在您想要在用户访问应用的某个页面路径时显示页面内容的地方,添加 dash.page_container 到您的应用布局中。

示例:使用 Pages 的简单多页面应用#

下面是使用 Dash Pages 的三页面应用结构的样子:

- app.py
- pages
   |-- analytics.py
   |-- home.py
   |-- archive.py

它有主 app.py 文件,这是我们多页面应用的入口点(我们在其中包含 dash.page_container),并且在 pages 目录中有三页。

import dash
from dash import html, dcc, callback, Input, Output

dash.register_page(__name__)

layout = html.Div([
    html.H1('This is our Analytics page'),
    html.Div([
        "Select a city: ",
        dcc.RadioItems(
            options=['New York City', 'Montreal', 'San Francisco'],
            value='Montreal',
            id='analytics-input'
        )
    ]),
    html.Br(),
    html.Div(id='analytics-output'),
])


@callback(
    Output('analytics-output', 'children'),
    Input('analytics-input', 'value')
)
def update_city_selected(input_value):
    return f'You selected: {input_value}'
import dash
from dash import html

dash.register_page(__name__, path='/')

layout = html.Div([
    html.H1('This is our Home page'),
    html.Div('This is our Home page content.'),
])
import dash
from dash import html

dash.register_page(__name__)

layout = html.Div([
    html.H1('This is our Archive page'),
    html.Div('This is our Archive page content.'),
])
import dash
from dash import Dash, html, dcc

app = Dash(__name__, use_pages=True)

app.layout = html.Div([
    html.H1('Multi-page app with Dash Pages'),
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),
    dash.page_container
])

if __name__ == '__main__':
    app.run(debug=True)

备注

  • path — 我们在应用的三个页面中的每一个上调用 dash.register_page。对于其中两个页面,我们没有设置 path 属性。如果您不设置 path 属性,它将根据模块名称自动生成。因此,当用户访问 /archives 时,会提供 archives.py 的布局。同样,当用户访问 /analytics 时,会提供 analytics.py 的布局。当我们为 home.py 调用 dash.register_page 时,我们确实设置了 path 属性。对于 home.py,我们设置 path 属性是因为我们不希望在用户访问 /home 时显示内容,而是在用户访问主页:/ 时显示。

  • page_registry — 包含对 dash.register_page 的调用的页面会被添加到我们应用的页面注册表中。这是一个我们可以从中提取有关应用页面信息的 OrderedDict。在我们的 app.py 中,我们遍历应用的所有页面(在 dash.page_registry.values() 中)并为每一个添加链接。我们也可以从 dash.page_registry 中单独选择这些链接。路径为 / 的页面总是在字典中的索引 0。其他页面按字母顺序排列。

  • page_containerapp.py 中有 dash.page_container。当用户导航到该页面的路径时,此处会显示页面内容。

layout#

在上面的示例中,我们在每个页面中使用名为 layout 的变量定义布局。例如,在上面的 home.py 中:

layout = html.Div([
    html.H1('This is our Home page'),
    html.Div('This is our Home page content.'),
])

您也可以使用名为 layout 的函数来返回页面内容:

def layout():
    return html.Div([
        html.H1('This is our Home page'),
        html.Div('This is our Home page content.'),
    ])

备注

页面布局必须使用名为 layout 的变量或函数来定义。在创建带有 Pages 的应用时,只在主 app.py 文件中使用 app.layout

dash.register_page#

在文件中调用 dash.register_page 是 Dash 知道将该文件作为多页面应用中的页面包含的方式。

正如我们所见,它可以仅使用模块名称进行调用:

dash.register_page(__name__)

在这种情况下,Dash 会根据模块名称生成页面的路径、标题和链接名称。

备注

标题是 HTML 的 <title>。名称是 Dash 注册表中该页面的键,创建页面链接时可以使用。路径是页面的 URL 路径名。

所以,如果我们有名为 analytics.py 的文件,页面的路径是 /analytics,标题是 Analytics,链接名称是 Analytics

如果我们不希望这些根据模块名称自动生成,我们也可以指定它们,就像我们在上面的示例中对 home.py 所做的那样。

设置路径、标题和链接名称:

pages/analytics.py

dash.register_page(
    __name__,
    path='/analytics-dashboard',
    title='Our Analytics Dashboard',
    name='Our Analytics Dashboard'
)

任何调用 dash.register_page 的页面都会被添加到应用的页面注册表中。

页面注册表是一个名为 dash.page_registryOrderedDict。每个注册表条目都有页面的信息,包括在调用 dash.register_page 时设置的属性值,以及由 Dash 推断的值。与任何字典一样,您可以在代码中访问和使用其数据。

在这里,我们访问我们的 analytics 的路径,并在 app.py 中的 dcc.Link 中使用它:

html.Div(dcc.Link('Dashboard', href=dash.page_registry['pages.analytics']['path']))

要从 pages/ 目录中的文件内访问 dash.page_registry,您需要在函数内部使用它。

在这里,我们在 pages 目录中有两个文件:side_bar.pytopic_1.pytopic_1 页面从 side_bar.py 导入了一个侧边栏。注意 side_bar.py 内的函数是如何访问 dash.page_registry 的。如果不在函数内,应用将无法工作,因为在页面加载时 dash.page_registry 还没有准备好。

import dash
from dash import html
import dash_bootstrap_components as dbc


def sidebar():
    return html.Div(
        dbc.Nav(
            [
                dbc.NavLink(
                    html.Div(page["name"], className="ms-2"),
                    href=page["path"],
                    active="exact",
                )
                for page in dash.page_registry.values()
                if page["path"].startswith("/topic")
            ],
            vertical=True,
            pills=True,
            className="bg-light",
        )
    )
import dash
from dash import html

import dash_bootstrap_components as dbc

from .side_bar import sidebar

dash.register_page(__name__, name="Topics")

def layout():
    return dbc.Row(
        [dbc.Col(sidebar(), width=2), dbc.Col(html.Div("Topics Home Page"), width=10)]
    )

Dash 页面注册表顺序#

默认情况下,路径定义为 / 的页面会以索引 0 添加到注册表中。然后根据文件名按字母顺序添加其他页面。

您也可以通过在每个页面上设置 order 属性来指定 dash.page_registry 中页面的顺序:

pages/analytics.py

dash.register_page(__name__, order=3)

如果您在一个或多个页面上设置了 order 属性,页面会按照以下顺序添加到注册表:

  1. 根据 order 属性指定的顺序。

  2. 在那之后,按照字母顺序(对于未设置 order 属性的页面)。

设置 order 属性在您想要在动态创建侧边栏或页眉时能够循环遍历链接时很有用。

默认和自定义 404#

如果用户访问的路径没有在应用的页面中声明,Pages 会向用户显示默认的 ‘404 - 页面未找到’ 消息。

这个页面可以被自定义。在您的应用的 pages 目录中放置一个名为 not_found_404.py 的文件,将 dash.register_page(__name__) 添加到文件中,并在 layout 变量或函数内定义自定义 404 的内容:

import dash
from dash import html

dash.register_page(__name__)

layout = html.H1("This is our custom 404 content")

可变路径#

您可以通过使用 path_template 参数来捕获路径中的动态变量。通过将其放置在 <variable_name> 内来指定 URL 的动态部分。variable_name 将是传递给您的 layout 函数的命名关键字参数。layout 函数从 URL 接收的值始终是 str 类型。

单变量路径#

import dash
from dash import html

dash.register_page(__name__, path_template="/report/<report_id>")


def layout(report_id=None):
    return html.Div(
        f"The user requested report ID: {report_id}."
    )

示例 - 两个路径变量以及更新标题和描述#

路径变量也可以用来更新页面的标题(您在浏览器标签中看到的)和页面的元描述(搜索引擎在索引和显示搜索结果时使用的信息,分享链接时也会在社交媒体上显示;否则不可见)。

import dash
from dash import html


def title(asset_id=None, dept_id=None):
    return f"Asset Analysis: {asset_id} {dept_id}"


def description(asset_id=None, dept_id=None):
    return f"This is the AVN Industries Asset Analysis: {asset_id} in {dept_id}"


dash.register_page(
    __name__,
    path_template="/asset/<asset_id>/department/hello-<dept_id>",
    title=title,
    description=description,
)


def layout(asset_id=None, dept_id=None, **other_unknown_query_strings):
    return html.Div(
        f"variables from pathname:  asset_id: {asset_id} dept_id: {dept_id}"
    )

查询字符串#

URL 中的查询字符串参数可以被 Pages 捕获。

示例 - 单个查询字符串参数#

在这个示例中,当用户访问 /archive?report_id=9 时,值 9layout 函数捕获并在页面上显示。layout 函数从 URL 接收的值始终是 str 类型。

pages/archive.py:

import dash
from dash import html

dash.register_page(__name__)


def layout(report_id=None, **other_unknown_query_strings):
    return html.Div([
        html.H1('This is our Archive page'),
        html.Div(f'This is report: {report_id}.'),
    ])

示例 - 两个查询字符串参数#

在这个示例中,当用户访问 /archive?report_id=9&department_id=55 时,值 955layout 函数捕获并在页面上显示。

pages/archive.py

import dash
from dash import html

dash.register_page(__name__)


def layout(report_id=None, department_id=None, **other_unknown_query_strings):
    return html.Div([
        html.H1('This is our Archive page'),
        html.Div(f'''
	    This is report: {report_id}.\n
	    This is department: {department_id}.
	    '''),
    ])

重定向#

如果您更改了页面的路径,最好定义一个重定向,以便访问旧链接的用户不会得到 ‘404 - 页面未找到’。您可以使用 redirects 参数设置其他路径直接指向一个页面。这需要一个包含所有重定向到该页面的路径的列表。

在这里,我们有一个名为 archive 的页面。当用户访问 /archive/archive-2021/archive-2020 时会显示它。

import dash
from dash import html

dash.register_page(
    __name__,
    path="/archive",
    redirect_from=["/archive-2021", "/archive-2020"]
)

layout = html.Div([
    html.H1('This is our Archive page'),
    html.Div('This is our Archive page content.'),
])

单个文件中的多页应用#

到目前为止,我们已经构建了多页面应用,其中我们在 pages 目录中为每个页面声明了单独的 .py 文件。也可以在 app.py 中声明多个页面。

为此,我们在 app.py 中注册页面,并将布局直接传递给 dash.register_page。在这个示例中,我们在 app.py 文件中定义了两个页面:homeanalytics 页面。对于 module,第一个参数,我们为每个页面分配一个唯一的名称(因为这些名称将作为 Dash 页面注册表中的键)。我们还在创建 Dash 应用实例时添加了 pages_folder="",以指定我们没有为应用的页面使用文件夹。

from dash import Dash, html, dcc
import dash

app = Dash(__name__, use_pages=True, pages_folder="")

dash.register_page("home", path='/', layout=html.Div('Home Page'))
dash.register_page("analytics", layout=html.Div('Analytics'))

app.layout = html.Div([
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),
    dash.page_container,
])

if __name__ == '__main__':
    app.run(debug=True)

页面路由的额外输入#

这个特性在 Dash 2.14 中是新加的

默认情况下,路由到不同页面是通过两个输入触发的:1. URL 路径名 2. URL 查询参数

在某些情况下,您可能希望向页面布局函数传递更多信息,或者从其他输入触发布局渲染。例如,您可能想要:

  • 当用户更改语言下拉菜单时重新渲染整个页面内容

  • 在页面渲染时访问额外信息,而无需在每次值更新时重新渲染页面(例如,在 URL 哈希中序列化应用状态)

您可以通过这种机制传递任何您想要的输入/状态,但这里有几件事情需要记住:

  • 这将在服务器端回调中使用,因此传递非常大量的数据可能会增加页面加载时间

  • 传递给页面的新输入/状态将被传递给每个页面。确保每个页面上都有输入组件可用,或者使用 ALL id 模式

示例 1:更新页面的语言内容#

from dash import Dash, Input, Output, State, callback, dcc, html, page_container

TRANSLATIONS = {
    "en": {
        "title": "My app",
    },
    "fr": {
        "title": "Mon app",
    },
}
DEFAULT_LANGUAGE = "en"

app = Dash(
   __name__,
   use_pages=True,
   routing_callback_inputs={
        # The language will be passed as a `layout` keyword argument to page layout functions
        "language": Input("language", "value"),
   },
   pages_folder="pages2"
)

app.layout = html.Div(
    [
        html.Div(
            [
                # We will need to update the title when the language changes as it is
                # rendered outside the page layout function
                html.Div(TRANSLATIONS[DEFAULT_LANGUAGE]["title"], id="app-title", style={"fontWeight": "bold"}),
                html.Div(
                    [
                        # Language dropdown
                        dcc.Dropdown(
                            id="language",
                            options=[
                                {"label": "English", "value": "en"},
                                {"label": "Français", "value": "fr"},
                            ],
                            value=DEFAULT_LANGUAGE,
                            persistence=True,
                            clearable=False,
                            searchable=False,
                            style={"minWidth": 150},
                        ),
                    ],
                    style={"marginLeft": "auto", "display": "flex", "gap": 10}
                )
            ],
            style={
                "background": "#CCC",
                "padding": 10,
                "marginBottom": 20,
                "display": "flex",
                "alignItems": "center",
            },
        ),
        page_container,
    ],
    style={"fontFamily": "sans-serif"}
)

@callback(
    Output("app-title", "children"),
    Input("language", "value"),
)
def update_main_layout_language(language: str):
    """Translate the parts of the layout outside of the pages layout functions."""
    return TRANSLATIONS.get(language, {}).get("title")

if __name__ == "__main__":
    app.run(debug=True)
from dash import Input, Output, State, callback, dcc, html, register_page

register_page(__name__, "/")

TRANSLATIONS = {
    "en": {
        "title": "Hello world",
        "subtitle": "This is the home page.",
        "country": "Country",
        "year": "Year",
    },
    "fr": {
        "title": "Bonjour le monde",
        "subtitle": "Ceci est la page d'accueil.",
        "country": "Pays",
        "year": "Année",
    }
}

def layout(language: str = "en", **_kwargs):
    """Home page layout

    It takes in a keyword arguments defined in `routing_callback_inputs`:
    * language (input coming from the language dropdown), it triggers re-render
    """
    default_country = "USA"
    default_year = 2020

    return [
        html.H1(TRANSLATIONS.get(language, {}).get("title")),
        html.H2(TRANSLATIONS.get(language, {}).get("subtitle")),
        html.Div(
            [
                dcc.Dropdown(
                    id={"type": "control", "id": "country"},
                    value=default_country,
                    options=["France", "USA", "Canada"],
                    style={"minWidth": 200},
                ),
                dcc.Dropdown(
                    id={"type": "control", "id": "year"},
                    value=default_year,
                    options=[{"label": str(y), "value": y} for y in range(1980, 2021)],
                    style={"minWidth": 200},
                ),
            ],
            style={"display": "flex", "gap": "1rem", "marginBottom": "2rem"},
        ),
        html.Div(contents(language, country=default_country, year=default_year), id="contents")
    ]

def contents(language: str, country: str, year: int, **_kwargs):
    lang = TRANSLATIONS.get(language, {})
    return f"{lang.get('country')}: {country}, {lang.get('year')}: {year}"


@callback(
    Output("contents", "children"),
    Input({"type": "control", "id": "country"}, "value"),
    Input({"type": "control", "id": "year"}, "value"),
    # NOTE: no need to make language an input here as the whole page will
    # re-render if language changes
    State("language", "value"),
    prevent_initial_call=True,
)
def update_contents(country, year, language):
    """Update the contents when the dropdowns are updated."""
    return contents(language, country, year)

示例 2:在 URL 哈希中序列化应用状态#

from dash import Dash, Input, Output, State, callback, dcc, html, page_container

app = Dash(
   __name__,
   use_pages=True,
   routing_callback_inputs={
        # The app state is serialised in the URL hash without refreshing the page
        # This URL can be copied and then parsed on page load
        "state": State("main-url", "hash"),
   },
   pages_folder="pages3"
)

app.layout = html.Div(
    [
        dcc.Location(id="main-url"),
        html.Div(
            html.Div("My app", style={"fontWeight": "bold"}),
            style={"background": "#CCC", "padding": 10, "marginBottom": 20},
        ),
        page_container,
    ],
    style={"fontFamily": "sans-serif"}
)

if __name__ == "__main__":
    app.run(debug=True)
import base64
import json

from dash import ALL, Input, Output, callback, html, dcc, register_page, ctx

register_page(__name__, "/", title="Home")

def layout(state: str = None, **_kwargs):
    """Home page layout

    It takes in a keyword arguments defined in `routing_callback_inputs`:
    * state (serialised state in the URL hash), it does not trigger re-render
    """

    # Define default state values
    defaults = {"country": "France", "year": 2020}
    # Decode the state from the hash
    state = defaults | (json.loads(base64.b64decode(state)) if state else {})

    return [
        html.H1("Hello world"),
        html.H2("This is the home page."),
        html.Div(
            [
                dcc.Dropdown(
                    id={"type": "control", "id": "country"},
                    value=state.get("country"),
                    options=["France", "USA", "Canada"],
                    style={"minWidth": 200},
                ),
                dcc.Dropdown(
                    id={"type": "control", "id": "year"},
                    value=state.get("year"),
                    options=[{"label": str(y), "value": y} for y in range(1980, 2021)],
                    style={"minWidth": 200},
                ),
            ],
            style={"display": "flex", "gap": "1rem", "marginBottom": "2rem"},
        ),
        html.Div(contents(**state), id="contents")
    ]

def contents(country: str, year: int, **_kwargs):
    return f"Country: {country}, Year: {year}"

@callback(
    Output("main-url", "hash", allow_duplicate=True),
    Input({"type": "control", "id": ALL}, "value"),
    prevent_initial_call=True,
)
def update_hash(_values):
    """Update the hash in the URL Location component to represent the app state.

    The app state is json serialised then base64 encoded and is treated with the
    reverse process in the layout function.
    """
    return "#" + base64.b64encode(
        json.dumps({inp["id"]["id"]: inp["value"] for inp in ctx.inputs_list[0]})
        .encode("utf-8")
    ).decode("utf-8")

@callback(
    Output("contents", "children"),
    Input({"type": "control", "id": "country"}, "value"),
    Input({"type": "control", "id": "year"}, "value"),
    prevent_initial_call=True,
)
def update_contents(country, year):
    """Update the contents when the dropdowns are updated."""
    return contents(country, year)