Building a Real-time Dashboard with FastAPI and Svelte

在本教程中,您将学习如何使用 FastAPISvelte 构建实时分析仪表板。我们将使用服务器发送事件 (SSE) 将实时数据更新从 FastAPI 流式传输到我们的 Svelte 前端,创建一个实时更新的交互式仪表板。

最终应用

仪表板最终版

依赖:

  • Svelte v5.23.2
  • SvelteKit v2.19.0
  • 节点 v22.14.0
  • npm v11.2.0
  • FastAPI 版本 0.115.11
  • Python 版本 3.13.2

目标

在本教程结束时,您应该能够:

  1. 设置具有实时数据流功能的 FastAPI 后端
  2. 使用 SvelteKit 创建现代 Svelte 应用程序
  3. 实施服务器发送事件 (SSE) 以实现实时数据更新
  4. 使用 Svelte 组件构建交互式图表和图形
  5. 在前端高效处理实时数据更新

我们在构建什么?

我们将创建一个分析控制面板,实时显示模拟传感器数据。仪表板将包括:

  • 显示温度趋势的折线图
  • 显示当前湿度水平的仪表图
  • 实时状态指示器
  • 历史数据视图

这是一个实际示例,可以适用于任何需要实时数据可视化的应用程序。

项目设置

让我们从创建项目结构开始,打开一个终端并运行以下命令:

$ mkdir svelte-fastapi-dashboard
$ cd svelte-fastapi-dashboard

我们将使用两个主要目录来组织我们的项目:

svelte-fastapi-dashboard/
├── backend/
└── frontend/

让我们从后端设置开始......

FastAPI 后端

首先,让我们设置我们的后端环境。创建并导航到后端目录:

$ mkdir backend
$ cd backend

创建并激活虚拟环境:

$ python -m venv venv
$ source venv/bin/activate
$ export PYTHONPATH=$PWD

安装所需的依赖项:

(venv)$ pip install fastapi==0.115.11 uvicorn==0.34.0 sse-starlette==2.2.1

我们在 FastAPI 中使用 sse-starlette 来支持服务器发送的事件。

在 “backend” 文件夹中创建以下目录结构:

backend/
├── app/
│   ├── __init__.py
│   ├── api.py
│   └── sensor.py
└── main.py

让我们在 backend/app/sensor.py 中实现一个模拟传感器数据生成器:

import random
from datetime import datetime
from typing import Dict

class SensorData:
    def __init__(self):
        self.min_temp = 18.0
        self.max_temp = 26.0
        self.min_humidity = 30.0
        self.max_humidity = 65.0

    def generate_reading(self) -> Dict:
        """Generate a mock sensor reading."""
        return {
            "timestamp": datetime.now().isoformat(),
            "temperature": round(random.uniform(self.min_temp, self.max_temp), 1),
            "humidity": round(random.uniform(self.min_humidity, self.max_humidity), 1),
            "status": random.choice(["normal", "warning", "critical"])
        }

现在,让我们在 backend/app/api.py 中创建我们的 FastAPI 应用程序:

import asyncio
import json

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sse_starlette.sse import EventSourceResponse

from .sensor import SensorData

app = FastAPI()
sensor = SensorData()

# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # Svelte dev server
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/")
async def root():
    return {"message": "Welcome to the Sensor Dashboard API"}

@app.get("/current")
async def get_current_reading():
    """Get the current sensor reading."""
    return sensor.generate_reading()

@app.get("/stream")
async def stream_data():
    """Stream sensor data using server-sent events."""
    async def event_generator():
        while True:
            data = sensor.generate_reading()
            yield {
                "event": "sensor_update",
                "data": json.dumps(data)
            }
            await asyncio.sleep(2)  # Update every 2 seconds

    return EventSourceResponse(event_generator())

在本节中,我们创建了一个 FastAPI 应用程序,它使用服务器发送事件 (SSE) 流式传输传感器数据。终端节点返回当前传感器读数,终端节点实时流式传输传感器数据更新。/current/stream

