10分鐘極速入門dash應用開發

費弗裡發表於2023-04-19

本文示例程式碼已上傳至我的Github倉庫https://github.com/CNFeffery/dash-master

大家好我是費老師,幾天前我釋出了由我開源維護的dash通用網頁元件庫fac0.2.x全新版本,為大家介紹了其具有的諸多實用特性功能,也吸引了很多對基於dashPython全棧應用開發感興趣的朋友,為了方便更多對dash應用開發不甚瞭解的朋友快速入門,今天的文章中,我將透過簡潔明瞭的內容帶大家快速掌握dash應用開發的必備基礎知識?。

10分鐘極速入門dash應用開發

閱讀本文大約需要10分鐘

1 環境搭建

dash應用作為Python專案,建議大家從一開始就養成好習慣,使用虛擬環境來構建我們的dash應用執行所需環境,以我最常用的conda為例,終端執行下列命令,建立名為dash-app-devPython版本為3.8的虛擬環境:

conda create -n dash-app-dev python=3.8 -y

啟用該環境:

conda activate dash-app-dev

在該環境下使用pip安裝必要依賴(dash+fac開發套件,以及用於開發階段程式碼格式自動美化的autopep8),這裡為了國內下載加速,使用了阿里雲映象:

pip install dash feffery-antd-components autopep8 -i https://mirrors.aliyun.com/pypi/simple/

至此我們就完成了標準dash應用所需Python虛擬環境的搭建工作了。

2 初始化專案

有了環境,接下來我們在自己熟悉的ide中建立專案進行初始化即可,以我最喜歡用的vscode為例(記得事先安裝微軟官方的Python外掛),我們先在本地某個位置新建示例工程目錄hello-dash,接著在vscode中將此目錄作為專案開啟:

10分鐘極速入門dash應用開發

在當前專案根目錄新建檔案app.py,即為我們本文演示用簡單小應用的主檔案,開啟app.py後,在vscode右下角選擇環境為我們先前建立的dash-app-dev即可:

10分鐘極速入門dash應用開發

3 dash應用基礎結構

有了作為應用主檔案的app.py之後,我們就可以開始編寫dash應用程式碼了,一個dash應用具有以下幾個基本構成部分:

3.1 相關包的匯入

首先我們需要在app.py檔案開頭匯入本文示例應用所需的各個模組,具體如下:

import dash # dash應用核心
from dash import html # dash自帶的原生html元件庫
import feffery_antd_components as fac # fac通用元件庫
from dash.dependencies import Input, Output, State # 用於構建應用互動功能的不同角色

3.2 例項化Dash()物件

接下來我們需要進行Dash()物件的例項化,其具有的其他功能引數我們在今後的文章中再分別作詳細介紹:

app = dash.Dash(__name__)

3.3 為dash應用定義初始元素

在已例項化的Dash()物件app的基礎上,我們需要為其layout屬性進行賦值,作為我們的dash應用被訪問時,初始化載入的頁面內容,layout可賦值為單個元件或返回單個元件的函式,通常我們會直接將一個html.Div()元件賦給它:

app.layout = html.Div()

在此基礎上,我們可以將賦給app.layouthtml.Div()元件作為最外層的容器,其他應用初始化時需要載入的更多元素,我們可以透過向下巢狀的方式傳給html.Div()children引數。

dash元件的世界中,一個元件只要允許接受children引數,就可以為其巢狀傳入單個元件,或由多個元件構成的列表,因為children引數也是對應元件的第一個位置引數,所以我們可以像下面這樣很方便的傳入一些其他元件,這裡以fac中的警告提示元件為例,我們將dashfac的版本資訊傳入其對應引數中:

app.layout = html.Div(
    [
        # 這裡以fac中的警告提示元件為例
        # 文件地址:https://fac.feffery.tech/AntdAlert
        fac.AntdAlert(
            message='Hello Dash!',
            description=f'當前應用dash版本:{dash.__version__} fac版本:{fac.__version__}',
            showIcon=True
        )
    ]
)

3.4 啟動應用

完成了上述過程後,我們先來啟動一下當前的應用,在app.py末尾新增下列程式碼,其中debug=True用於啟用開發除錯模式,這是我們在dash應用開發階段的好幫手,可以幫我們實現熱過載、錯誤資訊提示等便捷功能:

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

