Python+Dash快速web应用开发:回调交互篇(下)

简介: Python+Dash快速web应用开发:回调交互篇(下)

1 简介

这是我的系列教程「Python+Dash快速web应用开发」的第五期,在上一期的文章中,我们针对Dash中有关回调的一些技巧性的特性进行了介绍,使得我们可以更愉快地为Dash应用编写回调交互功能。

而今天的文章作为「回调交互」系统性内容的最后一期,我将带大家get一些Dash中实际应用效果惊人的「高级回调特性」,系好安全带,我们起飞~

图1

2 Dash中的高级回调特性

2.1 控制部分回调输出不更新

在很多应用场景下,我们给某个回调函数绑定了多个Output(),这时如果这些Output()并不是每次触发回调都需要被更新,那么就可以根据Input()值的不同,来配合dash.no_update作为对应Output()的返回值,从而实现部分Output()不更新,譬如下面的例子:

app1.py

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output
import time
app = dash.Dash(__name__)
app.layout = html.Div(
    dbc.Container(
        [
            html.Br(),
            html.Br(),
            html.Br(),
            dbc.Row(
                dbc.Col(
                    dbc.Button('按钮',
                               color='primary',
                               id='button',
                               n_clicks=0)
                )
            ),
            html.Br(),
            dbc.Row(
                [
                    dbc.Col('尚未触发', id='record-1'),
                    dbc.Col('尚未触发', id='record-2'),
                    dbc.Col('尚未触发', id='record-n')
                ]
            )
        ]
    )
)
@app.callback(
    [Output('record-1', 'children'),
     Output('record-2', 'children'),
     Output('record-n', 'children'),
     ],
    Input('button', 'n_clicks'),
    prevent_initial_call=True
)
def record_click_event(n_clicks):
    if n_clicks == 1:
        return (
            '第1次点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
            dash.no_update,
            dash.no_update
        )
    elif n_clicks == 2:
        return (
            dash.no_update,
            '第2次点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
            dash.no_update
        )
    elif n_clicks >= 3:
        return (
            dash.no_update,
            dash.no_update,
            '第3次及以上点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
        )
if __name__ == '__main__':
    app.run_server(debug=True)

图2

可以观察到,我们根据n_clicks数值的不同,在对应各个Output()返回值中对符合条件的部件进行更新,其他的都用dash.no_update来代替,从而实现了局部更新,非常实用且简单。

2.2 基于模式匹配的回调

这是Dash在1.11.0版本开始引入的新特性,它所实现的功能是将多个部件绑定组织在同一个id属性下,这听起来有一点抽象,我们先从一个形象的例子来出发:

假如我们要开发一个简单的「记账」应用,它通过第一排若干Input()部件及一个Button()部件来记录并提交每笔账对应的相关信息,并且在最下方输出已记录账目金额之和:

app2.py

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ALL
import re
app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Br(),
        html.Br(),
        dbc.Container(
            dbc.Row(
                [
                    dbc.Col(
                        dbc.InputGroup(
                            [
                                dbc.InputGroupAddon("金额", addon_type="prepend"),
                                dbc.Input(
                                    id='account-amount',
                                    placeholder='请输入金额',
                                    type="number",
                                ),
                                dbc.InputGroupAddon("元", addon_type="append"),
                            ],
                        ),
                        width=5
                    ),
                    dbc.Col(
                        dcc.Dropdown(
                            id='account-type',
                            options=[
                                {'label': '生活开销', 'value': '生活开销'},
                                {'label': '人情往来', 'value': '人情往来'},
                                {'label': '医疗保健', 'value': '医疗保健'},
                                {'label': '旅游休闲', 'value': '旅游休闲'},
                            ],
                            placeholder='请选择类型:'
                        ),
                        width=5
                    ),
                    dbc.Col(
                        dbc.Button('提交记录', id='account-submit'),
                        width=2
                    )
                ]
            )
        ),
        html.Br(),
        dbc.Container([], id='account-record-container'),
        dbc.Container('暂无记录!', id='account-record-sum')
    ]
)
@app.callback(
    Output('account-record-container', 'children'),
    Input('account-submit', 'n_clicks'),
    [State('account-record-container', 'children'),
     State('account-amount', 'value'),
     State('account-type', 'value')],
    prevent_initial_call=True
)
def update_account_records(n_clicks, children, account_amount, account_type):
    '''
    用于处理每一次的记账输入并渲染前端记录
    '''
    if account_amount and account_type:
        children.append(dbc.Row(
            dbc.Col(
                '【{}】类开销【{}】元'.format(account_type, account_amount)
            ),
            # 以字典形式定义id
            id={'type': 'single-account_record', 'index': children.__len__()}
        ))
        return children
