ASP.NET Core SignalR的基本使用

news2025/7/25 8:53:48

文章目录

  • 前言
  • 一、SignalR
  • 是什么?
    • 在 ASP.NET Core 中的关键特性:
    • SignalR 工作原理简图:
  • 二、使用步骤
    • 1.创建ASP.NET Core web Api 项目
    • 2.添加 SignalR 包
    • 3.创建 SignalR Hub
    • 4.配置服务与中间件
    • 5.创建控制器(模拟服务器向客户端发送消息)
    • 6.创建Vue前端项目(模拟客户端发送消息)
    • 7.运行使用
  • 三、关键配置说明
  • 四、故障排查
  • 总结


前言

在 ASP.NET Core 中, SignalR 是用于实现实时、双向通信的技术。

一、SignalR

是什么?

一个由微软开发的高级库,构建在 ASP.NET Core 之上,用于简化向应用添加实时 Web 功能。

  • 核心目标: 让开发者能够轻松实现服务器到客户端的实时推送(例如:聊天、通知、仪表盘更新、协作编辑)。

  • 抽象层: 它在底层自动选择并使用最佳的传输协议来建立实时连接。首选是 WebSocket,但如果 WebSocket 不可用(例如旧浏览器、某些网络限制),它会自动优雅降级到其他技术,如 Server-Sent Events (SSE)Long Polling。开发者无需关心底层使用的是哪种传输方式。

  • 基于 Hub 的模型SignalR 的核心抽象是 HubHub 是一个高级管道,允许客户端和服务器直接相互调用方法(RPC 风格)。

在 ASP.NET Core 中的关键特性:

  • 自动传输协商与回退: 无缝处理连接建立和传输选择。
  • 连接管理: 内置管理连接的生命周期、连接组(Groups)和用户(Users),方便实现广播(所有客户端)、组播(特定组)、单播(特定客户端或用户)。
  • 自动重新连接: 提供客户端 API 在连接意外断开时尝试自动重新连接。
  • 简单的编程模型 (RPC)
    • 服务器端: 定义继承自 Hub 的类,并在其中声明客户端可以调用的 public 方法。
    • 客户端: 提供多种语言的客户端库(JavaScript, .NET, Java 等),调用服务器 Hub 上的方法,并注册处理程序来响应服务器调用的方法。
  • 可扩展性: 支持通过 SignalR Backplane(如 Azure SignalR Service, Redis)将消息分发到多个服务器实例,实现横向扩展。
  • 与 ASP.NET Core 集成: 深度集成身份认证(如 [Authorize] 特性)、依赖注入等。

SignalR 工作原理简图:

```csharp
[Client]  <---(首选 WebSocket, 次选 SSE/Long Polling)--->  [ASP.NET Core Server]
    |                                                         |
    |----(调用) ServerMethod(args) ------------------->| (Hub 方法)
    |<---(调用) ClientMethod(args) --------------------| (Clients.Caller, Clients.All, etc.)
```

二、使用步骤

1.创建ASP.NET Core web Api 项目

2.添加 SignalR 包

  1. 执行安装命令
    install-package Microsoft.AspNetCore.SignalR
    

3.创建 SignalR Hub

  1. MyHubService.cs

    using Microsoft.AspNetCore.SignalR;
    
    namespace SignalRDemo.HubService
    {
        public class MyHubService:Hub
        {
            public Task SendMessageAsync(string user,string content)
            {
                var connectionId=this.Context.ConnectionId;
                string msg = $"{connectionId},{DateTime.Now.ToString()}:{user}";
                return Clients.All.SendAsync("ReceivePubMsg", msg, content);
            }
        }
    }
    

4.配置服务与中间件

  1. Program.cs
    using SignalRDemo.HubService;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    // 添加 SignalR 服务
    builder.Services.AddSignalR();
    //跨域
    string[] urls = new[] { "http://localhost:5173" };
    builder.Services.AddCors(opt => 
            opt.AddDefaultPolicy(builder => builder.WithOrigins(urls)
            .AllowAnyMethod().AllowAnyHeader().AllowCredentials())
            );
            
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    app.UseCors();
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    // 配置路由
    app.MapHub<MyHubService>("/Hubs/MyHubService");// SignalR 终结点
    app.MapControllers();
    
    app.Run();
    
    