接著在終端中切換到該專案根目錄,也就是app.py所在的目錄,在啟用dash-app-dev環境的前提下,執行命令python app.py即可臨時啟動我們的dash應用,應用預設執行在http://127.0.0.1:8050中,我們按照提示在瀏覽器中訪問即可:

10分鐘極速入門dash應用開發

在瀏覽器中就可以看到我們的dash應用當前的樣子了~

10分鐘極速入門dash應用開發

3.5 調整應用樣式

眼下雖然我們這個非常簡單的dash應用跑起來了,但是樣子著實簡陋,在dash應用中針對元件元素的樣式進行調整的方式有很多種,最直接的方式是透過對應元件的style引數進行相關css樣式屬性的設定,譬如我們可以為最外層的html.Div()容器設定一定的內邊距:

10分鐘極速入門dash應用開發

因為我們開啟了debug=True模式,因此在調整程式碼後,按下ctrl+s儲存app.py最新變動後,瀏覽器中正在訪問的dash應用會自動化重新整理,非常方便,可以看到,此時我們的應用已經有了內邊距:

10分鐘極速入門dash應用開發

3.6 基於回撥函式實現互動功能

到目前為止,我們的示例應用還僅僅是在展示靜態內容,當我們需要為dash應用新增互動功能時,就需要用到dash中的核心概念——回撥函式了,在回撥函式眼中,每個具有唯一id引數的元件的任意屬性,都可以被編排為回撥函式中的角色,我們書寫回撥函式的過程實際上就是在玩角色編排的遊戲,在dash中有InputOutputState三種角色,下面我們來舉例說明它們各自的作用:

假如我們現在需要在頁面中放置一個按鈕,並在使用者每次點選按鈕後,在按鈕旁邊展示其累計被點選的次數資訊,回撥函式就可以寫作(常規的回撥函式本質上是在用@app.callback()對定義回撥邏輯的函式進行裝飾):

10分鐘極速入門dash應用開發

其中@app.callback()中編排的內容翻譯成人話就是idbutton-demo的元件的nClicks屬性每次更新時,都會經過函式體內定義的邏輯將返回值更新到idbutton-demo-output的元件的children屬性,於是乎便實現了下面動圖展示的效果:

10分鐘極速入門dash應用開發

同時向多個Output角色進行輸出更新也是可以的,譬如我們每次點選按鈕時不僅更新按鈕一側的資訊,還順便彈出訊息提示,就可以將程式碼修改為:

10分鐘極速入門dash應用開發

互動效果如下:

10分鐘極速入門dash應用開發

美中不足的是我們剛訪問應用,並沒有進行按鈕點選時,回撥函式自動就先執行了一遍,這是因為dash應用預設會在應用初始化時對所有的回撥函式都自動執行一遍,不管其所編排的Input角色是否更新,如果你不希望這種機制發生,那麼在@app.callback()中設定引數prevent_initial_call=True即可:

10分鐘極速入門dash應用開發

可以看到,這時初始訪問應用就不會有相關資訊自動被刷出:

10分鐘極速入門dash應用開發

透過上面的簡單例子,我們已經掌握了dash回撥函式中InputOutput角色的作用,剩下的State角色就比較特殊,不同於Input那樣可以透過監聽目標元件的指定屬性變化從而觸發回撥函式執行,State角色用來在回撥函式中提供輔助屬性值,相當於每次回撥函式因為某個Input角色變化而被觸發時,會捎帶手把State角色對應的屬性值一併攜帶進回撥函式中,起到輔助計算的作用。

舉個實際的例子,假如我們在按鈕一側新增一個輸入框,每次按鈕被點選時,都順便將輸入框中的已輸入內容傳遞進回撥進行使用,就可以寫作下面的方式:

10分鐘極速入門dash應用開發

有了額外State角色的輔助,我們的應用互動效果就變成下面動圖所示:

10分鐘極速入門dash應用開發

至此,我們就get到dash中回撥函式的基本寫法——即在@app.callback()中按照OutputInputState的順序依次編排角色,且回撥函式輸入引數(引數名隨意)與已編排的InputState角色順序一致即可~

3.7 更復雜的應用示例