@app.callback(
    Output('account-record-sum', 'children'),
    Input({'type': 'single-account_record', 'index': ALL}, 'children'),
    prevent_initial_call=True
)
def refresh_account_sum(children):
    '''
    对多部件集合single-account_record下所有账目记录进行求和
    '''
    return '账本总开销:{}'.format(sum([int(re.findall('\d+',
                                                 child['props']['children'])[0])
                                  for child in children]))
if __name__ == '__main__':
    app.run_server(debug=True)

图3

上面这个应用中,体现出的「模式匹配」内容即为开头从dash.dependencies引入的ALL,它是Dash「模式匹配」中的一种模式,而我们在回调函数update_account_records()中为已有记账记录追加新纪录时,使用到:

# 以字典形式定义id
id={'type': 'single-account_record', 'index': children.__len__()}

这里不同于以前我们采取的id=某个字符串的定义方法,换成字典之后,其type键值对用来记录唯一id信息,每一次新纪录追加时type值都相等,因为它们被组织为「同id部件集合」,而键值对index则用于在type值相同的一个部件集合下,区分出不同的独立部件元素。

因为将传统的「唯一id部件」替换成「同id部件集合」,所以我们后面的回调函数refresh_account_sum()的输入元素只需要定义单个Input()即可,再在函数内部按照不同的index值取出需要的集合内各成员记录值,非常便于我们书写出简练清爽的Dash代码,便于之后进一步的修改与重构。

你可以通过最下面打印出的每次refresh_account_sum()所接收到的children参数json格式结果来弄清我是如何在return值的地方取出历史记账金额并计算的。

而除了上面介绍的一股脑返回所有集合内成员部件的ALL模式之外,还有另一种更有针对性的MATCH模式,它应用于结合内成员部件可交互输入值的情况,譬如下面这个简单的例子,我们定义一个简单的用于查询省份行政代码的应用,配合MATCH模式来实现彼此成对独立输出:

app3.py

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH
import dash_core_components as dcc
app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Br(),
        html.Br(),
        html.Br(),
        dbc.Container(
            [
                dbc.Row(
                    dbc.Col(
                        dbc.Button('新增查询', id='add-item', outline=True)
                    )
                ),
                html.Hr()
            ]
        ),
        dbc.Container([], id='query-container')
    ]
)
region2code = {
    '北京市': '110000000000',
    '重庆市': '500000000000',
    '安徽省': '340000000000'
}
@app.callback(
    Output('query-container', 'children'),
    Input('add-item', 'n_clicks'),
    State('query-container', 'children'),
    prevent_initial_call=True
)
def add_query_item(n_clicks, children):
    children.append(
        dbc.Row(
            [
                dbc.Col(
                    [
                        # 生成index相同的dropdown部件与文字输出部件
                        dcc.Dropdown(id={'type': 'select-province', 'index': children.__len__()},
                                     options=[{'label': label, 'value': label} for label in region2code.keys()],
                                     placeholder='选择省份:'),
                        html.P('请输入要查询的省份!', id={'type': 'code-output', 'index': children.__len__()})
                    ]
                )
            ]
        )
    )
    return children
@app.callback(
    Output({'type': 'code-output', 'index': MATCH}, 'children'),
    Input({'type': 'select-province', 'index': MATCH}, 'value')
)
def refresh_code_output(value):
    if value:
        return region2code[value]
    else:
        return dash.no_update
if __name__ == '__main__':
    app.run_server(debug=True)

