- Published on
FastAPI+PyWebView+前端打造Python应用界面
- Authors
- Name
- Kto
告别传统GUI:用FastAPI + PyWebView + 现代前端技术打造Python应用界面
引言
在Python应用程序开发中,GUI(图形用户界面)的实现一直是一个痛点。传统的GUI库如 PySide6、Tkinter、wxPython 等虽然功能强大,但开发复杂、样式定制困难,且难以适应现代前端技术的快速发展。此外,像 Kivy 这样的库虽然支持跨平台和丰富的交互效果,但其学习曲线较陡,且对现代Web技术的支持有限。而 Dear PyGui 和 PySimpleGUI 等新兴库虽然简化了开发流程,但在复杂应用场景中仍存在一定的局限性。
为了解决这一问题,我们提出了一种全新的解决方案:使用 FastAPI 作为后端服务,结合 PyWebView 和现代前端技术(如 React、Vue.js 或 Tailwind CSS),构建现代化的 Python 应用程序 GUI。这种架构不仅能够充分利用现代前端技术的灵活性和强大功能,还能通过 FastAPI 提供高效的后端支持,实现前后端分离的开发模式,从而显著提升开发效率和用户体验。
这种方案的核心思想是:
- 后端使用FastAPI:提供高性能的API和WebSocket支持。
- 前端使用现代前端技术:无论是React、Vue、Angular等前端框架,还是Tailwind CSS、Bootstrap等CSS框架,甚至是Three.js、D3.js等可视化库,都可以无缝集成。
- 通过WebView嵌入前端页面:将前端页面直接嵌入到桌面应用中,实现无缝的GUI体验。
为了帮助开发者更好地理解和实践这一方案,我创建了一个示例项目:fastapi-blog-tutorial。这个项目展示了如何使用FastAPI、WebView和现代前端技术构建一个完整的Python应用程序,涵盖了从后端API设计到前端页面开发的完整流程。如果你访问不了github,可以使用gitee链接 fastapi-blog-tutorial
接下来的教学将基于此项目,逐步讲解如何实现以下功能:
- 使用FastAPI构建RESTful API和WebSocket服务。
- 使用WebView将前端页面嵌入到Python应用中。
- 使用现代前端技术(如HTML、CSS、JavaScript)构建美观的用户界面。
- 实现前后端的实时通信和数据交互。
通过这个项目,你将掌握如何利用FastAPI和现代前端技术,快速构建现代化、高性能的Python应用程序。无论你是Python开发者,还是前端开发者,都可以从中获得启发和实用的技巧。
架构畅想
1. 架构图
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| FastAPI 后端 |<----->| WebView 前端 |<----->| 现代前端技术 |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
2. 组件说明
FastAPI 后端:
- 提供RESTful API和WebSocket支持。
- 处理业务逻辑、数据存储和通信。
- 通过
uvicorn
运行,支持高并发和实时通信。
WebView 前端:
- 使用
pywebview
库将前端页面嵌入到桌面应用中。 - 提供原生的窗口管理功能(如最小化、最大化、关闭)。
- 支持与后端的双向通信(通过HTTP API或WebSocket)。
- 使用
现代前端技术:
- 使用React、Vue、Angular等前端框架构建用户界面。
- 利用Tailwind CSS、Bootstrap等CSS框架实现快速样式开发。
- 集成Three.js、D3.js等可视化库,实现复杂的数据可视化。
- 通过WebSocket与后端实时交互,实现动态数据更新。
优缺点分析
优点
开发效率高:
- 前端使用现代前端技术,开发速度快,样式定制灵活。
- 后端使用FastAPI,代码简洁,易于维护。
跨平台支持:
- WebView和现代前端技术天然支持跨平台,可以在Windows、macOS和Linux上运行。
现代化UI:
- 利用现代前端技术(如Flexbox、CSS Grid、动画效果)实现美观的用户界面。
实时通信:
- 通过WebSocket实现前后端的实时通信,适合需要实时更新的应用场景。
可扩展性强:
- 前后端分离,便于团队协作和功能扩展。
缺点
性能开销:
- WebView和前端渲染需要一定的系统资源,可能不适合对性能要求极高的场景。
依赖浏览器引擎:
- WebView依赖于系统或内置的浏览器引擎,可能存在兼容性问题。
打包体积较大:
- 由于需要嵌入浏览器引擎,打包后的应用程序体积较大。
学习曲线:
- 需要掌握现代前端技术,对纯Python开发者可能有一定学习成本。
适用场景
- 桌面应用:需要现代化UI的Python桌面应用程序。
- 实时数据展示:如监控系统、实时数据仪表盘。
- 跨平台工具:需要在多个操作系统上运行的工具类应用。
- 快速原型开发:需要快速构建和迭代的应用场景。
通过这种方案,开发者可以摆脱传统GUI库的束缚,充分利用现代前端技术的优势,快速构建现代化、高性能的Python应用程序。如果你正在为Python GUI开发而烦恼,不妨试试这种全新的解决方案!
环境准备
在开始之前,确保你已经安装了以下Python库:
fastapi
uvicorn
loguru
jinja2
你可以通过以下命令安装这些依赖:
pip install fastapi uvicorn loguru jinja2
项目结构
以下是项目的目录结构:
.
├── fastapi-blog-tutorial/ # 应用代码目录
│ ├── main.py # 主入口文件
│ ├── routers/ # 路由定义
│ ├── templates/ # 模板文件
│ └── models/ # 数据模型(如果使用数据库)
├── tests/ # 测试代码目录
├── requirements.txt # 项目依赖文件
└── README.md # 项目介绍文件
main.py
:主程序入口,包含FastAPI应用实例。app.py
:路由发现及注册器。routers/
:存放所有路由模块的目录。templates/
:存放Jinja2模板文件的目录。static/
:存放静态文件的目录。
代码解析
1. WebSocket命令处理
我们实现了一个WebSocket服务器,支持多种命令处理。以下是核心代码:
1.1 导入必要的库
# 导入必要的库和模块
import datetime
import json
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from loguru import logger
# 创建一个API路由实例,用于定义WebSocket端点
router = APIRouter()
# 定义一个字典来存储命令及其对应的处理函数
command_handlers = {}
1.2 注册命令处理器
我们定义了一个装饰器register_command
,用于注册命令处理器:
def register_command(command):
"""
注册命令处理器的装饰器函数。
参数:
command (str): 要注册的命令名称。
返回:
decorator (function): 实际的装饰器函数,用于包装命令处理函数。
"""
def decorator(func):
# 记录正在注册的命令及其处理函数名
logger.debug(f"正在注册命令 '{command}',处理函数为 '{func.__name__}'")
command_handlers[command] = func
# 记录当前所有已注册的命令处理器
logger.info(
f"当前注册的命令处理器: {', '.join(f'{cmd}: {handler.__name__}' for cmd, handler in command_handlers.items())}")
return func # 返回原始函数,不改变其行为
return decorator
1.3 定义命令处理器
我们定义了三个示例命令处理器:
@register_command('echo')
async def echo_handler(message, websocket):
"""
处理 'echo' 命令的异步函数。
参数:
message (str): 收到的消息内容。
websocket (WebSocket): WebSocket连接对象。
"""
logger.debug(f"echo_handler 被调用,消息为: {message}")
try:
# 向客户端发送回显消息
await websocket.send_text(f"回显: {message}")
except Exception as e:
# 捕获并记录任何异常
logger.error(f"echo_handler 中发生错误: {e}")
@register_command('custom')
async def custom_handler(message, websocket):
"""
处理 'custom' 命令的异步函数。
参数:
message (str): 收到的消息内容。
websocket (WebSocket): WebSocket连接对象。
"""
logger.debug(f"custom_handler 被调用,消息为: {message}")
try:
# 向客户端发送自定义回显消息
await websocket.send_text(f"自定义回显: {message}")
except Exception as e:
# 捕获并记录任何异常
logger.error(f"custom_handler 中发生错误: {e}")
@register_command('time')
async def time_handler(message, websocket):
"""
处理 'time' 命令的异步函数。
参数:
message (str): 收到的消息内容(本例中未使用)。
websocket (WebSocket): WebSocket连接对象。
"""
logger.debug(f"time_handler 被调用,消息为: {message}")
try:
# 获取当前时间并格式化为ISO8601字符串
now = datetime.datetime.now().isoformat()
# 向客户端发送服务器时间
await websocket.send_text(f"服务器时间是 {now}")
except Exception as e:
# 捕获并记录任何异常
logger.error(f"time_handler 中发生错误: {e}")
1.4 定义WebSocket路由
我们定义了一个WebSocket路由,处理客户端连接和消息:
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
WebSocket端点的主处理函数。
参数:
websocket (WebSocket): WebSocket连接对象。
"""
await websocket.accept() # 接受WebSocket连接
logger.info("正在处理连接")
try:
while True:
# 接收来自客户端的消息
data = await websocket.receive_text()
# 尝试将接收到的数据解析为JSON格式
try:
request = json.loads(data)
except json.JSONDecodeError:
logger.error(f"无法解码JSON消息: {data}")
await websocket.send_text("无效的JSON格式")
continue
# 提取命令和数据
command = request.get('command')
data = request.get('data', '')
# 检查命令是否合法
if command not in command_handlers:
logger.warning(f"非法命令: {command}")
await websocket.send_text("非法命令")
continue
# 记录解析出的命令和数据
logger.debug(f"解析命令: {command}, 数据: {data}")
# 查找并调用相应的命令处理函数
if command in command_handlers:
logger.debug(f"找到命令 '{command}',调用处理函数 '{command_handlers[command].__name__}'")
await command_handlers[command](data, websocket)
else:
logger.warning(f"未知命令: {command}")
await websocket.send_text(f"未知命令: {command}")
except WebSocketDisconnect:
# 处理客户端断开连接的情况
logger.info("客户端已断开连接")
except Exception as e:
# 捕获并记录其他任何异常
logger.error(f"处理消息时发生错误: {data}, 错误: {e}")
2. 模板渲染
我们使用Jinja2模板引擎渲染动态HTML页面。以下是核心代码:
2.1 导入必要的库
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
# 创建一个APIRouter实例,用于定义API的路由
router = APIRouter()
# 初始化Jinja2Templates实例,指定模板文件的目录
templates = Jinja2Templates(directory='templates')
2.2 定义HTTP路由
我们定义了三个HTTP路由,用于渲染HTML页面:
# 定义根路径的GET请求处理函数
# 返回index.html模板,同时传入一个空的request对象
@router.get("/")
async def get():
# 使用TemplateResponse方法渲染index.html模板,并传递一个空的request对象
return templates.TemplateResponse("index.html", {"request": {}})
# 定义/test路径的GET请求处理函数
# 在控制台打印"test",并返回"test"作为HTTP响应
@router.get("/test")
async def test():
# 在控制台中打印 "test"
print("test")
# 返回 "test" 作为响应
return "test"
# 定义/info路径的GET请求处理函数
# 接收Request对象作为参数,用于获取请求相关数据
@router.get("/info")
async def info(request: Request):
# 下面是用于演示的变量,包含不同类型的参数
custom_param = "这是一个自定义参数" # 自定义字符串参数
number_param = 42 # 整数参数
list_param = ["item1", "item2", "item3"] # 列表参数
dict_param = {"key1": "value1", "key2": "value2"} # 字典参数
# 构造静态文件的URL,这里假设你有一个 style.css 文件在 static 目录下
static_file_url = request.url_for('static', path='style.css')
# 返回info.html模板,传入request对象和多个参数
return templates.TemplateResponse("info.html", {
"request": request, # 传递request对象,用于模板渲染
"custom_param": custom_param, # 传递自定义字符串参数
"number_param": number_param, # 传递整数参数
"list_param": list_param, # 传递列表参数
"dict_param": dict_param, # 传递字典参数
"static_file_url": static_file_url # 传递静态文件的URL
})
3. APP路由注册中转
在app.py
中,我们使用自动注册函数发现并注册所有路由地址,并挂载静态资源:
# 导入必要的模块
from importlib import import_module # 动态导入模块
from pathlib import Path # 操作文件路径
from pkgutil import iter_modules # 遍历包中的模块
from fastapi import FastAPI, APIRouter # 创建FastAPI应用和API路由
from fastapi.staticfiles import StaticFiles # 提供静态文件服务
from loguru import logger # 记录日志
# 创建FastAPI应用实例
app = FastAPI()
# 挂载静态文件目录,使得可以通过/static/访问静态资源
app.mount("/static", StaticFiles(directory="static"), name="static")
# 定义一个字典用于缓存已导入的模块,避免重复导入
_imported_modules = {}
def register_routers(package_name='routers'):
"""
自动注册指定包下的所有API路由。
参数:
package_name (str): 包含API路由的包名,默认为'routers'
"""
# 获取当前文件所在目录,并拼接上包名得到包的实际路径
package_dir = Path(__file__).resolve().parent / package_name
# 记录正在注册路由的日志信息
logger.info(f"正在注册路由,包目录: {package_dir}")
try:
# 遍历包中的所有模块
for (_, module_name, _) in iter_modules([str(package_dir)]):
# 如果模块已经导入过,则直接使用缓存中的模块
if module_name in _imported_modules:
module = _imported_modules[module_name]
else:
# 否则动态导入模块并缓存
module = import_module(f"{package_name}.{module_name}")
_imported_modules[module_name] = module
# 记录成功导入模块的日志信息
logger.debug(f"导入模块: {module_name}")
# 尝试从模块中获取名为'router'的对象
router = getattr(module, 'router', None)
# 如果获取到的对象是APIRouter实例,则将其注册到FastAPI应用中
if isinstance(router, APIRouter):
app.include_router(router)
logger.debug(f"已注册路由: {module_name}")
else:
# 如果未找到有效的APIRouter实例,记录警告日志
logger.warning(f"模块 {module_name} 没有找到有效的 APIRouter 实例")
except Exception as e:
# 如果发生任何异常,记录错误日志
logger.error(f"注册路由时发生错误: {e}")
# 调用函数注册所有路由
register_routers()
4. 主程序入口
在main.py
中,我们整合了所有路由并启动FastAPI应用:
# 导入必要的库和模块
import os # 提供与操作系统交互的功能,如文件路径操作
import threading # 允许程序并发运行多个线程,用于同时启动服务器和webview窗口
import time # 提供时间处理函数,如休眠
import argparse # 解析命令行参数
import uvicorn # 用于运行FastAPI应用的ASGI服务器实现
import webview # 创建原生桌面GUI窗口,用于显示HTML页面
from loguru import logger # 强大的日志记录库,提供简洁的日志输出
from app import app # 导入FastAPI应用实例
class WebSocketServer:
def __init__(self, **kwargs):
"""
初始化WebSocketServer类的实例。
参数:
kwargs (dict): 包含配置项的字典,支持以下键值:
- host (str): 服务器监听地址,默认为 '0.0.0.0'
- port (int): 服务器监听端口,默认为 8000
- index_path (str): 网页入口文件路径,默认为 "templates/index.html"
"""
self.host = kwargs.get('host', '0.0.0.0') # 设置服务器监听地址
self.port = kwargs.get('port', 8000) # 设置服务器监听端口
self.index_path = kwargs.get('index_path', os.path.join("templates", "index.html")) # 设置网页入口文件路径
self.app = app # 绑定FastAPI应用实例
def start_webview(self):
"""
启动webview窗口,加载指定的HTML文件。
"""
webview.create_window('Web Socket Example', str(self.index_path), width=1000, height=800, resizable=True)
# 创建一个名为'Web Socket Example'的窗口,加载index_path指定的HTML文件,
# 设置窗口宽度为1000px,高度为800px,并允许调整大小
webview.start(self.start_server, gui='cef') # 启动webview事件循环,保持窗口打开,并在启动时调用start_server方法
def start_server(self):
"""
启动FastAPI服务器。
"""
uvicorn.run(self.app, host=self.host, port=self.port)
# 使用uvicorn运行绑定的FastAPI应用实例,
# 监听指定的host和port
if __name__ == "__main__":
# 创建参数解析器
parser = argparse.ArgumentParser(description='启动WebSocket服务器和Webview窗口')
parser.add_argument('--server', action='store_true', help='仅启动服务器')
parser.add_argument('--host', default='0.0.0.0', help='服务器监听地址')
parser.add_argument('--port', type=int, default=8000, help='服务器监听端口')
parser.add_argument('--index-path', default=os.path.join("templates", "index.html"), help='网页入口文件路径')
# 解析命令行参数
args = parser.parse_args()
# 检查 index 文件是否存在
if not os.path.exists(args.index_path):
logger.error(f"文件 {args.index_path} 不存在,无法启动服务器。")
else:
logger.info(
f"服务器已启动,地址为 http://localhost:{args.port} 和 ws://localhost:{args.port}/ws")
# 实例化WebSocketServer类
server = WebSocketServer(host=args.host, port=args.port, index_path=args.index_path)
if args.server:
# 仅启动服务器
server.start_server()
else:
# 启动webview窗口
server.start_webview()
网页部分
1. 前端页面设计
我们使用HTML、CSS和JavaScript实现了一个美观的WebSocket客户端页面。以下是核心代码:
1.1 HTML结构
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket 客户端 v1.1</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<h1>WebSocket 客户端 v1.1</h1>
<div id="messages"></div>
<div class="button-container">
<button class="button" onclick="sendMessage('echo', 'Hello Server!')">
💬 发送 Echo 命令
</button>
<button class="button" onclick="sendMessage('time', 'Get Time')">🕒 发送 Time 命令</button>
<button class="button" onclick="clearMessages()">🗑️ 清除消息</button>
<button class="button" onclick="reconnect()">🔄 重新连接</button>
<button class="button" onclick="openSettings()">⚙️ 设置</button>
</div>
<div class="input-container">
<input type="text" id="messageInput" placeholder="输入消息..." />
<button
class="button"
onclick="sendMessage('custom', document.getElementById('messageInput').value.trim())"
>
📤 发送
</button>
</div>
<div id="connectionStatus" class="connection-status">
<div class="status-icon"></div>
<span>连接状态:未连接</span>
</div>
<div id="toast" class="toast"></div>
<div class="overlay" id="overlay"></div>
<div class="settings-panel" id="settingsPanel">
<h2>设置</h2>
<label for="serverPort">端口号</label>
<input type="text" id="serverPort" placeholder="例如: 8000" />
<label for="serverPath">路径</label>
<input type="text" id="serverPath" placeholder="例如: /ws" />
<button onclick="saveSettings()">保存</button>
</div>
<script src="/static/script.js"></script>
</body>
</html>
1.2 CSS样式
:root {
--primary-color: #6c7a89;
--secondary-color: #5c6a79;
--background-gradient: linear-gradient(135deg, #f5f7fa, #c3cfe2);
--box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
--text-color: #2c3e50;
--white: rgba(255, 255, 255, 0.95);
--success-color: #27ae60;
--error-color: #e74c3c;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 20px;
background: var(--background-gradient);
color: var(--text-color);
height: 100vh;
overflow: hidden;
}
h1 {
margin-bottom: 20px;
font-size: clamp(2rem, 5vw, 2.5rem);
color: #34495e;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
animation: fadeIn 0.8s ease-in-out;
}
.button-container {
display: flex;
justify-content: center;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
animation: fadeIn 0.8s ease-in-out;
}
.button {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: #fff;
border: none;
padding: 12px 24px;
cursor: pointer;
border-radius: 30px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
transition: var(--transition);
box-shadow: var(--box-shadow);
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
opacity: 0.9;
}
#messages {
margin: 20px 0;
padding: 20px;
width: 90%;
max-width: 800px;
height: 300px;
overflow-y: scroll;
background: var(--white);
border-radius: 15px;
box-shadow: var(--box-shadow);
backdrop-filter: blur(10px);
}
.message {
padding: 12px;
border-radius: 15px;
margin-bottom: 15px;
max-width: 80%;
word-wrap: break-word;
background: var(--white);
box-shadow: var(--box-shadow);
display: flex;
justify-content: space-between;
align-items: center;
opacity: 0;
animation: fadeIn 0.5s forwards ease-in-out;
transition: var(--transition);
will-change: transform, opacity;
}
.message-client {
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
margin-left: auto;
border-radius: 15px 15px 0 15px;
}
.message-server {
background: linear-gradient(135deg, #f0f4f8, #d9e2ec);
margin-right: auto;
border-radius: 15px 15px 15px 0;
color: #2d3748;
}
.input-container {
display: flex;
justify-content: center;
align-items: center;
width: 90%;
max-width: 800px;
margin-top: 20px;
gap: 10px;
}
input[type='text'] {
flex: 1;
padding: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 30px;
outline: none;
background: var(--white);
font-size: 14px;
color: inherit;
transition: var(--transition);
}
input[type='text']:focus {
border-color: var(--primary-color);
box-shadow: 0 0 12px rgba(108, 122, 137, 0.3);
background: rgba(255, 255, 255, 1);
}
.connection-status {
margin-top: 10px;
font-size: 14px;
color: inherit;
display: flex;
animation: fadeIn 0.8s ease-in-out;
align-items: center;
gap: 8px;
}
.connection-status.connected {
color: var(--success-color);
}
.connection-status.disconnected {
color: var(--error-color);
}
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--primary-color);
color: #fff;
padding: 16px 32px;
border-radius: 50px;
box-shadow: var(--box-shadow);
opacity: 0;
transform: translate(-50%, 20px);
transition: var(--transition);
font-size: 16px;
z-index: 1000;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
.settings-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--white);
padding: 30px;
border-radius: 15px;
box-shadow: var(--box-shadow);
backdrop-filter: blur(10px);
z-index: 1001;
display: none;
width: 90%;
max-width: 400px;
text-align: center;
}
.settings-panel.show {
display: block;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: none;
}
.overlay.show {
display: block;
}
1.3 JavaScript逻辑
let ws
const connectionStatus = document.getElementById('connectionStatus')
const toast = document.getElementById('toast')
const overlay = document.getElementById('overlay')
const settingsPanel = document.getElementById('settingsPanel')
const serverPortInput = document.getElementById('serverPort')
const serverPathInput = document.getElementById('serverPath')
function showToast(message, duration = 2000) {
toast.textContent = message
toast.classList.add('show')
setTimeout(() => toast.classList.remove('show'), duration)
}
function openSettings() {
overlay.classList.add('show')
settingsPanel.classList.add('show')
const savedPort = localStorage.getItem('serverPort') || '8000'
const savedPath = localStorage.getItem('serverPath') || '/ws'
serverPortInput.value = savedPort
serverPathInput.value = savedPath
}
function saveSettings() {
const port = serverPortInput.value.trim()
const path = serverPathInput.value.trim()
localStorage.setItem('serverPort', port)
localStorage.setItem('serverPath', path)
overlay.classList.remove('show')
settingsPanel.classList.remove('show')
reconnect()
}
function initWebSocket() {
const port = localStorage.getItem('serverPort') || '8000'
const path = localStorage.getItem('serverPath') || '/ws'
ws = new WebSocket(`ws://localhost:${port}${path}`)
ws.onopen = () => {
addMessage('已连接到服务器', 'server')
connectionStatus.innerHTML = `<div class="status-icon"></div><span>连接状态:已连接</span>`
connectionStatus.classList.add('connected')
connectionStatus.classList.remove('disconnected')
showToast('已连接到服务器')
}
ws.onmessage = (e) => addMessage(`服务器消息: ${e.data}`, 'server')
ws.onclose = () => {
addMessage('已断开与服务器的连接', 'server')
connectionStatus.innerHTML = `<div class="status-icon"></div><span>连接状态:已断开</span>`
connectionStatus.classList.add('disconnected')
connectionStatus.classList.remove('connected')
showToast('已断开与服务器的连接')
}
ws.onerror = (e) => {
addMessage(`WebSocket 错误: ${e.message}`, 'server')
connectionStatus.innerHTML = `<div class="status-icon"></div><span>连接状态:错误</span>`
connectionStatus.classList.add('disconnected')
showToast('WebSocket 错误: ' + e.message)
}
}
function sendMessage(command, data) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command, data }))
addMessage(`客户端消息: ${data}`, 'client')
document.getElementById('messageInput').value = ''
showToast('消息已发送')
} else {
addMessage('WebSocket 连接未打开,请重新连接', 'server')
showToast('WebSocket 连接未打开,请重新连接')
}
}
function clearMessages() {
document.getElementById('messages').innerHTML = ''
showToast('消息已清除')
}
function reconnect() {
clearMessages()
connectionStatus.innerHTML = `<div class="loading"></div><span>连接状态:重新连接中...</span>`
initWebSocket()
showToast('正在重新连接...')
}
function addMessage(message, type) {
const messagesDiv = document.getElementById('messages')
const messageDiv = document.createElement('div')
messageDiv.classList.add('message', `message-${type}`)
messageDiv.innerHTML = `<p>${message}</p><time datetime="${new Date().toISOString()}">${new Date().toLocaleTimeString()}</time>`
messagesDiv.appendChild(messageDiv)
messagesDiv.scrollTop = messagesDiv.scrollHeight
}
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage('custom', document.getElementById('messageInput').value.trim())
})
initWebSocket()
程序展示
1.运行项目
- 克隆我的示例项目:
git clone https://github.com/Ktovoz/fastapi-blog-tutorial.git
cd fastapi-blog-tutorial
2. 安装依赖
确保你已经安装了所有必要的依赖项,可以通过运行以下命令来完成:
pip install -r requirements.txt
3. 启动应用
3.1 启动 FastAPI 服务器
在终端中运行以下命令来启动 FastAPI 服务器:
python main.py --server
3.2 启动桌面应用
在终端中运行以下命令来启动 PyWebview 桌面应用:
python main.py
3.3 参数说明
你可以通过以下参数来配置应用的启动行为:
--server
: 仅启动 FastAPI 服务器,不启动 PyWebview 桌面应用。--host <地址>
: 指定 FastAPI 服务器监听的地址,默认为0.0.0.0
。--port <端口>
: 指定 FastAPI 服务器监听的端口,默认为8000
。--index-path <路径>
: 指定网页入口文件的路径,默认为templates/index.html
。
例如,如果你想启动服务器并指定监听地址和端口,可以运行:
python app/main.py --server --host 127.0.0.1 --port 8080
如果你想启动桌面应用并指定网页入口文件路径,可以运行:
python app/main.py --index-path templates/custom_index.html
4. 访问主页
打开浏览器,访问 http://127.0.0.1:8000,你应该能够看到主页。
总结
通过本文,你已经学会了如何使用FastAPI构建一个支持WebSocket和模板渲染的完整Web应用。这种方法非常适合需要实时通信和动态页面生成的项目。
如果你有任何问题或建议,欢迎在评论区留言,或者直接在我的GitHub项目 fastapi-blog-tutorial 或者gitee项目 fastapi-blog-tutorial中提交Issue。
参考文档:
版权声明:本文为博主的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
希望这篇博文对你的项目有所帮助!如果有其他需求,欢迎随时提出!