5.创建控制器(模拟服务器向客户端发送消息)

  1. TestController.cs
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.SignalR;
    using SignalRDemo.Entity;
    using SignalRDemo.HubService;
    
    namespace SignalRDemo.Controllers
    {
        [Route("api/[controller]/[action]")]
        [ApiController]
        public class TestController : ControllerBase
        {
            private readonly IHubContext<MyHubService> _hubContext;
    
            public TestController(IHubContext<MyHubService> hubContext)
            {
                _hubContext = hubContext;
            }
    
            [HttpPost("broadcast")]
            public async Task<IActionResult> BroadcastMessage([FromBody] MessageModel msg)
            {
                // 从服务端主动推送消息
                await _hubContext.Clients.All.SendAsync("ReceivePubMsg", msg.User, msg.Content);
                return Ok();
            }
        }
    }
    

6.创建Vue前端项目(模拟客户端发送消息)

  1. 打开文件夹(D:\Project\MyProject\SignalRProject\SignalRDemo)
  2. 创建文件夹:Front
  3. 当前文件夹下运行cmd
  4. 执行命令
    • npm create vite@latest SignalRClient1
    • 输入y,回车
    • 选择JavaScript
    • 等待项目创建完成
    • npm
    • npm run dev
  5. 进入前端项目文件夹D:\Project\MyProject\SignalRProject\SignalRDemo\Front\SignalClient1\src\components,编辑HelloWorld.vue文件。
  6. HelloWorld.vue
    <template>
        <div style="padding: 20px; max-width: 800px; margin: 0 auto;">
          <h2 style="color: #2c3e50;">SignalR 聊天室</h2>
          
          <div style="margin-bottom: 20px; display: flex; align-items: center;">
            <label style="margin-right: 10px; font-weight: bold; min-width: 80px;">用户:</label>
            <input 
              type="text" 
              v-model="state.userMsg" 
              @keydown.enter="sendMessage"
              placeholder="输入消息后按回车发送"
              :disabled="!state.isConnected || state.isConnecting"
              style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1;"
            />
     	<label style="margin-right: 10px; font-weight: bold; min-width: 80px;">消息内容:</label>
            <input 
              type="text" 
              v-model="state.contentMsg" 
              @keydown.enter="sendMessage"
              placeholder="输入消息后按回车发送"
              :disabled="!state.isConnected || state.isConnecting"
              style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1;"
            />
          </div>
          
          <div style="margin-bottom: 20px; background: #f8f9fa; padding: 15px; border-radius: 4px;">
            <div style="display: flex; margin-bottom: 10px;">
              <label style="margin-right: 10px; font-weight: bold; min-width: 80px;">服务器:</label>
              <input
                type="text"
                v-model="state.serverUrl"
                placeholder="输入 SignalR Hub URL"
                style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; flex: 1;"
              />
            </div>
            <button 
              @click="reconnect"
              style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;"
            >
              {{ state.isConnected ? '重新连接' : '连接' }}
            </button>
          </div>
          
          <div style="border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden; margin-bottom: 20px;">
            <div style="background: #f0f0f0; padding: 10px; font-weight: bold;">消息记录</div>
            <div style="max-height: 300px; overflow-y: auto; padding: 10px; background: white;">
              <div v-for="(msg, index) in state.messages" :key="index" style="padding: 8px 0; border-bottom: 1px solid #f5f5f5;">
               	  {{ msg }}
              </div>
               <div v-if="state.messages.length === 0" style="text-align: center; color: #999; padding: 20px;">
                暂无消息
              </div>
            </div>
          </div>
          
          <div :style="{
            padding: '12px',
            borderRadius: '4px',
            marginBottom: '15px',
            backgroundColor: state.connectionStatus.includes('失败') ? '#ffebee' : 
                            state.connectionStatus.includes('连接') ? '#e8f5e9' : '#e3f2fd',
            color: state.connectionStatus.includes('失败') ? '#b71c1c' : 
                   state.connectionStatus.includes('连接') ? '#1b5e20' : '#0d47a1',
            border: state.connectionStatus.includes('失败') ? '1px solid #ffcdd2' : 'none'
          }">
            <div style="font-weight: bold; margin-bottom: 5px;">连接状态:</div>
            <div>{{ state.connectionStatus }}</div>
            <div v-if="state.errorDetails" style="margin-top: 10px; font-size: 0.9em; color: #b71c1c;">
              <div style="font-weight: bold;">错误详情:</div>
              <div style="word-break: break-all;">{{ state.errorDetails }}</div>
            </div>
          </div>
        </div>
      </template>
      
      <script>
      import { reactive, onMounted, onUnmounted } from 'vue';
      import * as signalR from '@microsoft/signalr';
      
      export default {
        setup() {
          const state = reactive({
            userMsg: "",
            contentMsg:"",
            messages: [],
            connectionStatus: "正在初始化...",
            isConnected: false,
            isConnecting: false,
            serverUrl: "https://localhost:7183/Hubs/MyHubService",
            errorDetails: "",
            connection: null,
            retryCount: 0
          });
      
          const sendMessage = async () => {
            if (!state.userMsg.trim()) return;
            
            if (!state.isConnected || !state.connection) {
              state.connectionStatus = "连接尚未建立,无法发送消息";
              return;
            }
            
            try {
            // 尝试多种可能的服务端方法名
            const possibleMethods = [
              "SendMessage",          // 标准命名
              "SendMessageAsync",     // Async后缀命名
              "BroadcastMessage",     // 其他可能命名
              "SendToAll",            // 另一种常见命名
              "PublishMessage"        // 备用命名
            ];
            
            let lastError = null;
            
            // 依次尝试所有可能的方法名
            for (const method of possibleMethods) {
              try {
                await state.connection.invoke(method, state.userMsg,state.contentMsg);
                state.userMsg = "";
                state.contentMsg="";
                return; // 成功发送则退出
              } catch (error) {
                lastError = error;
                console.log(`尝试调用 ${method} 失败:`, error.message);
              }
            }
            
            // 所有方法都失败
            state.connectionStatus = `发送失败: 未找到服务端方法`;
            state.errorDetails = `尝试的方法: ${possibleMethods.join(", ")}\n错误: ${lastError.message}`;
            
            } catch (error) {
                state.connectionStatus = `发送失败: ${error.message}`;
                state.errorDetails = error.toString();
            }
          };
      
          const initSignalRConnection = async () => {
            state.isConnecting = true;
            state.connectionStatus = "正在连接...";
            state.errorDetails = "";
            
            try {
              // 清理现有连接
              if (state.connection) {
                await state.connection.stop();
                state.connection = null;
              }
              
              // 创建新连接
              state.connection = new signalR.HubConnectionBuilder()
                .withUrl(state.serverUrl, {
                  skipNegotiation: true, // 尝试跳过协商步骤
                  transport: signalR.HttpTransportType.WebSockets // 强制使用 WebSockets
                })
                .withAutomaticReconnect({
                  nextRetryDelayInMilliseconds: retryContext => {
                    state.retryCount = retryContext.previousRetryCount + 1;
                    return Math.min(1000 * Math.pow(2, state.retryCount), 30000);
                  }
                })
                .configureLogging(signalR.LogLevel.Debug) // 启用详细调试日志
                .build();
              
              // 消息接收处理
              state.connection.on('ReceiveMessage', rcvMsg => {
                state.messages.push(rcvMsg);
              });
              
              state.connection.on('ReceivePubMsg', (rcvMsg,rcvContent) => {
                state.messages.push(rcvMsg,rcvContent);
              });
              
              // 连接状态变化
              state.connection.onreconnecting(() => {
                state.isConnected = false;
                state.isConnecting = true;
                state.connectionStatus = "连接丢失,正在重连...";
              });
              
              state.connection.onreconnected(connectionId => {
                state.isConnected = true;
                state.isConnecting = false;
                state.retryCount = 0;
                state.connectionStatus = `已重新连接 (ID: ${connectionId})`;
              });
              
              state.connection.onclose(error => {
                state.isConnected = false;
                state.isConnecting = false;
                state.connectionStatus = error 
                  ? `连接关闭: ${error.message}` 
                  : "连接已关闭";
              });
              
              // 启动连接
              await state.connection.start();
              state.isConnected = true;
              state.isConnecting = false;
              state.retryCount = 0;
              state.connectionStatus = `已连接 (ID: ${state.connection.connectionId})`;
              
              console.log("SignalR 连接详情:", state.connection);
            } catch (error) {
              console.error("SignalR 连接失败:", error);
              state.isConnected = false;
              state.isConnecting = false;
              state.connectionStatus = `连接失败: ${error.message}`;
              state.errorDetails = error.toString();
              
              // 提供详细的错误诊断
              if (error.message.includes("Failed to fetch")) {
                state.errorDetails += "\n\n可能的原因:\n" +
                  "1. CORS 问题 - 确保服务器已启用 CORS\n" +
                  "2. URL 错误 - 检查服务器地址是否正确\n" +
                  "3. 证书问题 - 尝试访问服务器URL查看证书是否有效\n" +
                  "4. 服务器未运行 - 确保后端服务正在运行";
              }
            }
          };
      
          const reconnect = async () => {
            await initSignalRConnection();
          };
      
          onMounted(() => {
            initSignalRConnection();
          });
      
          onUnmounted(() => {
            if (state.connection) {
              state.connection.stop();
            }
          });
      
          return { state, sendMessage, reconnect };
        }
      }
      </script>
      
      <style>
      body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        background-color: #f5f7fa;
        margin: 0;
        padding: 20px;
        color: #333;
      }
      
      input {
        font-size: 1rem;
        transition: border-color 0.3s;
      }
      
      input:focus {
        outline: none;
        border-color: #3498db;
        box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
      }
      
      button {
        font-weight: 500;
        transition: background-color 0.3s;
      }
      
      button:hover {
        background-color: #2980b9 !important;
      }
      
      button:disabled {
        background-color: #bdc3c7 !important;
        cursor: not-allowed;
      }
      </style>
    