最后,在 backend/main.py 中创建入口点:

import uvicorn

if __name__ == "__main__":
    uvicorn.run("app.api:app", host="0.0.0.0", port=8000, reload=True)

启动服务器:

(venv)$ python main.py

您的 API 现在应该以 http://localhost:8000 运行。您可以在 http://localhost:8000/docs 查看 API 文档。 访问 http://localhost:8000 后,您应该会看到以下输出:

{
"message": "Welcome to the Sensor Dashboard API"
}

Svelte 前端

现在让我们使用 SvelteKit 创建 Svelte 应用程序。导航回项目根目录并创建前端:

$ cd ..
$ npx [email protected] create frontend

出现提示时,选择以下选项:

  • 您想要哪个模板?› SvelteKit minimal
  • 使用 TypeScript 添加类型检查?› 是的,使用 TypeScript 语法
  • 您想为您的项目添加什么?(使用箭头键/空格键):
    • ✓ 更漂亮
    • ✓ ESLint
    • ✓ 维斯特
  • 您希望使用哪个软件包管理器安装依赖项?› npm

安装依赖项:

$ cd frontend
$ npm install

我们还需要一些额外的 dashboard 包:

$ npm install [email protected] @types/[email protected]

让我们创建主仪表板布局。将 frontend/src/routes/+page.svelte 的内容替换为:


  import { onMount } from 'svelte';
  import type { SensorReading } from '$lib/types';

  let currentReading = $state(null);
  let eventSource = $state(undefined);

  onMount(async () => {
    // Initial data fetch
    const response = await fetch('http://localhost:8000/current');
    currentReading = await response.json();

    // Set up SSE connection
    eventSource = new EventSource('http://localhost:8000/stream');
    eventSource.addEventListener('sensor_update', (event) => {
      currentReading = JSON.parse(event.data);
    });

    return () => {
      if (eventSource) eventSource.close();
    };
  });

Sensor Dashboard

{#if currentReading}

Temperature

{currentReading.temperature}°C

Humidity

{currentReading.humidity}%

Status

{currentReading.status}

{/if}
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-top: 2rem; } .card { background: #fff; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .reading { font-size: 2rem; font-weight: bold; margin: 1rem 0; } .status { text-transform: uppercase; font-weight: bold; } .status.normal { color: #2ecc71; } .status.warning { color: #f1c40f; } .status.critical { color: #e74c3c; }

创建一个新文件 frontend/src/lib/types.ts 来定义我们的类型:

export interface SensorReading {
  timestamp: string;
  temperature: number;
  humidity: number;
  status: 'normal' | 'warning' | 'critical';
}

在一个终端窗口中运行后端后,启动 Svelte 开发服务器:

$ npm run dev

您的仪表板现在应该可以在 http://localhost:5173 时访问,显示实时传感器数据更新!

仪表板初始

这段代码中发生了什么变化?

  1. 组件结构:仪表板组件遵循典型的 Svelte 结构,包含三个主要部分:
    1. 脚本 (logic)
    2. 模板 (HTML)
    3. 样式 (CSS)
  2. 数据管理
    1. 使用 TypeScript 实现类型安全
    2. 维护两个关键状态:
      1. currentReading:存储最新的传感器数据
      2. eventSource:管理实时连接
  3. 实时数据流:
    1. 初始加载:在元件安装时获取电流传感器数据
    2. 实时更新:建立 SSE 连接以进行实时更新
    3. 清理:销毁组件时正确关闭连接

温度、湿度和状态是从后端从 sensor.py 中定义的值中提取的,并显示在控制面板中。

实时图表

让我们使用 Chart.js 使用交互式图表来增强我们的仪表板。首先,创建一个新的 components 目录:

$ mkdir src/lib/components

frontend/src/lib/components/TemperatureChart.svelte 中为我们的温度图表创建一个新组件:


  import { onMount } from 'svelte';
  import Chart from 'chart.js/auto';
  import type { SensorReading } from '$lib/types';

  const { data } = $props();
  let canvas: HTMLCanvasElement;
  let chart = $state(undefined);

  $effect(() => {
    if (chart && data) {
      chart.data.labels = data.map(reading => {
        const date = new Date(reading.timestamp);
        return date.toLocaleTimeString();
      });
      chart.data.datasets[0].data = data.map(reading => reading.temperature);
      chart.update();
    }
  });

  onMount(() => {
    chart = new Chart(canvas, {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: 'Temperature (°C)',
          data: [],
          borderColor: '#3498db',
          tension: 0.4,
          fill: false
        }]
      },
      options: {
        responsive: true,
        animation: {
          duration: 0 // Disable animations for real-time updates
        },
        scales: {
          y: {
            beginAtZero: false,
            suggestedMin: 15,
            suggestedMax: 30
          }
        }
      }
    });

    return () => {
      if (chart) chart.destroy();
    };
  });