掌握了上文所述的dash應用最基礎概念後,下面我們就可以嘗試編寫更復雜的互動應用場景,譬如下面的簡單例子,我們在頁面中放置了若干表單輸入類元件,配合fac.AntdForm()fac.AntdFormItem()進行表單的快捷構建,並透過回撥函式與下方的表格實現聯動篩選(以pandas資料框為例),效果如下:

10分鐘極速入門dash應用開發

上面例子的完整程式碼如下,執行前請記得額外安裝pandas

# 相關包的匯入
import dash  # dash應用核心
import pandas as pd
from dash import html  # dash自帶的原生html元件庫
import feffery_antd_components as fac  # fac通用元件庫
from dash.dependencies import Input, Output, State  # 用於構建應用互動功能的不同角色


# 例項化Dash()物件
app = dash.Dash(__name__)

# 建立示例表格
demo_df = pd.DataFrame(
    {
        '欄位1': [f'類別{i}' for i in range(1, 11)],
        '欄位2': [10*i for i in range(10)],
        '欄位3': [(pd.Timestamp('2023-01-01') + pd.Timedelta(days=i)).strftime('%Y-%m-%d')
                for i in range(10)]
    }
)

# 為dash應用定義初始元素
app.layout = html.Div(
    [
        # 這裡以fac中的警告提示元件為例
        # 文件地址:https://fac.feffery.tech/AntdAlert
        fac.AntdAlert(
            message='Hello Dash!',
            description=f'當前應用dash版本:{dash.__version__} fac版本:{fac.__version__}',
            showIcon=True
        ),
        # 放置水平分割虛線
        fac.AntdDivider(isDashed=True),
        fac.AntdForm(
            [
                fac.AntdFormItem(
                    fac.AntdSelect(
                        id='field1-range',
                        options=[
                            {
                                'label': x,
                                'value': x
                            }
                            for x in demo_df['欄位1'].unique()
                        ],
                        mode='multiple',
                        maxTagCount='responsive',
                        style={
                            'width': 200
                        }
                    ),
                    label='欄位1'
                ),
                fac.AntdFormItem(
                    fac.AntdSlider(
                        id='field2-range',
                        min=0,
                        max=100,
                        range=True,
                        defaultValue=[0, 100],
                        style={
                            'width': 150
                        }
                    ),
                    label='欄位2'
                ),
                fac.AntdFormItem(
                    fac.AntdDateRangePicker(
                        id='field3-range',
                        defaultPickerValue=demo_df['欄位3'].min(),
                        style={
                            'width': 200
                        }
                    ),
                    label='欄位3'
                ),
                fac.AntdButton(
                    '查詢',
                    id='execute-query',
                    icon=fac.AntdIcon(
                        icon='antd-search'
                    ),
                    type='primary'
                )
            ],
            layout='inline',
            style={
                'marginBottom': 15
            }
        ),
        html.Div(id='table-result-container')
    ],
    style={
        # 這裡基於css中的padding引數,設定上下內邊距50畫素,左右內邊距100畫素
        'padding': '50px 100px'
    }
)


@app.callback(
    Output('table-result-container', 'children'),
    Input('execute-query', 'nClicks'),
    [State('field1-range', 'value'),
     State('field2-range', 'value'),
     State('field3-range', 'value')]
)
def query_table(nClicks, field1_range, field2_range, field3_range):

    demo_df_copy = demo_df.copy()

    if field1_range:
        demo_df_copy.query('欄位1 == @field1_range', inplace=True)

    if field2_range:
        demo_df_copy.query(f'{field2_range[0]} <= 欄位2 <= {field2_range[1]}',
                           inplace=True)

    if field3_range:
        demo_df_copy.query(f'"{field3_range[0]}" <= 欄位3 <= "{field3_range[1]}"',
                           inplace=True)

    if not demo_df_copy.empty:
        return fac.AntdTable(
            columns=[
                {
                    'title': column,
                    'dataIndex': column
                }
                for column in demo_df_copy.columns
            ],
            data=demo_df_copy.to_dict('records'),
            bordered=True
        )

    # 否則返回無匹配資料提示
    return fac.AntdEmpty(
        description='當前條件組合下無匹配資料'
    )


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

以上就是本文的全部內容,更多有關dash應用開發的前沿知識和技巧歡迎持續關注玩轉dash公眾號。

相關文章