告别传统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布局
[code]<!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!')"> |