此组件使用 Chart.js 创建实时温度折线图。挂载后,它会使用适当的样式和刻度设置初始化一个空图表。反应式语句 , 监视数据数组中的变化,自动使用新的温度读数更新图表,并将时间戳转换为可读的时间格式。($:)

frontend/src/lib/components/HumidityGauge.svelte 中为湿度创建一个类似的组件:


  import { onMount } from 'svelte';
  import Chart from 'chart.js/auto';

  const { value } = $props();
  let canvas: HTMLCanvasElement;
  let chart = $state(undefined);

  $effect(() => {
    if (chart && value !== undefined) {
      chart.data.datasets[0].data = [value];
      chart.update();
    }
  });

  onMount(() => {
    chart = new Chart(canvas, {
      type: 'doughnut',
      data: {
        datasets: [{
          data: [value],
          backgroundColor: ['#2ecc71'],
          circumference: 180,
          rotation: 270,
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          tooltip: {
            enabled: false
          }
        }
      }
    });

    return () => {
      if (chart) chart.destroy();
    };
  });

{value}%
.gauge-container { position: relative; height: 200px; } .gauge-value { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); font-size: 1.5rem; font-weight: bold; }

在本节中,我们使用 Svelte 5 的 runes 创建了一个仪表图组件。该组件用于声明其属性和反应式状态管理。rune 取代了传统的反应式语句,以便在值发生变化时更新图表。通过使用带有自定义配置 (, ) 的 Chart.js' 甜甜圈类型,我们创建了一个显示当前湿度值的半圆形仪表。$props()$state()$effect()circumference: 180rotation: 270

现在,让我们更新我们的主页以包含历史数据和新的图表组件。更新 frontend/src/routes/+page.svelte


  import { onMount } from 'svelte';
  import TemperatureChart from '$lib/components/TemperatureChart.svelte';
  import HumidityGauge from '$lib/components/HumidityGauge.svelte';
  import type { SensorReading } from '$lib/types';

  let currentReading = $state(null);
  let historicalData = $state([]);
  let eventSource = $state(undefined);

  onMount(async () => {
    // Initial data fetch
    const response = await fetch('http://localhost:8000/current');
    currentReading = await response.json();
    historicalData = [currentReading];

    // Set up SSE connection
    eventSource = new EventSource('http://localhost:8000/stream');
    eventSource.addEventListener('sensor_update', (event) => {
      currentReading = JSON.parse(event.data);
      historicalData = [...historicalData, currentReading].slice(-30); // Keep last 30 readings
    });

    return () => {
      if (eventSource) eventSource.close();
    };
  });

Sensor Dashboard