图4

可以看到,在refresh_code_output()前应用MATCH模式匹配后,我们点击某个部件时,只有跟它index匹配的部件才会打印出相对应的输出,非常的方便~

2.3 多输入情况下获取部件触发情况

在很多应用场景下,我们的某个回调可能拥有多个Input输入,但学过前面的内容我们已经清楚,不管有几个Input,只要其中有一个部件其输入属性发生变化,都会触发本轮回调,但是如果我们就想知道究竟是「哪个」Input触发了本轮回调该怎么办呢?

这在Dash中可以通过dash.callback_context来方便的实现,它只能在回调函数中被执行,从而获取回调过程的诸多上下文信息,先从下面这个简单的例子出发看看dash.callback_context到底给我们带来了哪些有价值的信息:

app4.py

import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import json
app = dash.Dash(__name__)
app.layout = html.Div(
    dbc.Container(
        [
            html.Br(),
            html.Br(),
            html.Br(),
            dbc.Row(
                [
                    dbc.Col(dbc.Button('A', id='A', n_clicks=0)),
                    dbc.Col(dbc.Button('B', id='B', n_clicks=0)),
                    dbc.Col(dbc.Button('C', id='C', n_clicks=0))
                ]
            ),
            dbc.Row(
                [
                    dbc.Col(html.P('按钮A未点击', id='A-output')),
                    dbc.Col(html.P('按钮B未点击', id='B-output')),
                    dbc.Col(html.P('按钮C未点击', id='C-output'))
                ]
            ),
            dbc.Row(
                dbc.Col(
                    html.Pre(id='raw-json')
                )
            )
        ]
    )
)
@app.callback(
    [Output('A-output', 'children'),
     Output('B-output', 'children'),
     Output('C-output', 'children'),
     Output('raw-json', 'children')],
    [Input('A', 'n_clicks'),
     Input('B', 'n_clicks'),
     Input('C', 'n_clicks')],
    prevent_initial_call=True
)
def refresh_output(A_n_clicks, B_n_clicks, C_n_clicks):
    # 获取本轮回调状态下的上下文信息
    ctx = dash.callback_context
    # 取出对应State、最近一次触发部件以及Input信息
    ctx_msg = json.dumps({
        'states': ctx.states,
        'triggered': ctx.triggered,
        'inputs': ctx.inputs
    }, indent=2)
    return A_n_clicks, B_n_clicks, C_n_clicks, ctx_msg
if __name__ == '__main__':
    app.run_server(debug=True)

图5

可以看到,我们安插在回调函数里的dash.callback_context帮我们记录了从访问Dash开始,到最近一次执行回调期间,对应回调的输入输出信息变化情况、最近一次触发信息,非常的实用,可以支撑起很多复杂应用场景。

2.4 在浏览器端执行回调过程

Dash虽然很方便,使得我们可以完全不用书写js代码就可以实现各种回调交互,但把所有的交互响应计算过程都交给服务端来做,省事倒是很省事,但会给服务器带来不小的计算和网络传输压力。

因此很多容易频繁触发且与主要的数值计算无关的交互行为,完全可以搬到浏览器端执行,既快速又不吃服务器的计算资源,这也是当初JavaScript被发明的一个重要原因,而在Dash中,也为略懂js的用户提供了在浏览器端执行一些回调的贴心功能。

从一个很简单的点击按钮,实现部分网页内容的打开与关闭出发,这里我们提前使用到dbc.Collapse部件,用于将所包含的网页内容与其它按钮部件的点击行为进行绑定:

app5.py

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output, State
app = dash.Dash(__name__)
app.layout = html.Div(
    dbc.Container(
        [
            html.Br(),
            html.Br(),
            html.Br(),
            dbc.Button('服务端回调', id='server-button'),
            dbc.Collapse('服务端折叠内容', id='server-collapse'),
            html.Hr(),
            dbc.Button('浏览器端回调', id='browser-button'),
            dbc.Collapse('浏览器端折叠内容', id='browser-collapse'),
        ]
    )
)
@app.callback(
    Output('server-collapse', 'is_open'),
    Input('server-button', 'n_clicks'),
    State('server-collapse', 'is_open'),
    prevent_initial_call=True
)
def server_callback(n_clicks, is_open):
    return not is_open
