在本教程中,您将学习如何使用 FastAPI 和 Svelte 构建实时分析仪表板。我们将使用服务器发送事件 (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
目标
在本教程结束时,您应该能够:
- 设置具有实时数据流功能的 FastAPI 后端
- 使用 SvelteKit 创建现代 Svelte 应用程序
- 实施服务器发送事件 (SSE) 以实现实时数据更新
- 使用 Svelte 组件构建交互式图表和图形
- 在前端高效处理实时数据更新
我们在构建什么?
我们将创建一个分析仪表板,实时显示模拟传感器数据。该仪表板将包括:
- 显示温度趋势的折线图
- 显示当前湿度水平的仪表图
- 实时状态指示器
- 历史数据视图
这是一个实际示例,可以适用于任何需要实时数据可视化的应用程序。
项目设置
让我们从创建项目结构开始,打开一个终端并运行以下命令:
$ 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 时访问,显示实时传感器数据更新!
这段代码中发生了什么变化?
- 组件结构:仪表板组件遵循典型的 Svelte 结构,包含三个主要部分:
- 脚本 (logic)
- 模板 (HTML)
- 样式 (CSS)
- 数据管理
- 使用 TypeScript 实现类型安全
- 维护两个关键状态:
currentReading
:存储最新的传感器数据eventSource
:管理实时连接
- 实时数据流:
- 初始加载:在元件安装时获取电流传感器数据
- 实时更新:建立 SSE 连接以进行实时更新
- 清理:销毁组件时正确关闭连接
温度、湿度和状态是从后端从 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: 180
rotation: 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}
{/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}
{/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;
}
{message}
该组件使用 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
- 组件接收当前值作为 props (,
tempThreshold
humidityThreshold
) - 它还接收回调函数 (, ) 以通知父级更改
onTempChange
onHumidityChange
- 它使用表单 inputs 维护自己的本地状态
$state
- 当用户点击 “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}
{/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 上找到。