SSE打扮你的AI应用,让它美美哒

news2024/11/16 23:41:48

我从不幻想成功。我只会为了成功努力实践

大家好,我是柒八九。一个专注于前端开发技术/RustAI应用知识分享Coder

此篇文章所涉及到的技术有

  1. SSE
  2. Node( Express)
  3. EventSource
  4. React
  5. Tailwindcss
  6. 打字效果

因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。


前言

老粉丝都知道,我们有一个文档问答AI产品,然后有一个前端要求就是模仿ChatGPT展示后端返回的数据信息(打字效果)。刚开始呢,由于问答比较简单,只是一些简单的文本类型,并且后端返回的结果也有限,加上工期比较紧(反正就是各种原因),我们选择了最原始的前后端数据交互方法。

前端发送问题,后端接入模型分析数据,然后将最后的结果一股脑的返回给前端。就这样岁月静好的度过了一段时间,但是由于需求的变更。后端返回的信息又臭又长,然后还是沿用之前的数据获取和展示方式,就显得捉襟见肘了。

所以,此时我们就从我们知识百宝箱中搜索,然后一眼就相中SSE。之前在写一个类ChatGPT应用,前后端数据交互有哪几种文章中,我们就对其有过简单的介绍。

今天我们就来聊聊,如何实现基于SSE的前后端项目。(我们讲主要逻辑,有些细节例如样式等就不那么考究了)

效果展示

最终,我们就会得到一个类似下面的程序。 alt


好了,天不早了,干点正事哇。

alt

我们能所学到的知识点

  1. SSE是个啥?
  2. 用Node实现一个SSE服务
  3. SSE前端部分(React版本)
  4. 实现一个打字组件

1. SSE是个啥?

[服务器发送事件]((https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events "服务器发送事件"))(Server-Sent EventsSSE)提供了一种标准方法,通过 HTTP服务器数据推送到客户端。与 WebSockets 不同,SSE 专门设计用于服务器到客户端的单向通信,使其非常适用于实时信息的更新或者那些在不向服务器发送数据的情况下实时更新客户端的情况。

服务器发送事件 (SSE) 允许服务器在任何时候向浏览器推送数据:

  • 浏览器仍然会发出 初始请求以建立连接。
  • 服务器返回一个 事件流响应保持连接打开
  • 服务器可以使用这个连接在任何时候发送 文本消息
  • 传入的数据在浏览器中触发一个 JavaScript 事件。事件处理程序函数可以解析数据并更新 DOM。

本质上,SSE 是一个无尽的数据流。可以将其视为下载一个无限大的文件,以小块形式拦截和读取。(类比我们之前讲过的大文件分片上传和分片下载)

SSE 首次实现于 2006 年,所有主要浏览器都支持这个标准。它可能不如 WebSockets[1] 知名,但SSE更简单,使用标准 HTTP,支持单向通信,并提供自动重新连接功能。

SSE组件

我们可以将服务器发送事件视为单个 HTTP 请求,其中后端不会立即发送整个主体,而是保持连接打开,并通过每次发送事件时发送单个行来逐步传输答复。

alt

SSE是一个由两个组件组成的标准:

  1. 浏览器中的 EventSource 接口 [2],允许客户端订阅事件:它提供了一种通过抽象较低级别的连接和消息处理来订阅事件流的便捷方法。
  2. 事件流协议:描述服务器发送的事件必须遵循的标准纯文本格式,以便 EventSource 客户端理解和传播它们

EventSource

作为核心的组件,EventSource的兼容性良好。 alt

工作原理

服务端部分

服务器需要设置 HTTP 头部 Content-Type: text/event-stream 并保持连接不断开,以持续发送事件。典型的服务器发送事件的格式如下:

data: 这是一个事件消息

data: 这是另一个事件消息

可以包含多个字段:

id: 1234
event: customEvent
data: {"message": "这是一个自定义事件"}
retry: 10000
  • id:事件 ID,客户端会自动保存这个 ID,并在重连时发送 Last-Event-ID 头部。
  • event:事件类型,客户端可以根据类型进行不同处理。
  • data:事件数据。
  • retry:建议客户端重新连接的时间间隔(毫秒)。