# 在dash中定义浏览器端回调函数的特殊格式
app.clientside_callback(
    """
    function(n_clicks, is_open) {
        return !is_open;
    }
    """,
    Output('browser-collapse', 'is_open'),
    Input('browser-button', 'n_clicks'),
    State('browser-collapse', 'is_open'),
    prevent_initial_call=True
)
if __name__ == '__main__':
    app.run_server(debug=True)

可以看到,服务端回调我们照常写,而浏览器端回调通过传入一个非常简单的js函数,在每次回调时接受输入并输出is_open的逻辑反值,从而实现了折叠内容的打开与关闭切换:

function(n_clicks, is_open) {
        return !is_open;
}

便实现了浏览器端回调!

图6

而如果你想要执行的浏览器端js回调函数代码有点长,还可以按照下图格式,把你的大段js回调函数代码放置于assets目录下对应路径里的js脚本中:

图7

接着再在dash中按照下列格式编写关联输入输出与上述js回调的简短语句即可:

app.clientside_callback(
    ClientsideFunction(
        namespace='命名空间名称',
        function_name='对应js回调函数名'
    ),
    '''
    按顺序组织你的Output、Input以及State... ...
    '''
)

下面我们直接以大家喜闻乐见的数据可视化顶级框架echarts为例,来写一个根据不同输入值切换渲染出的图表类型,「注意」请从官网把依赖的echarts.min.js下载到我们的assets路径下对应位置,它会在我们的Dash应用启动时与所有assets下的资源一起自动被载入到浏览器中:

app6.py

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, ClientsideFunction
app = dash.Dash(__name__)
# 编写一个根据dropdown不同输入值切换对应图表类型的小应用
app.layout = html.Div(
    dbc.Container(
        [
            html.Br(),
            dbc.Row(
                dbc.Col(
                    dcc.Dropdown(
                        id='chart-type',
                        options=[
                            {'label': '折线图', 'value': '折线图'},
                            {'label': '堆积面积图', 'value': '堆积面积图'},
                        ],
                        value='折线图'
                    ),
                    width=3
                )
            ),
            html.Br(),
            dbc.Row(
                dbc.Col(
                    html.Div(
                        html.Div(
                            id='main',
                            style={
                                'height': '100%',
                                'width': '100%'
                            }
                        ),
                        style={
                            'width': '800px',
                            'height': '500px'
                        }
                    )
                )
            )
        ]
    )
)
app.clientside_callback(
    # 关联自编js脚本中的相应回调函数
    ClientsideFunction(
        namespace='clientside',
        function_name='switch_chart'
    ),
    Output('main', 'children'),
    Input('chart-type', 'value')
)
if __name__ == '__main__':
    app.run_server(debug=True)

图8