{#if currentReading}

Temperature History

Current Humidity

System Status

{currentReading.status}

Last updated: {new Date(currentReading.timestamp).toLocaleTimeString()}

{/if}
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .dashboard-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-top: 2rem; } .card { background: #fff; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .span-2 { grid-column: span 2; } .status-container { display: flex; flex-direction: column; align-items: center; gap: 1rem; } .status-indicator { width: 80px; height: 80px; border-radius: 50%; margin: 1rem 0; } .status-indicator.normal { background-color: #2ecc71; } .status-indicator.warning { background-color: #f1c40f; } .status-indicator.critical { background-color: #e74c3c; } .status-text { text-transform: uppercase; font-weight: bold; } .timestamp { color: #666; font-size: 0.9rem; } @media (max-width: 768px) { .dashboard-grid { grid-template-columns: 1fr; } .span-2 { grid-column: auto; } }

现在,在浏览器中,您应该会看到以下内容:

仪表板初始

设置和警报

警报通知

让我们创建一个通知系统,用于传感器值超过特定阈值时。在 frontend/src/lib/components/AlertBanner.svelte 中创建一个新组件:


  import { fade } from 'svelte/transition';

  const { message, type = 'warning' } = $props();

{#if message}
  
⚠️ {message}
{/if} .alert { position: fixed; top: 1rem; right: 1rem; padding: 1rem; border-radius: 4px; color: white; display: flex; align-items: center; gap: 0.5rem; z-index: 1000; } .warning { background-color: #f1c40f; } .critical { background-color: #e74c3c; } .alert-icon { font-size: 1.2rem; }

该组件使用 Svelte 的块模式实现条件 UI 渲染,以切换警报可见性。当有警报消息时,该组件会显示一个带有图标和消息的横幅。AlertBanner{#if}

“设置”面板

最后,让我们在 frontend/src/lib/components/SettingsPanel.svelte 中添加一个设置面板来配置警报阈值:


  import { fade } from 'svelte/transition';

  const props = $props void;
    onHumidityChange?: (value: number) => void;
  }>();

  let localTempThreshold = $state(props.tempThreshold ?? 25);
  let localHumidityThreshold = $state(props.humidityThreshold ?? 60);
  let isOpen = $state(false);

  function updateSettings() {
    // Send values back to parent
    props.onTempChange?.(localTempThreshold);
    props.onHumidityChange?.(localHumidityThreshold);
    isOpen = false;
  }

{#if isOpen}

Alert Thresholds

{/if}
.settings-container { position: fixed; bottom: 1rem; right: 1rem; z-index: 1000; } .settings-button { background: #2c3e50; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } .settings-button:hover { background: #34495e; } .settings-panel { position: absolute; bottom: 100%; right: 0; background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); margin-bottom: 0.5rem; min-width: 280px; color: #2c3e50; } h3 { margin: 0 0 1rem 0; font-size: 1.2rem; color: #2c3e50; } .setting-group { margin: 1rem 0; } label { display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.9rem; color: #34495e; } input { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; width: 100%; } input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); } .button-group { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.5rem; } .button-group button { padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem; transition: opacity 0.2s; } .button-group button:hover { opacity: 0.9; } .save { background: #2ecc71; color: white; } .cancel { background: #95a5a6; color: white; }

在该组件中,我们实现了一个可配置的接口,用于警报阈值管理。我们将回调模式用于以下步骤:SettingsPanel

  1. 组件接收当前值作为 props (,tempThresholdhumidityThreshold)
  2. 它还接收回调函数 (, ) 以通知父级更改onTempChangeonHumidityChange
  3. 它使用表单 inputs 维护自己的本地状态$state
  4. 当用户点击 “Save” 时,它会使用更新的值调用回调

更新主页面 frontend/src/routes/+page.svelte,以包含警报处理和设置面板:


  import { onMount } from 'svelte';
  import TemperatureChart from '$lib/components/TemperatureChart.svelte';
  import HumidityGauge from '$lib/components/HumidityGauge.svelte';
  import SettingsPanel from '$lib/components/SettingsPanel.svelte';
  import AlertBanner from '$lib/components/AlertBanner.svelte';
  import type { SensorReading } from '$lib/types';

  let currentReading = $state(null);
  let historicalData = $state([]);
  let eventSource = $state(undefined);
  let tempThreshold = $state(25);
  let humidityThreshold = $state(60);
  let alertMessage = $state('');
  let alertType = $state('warning');

  function checkAlertConditions(reading: SensorReading) {
    if (reading.temperature > tempThreshold) {
      alertMessage = High temperature detected: ${reading.temperature}°C;
      alertType = 'critical';
    } else if (reading.humidity > humidityThreshold) {
      alertMessage = High humidity detected: ${reading.humidity}%;
      alertType = 'warning';
    } else {
      alertMessage = '';
    }
  }

  // Reactive effect to check alerts when thresholds or readings change
  $effect(() => {
    if (currentReading) {
      checkAlertConditions(currentReading);
    }
  });

  onMount(async () => {
    // Initial data fetch
    const response = await fetch('http://localhost:8000/current');
    currentReading = await response.json();
    historicalData = [currentReading];

    // Set up SSE connection
    eventSource = new EventSource('http://localhost:8000/stream');
    eventSource.addEventListener('sensor_update', (event) => {
      currentReading = JSON.parse(event.data);
      historicalData = [...historicalData, currentReading].slice(-30); // Keep last 30 readings
      if (currentReading) {
        checkAlertConditions(currentReading);
      }
    });

    return () => {
      if (eventSource) eventSource.close();
    };
  });

Sensor Dashboard

{#if currentReading}

Temperature History

Current Humidity

System Status

{currentReading.status}

Last updated: {new Date(currentReading.timestamp).toLocaleTimeString()}

{/if}
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .dashboard-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-top: 2rem; } .card { background: #fff; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .span-2 { grid-column: span 2; } .status-container { display: flex; flex-direction: column; align-items: center; gap: 1rem; } .status-indicator { width: 80px; height: 80px; border-radius: 50%; margin: 1rem 0; } .status-indicator.normal { background-color: #2ecc71; } .status-indicator.warning { background-color: #f1c40f; } .status-indicator.critical { background-color: #e74c3c; } .status-text { text-transform: uppercase; font-weight: bold; } .timestamp { color: #666; font-size: 0.9rem; } @media (max-width: 768px) { .dashboard-grid { grid-template-columns: 1fr; } .span-2 { grid-column: auto; } } tempThreshold = value} onHumidityChange={(value) => humidityThreshold = value} />

最终的仪表板结合了所有组件:温度历史图表、湿度计、系统状态指示器、警报横幅和设置面板。

您的实时控制面板现在包含图表、警报和可配置设置!最终结果应如下所示:

仪表板最终版

结论

在本教程中,我们使用 FastAPI 和 Svelte 构建了一个实时仪表板。我们介绍了:

  • 使用 SSE 设置 FastAPI 后端以进行实时数据流
  • 使用交互式图表创建响应式 Svelte 前端
  • 实现实时数据更新和历史数据跟踪
  • 添加用于监控阈值冲突的警报系统
  • 创建可配置的设置面板

此控制面板可以作为更复杂的监控应用程序的基础。一些可能的增强功能可能包括:

  • 添加身份验证
  • 在数据库中保存历史数据
  • 添加更多可视化类型
  • 实现双向实时更新的 websocket 通信
  • 为历史数据添加导出功能

此项目的完整源代码可在 GitHub 上找到。

特色课程

使用 FastAPI 和 Docker 进行测试驱动开发

在本课程中,您将学习如何使用 Python、FastAPI 和 Docker 构建、测试和部署文本摘要服务。该服务本身将通过 RESTful API 公开,并通过 Docker 部署到 Heroku。

查看课程

特色课程

使用 FastAPI 和 Docker 进行测试驱动开发

在本课程中,您将学习如何使用 Python、FastAPI 和 Docker 构建、测试和部署文本摘要服务。该服务本身将通过 RESTful API 公开,并通过 Docker 部署到 Heroku。

查看课程

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