streamlit 中默认如何设计页面与页面之间的联动
上一次的文章中也说过 streamlit 是没有路由的概念的, 它所有的东西其实都是在一个页面展示的。 那它如何控制什么时候应该展示什么样的内容呢? 还是通过上次介绍的 session_state 这个缓存来控制的。 我们可以设置一个 button,点击这个 button 后就往 session_state 中添加一个数据,然后在页面展示的时候判断这个数据是否存在于 session_state 中,如果存在就展示特定的内容。 如下:
import streamlit as st
# 设置 3 个按钮
button_1 = st.button("添加数据 A")
button_2 = st.button("添加数据 B")
button_3 = st.button("添加数据 C")
# 根据按钮点击情况往 session_state 中添加数据
if button_1:
st.session_state["data"] = "A"
elif button_2:
st.session_state["data"] = "B"
elif button_3:
st.session_state["data"] = "C"
if 'data' in st.session_state:
if st.session_state['data'] == "A":
st.write('当前展示的是dataA')
elif st.session_state['data'] == "B":
st.write('当前展示的是dataB')
elif st.session_state['data'] == "C":
st.write('当前展示的是dataC')
效果如下:
所以其实在 streamlit 我们是可以把所有的逻辑都写在一个 py 文件中, 或者也可以写在多个 py 文件中,并用一个 main.py 来决定应该显示哪个 py 文件中的哪个函数的内容。 但这样不符合我们的习惯,并且有诸多缺点, 比如最大的缺点是无法通过 url 定位到特定的页面功能上。 比如我们写了测试平台, 然后有个测试报告想给其他人看。 你没办法给对方一个 url 来快速访问到这个测试报告的内容。
streamlit 自带的多页面应用
# main.py
import streamlit as st
from pages import home, about, contact
PAGES = {
"Home": home,
"About": about,
"Contact": contact
}
def main():
st.sidebar.title("Navigation")
page = st.sidebar.radio("Go to", list(PAGES.keys()))
PAGES[page]()
if __name__ == "__main__":
main()
# pages/home.py
import streamlit as st
def main():
st.title("Home Page")
# Home page content
main()
# pages/about.py
import streamlit as st
def main():
st.title("About Page")
# About page content
main()
# pages/contact.py
import streamlit as st
def main():
st.title("Contact Page")
# Contact page content
main()
上面的代码片段中我们定了 4 个 py 文件, 一个主文件和 3 个子页面文件, 我们用 st.sidebar.radio 这个组件来切换应该显示哪个 python 文件。 但这个方式仍然无法解决我们上面解决的问题。
利用 st.query_params 来封装路由功能
在 streamlit 中用户可以使用 st.query_params 来对页面当前的 url 参数进行处理。 可以从 url 中获取参数的值, 也可以设置 url 参数的值。
我们可以从 url 中获取对应的参数,来决定渲染哪个页面:
page = st.query_params["page"]
if page == 'home_page':
home_page.write()
if page == 'nav_page':
nav_page.write()
首先,程序通过 query_params 获取 url 中的 page 参数的值, 然后根据不同的值来判断应该渲染哪个页面。
而当我们需要在代码中跳转页面的时候, 可以像下面一样:
st.query_params["page"] = 'home_page'
st.query_params.task = 'your_task_id'
st.rerun()
st.rerun 用于重新渲染页面,但是 url 中的参数不变, 这就可以让页面通过上面的代码根据 page 的值来判断应该渲染哪一个页面了。
一个页面架构推荐的形态
首先定义一个 Page 类:
class Page:
def __init__(self, route):
self.route_path = route
def refresh_route(self):
st.query_params["page"] = self.route_path
def route(self):
st.query_params["page"] = self.route_path
time.sleep(0.1) # 需要等待路由更新
st.rerun()
def get_route(self):
return self.route_path
def write(self):
pass
- 在我们的设计中, url 中一定要带一个名字叫 page 的参数,用来指定当前应该渲染哪个页面,所以 init 方法中定义了要传一个参数来定义
- Page 类的主要作用就是定义一些公共的能力。 其中就包括了 route 方法。其中 st.rerun 用来重新渲染页面。这通常用于在代码中进行页面的跳转。
- wirte 方法用于让子类重写, 所有的页面渲染逻辑就在这里编写。 之所以取名 write,也是为了跟 streamlit 的风格保持一致。
然后我们每个页面的代码如下:
class DocParse(Page):
def write(self):
# 页面渲染代码
doc_parse = DocParse('doc_parse')
然后我们还需要一个整个平台的主页:
import streamlit as st
from page.page import Page
from page.mllm_task import mllm_test
from page.mllm_task_detail import mllm_test_detail
from page.mllm_test_compare import mllm_test_compare
from page.doc_parse import doc_parse
from page.doc_parse_compare import doc_parse_compare
from page.doc_parse_data_detail import doc_parse_data_detail
class MultiApp:
""" 整个平台的主页面,所有子页面需要按照它的标准进行初始化. 每个子页面对象都要集成page.page中的Page父类
Usage:
app = MultiApp()
app.add_xiaoguo_app("项目管理", project)
app.run()
"""
def __init__(self):
self.apps = []
self.extra_apps = []
self.buttons_status = []
def add_xiaoguo_app(self, title, page):
""" 这里定义的页面会显示在侧边导航栏
Parameters
----------
page:
页面对象,该对象需要继承page.page中的Page父类
title:
显示在侧边导航栏的名字
"""
self.apps.append({
"title": title,
"page": page,
})
def add_extra_app(self, page):
""" 这里定义的页面不会显示在侧边导航栏
Parameters
----------
page:
页面对象, 该页面不会显示在侧边导航栏
"""
self.extra_apps.append({
"page": page,
})
def run(self):
"""
负责主页面显示的函数,主要通过以下步骤:
1. 定义侧边导航栏架构。
2. 获取当前url中是否已经设定了page参数,如果有page参数则并遍历已注册的所有页面对象并导航到对应的页面那种, 如果没有则直接显示默认首页
"""
# ratio的回调函数, 负责设置url并导航到对应子页面
def change_route():
app = st.session_state['app_key'] # 获取当前被选中的页面
app['page'].refresh_route() # 要重置一下url中的参数
# 获取当前URL中是否已经带了page参数, page参数决定了应该显示哪个子页面.
route = st.query_params.get('page')
default_page = 0
if route:
for a in self.apps:
pa: Page = a['page']
print(pa.get_route())
if route == pa.get_route():
default_page = self.apps.index(a)
# step 1: 定义侧边导航栏架构
st.sidebar.title("任务导航")
with st.sidebar.expander("效果测试管理", expanded=True):
app = st.radio(
'',
self.apps,
format_func=lambda app: app['title'],
on_change=change_route,
key="app_key",
index=default_page,
)
url = 'http://localhost:8501/'
st.markdown(f'<a href="{url}" target="_self">{"返回首页"}</a>', unsafe_allow_html=True)
if route:
for a in self.apps:
pa: Page = a['page']
if route == pa.get_route():
pa.write()
for a in self.extra_apps:
pa: Page = a['page']
if route == pa.get_route():
pa.write()
else:
app['page'].refresh_route()
app['page'].write()
st.set_page_config(layout='wide')
app = MultiApp()
# 开始注册效果测试侧边栏
app.add_xiaoguo_app("多模态", mllm_test)
app.add_xiaoguo_app("文档解析", doc_parse)
# app.add_extra_app(doc_split_detail)
app.add_extra_app(mllm_test_detail)
app.add_extra_app(mllm_test_compare)
app.add_extra_app(doc_parse_compare)
app.add_extra_app(doc_parse_data_detail)
app.run()
效果如下图:
下面是主页的整体思路:
- 需要一个侧边栏组件:st.sidebar.expander,通过侧边栏来协调各个子模块的展示。
- 各个子模块都有独立的 page 页面来负责, 这些页面都是要继承上面说的 Page 类的,在主页的类 MultiApp 中有一个 add_xiaoguo_app 方法,用这个方法把子模块的页面类加入到一个 dict 中。而不需要在侧边栏展示的页面, 则使用 add_extra_app 加入到另一个 dict 中
- 通过 st.radio 来展示侧边栏中每个子模块的条目。 用户选择哪个,就去调用对应的子模块页面的 write() 方法来渲染
- 渲染页面的时候需要通过 st.query_params 获取当前 url 中 page 参数,来判断应该渲染哪个页面。
结尾
好了,今天先讲到这里, 单独介绍 streamlit 的基础部分的内容就到这里了, 下一期直接讲多模态大模型的测试平台的构建。 更多 streamlit 的内容会在实战这部分内容的时候介绍。 顺便再推销一下自己的星球。