我如何解决 Streamlit 会话持久化(在三次失败尝试后)
Source: Dev.to
问题
我在构建一个 Streamlit 应用,需要在页面刷新后记住用户会话。听起来很简单,对吧?只需像普通网页一样使用 localStorage 或 cookie 即可。
错了。
Streamlit Cloud 在一个沙箱 iframe 中运行你的应用。这意味着:
- 标准的
localStorage被限制在组件 iframe 中 - Cookie 也面临相同的隔离问题
- 直接操作 DOM 受到安全限制
结果呢?三天的挫败感和三次失败的尝试。
尝试 1:通过 streamlit_js_eval 使用 localStorage
我的第一想法是:“我只要用 JavaScript 访问 localStorage 就行了。”
from streamlit_js_eval import streamlit_js_eval
# 尝试存储会话
streamlit_js_eval(js_expressions="""
localStorage.setItem('session_token', 'abc123');
""")
# 尝试检索
token = streamlit_js_eval(js_expressions="""
localStorage.getItem('session_token');
""")
失败原因: JavaScript 在 Streamlit 的组件 iframe 中运行,该 iframe 拥有独立的存储空间。数据虽然被保存了,但它与应用的主上下文完全分离。页面重新加载后,这些数据就不存在了。
尝试 2:通过 extra-streamlit-components 使用 Cookie
接下来:Cookie。它们肯定可以跨 iframe 工作,对吧?
from extra_streamlit_components import CookieManager
from datetime import datetime, timedelta
cookie_manager = CookieManager()
# Set cookie
cookie_manager.set('session_token', 'abc123', expires_at=datetime.now() + timedelta(days=1))
# Retrieve
token = cookie_manager.get('session_token')
为什么会失败: 同样的 iframe 隔离问题。Cookie 在组件的上下文中设置,而不是在父框架中。Streamlit Cloud 的沙箱机制导致该 Cookie 在你需要的地方不可访问。
Attempt 3: window.parent.localStorage
在走投无路的情况下,我尝试直接访问父窗口的 localStorage:
streamlit_js_eval(js_expressions="""
window.parent.localStorage.setItem('session_token', 'abc123');
""")
为什么会失败: 浏览器安全模型。跨源 iframe 访问被设计上阻止。Streamlit Cloud 的 iframe 沙箱机制触发了这些保护,这是有充分理由的——如果允许这种行为,将会是安全噩梦。
解决方案:st.query_params
在对 iframe 墙壁“撞头”之后,我发现一直在那里的是:st.query_params。
该功能在 Streamlit 1.30 中引入,允许你直接在 URL 中以查询参数的形式存储数据。它具有:
- 在首次渲染时同步可用
- 在页面刷新之间持久化
- 在 Streamlit Cloud 的 iframe 环境中完美工作
- 零外部依赖
它的使用方式如下:
import streamlit as st
import base64
import json
def save_session(session_data):
# Encode session data as base64 to handle special characters
json_str = json.dumps(session_data)
encoded = base64.b64encode(json_str.encode()).decode()
# Store in URL
st.query_params['s'] = encoded
def load_session():
# Retrieve from URL
if 's' in st.query_params:
try:
encoded = st.query_params['s']
json_str = base64.b64decode(encoded).decode()
return json.loads(json_str)
except Exception:
return {}
return {}
# Usage
if 'session' not in st.session_state:
st.session_state.session = load_session()
# Save whenever session changes
if st.button('Save'):
save_session(st.session_state.session)
为什么这样有效
- 基于 URL 的存储:查询参数是 URL 的一部分,可在任何地方访问——没有 iframe 问题。
- 内置于 Streamlit:无需外部组件或 JavaScript hack。
- 同步访问:在页面加载时立即可用,组件渲染之前。
- Base64 编码:在 URL 格式中安全处理复杂数据结构。
课程
有时候,最好的解决方案就是最简单的。我花了三天时间与外部库和 JavaScript 变通方法斗争,而 Streamlit 一直都有内置的解决方案。
在求助第三方库或巧妙的 hack 之前:
- 检查是否有内置解决方案。
- 阅读最近的更新日志——像
st.query_params这样的特性很容易被忽视。 - 了解你的部署环境(本例中的 iframe 沙箱)。
想了解更多?
阅读完整故事,包括所有调试步骤和代码示例,请参阅 Chronicle 001: Genesis。