返回列表

python-webui

Python + Vue 3 实时数据可视化演示项目,探索 WebSocket 双向通信与全局变量实时渲染方案。

PythonVue 3WebSocketasyncioElement Plus

项目简介

python-webui 是一个探索 Python 后端与 Vue 前端实时通信方案的演示项目。项目实现了 Python 全局变量的实时渲染和日志的实时推送,通过 WebSocket 双向通信机制,将后端数据变化毫秒级同步到前端界面。项目采用前后端分离架构,适合学习 Python-Vue 集成和异步编程模式。

主要特性

  • ⚡ 实时数据同步:WebSocket 实现毫秒级数据推送
  • 📊 多类型数据展示:支持 JSON、文本、数字、图片四种数据类型
  • 📝 实时日志面板:Python 日志实时推送到前端,支持颜色区分
  • 🔄 自动重连机制:断线后自动尝试重新连接
  • 🚀 异步并发架构:多任务并发运行,充分利用 asyncio

技术栈

类别技术
后端语言Python 3.x
WebSocketwebsockets 库
异步框架asyncio
前端框架Vue 3
构建工具Vite
UI 组件库Element Plus
通信协议WebSocket

技术亮点

亮点一:自定义 Logging Handler + asyncio.Queue 桥梁

将 Python 标准日志系统与 WebSocket 无缝集成:

class WebSocketLogHandler(logging.Handler):
    def emit(self, record):
        msg = self.format(record)
        log_entry = {
            "message": msg,
            "level": record.levelname,
            "timestamp": datetime.fromtimestamp(record.created).isoformat(),
        }
        log_queue.put_nowait(log_entry)  # 放入异步队列

设计优势

  • 解耦日志产生和日志推送
  • 利用 asyncio.Queue 实现线程安全的数据传递
  • 日志系统无需关心 WebSocket 实现

亮点二:图片 Base64 预编码优化

启动时一次性编码,避免运行时重复计算:

# 启动时预编码
for img_path in sorted(imgs_dir.iterdir()):
    raw = img_path.read_bytes()
    b64 = base64.b64encode(raw).decode("ascii")
    image_list.append({"filename": img_path.name, "base64": b64})

亮点三:Vue Composition API 最佳实践

使用 Composable 模式封装 WebSocket 逻辑:

export function useWebSocket(url = 'ws://localhost:8765') {
  const jsonData = ref(null)
  const logs = ref([])

  onMounted(() => connect())
  onUnmounted(() => disconnect())

  return { jsonData, logs, connected, ... }
}

优势

  • 逻辑复用性强
  • 生命周期管理清晰
  • 响应式状态自动更新 UI

亮点四:多任务并发架构

使用 asyncio.gather 并发运行多个独立任务:

任务职责频率
start_serverWebSocket 服务器持续运行
data_selector_task更新全局变量1-2秒随机
ws_broadcaster_task推送数据每1秒
log_pusher_task推送日志实时
self_talking_task模拟日志0.1-1秒随机

亮点五:前端自动重连机制

断线后自动尝试重新连接:

ws.onclose = () => {
  connected.value = false;
  reconnectTimer = setTimeout(connect, 2000);
};

亮点六:日志面板自动滚动

新日志到达时自动滚动到底部:

watch(
  logs,
  async () => {
    await nextTick();
    if (logContainer.value) {
      logContainer.value.scrollTop = logContainer.value.scrollHeight;
    }
  },
  { deep: true },
);

相关链接