7.运行使用

  1. 客户端推送消息
    • 访问前端地址: http://localhost:5173/ (打开多个浏览器窗口测试消息广播)
    • 输入消息,回车
      在这里插入图片描述
      在这里插入图片描述
  2. 服务端推送(多个客户端都可接收到服务端推送的消息)
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

三、关键配置说明

  • 跨域支持 (CORS)
    若客户端在不同域,在 Program.cs 添加:
    .....
    //跨域
    string[] urls = new[] { "http://localhost:5173" };
    builder.Services.AddCors(opt => 
            opt.AddDefaultPolicy(builder => builder.WithOrigins(urls)
            .AllowAnyMethod().AllowAnyHeader().AllowCredentials())
            );
     ....
    
    app.UseCors();
    app.UseHttpsRedirection();
    ......
    
  • 配置路由
    在 Program.cs 添加:
    app.MapHub<MyHubService>("/Hubs/MyHubService");// SignalR 终结点
    app.MapControllers();
    

四、故障排查

  • 连接失败 404
    检查终结点路由是否匹配 app.MapHub(“/chatHub”)

  • 跨域问题
    确保启用 CORS 并正确配置

  • HTTPS 证书
    开发环境下信任本地证书或改用 HTTP


总结

SignalR: 是构建在 WebSocket (和其他传输) 之上的高级框架。它抽象了底层复杂性,提供了极其便利的编程模型(Hub)、内置的连接管理、自动传输回退和重连机制,极大地简化了在 ASP.NET Core 中开发各种实时功能的过程。它是大多数需要服务器主动推送消息的 ASP.NET Core 实时应用的首选。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2394722.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言】讲解 程序分配的区域(新手)