客户端部分

客户端使用 JavaScript 创建一个 EventSource 对象并监听事件:

const eventSource = new EventSource('server-url');

eventSource.onmessage = function(event{
    console.log('收到事件数据:', event.data);
};

eventSource.onerror = function(event{
    console.log('事件源连接错误:', event);
};

eventSource.addEventListener('customEvent'function(event{
    console.log('收到自定义事件:', event.data);
});

更高级用法

在单个频道上发送不同的数据

服务器发送的消息可以有一个相关的事件:在 data: 行上方传递,以识别特定类型的信息:

event: React
data: React is great!

event: Rust
data: { "Rust": "我很喜欢", }

event: AI
data: { "name": "OpenAI" }

这些不会触发客户端的 message 事件处理程序。我们必须为每种类型的事件添加处理程序。例如:

// react 消息处理程序
source.addEventListener('React', e => {
  document.getElementById('React')
    .textContent = e.data;
});

// Rust 消息处理程序
source.addEventListener('Rust', e => {
  const r = JSON.parse(e.data);
  document.getElementById('Rust')
    .textContent = `${r.Rust}`;
});

// AI 消息处理程序
source.addEventListener('AI', e => {
  const ai = JSON.parse(e.data);
  document.getElementById(`ai`)
    .textContent = `${ai.name}`;
});

使用数据标识符

可选地,服务器也可以在 data: 行之后发送一个 id:

event: React
data: React is great!
id: 42

如果连接断开,浏览器会在 Last-Event-ID HTTP 头中发送最后的 id,以便服务器可以重新发送任何丢失的消息。

最新的 ID 也可以在客户端的事件对象的 .lastEventId 属性中获取:

// news 消息处理程序
source.addEventListener('React', e => {
  console.log(`last ID: ${e.lastEventId}`);
  document.getElementById('React')
    .textContent = e.data;
});

指定重试延迟

虽然重新连接是自动的,但我们的服务器可能知道在特定时间段内不会有新数据,因此无需保持活动的通信通道。服务器可以发送一个包含毫秒值的 retry: 响应,无论是单独发送还是作为最终消息的一部分。例如:

retry: 60000
data: 你很好,这段时间我们还是别联系了!

收到后,浏览器会断开 SSE 连接,并在延迟期过后尝试重新连接。

其他事件处理程序

除了 message 和命名事件,我们还可以在客户端 JavaScript 中创建 openerror 处理程序。

open 事件在服务器连接建立时触发。可以用于运行额外的配置代码或初始化 DOM 元素:

const source = new EventSource('/sse1');

source.addEventListener('open', e => {
  console.log('SSE connection established.');
});

error 事件在服务器连接失败或终止时触发。我们可以检查事件对象的 .eventPhase 属性以查看发生了什么:

source.addEventListener('error', e => {
  if (e.eventPhase === EventSource.CLOSED) {
    console.log('SSE connection closed');
  } else {
    console.log('error', e);
  }
});

无需重新连接:它会自动进行。

终止 SSE 通信

浏览器可以使用 EventSource 对象的 .close() 方法终止 SSE 通信。例如:

const source = new EventSource('/sse1');

// 一小时后关闭
setTimeout(() => source.close(), 3600000);

服务器可以通过以下方式终止连接:

  1. 触发 res.end() 或发送一个 retry: 延迟,然后
  2. 当相同的浏览器尝试重新连接时返回 HTTP 状态 204。

只有浏览器可以通过创建一个新的 EventSource 对象重新建立连接。


优点

优点描述
简单性比 WebSocket 更简单的 API 设计
自动管理重连内置的重连机制使开发更简便
浏览器支持现代浏览器普遍支持 EventSource

缺点

缺点描述
单向通信无法从客户端向服务器发送数据
基于 HTTP相比 WebSocket,SSE 在处理高频率数据传输时性能可能较低
受限于同源策略跨域通信需要额外配置 CORS(跨域资源共享)

在讲代码前,我们来简单说一下我们要实现的交互

  1. 前端输入信息
  2. 通过 Post接口传人后端
  3. 后端处理请求,拼接数据,返回 SSE格式数据
  4. 前端通过 EventSource事件接收数据

2. 用Node实现一个SSE服务

如果想了解一个事物的全貌,那就是接近它,了解它,实现它。

那么,我们就来自己用Node实现一个SSE服务。我们使用express[3]来搭建后端服务。

在我们心仪的目录下,执行如下命令

mkdir SSE && 
cd SSE &&
mkdir Server &&
cd Server &&
npm init

构建一个简单的Node项目。

然后,更新我们的package.json,这里就偷懒了,我直接把本地的内容复制下来了。

{
  "name""server",
  "version""1.0.0",
  "description""",
  "main""index.js",
  "scripts": {
    "build""tsc && node -v",
    "dev""tsc && tsc-watch --onSuccess \"node dist/index.js\""
  },
  "dependencies": {
    "@types/uuid""^10.0.0",
    "body-parser""^1.20.2",
    "cors""^2.8.5",
    "express""^4.18.2",
    "uuid""^10.0.0"
  },
  "devDependencies": {
    "@types/cors""^2.8.13",
    "@types/express""^4.17.17",
    "tsc-watch""^6.0.4",
    "typescript""^5.1.6"
  },
  "author""Front789",
  "license""ISC"
}

处理主要逻辑

我们将只要的逻辑方式在src/index.ts中。

alt

让我们来挑几个重要的点来解释一下

导入依赖和初始化Express

import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import { v4 as uuidv4 } from "uuid";

const app = express();
app.use(cors());
app.use(bodyParser.json());
const port = 4000;

这是一段实例化Express的代码。不做过多解释。我们是用了两个中间件

  • app.use(cors()): 应用 CORS 中间件,使服务器能够处理跨域请求。
  • app.use(bodyParser.json()): 应用 Body Parser 中间件,自动解析请求体中的 JSON 数据,并将其存储在 req.body 中。

处理SSE链接

// SSE连接处理
app.get('/api/events', (req, res) => {
  res.setHeader('Content-Type''text/event-stream');
  res.setHeader('Cache-Control''no-cache');
  res.setHeader('Connection''keep-alive');
  res.flushHeaders(); // 发送头部信息到客户端

  const clientId = uuidv4();
  const newClient = {
    id: clientId,
    res,
  };

  clients.push(newClient);

  req.on('close', () => {
    console.log(`${clientId} Connection closed`);
    clients = clients.filter((client) => client.id !== clientId);
  });
});

这部分其实也很简单,但是呢,我们要特别注意一下res.setHeader()部分。

  • Content-Type: text/event-stream: 设置内容类型为 text/event-stream,表明这是一个 SSE 连接。
  • Cache-Control: no-cache: 禁用缓存,以确保实时数据传输。
  • Connection: keep-alive: 保持连接不断开。
  • res.flushHeaders(): 立即将响应头部发送给客户端,确保连接保持活跃状态。

当我们每次接收到/api/events时,没有立马向请求方返回数据,而是构建一个newClient,并且将其push到一个全局变量clients中。

当客户端关闭连接时,从列表中移除相应的客户端,我们在close中执行对应的移除操作。

处理Post请求

// 处理POST请求
app.post('/api/message', (req, res) => {
  const userInput = req.body.message;
    // 模拟处理消息并推送给所有客户端
    const responses = generateChatGPTResponse(userInput);

  let index = 0;
  const intervalId = setInterval(() => {
      if (index < responses.length) {
        clients.forEach((client) => client.res.write(`data: ${JSON.stringify({ message: responses[index] })}\n\n`));
      index++;
    } else {
      clearInterval(intervalId);
      res.end();
    }
  }, 1000); // 每秒发送一个响应
  
  res.status(200).send();
});

function generateChatGPTResponse(input:string{
  // 模拟AI模型的响应,这里可以替换为实际的模型调用
  return [
    `你说的是: ${input}`,
    "这是AI模型的第一段响应。",
    "这是AI模型的第二段响应。",
    "这是AI模型的第三段响应。",
  ];
}

该段代码代码也是我们常见的用于处理Post请求的方法。有几点需要额外注意一下

  • 使用 req.body.message 获取客户端发送的消息内容,这需要 body-parser 中间件来解析请求体中的 JSON 数据
  • 使用 setInterval 定时器每秒推送一条消息给所有 SSE 连接的客户端
  • 在消息推送开始之前,立即向发送 POST 请求的客户端返回一个 200 状态码,表示请求已成功接收。

服务启动

然后我们就可以使用yarn devport4000的端口中启动一个SSE服务,此时坐等对应的请求到来即可。


3. SSE前端部分(React版本)

既然,SSE后端服务已经有了,那么我们来在前端接入对应的服务。

我们在SSE目录下,使用我们的脚手架在生成一个前端服务。

npx f_cli_f create Client

然后选择自己擅长的技术即可。然后按照对应的提示按照并启动服务即可。如果对我们的脚手架还不了解,可以翻看之前的文章Rust 赋能前端-开发一款属于你的前端脚手架

最后,我们在SSE目录下,就会生成如下的目录信息。

---SSE
  ---Client(前端项目)
  ---Server (后端服务)

前端代码逻辑

我们在Client/src/pages/新建一个ChatComponent组件。

UI部分

 <div className='flex flex-col justify-center items-center w-full h-full'>
    <div className='flex flex-col justify-center items-center flex-1 w-full'>
        <Typewriter text={'大家好,我是柒八九。一个专注于前端开发技术/RustAI应用知识分享的Coder'} delay={100} infinite={false} />
        {messages.map((msg, index) => (
            // <p key={index}>{msg}</p>
            <Typewriter text={msg} delay={100} infinite={false} key={index}/>
        ))}

    </div>

    <Form  form={form} className='w-9/12'>
        <Form.Item className={styles['message-item']} name="message">
            <Input.TextArea
                autoSize={{ minRows: 1maxRows: 3 }}
                placeholder="输入内容开始对话(Shift + Enter 换行)"
                onPressEnter={handleTextAreaKeyDown}
            />

        </Form.Item>
    </Form>

</div>

UI部分呢,我们大致分为两部分

  • 展示后端返回信息
  • TextArea收集用户输入信息

然后,我们在TextAreaPressEnter中执行向后端发送的操作。

注册EventSource

我们在Effect中注册EventSource相关事件。

 useEffect(() => {
    const eventSource = new EventSource('http://localhost:4000/api/events');
    eventSource.onmessage = function (event
        const data = JSON.parse(event.data);
        const { message } = data;
        setMessages((prevMessages) => [...prevMessages, message]);
    };
    return () => {
        eventSource.close();
    };
}, []);

有几点需要说明

  1. 我们是在组件初始化的时候,注册 EventSource
  2. 由于我们在上一节中已经在 http://localhost:4000中启用了 SSE服务,所以在 EventSource中传人的是对应的 SSE地址
  3. onmessage中我们解析从后端返回的数据,并存入到 state-message中。

当数据返回后,对应的state-message发生变化,那也就触发了React的重新渲染。就可以在UI部分看到后端返回的信息。

handleTextAreaKeyDown

这部分是调用指定的后端接口,将用户信息传递给后端服务,用于做指定信息的处理。

 const handleTextAreaKeyDown= async (event) => {
    const { keyCode, shiftKey } = event;
    if (keyCode == 13 && !shiftKey) {
        event.preventDefault();
        const message = form.getFieldValue('message');
        if (message && message.trim().length > 0) {
            if (message.trim().length > 0) {
                await fetch('http://localhost:4000/api/message', {
                    method'POST',
                    headers: {
                        'Content-Type''application/json',
                    },
                    bodyJSON.stringify({ message }),
                });
            }
            form.setFieldValue('message''');
        }
    }
};

PressEnter事件中,我们还判断eventkeyCodeshiftKey来实现在TextArea中换行的操作。也就是只有在单纯的触发Enter才会向后端传递数据。

我们之所以选择用Post来向后端发起情况,因为我们用户输入的信息,不单单是文本信息,也可以是PDF/Word/Text等文本资源。

最终,我们就会得到一个和本文开头的那个效果。

alt

求豆麻袋,好像有一个东西没给大家展示,那就是实现打字效果。别着急,我们这就说。


4. 实现一个打字组件

其实呢,针对一个完整的应用,我们不仅仅需要处理纯文本信息,我们还需要处理类似Table/Code/Img等富文本的展示。

此时,最好的后端数据返回是啥呢,MarkDown。没错,ChatGPT也是这种格式,只不过它在前端显示的时候,用了针对这类信息的展示处理。

而,我们今天的主要方向是讲SSE,而针对其他类型的信息展示不在此篇文章内。如果大家有兴趣了解,后面我们也可以针对此处的内容展开聊聊。

话题扯的有点远了,我们现在进入这节的主题,写一个纯前端的打字效果

其实呢,针对现成的打字效果有很多。例如

  • typed-js [4]
  • react-typed [5]

但是呢,本着知识探索的精神,我们今天来实现一个属于自己的打字效果

ChatComponent目录下,新建一个Typewriter文件夹。

然后新建三个文件

  1. index.tsx:只要逻辑
  2. Cursor.tsx:处理光标逻辑
  3. index.module.scss:存放样式信息。

下面我们就来看看它们各自的实现逻辑。

index.tsx

import React, { useState, useEffect } from 'react';
import style from './index.module.scss';
import Cursor from './Cursor';
interface TypewriterProps {
    text: string | React.ReactNode;
    delay: number;
    infinite?: boolean;
}

const Typewriter: React.FC<TypewriterProps> = ({ text, delay, infinite }) => {
    const [currentText, setCurrentText] = useState<string>('');
    const [currentIndex, setCurrentIndex] = useState<number>(0);

    useEffect(() => {
        let timeout: number;

        if (currentIndex < text.length) {
            timeout = setTimeout(() => {
                setCurrentText((prevText) => prevText + text[currentIndex]);
                setCurrentIndex((prevIndex) => prevIndex + 1);
            }, delay);
        } else if (infinite) {
            setCurrentIndex(0);
            setCurrentText('');
        }

        return () => clearTimeout(timeout);
    }, [currentIndex, delay, infinite, text]);

    return (
        <span className={style['text-writer-wrapper']}>
            <span dangerouslySetInnerHTML={{ __html: currentText }}></span>
            {currentIndex text.length && <Cursor />}
        </span>

    );
};

export default Typewriter;

其实呢,上面的逻辑很简单。

Typewriter接收三个参数

  1. text:要显示的文本,可以是字符串或 React 节点。
  2. delay:每个字符之间的延迟时间(以毫秒为单位)。
  3. infinite:是否无限循环显示文本,默认为 false

使用 useEffect 钩子在每次 currentIndex 改变时运行:

  • 如果 currentIndex 小于 text 的长度:
    • 设置一个 setTimeout 以延迟添加下一个字符到 currentText
    • 递增 currentIndex
  • 否则如果 infinitetrue
    • 重置 currentIndexcurrentText 以开始新的循环。
  • 返回一个清除定时器的函数,以避免内存泄漏。

然后,我们使用dangerouslySetInnerHTML来更新文本信息。

Cursor.tsx

这个组件就更简单了,就是绘制了一个svg,用于在文本输入过程中显示光标。

import style from './index.module.scss';
export default function CursorSVG({
    return (
        <svg viewBox="8 4 8 16" xmlns="http://www.w3.org/2000/svg" className={style['cursor']}>
            <rect x="10" y="6" width="2" height="100%" fill="black" />
        </svg>

    );
}

index.module.scss

.text-writer-wrapper{
     @keyframes flicker {
        0% {
            opacity0;
        }
        50% {
            opacity1;
        }
        100% {
            opacity0;
        }
    }
    .cursor {
        display: inline-block;
        width1ch;
        animation: flicker 0.5s infinite;
    }
}

这段代码主要用于创建打字机效果中的光标闪烁效果:

  • @keyframes flicker 动画定义了光标的闪烁效果,通过改变透明度来实现闪烁。
  • .cursor 类应用了闪烁动画,并设置了宽度,使其显示为一个闪烁的光标。

最终效果是在 .text-writer-wrapper 中显示的光标会每 0.5 秒闪烁一次,模拟文本编辑器中的光标效果。


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

alt
Reference
[1]

WebSockets: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket

[2]

EventSource 接口: https://developer.mozilla.org/en-US/docs/Web/API/EventSource

[3]

express: https://expressjs.com/

[4]

typed-js: https://mattboldt.com/demos/typed-js/

[5]

react-typed: https://www.npmjs.com/package/react-typed?activeTab=readme

本文由 mdnice 多平台发布

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

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

相关文章

【React】Google 账号之个性化一键登录按钮功能

“使用 Google 帐号登录”功能可快速管理网站上的用户身份验证。用户登录 Google 账号、表示同意&#xff0c;并安全地与平台共享其个人基础资料信息。 官方文档&#xff1a;链接 一、获取 Google API 客户端 ID 打开 Google API 控制台 中的凭据页面 创建或选择 Google API 项…

小米采取措施禁止国行版设备安装国际版系统 刷机后将报错无法进入系统

据知名官改版系统 Xiaomi.EU 测试者 Kacper Skrzypek 发布的消息&#xff0c;小米目前已经在开机引导中新增区域检测机制&#xff0c;该机制将识别硬件所属的市场版本&#xff0c;例如中国大陆市场销售的小米即将在安装国际版系统后将无法正常启动。 测试显示该检测机制是在开…

有浏览器就行,手把手带你从零微调大模型!

有浏览器就行&#xff0c;手把手带你从零微调大模型&#xff01; 今天分享一篇技术文章&#xff0c;你可能听说过很多大模型的知识&#xff0c;但却从未亲自使用或微调过大模型。 今天这篇文章&#xff0c;就手把手带你从零微调一个大模型。 大模型微调本身是一件非常复杂且…

自动化测试全攻略:从入门到精通!

1、自动化测试专栏 随着技术的发展和工作需求的增长&#xff0c;自动化测试已成为软件质量保障体系中不可或缺的一环。 为了帮助广大测试工程师、开发者和对自动化测试感兴趣的读者们更好地掌握这一技能&#xff0c;今年特别推出了全新的《自动化测试全攻略&#xff1a;从入门…

[Python爬虫] 抓取京东商品数据||京东商品API接口采集

本文结构&#xff1a; 一、引言 二、代码分享 三、问题总结 引言 这两天因为一些需求&#xff0c;研究了一下如何爬取京东商品数据。最开始还是常规地使用selenium库进行商品页的商品抓取&#xff0c;后来因为想要获取优惠信息&#xff0c;只能进入到商品详情页进行抓取&#x…

阶段三:项目开发---民航功能模块实现:任务24:航空实时监控

任务描述 内 容&#xff1a;地图展示、飞机飞行轨迹、扇区控制。航空实时监控&#xff0c;是飞机每秒发送坐标&#xff0c;经过终端转换实时发送给塔台&#xff0c;为了飞机位置的精准度&#xff0c;传输位置的密度很大&#xff0c;在地图位置显示不明显。本次为了案例展示效…

如何成功的设计BGA?

目前&#xff0c;用于容纳各种先进多功能半导体器件&#xff08;如 FPGA 和微处理器&#xff09;的标准封装是球栅阵列 &#xff08;BGA&#xff09;。BGA 封装中的组件用于各种嵌入式设计中&#xff0c;既可用作主机处理器&#xff0c;也可用作存储器等外设。多年来&#xff0…

抖音机构号授权源码全解析

抖音机构号授权源码是抖音平台为了方便机构用户进行授权管理而推出的一项服务。随着抖音平台的快速发展&#xff0c;越来越多的机构开始意识到抖音作为一种强大的营销渠道的价值。为了更好地利用抖音平台的资源&#xff0c;许多机构开始了解抖音机构号的功能和优势&#xff0c;…

css 文件重复类样式删除

上传文件 进行无关 className 删除 <div style"display: flex;"><input type"file" change"handleFileUpload" /><el-button click"removeStyles" :disabled"!fileContent">Remove Styles and Download&…

科普文:Linux服务器常用命令和脚本

Linux服务器常用的命令&#xff1a;find、grep、xargs、sort、uniq、tr、cut、paste、wc、sed、awk&#xff1b;提供的例子和参数都是最常用和最为实用的。 1.find 文件查找 查找txt和pdf文件 find . \( -name "*.txt" -o -name "*.pdf" \) -print 正…

VS2019 因公司加密无法运行程序原因

问题原因&#xff0c;点击编译运行按钮出现以下问题 首先右击项目&#xff0c;然后点击属性&#xff0c;点击应用程序&#xff0c;将资源里面的清单修改为 创建不带清单的应用程序&#xff0c;即可

竞速赛车游戏推荐:极品飞车14:热力追踪 (Win/Mac) 安装包

《极品飞车14&#xff1a;热力追踪》&#xff08;Need for Speed: Hot Pursuit&#xff09;是《极品飞车》系列的第14部作品&#xff0c;于2010年11月发行。这款游戏是继《极品飞车&#xff1a;热力追踪2》后&#xff0c;系列再次回归该名称。 游戏采用 Criterion Games 的开放…

STM32的独立看门狗详解

目录 1.独立看门狗是什么&#xff1f; 2.独立看门狗的作用 3.独立看门狗的实现原理 4.独立看门狗用到的寄存器 4.1 IWDG_KR &#xff08;关键字计时器&#xff09; 4.2 IWDG_PR&#xff08;预分频寄存器&#xff09; 4.3 IWDG_RLR&#xff08;重装载寄存器&#xff09…

土壤水分及其频谱分析

祁连山综合观测网&#xff1a;黑河流域地表过程综合观测网&#xff08;混合林站自动气象站-2018&#xff09; import pandas as pd dfpd.read_excel(rC:\Users\mengx\Desktop\土壤水分的频谱分析\祁连山综合观测网&#xff1a;黑河流域地表过程综合观测网&#xff08;混合林站…

RocketMQ之消费者,重平衡机制与流程详解附带源码解析

1. 背景 本文是 RocketMQ 消费者系列的第三篇&#xff0c;介绍消费者重平衡。 我把 RocketMQ 消费分成如下几个步骤 重平衡 消费者拉取消息 Broker 接收拉取请求后从存储中查询消息并返回 消费者消费消息 其中重平衡是消费者开始消费的起点。 1.1 重平衡的含义 RocketMQ 的 To…

【单片机毕业设计选题24052】-基于STM32的智能书桌设计

系统功能: 基于stm32单片机的智能书桌设计 1.手动&#xff1a;升降桌&#xff0c;调整桌面高度&#xff0c;实现升降功能&#xff0c; 2.自动&#xff1a;光敏控制灯的亮度 手动&#xff1a;开关灯 3.自动&#xff1a;检测学习姿势报警&#xff0c;超声波检测人是否坐的太…

【单片机毕业设计选题24051】-基于STM32的温室大棚控制系统

系统功能: 1、检测环境温湿度&#xff0c;土壤湿度&#xff0c;光照强度和二氧化碳浓度并在OLED和APP上显示。 2、当空气温度过高时则打开风扇通风。 3、当空气湿度过低时打开加湿器加湿。 4、土壤湿度过低&#xff0c;打开水泵。 5、光照不足则打开LED,反之则关闭…

麒麟系统设置中添加打印机按钮无响应的解决办法

原文链接&#xff1a;麒麟系统设置中添加打印机按钮无响应的解决办法 Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇关于在麒麟桌面操作系统上点击设置中的添加打印机按钮无响应的解决办法的文章。打印机是我们日常办公的重要工具&#xff0c;如果添加打印机按钮无…

昇思25天学习打卡营第15天|应用实践之ShuffleNet图像分类

基本介绍 今天的应用实践的领域是计算机视觉领域&#xff0c;更确切的说是图像分类任务&#xff0c;不过&#xff0c;与昨日不同的是&#xff0c;今天所使用的模型是ShuffleNet模型。ShuffleNetV1是旷视科技提出的一种计算高效的CNN模型&#xff0c;和MobileNet, SqueezeNet等一…

柳永,市井生活的吟游者

柳永&#xff0c;原名柳三变&#xff0c;字景庄&#xff0c;后改名为柳永&#xff0c;字耆卿&#xff0c;约生于宋太宗雍熙元年&#xff08;公元984年&#xff09;&#xff0c;卒于宋仁宗皇祐五年&#xff08;公元1053年&#xff09;&#xff0c;享年69岁。他是北宋著名词人&am…