
本文探讨了在streamlit应用中,如何高效地将javascript前端(特别是来自iframe或父窗口)的值传递给python后端。针对复杂双向组件的痛点,我们介绍了一种基于`streamlit_javascript`包的简洁方案,通过执行javascript代码并结合简单的重试逻辑,实现异步数据的可靠获取,极大地简化了前端与后端之间的数据交互。
在构建交互式Streamlit应用时,我们有时需要集成外部javascript逻辑,例如通过st.components.v1.iframe嵌入的第三方认证页面,或者自定义的JavaScript脚本。这些前端脚本可能会生成一些关键数据(如认证令牌),并需要将其回传给Streamlit的python后端进行进一步处理。虽然Streamlit提供了双向组件(Bi-directional Streamlit Components)来实现复杂的js与Python交互,但对于仅仅传递一个或少量值的简单场景,其实现过程可能显得过于繁琐。
场景描述:从JavaScript获取认证令牌
考虑一个常见的认证场景:
- 用户通过一个嵌入在Streamlit应用中的iframe进行认证。
- iframe内的JavaScript在认证成功后,通过parent.postMessage(Token, ‘*’)将认证令牌(token)发送给其父窗口(即Streamlit应用所在的浏览器窗口)。
- Streamlit应用页面的JavaScript通过parent.window.addEventListener(‘message’, …)监听并接收这个令牌,并将其存储在parent.window.token变量中。
- 此时,我们需要将存储在parent.window.token中的值获取到Streamlit的Python后端,以便根据该令牌授予用户访问权限。
原始的JavaScript和Streamlit集成代码可能如下所示:
iframe内容 (html/JS):
立即学习“Java免费学习笔记(深入)”;
<script src="remote/auth.js"></script> <script> // 假设getAuth()函数返回认证令牌 token = window.getAuth(); // 将令牌发送给父窗口 parent.postMessage(token, '*'); </script>
Streamlit应用 (Python + JS):
from streamlit.components.v1 import html, iframe # 注入JavaScript监听器,将接收到的令牌存储在parent.window.token html(""" <script> parent.window.addEventListener('message', e => { const key = e.message ? 'message' : 'data'; const token = e[key]; parent.window.token = token; // 将令牌存储在全局变量中 },false); </script> """) # 嵌入认证iframe iframe("RemoteIframeLocation") # 此时,目标是获取parent.window.token到Python后端
解决方案:利用 streamlit_javascript 包
对于这种单向、简单的数据传递需求,streamlit_javascript 包提供了一个非常简洁高效的解决方案。该包允许你在Streamlit的Python代码中直接执行任意JavaScript代码,并获取其返回值。
核心原理
streamlit_javascript 的 st_javascript() 函数会在浏览器端执行其接收到的JavaScript字符串,并将执行结果返回给Streamlit的Python脚本。由于JavaScript的执行通常是异步的,并且我们期望的值可能不会立即可用,因此需要结合一个简单的重试机制来确保数据的可靠获取。
实现步骤
-
安装 streamlit_javascript: 如果尚未安装,请先通过pip安装该包:
pip install streamlit-javascript
-
在Streamlit应用中集成: 使用一个循环和 time.sleep() 来轮询 parent.window.token 的值,直到它被设置。
from streamlit_javascript import st_javascript import time import streamlit as st # ... (上述的iframe和JS监听器代码) ... # 初始化一个会话状态变量来存储令牌,避免重复获取 if 'auth_token' not in st.session_state: st.session_state.auth_token = None # 如果令牌尚未获取,则尝试获取 if st.session_state.auth_token is None: st.info("正在等待认证令牌...") # 循环尝试从JavaScript获取令牌 while True: # 执行JavaScript代码 'parent.window.token',获取其值 token = st_javascript('parent.window.token') if token: st.session_state.auth_token = token st.success("认证令牌已获取!") break # 令牌获取成功,退出循环 time.sleep(1) # 等待1秒后重试,避免CPU空转 # 可以添加一个退出条件,例如最多重试N次 # if retry_count > MAX_RETRIES: break # 令牌获取后,可以在Python后端使用它 if st.session_state.auth_token: st.write(f"在Python后端获取到的令牌是: {st.session_state.auth_token}") # 这里可以进行令牌验证、用户数据加载等操作
代码解析
- st_javascript(‘parent.window.token’): 这是核心。它告诉浏览器执行 parent.window.token 这段JavaScript代码,并将其结果返回给Python。如果 parent.window.token 尚未定义或为 NULL/undefined,Python会收到 None。
- while True: 循环:由于JavaScript的执行和令牌的设置是异步的,Python代码可能在令牌实际可用之前就尝试读取它。因此,需要一个循环来持续检查。
- time.sleep(1):在每次尝试之间暂停1秒。这非常重要,可以防止Python代码在一个紧密的循环中不断消耗CPU资源,同时给JavaScript足够的时间来处理和设置令牌。
- if token: break: 一旦 st_javascript 返回一个非空(即有效的)令牌,我们就将其存储到 st.session_state.auth_token 中,并退出循环。
- st.session_state.auth_token: 使用Streamlit的会话状态来存储获取到的令牌,确保在页面重新渲染时令牌不会丢失,并且只获取一次。
注意事项与最佳实践
- 异步性处理: 始终记住JavaScript的执行是异步的。上述的 while True 循环是一种简单的轮询机制。对于更复杂的场景,你可能需要在JavaScript端使用 promise 或 async/await 来确保值在Python尝试读取时已经准备就绪,或者在Python端添加超时机制以防止无限等待。
- 错误处理与超时: 示例中的 while True 是一个无限循环。在生产环境中,强烈建议添加一个最大重试次数或超时机制,以防止在令牌永远无法获取时应用卡死。
max_retries = 10 retry_count = 0 while retry_count < max_retries: token = st_javascript('parent.window.token') if token: st.session_state.auth_token = token st.success("认证令牌已获取!") break time.sleep(1) retry_count += 1 if not st.session_state.auth_token: st.error("未能获取认证令牌,请重试或检查认证流程。") - 安全性: 如果传递的是敏感信息(如认证令牌),请确保整个传输过程是安全的(例如,使用https)。此外,避免在前端JavaScript中不必要地暴露敏感数据。
- 适用场景: streamlit_javascript 包非常适合于从JavaScript获取单个或少量值、触发简单JavaScript函数等场景。对于需要复杂ui交互、双向数据绑定或大量数据交换的场景,Streamlit的双向组件(st.components.v1.declare_component) 可能是更合适的选择,尽管它们实现起来更复杂。
- 浏览器兼容性: 确保你执行的JavaScript代码在目标用户的浏览器环境中能够正确运行。
总结
通过 streamlit_javascript 包,我们可以轻松地在Streamlit的Python后端获取浏览器端JavaScript变量的值,有效解决了从前端向后端传递简单数据的需求,避免了实现复杂双向组件的开销。结合简单的重试逻辑和适当的错误处理,这种方法为Streamlit应用中的前端-后端数据交互提供了一个简洁而强大的工具。