目录 代码区 数据区 堆区 栈区 常量区 重点比较一下堆区与 栈区 总结&#xff1a; 前言&#xff1a; C语言程序的内存分配区域是理解其运行机制的重要部分。根据提供的多条证据&#xff0c;我们可以总结出C语言程序在运行时主要涉及以下五个关键内存区域&#xff1a; 代…

LeetCode 算 法 实 战 - - - 移 除 链 表 元 素、反 转 链 表

LeetCode 算 法 实 战 - - - 移 除 链 表 元 素、反 转 链 表 第 一 题 - - - 移 除 链 表 元 素方 法 一 - - - 原 地 删 除方 法 二 - - - 双 指 针方 法 三 - - - 尾 插 第 二 题 - - - 反 转 链 表方 法 一 - - - 迭 代方 法 二 - - - 采 用 头 插 创 建 新 链 表 总 结 &a…

从头认识AI-----循环神经网络(RNN)

前言 前面我们讲了传统的神经网络&#xff0c;如MLP、CNN&#xff0c;这些网络中的输入都被单独处理&#xff0c;没有上下文之间的信息传递机制&#xff0c;这在处理序列数据&#xff08;如语音、文本、时间序列&#xff09;时很鸡肋&#xff1a; 如何理解一句话中“前后文”的…

