工具
-
@microsoft/fetch-event-source
处理服务器发送事件SSE
的JavaScript
库。 -
react-markdown
在React
应用中渲染Markdown
内容的库。 -
react-syntax-highlighter
在React
应用中实现代码语法高亮显示的库。
前端SSE处理
import fetchEventSource from '@microsoft/fetch-event-source';
export const requestSSE = (props: any) => {
const { url, access_token, openCb, messageCb, errorCb, closeCb } = props;
fetchEventSource(
url,
method: 'GET',
// 当标签页处于隐藏状态时,是否打开SSE连接
openWhenHidden: true,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${access_token}`
},
onopen: () => {
// 对话初始化的一些操作,例如新建立的一次对话
openCb && openCb();
},
onmessage: () => {
// 接受 SSE 传输过来的信息并处理
messageCb && messageCb();
},
onerror: () => {
// SSE 连接错误时,进行的处理
errorCb && errorCb();
},
onclose: () => {
// SSE连接结束后的操作,例如,加载/终止布局消失
closeCb && closeCb();
}
)
}
上述方式SSE连接的建立,同时,为了方便调用封装成一个文件
requestSSE.tsx
,供以调用,access_token
,onopen
可以作为回调函数传入封装文件。
Markdown 数据展示
将
Markdown
封装为一个调用文件AnswerMarkdown.tsx
,SSE
传输的内容作为参数传入。
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
export default function StringToMarktown(props: any) {
const { markdown } = props;
const markdownContent = (content: string) => {
try {
if (typeof JSON.parse(content) === 'string') return JSON.parse(content)
return content
} catch (error) {
return error
}
}
const transformLinkUri = (uri: string) => {
try {
const url = new URL(uri, window.location.origin)
} catch (error) {
return uri
}
}
return (
<div>
<ReactMarkdown
// 链接打开方式
linkTarget="_blank"
// 需要转义的 markdown 内容
children={markdownContent(markdown)}
// 链接 url 的特殊处理
transformLinkUri={transformLinkUri}
// 一个对象,键为 Markdown 元素的名称 a、h1、p,值为对应的 React 组件
components={{
a: (( node, href, children, ...props )) => {
return (
<a href={href} {...props} target="_self">
{children}
</a>
)
}
}}
// 集成各种 remark 插件,从而扩展其处理 markdown 内容的能力
remarkPlugins={[
[remarkGfm, { singleTitle: false }],
remarkMath
]}
/>
<div/>
)
}
上述代码处理了
Markdown
渲染中的链接,内容,还有a
标签,集成了插件。
-
remark-gfm
让markdown
支持gfm
特性,包括表格、任务列表、删除线等。 -
remark-math
让markdown
支持数学公式,可以识别并处理行内和块级的LaTeX
数学表达式。
代码高亮显示
React
应用使用react-syntax-highlighter
实现代码高亮显示。
import { message } from 'antd';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import copyImg from 'copyImg.svg';
const customHighlighter = (match: any, children: any, style: any, props: any) => {
const handleCopy = (text: any) => {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
message.success('复制成功');
}
return <>
<div className={styles.header}>
<div>{match[1]}</div>
<img src={copyImg.src} onClick={() => {handleCopy(children)}} />
</div>
</>
}
//...
<ReactMarkdown
components={{
code: ({ node, inline, className, children, ...props }) => {
let match = /language-(\w+)/.exec(className || '');
return !inline && match ? <>
<SyntaxHighlighter
{...props}
language={match[1]}
showLineNumbers={false}
renderer={() => {
customHighlighter({ match, children, style, props })
}}
/>
</> : (<code
{...props}
>
{children}
</code>)
}
}}
/>
上述便是
react-markdown
中高亮代码的处理方式,以上就是接受SSE
数据,markdown
格式展示的主要思路,主要文件内容。
在主页面的文件中直接调用requestSSE.tsx
文件和AnswerMarkdown.tsx
文件实现SSE
流式输出,markdown
格式渲染。
扩展
流式输出
现在浏览器基本都已经支持
EventSource
,只有IE浏览器不管对于EventSource
还是@microsoft/fetch-event-source
都支持不足。
自定义请求头
-
EventSource
现代浏览器基本都支持SSE API
,但SSE
底层只支持HTTP GET
,并且不能发送自定义的Header
。
像access_token
这种一般需要在Header
中写入传输的,就无法实现,需要使用BFF
处理一下。 -
@microsoft/fetch-event-source
是微软提供了一种方便的方式来使用fetch API
与服务器发送事件(Server-Sent Events,SSE
)进行交互。
可以自定义Header
,如同上面代码所示。
错误重连
EventSource
和@microsoft/fetch-event-source
的错误重连都是默认的自动重连,使用的都是指数退避策略,都是在onerror
中可以自定义重连规则,不过两者写法不同。
- 指数退避策略
采用指数退避算法来控制重连的时间间隔。这意味着每次重连失败后,下次尝试重连的等待时间会逐渐增加。这样做的目的是避免在服务器出现问题或网络状况不佳时,客户端过于频繁地发起重连请求,从而给服务器带来额外的负担。
-
onerror
修改
1.EventSource
const eventSourceUrl = 'https://your-sse-endpoint.com';
let reconnectCount = 0;
const maxReconnects = 5;
const reconnectInterval = 2000; // 重连间隔,单位:毫秒
function connect() {
const eventSource = new EventSource(eventSourceUrl);
eventSource.onopen = function () {
console.log('连接已成功打开');
reconnectCount = 0; // 连接成功后重置重连次数
};
eventSource.onmessage = function (event) {
console.log('接收到事件:', event.data);
};
eventSource.onerror = function (error) {
console.error('发生错误:', error);
reconnectCount++;
if (reconnectCount <= maxReconnects) {
console.log(`尝试第 ${reconnectCount} 次重连,将在 ${reconnectInterval / 1000} 秒后进行...`);
setTimeout(() => {
connect(); // 递归调用 connect 函数进行重连
}, reconnectInterval);
} else {
console.log('达到最大重连次数,停止重连');
}
};
}
// 初始连接
connect();
2.@microsoft/fetch-event-source
@microsoft/fetch-event-source
在onerror
中只需要throw error
就会触发重连。
import { fetchEventSource } from '@microsoft/fetch-event-source';
const eventSourceUrl = 'https://your-sse-endpoint.com';
let reconnectCount = 0;
const maxReconnects = 5;
fetchEventSource(eventSourceUrl, {
onopen(response) {
if (response.ok && response.headers.get('content-type') === 'text/event-stream') {
console.log('连接已成功打开');
reconnectCount = 0; // 连接成功后重置重连次数
} else {
throw new Error('无法连接到事件源');
}
},
onmessage(event) {
console.log('接收到事件:', event.data);
},
onclose() {
console.log('连接已关闭,等待重连...');
},
onerror(error) {
console.error('事件源出现错误:', error);
reconnectCount++;
if (reconnectCount > maxReconnects) {
console.log('达到最大重连次数,停止重连');
return; // 停止重连
}
// 继续抛出错误,让库进行重连
throw error;
},
});