SSE流式输出和markdown渲染

工具

  • @microsoft/fetch-event-source
    处理服务器发送事件SSEJavaScript库。
  • 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_tokenonopen可以作为回调函数传入封装文件。


Markdown 数据展示

Markdown封装为一个调用文件AnswerMarkdown.tsxSSE传输的内容作为参数传入。

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-sourceonerror中只需要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;
    },
});
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容