1 简介
这是我的系列教程「Python+Dash快速web应用开发」的第十八期,通过前面十七期的内容,如果你有用心学习的话,那么恭喜你已经具备使用Dash
编写常规web应用的能力了。
而在使用Dash
开发web应用时,页面内容和功能逻辑简单倒还好,一旦你的功能内容开始复杂化系统化起来,那么像过往文章示例中简单一个app.py
存放所有功能代码就不适用了。
而在今天的教程中,我就将为大家介绍我在日常使用过程中总结出的一套针对Dash
项目的前后端分离的项目结构基础范式,并以搭建「全国七普部分数据可视化看板」为例,供大家参考借鉴,从而更有条理的编写和管理Dash
应用项目。
图1
2 Dash项目结构基础范式
2.1 总体结构一览
开门见山,我们直接先来一览今天要介绍的Dash
基础项目结构:
+ dash_demo_project/ + assets/ + css/ + img/ + js/ • favicon.ico + callbacks/ + models/ + views/ • app.py • server.py
在不考虑「外部参数导入」、「用户登陆验证」、「应用部署」等额外配置文件及功能内容的前提下,上面的结构就可以满足常规Dash
应用的需求了。
下面我们基于和鲸上获取到的「第七次全国人口普查」公开数据集,以搭建下面这个简单的数据可视化看板为例,介绍上述各部分的实际功能意义(完整项目源码见文章开头链接或公众号后台回复七普可视化)。
图2
2.2 各部分结构介绍
2.2.1 再谈assets
在「页面布局篇」中我们提到过assets
目录,它是官方推荐的用于存放我们的Dash
应用所依赖静态资源文件的目录,如依赖的css
、js
、favicon.ico
、各种图片及字体等静态资源,在本文的可视化看板案例中,assets
目录资源放置情况如下:
+ assets/ + css/ • bootstrap.min.css • custom.css + img/ • wxgzh.png • zsxq.png + js/ • favicon.ico
其中img
目录下存放的是首页的两张二维码图片,在Dash
中可以配合Img()
与get_asset_url()
来获取assets
目录下指定文件路径并渲染:
html.Img(src=app.get_asset_url('img/zsxq.png'), style={'width': '100%'})
而css
目录下则放置了dash_bootstrap-components
所依赖的css
文件,而custom.css
则是我自己编写的一些用于样式美化的css
代码:
.nav-link.active { background-color: #4fc3f7!important; } #index-desc > * { font-size: 26px; } .table td, .table th { text-align: center; }
直接放置于assets
根目录下的favicon.ico
则用来替换Dash
默认的网页图标:
图3
你可以根据自己Dash
项目的实际需求灵活变通,譬如需要用到echarts
就可以在js
目录下放置echarts.min.js
文件。
2.2.2 在server.py中实例化配置Dash对象
跟以往的例子不同,在严谨的Dash
工程下,推荐构建单独的server.py
文件来完成对Dash
对象的实例化配置等工作,在今天的可视化看板案例中server.py
比较简单,内容如下:
import dash app = dash.Dash( __name__, suppress_callback_exceptions=True ) # 设置网页title app.title = '七普部分数据看板' server = app.server
2.2.3 在app.py中编写前端骨架与路由
如果你的Dash
项目非常简单,那么from server import app
之后,就可以像往常一样在app.py
中组织你的前端与回调部分内容。
但如果你的Dash
项目功能较为复杂,亦或是url联结的页面较多时,就可以只在app.py
中编写前端layout
「骨架」,包含了必要的Location()
部件、保持不变的前端部分以及由url
变化所触发的页面内容容器,譬如今天的可视化看板中左侧边栏部分以及Location()
监听部件:
app.layout = html.Div( [ # 监听url变化 dcc.Location(id='url'), html.Div( [ # 标题区域 html.Div( html.H3( '七普部分数据看板', style={ 'marginTop': '20px', 'fontFamily': 'SimSun', 'fontWeight': 'bold' } ), style={ 'textAlign': 'center', 'margin': '0 10px 0 10px', 'borderBottom': '2px solid black' } ), # 子页面区域 html.Hr(), dbc.Nav( [ dbc.NavLink('首页', href='/', active="exact"), dbc.NavLink('年龄结构', href='/age', active="exact"), dbc.NavLink('性别结构', href='/sex', active="exact"), dbc.NavLink('六普vs七普', href='/statistics', active="exact"), ], vertical=True, pills=True ) ], style={ 'flex': 'none', 'width': '300px', 'backgroundColor': '#fafafa' } ), html.Div( id='page-content', style={ 'flex': 'auto' } ) ], style={ 'width': '100vw', 'height': '100vh', 'display': 'flex' } )
同样地,也推荐将监听url变化从而渲染不同页面的「路由回调」一并写在app.py
中,方便后续的管理与升级:
# 路由总控 @app.callback( Output('page-content', 'children'), Input('url', 'pathname') ) def render_page_content(pathname): if pathname == '/': return index_page elif pathname == '/age': return age_page elif pathname == '/sex': return sex_page elif pathname == '/statistics': return statistics_page return html.H1('您访问的页面不存在!')
2.2.4 在views子模块中构建多页面前端内容
在上一小节的路由回调中你可能会好奇不同url下的返回值index_page
、age_page
等都是什么,这些都构建在「子模块」views
下:
+ views/ • age.py • index.py • sex.py • statistics.py • __init__.py
譬如其中之一的age.py
内容如下:
import dash_html_components as html import dash_core_components as dcc import dash_bootstrap_components as dbc import pandas as pd import plotly.express as px from models.age import Age age_data = ( pd.DataFrame(Age.fetch_all()).rename(columns={ 'region': '地区', 'prop_0_to_14': '0到14岁人口占比', 'prop_15_59': '15到59岁人口占比', 'prop_60_above': '60岁以上人口占比', 'prop_65_above': '65岁以上人口占比' }) ) fig = px.bar(age_data.melt(id_vars=['地区'], value_vars=['0到14岁人口占比', '15到59岁人口占比', '60岁以上人口占比'], var_name='年龄段', value_name='占比(%)'), y="地区", x="占比(%)", color="年龄段", title="七普各地区人口年龄结构", color_discrete_map={ '0到14岁人口占比': '#0868ac', '15到59岁人口占比': '#43a2ca', '60岁以上人口占比': '#a8ddb5' }, orientation='h') fig.update_layout( font=dict( family="Times New Roman, SimSun" ) ) fig.update_layout(xaxis_range=[0, 100]) fig.update_layout( margin=dict(t=50, b=10) ) age_page = html.Div( [ html.Div( dbc.Table.from_dataframe(age_data, striped=True), style={ 'overflowY': 'auto', 'flex': '1' } ), html.Div( dcc.Graph(figure=fig, style={'height': '100%'}), style={ 'flex': '1', 'height': '100%' } ) ], style={ 'display': 'flex', 'height': '100%' } )
通过这种方式针对不同页面构建相应的前端对象,从而在app.py
中按照下列方式导入就可以使用了:
from views.index import index_page from views.age import age_page from views.sex import sex_page from views.statistics import statistics_page
2.2.5 在callbacks子模块中构建多页面后端逻辑
当你在views
下构建的页面内容中涉及到回调交互的功能时,我推荐将对应的后端回调逻辑拆分到callbacks
子模块下同名文件中,这样非常便于编写与维护。
同时「一定要」记住在views
下对应的前端子模块中,一定要导入callbacks
中对应的回调子模块内部的「至少」一个对象,否则Dash
在打包应用时是扫描不到相应的回调函数内容进行编译的,进而会导致应用启动时回调无效,譬如在views/statistics.py
中我们就执行了from callbacks.statistics import statistics_data
。
2.2.6 在models子模块下定义数据模型
前面说的很多内容都关乎Dash
应用的构建,而当你的Dash
应用依赖外部数据时,推荐的方式是类似flask
项目那样构建子模块models
来定义数据模型,实现与数据库的关联。
而我们今天的可视化看板案例中就配合「整合数据库篇」介绍的peewee
相关知识,分别定义了数据模型对应了七普中的年龄结构
、性别结构
以及六普七普对比
数据表,并在views
、callbacks
等涉及的子模块中导入并调用,以年龄结构models/age.py
为例:
from peewee import SqliteDatabase, Model from peewee import CharField, FloatField db = SqliteDatabase('models/age.db') class Age(Model): # 地区,唯一 region = CharField(unique=True) # 0-14岁占比 prop_0_to_14 = FloatField() # 15-59岁占比 prop_15_59 = FloatField() # 60岁及以上占比 prop_60_above = FloatField() # 65岁及以上占比 prop_65_above = FloatField() class Meta: database = db primary_key = False # 禁止自动生成唯一id列 @classmethod def fetch_all(cls): return list(cls.select().dicts())