kafka学习笔记(三、消费者Consumer使用教程——使用实例及及核心流程源码讲解)

1.核心概念与架构 1.1.消费者与消费者组 Kafka消费者是订阅主题&#xff08;Topic&#xff09;并拉取消息的客户端实例&#xff0c;其核心逻辑通过KafkaConsumer类实现。消费者组&#xff08;Consumer Group&#xff09;是由多个逻辑关联的消费者组成的集合。 核心规则 同一…

鸿蒙 Form Kit(卡片开发服务)

Form Kit&#xff08;卡片开发服务&#xff09; 鸿蒙应用中&#xff0c;Form / Card / Widget 都翻译为“卡片” Form Kit&#xff08;卡片开发服务&#xff09;提供一种界面展示形式&#xff0c;可以将应用的重要信息或操作前置到服务卡片&#xff0c;以达到服务直达、减少跳转…

算力卡上部署OCR文本识别服务与测试

使用modelscope上的图像文本行检测和文本识别模型进行本地部署并转为API服务。 本地部署时把代码中的检测和识别模型路径改为本地模型的路径。 关于模型和代码原理可以参见modelscope上这两个模型相关的页面&#xff1a; iic/cv_resnet18_ocr-detection-db-line-level_damo iic…

KWIC—Implicit Invocation

KWIC—Implicit Invocation ✏️ KWIC—Implicit Invocation 文章目录 KWIC—Implicit Invocation&#x1f4dd;KWIC—Implicit Invocation&#x1f9e9;KWIC&#x1f9e9;核心组件&#x1f9e9;ImplementationScheme⚖️ 隐式调用 vs 显式调用对比 &#x1f31f; 总结 &#x…

Visual Studio 2022 发布独立的 exe 文件

我们在用 Visual Studio 2022 写好一个 exe 程序之后&#xff0c;如果想把这个拿到其他地方运行&#xff0c;需要把 exe 所在的文件夹一起拿过去。 编译出来的 exe 文件需要其他几个文件一同放在同一目录才能运行&#xff0c;原因在于默认情况下&#xff0c;Visual Studio 是把…

11.4java语言执行浅析4

编译成字节码&#xff08;.class 文件&#xff09; 使用 javac 命令将源代码编译为 Java 字节码&#xff08;bytecode&#xff09; 它不是机器码&#xff0c;而是 JVM 能理解的中间语言&#xff08;字节码&#xff09;&#xff0c;具有平台无关性。 编译过程简要&#xff1…