目录
相关文章
|
2月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
224 4
|
4月前
|
供应链 新能源 调度
微电网调度(风、光、储能、电网交互)(Matlab&Python代码实现)
微电网调度(风、光、储能、电网交互)(Matlab&Python代码实现)
121 0
|
6月前
|
缓存 JavaScript 前端开发
鸿蒙5开发宝藏案例分享---Web开发优化案例分享
本文深入解读鸿蒙官方文档中的 `ArkWeb` 性能优化技巧,从预启动进程到预渲染,涵盖预下载、预连接、预取POST等八大优化策略。通过代码示例详解如何提升Web页面加载速度,助你打造流畅的HarmonyOS应用体验。内容实用,按需选用,让H5页面快到飞起!
|
6月前
|
JavaScript 前端开发 API
鸿蒙5开发宝藏案例分享---Web加载时延优化解析
本文深入解析了鸿蒙开发中Web加载完成时延的优化技巧,结合官方案例与实际代码,助你提升性能。核心内容包括:使用DevEco Profiler和DevTools定位瓶颈、四大优化方向(资源合并、接口预取、图片懒加载、任务拆解)及高频手段总结。同时提供性能优化黄金准则,如首屏资源控制在300KB内、关键接口响应≤200ms等,帮助开发者实现丝般流畅体验。
|
前端开发 JavaScript Shell
鸿蒙5开发宝藏案例分享---Web页面内点击响应时延分析
本文为鸿蒙开发者整理了Web性能优化的实战案例解析,结合官方文档深度扩展。内容涵盖点击响应时延核心指标(≤100ms)、性能分析工具链(如DevTools时间线、ArkUI Trace抓取)以及高频优化场景,包括递归函数优化、网络请求阻塞解决方案和setTimeout滥用问题等。同时提供进阶技巧,如首帧加速、透明动画陷阱规避及Web组件初始化加速,并通过优化前后Trace对比展示成果。最后总结了快速定位问题的方法与开发建议,助力开发者提升Web应用性能。
|
6月前
|
JSON 开发框架 自然语言处理
【HarmonyOS Next之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(三)
本文主要介绍了应用开发中的三大核心内容:生命周期管理、资源限定与访问以及多语言支持。在生命周期部分,详细说明了应用和页面的生命周期函数及其触发时机,帮助开发者更好地掌控应用状态变化。资源限定与访问章节,则聚焦于资源限定词的定义、命名规则及匹配逻辑,并阐述了如何通过 `$r` 引用 JS 模块内的资源。最后,多语言支持部分讲解了如何通过 JSON 文件定义多语言资源,使用 `$t` 和 `$tc` 方法实现简单格式化与单复数格式化,为全球化应用提供便利。
254 104
|
6月前
|
JavaScript 前端开发 API
【HarmonyOS Next之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(二)
本文介绍了HarmonyOS应用开发中的HML、CSS和JS语法。HML作为标记语言,支持数据绑定、事件处理、列表渲染等功能;CSS用于样式定义,涵盖尺寸单位、样式导入、选择器及伪类等特性;JS实现业务逻辑,包括ES6语法支持、对象属性、数据方法及事件处理。通过具体代码示例,详细解析了页面构建与交互的实现方式,为开发者提供全面的技术指导。
273 104
|
6月前
|
开发框架 编解码 JavaScript
【HarmonyOS Next之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(一)
该文档详细介绍了一个兼容JS的类Web开发范式的方舟开发框架,涵盖概述、文件组织、js标签配置及app.js等内容。框架采用HML、CSS、JavaScript三段式开发方式,支持单向数据绑定,适合中小型应用开发。文件组织部分说明了目录结构、访问规则和媒体文件格式;js标签配置包括实例名称、页面路由和窗口样式信息;app.js则描述了应用生命周期与对象管理。整体内容旨在帮助开发者快速构建基于方舟框架的应用程序。
269 102
|
6月前
|
Linux 数据库 数据安全/隐私保护
Python web Django快速入门手册全栈版,共2590字,短小精悍
本教程涵盖Django从安装到数据库模型创建的全流程。第一章介绍Windows、Linux及macOS下虚拟环境搭建与Django安装验证;第二章讲解项目创建、迁移与运行;第三章演示应用APP创建及项目汉化;第四章说明超级用户创建与后台登录;第五章深入数据库模型设计,包括类与表的对应关系及模型创建步骤。内容精炼实用,适合快速入门Django全栈开发。
243 1
|
8月前
|
数据采集 人工智能 测试技术
Python有哪些好用且实用的Web框架?
Python 是一门功能强大的编程语言,在多个领域中得到广泛应用,包括爬虫、人工智能、游戏开发、自动化测试和 Web 开发。在 Web 开发中,Python 提供了多种框架以提高效率。以下是几个常用的 Python Web 框架:1) Django:开源框架,支持多种数据库引擎,适合新手;2) Flask:轻量级框架,基于简单核心并通过扩展增加功能;3) Web2py:免费开源框架,支持快速开发;4) Tornado:同时作为 Web 服务器和框架,适合高并发场景;5) CherryPy:简单易用的框架,连接 Web 服务器与 Python 代码。这些框架各有特色,可根据需求选择合适的工具。
394 14

推荐镜像

更多