python分配方案数 2023年信息素养大赛复赛/决赛真题 小学组/初中组 python编程挑战赛 真题详细解析

python分配方案数 2023全国青少年信息素养大赛Python编程挑战赛复赛真题解析 博主推荐 所有考级比赛学习相关资料合集【推荐收藏】1、Python比赛 信息素养大赛Python编程挑战赛 蓝桥杯python选拔赛真题详解

《信号与系统》第 5 章 离散时间傅里叶变换

5.0 引言 这一章将介绍并研究离散时间傅里叶变换&#xff0c;这样就完整地建立了傅里叶分析方法。 5.1 非周期信号的表示&#xff1a;离散时间傅里叶变换 5.1.1 离散时间傅里叶变换的导出 在第4章看到&#xff1a;一个连续时间周期方波的傅里叶级数可以看成一个包络函数的采…

动态IP与区块链:重构网络信任的底层革命

在数字经济蓬勃发展的今天&#xff0c;网络安全与数据隐私正面临前所未有的挑战。动态IP技术与区块链的深度融合&#xff0c;正在构建一个去中心化、高可信的网络基础设施&#xff0c;为Web3.0时代的到来奠定基础。 一、技术碰撞&#xff1a;动态IP与区块链的天然契合 动态I…

uniapp使用Canvas生成电子名片

uniapp使用Canvas生成电子名片 工作中有生成电子名片的一个需求&#xff0c;刚刚好弄了发一下分享分享 文章目录 uniapp使用Canvas生成电子名片前言一、上代码&#xff1f;总结 前言 先看效果 一、上代码&#xff1f; 不对不对应该是上才艺&#xff0c;哈哈哈 <template…

世冠科技亮相中汽中心科技周MBDE会议,共探汽车研发数字化转型新路径

近日&#xff0c;中汽中心2025年科技周MBDE前沿应用主题会议在天津成功举办。本次会议以“智汇津门共探MBDE前沿应用新征程”为主题&#xff0c;聚焦基于模型的数字工程&#xff08;MBDE&#xff09;方法论在汽车复杂系统研发中的创新实践与跨领域协同&#xff0c;旨在推动行业…

Linux笔记---线程

1. 线程的介绍 1.1 线程的概念 基本定义&#xff1a; 线程&#xff08;Thread&#xff09;是操作系统能够进行运算调度的最小单位。它被包含在进程&#xff08;Process&#xff09;之中&#xff08;或者说是进程的一部分、对进程的划分&#xff09;&#xff0c;是进程中的实际…

MCP架构深度解析:从基础原理到核心设计

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 持续学习&#xff0c;不断…

数据库暴露--Get型注入攻击

1.背景知识 1.1Post、Get的对比 特性GET 方法POST 方法HTTP 方法类型GETPOST数据位置URL 查询字符串(?key=value)请求体(Request Body)数据可见性明文显示在 URL 和浏览器历史中不可见(除非开发者工具查看)数据长度限制受 URL 长度限制(通常约 2048 字符)无明确限制(…

AI炼丹日志-26 - crawl4ai 专为 AI 打造的爬虫爬取库 上手指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇&#xff1a; MyBatis 更新完毕目前开始更新 Spring&#xff0c;一起深入浅出&#xff01; 大数据篇 300&#xff1a; Hadoop&…

ESP32-idf学习(四)esp32C3驱动lcd

一、前言 屏幕是人机交互的重要媒介&#xff0c;而且现在我们产品升级的趋势越来越高大尚&#xff0c;不少产品都会用lcd来做界面&#xff0c;而esp32c3在一些项目上是可以替代主mcu&#xff0c;所以驱动lcd也是必须学会的啦 我新买的这块st7789&#xff0c;突然发现是带触摸…

【python】uv管理器

uv是一个速度极快的 Python 包和项目管理器&#xff0c;用 Rust 编写。 安装 安装uv之前&#xff0c;确保你的电脑不需要安装了python 在Windows下&#xff0c;可以使用官方的脚本直接安装 powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.…