React组件渲染OpenAI API返回的JSON字符串化响应的格式化方案咨询
解决React中安全渲染OpenAI格式化文本的方案
1. 纯文本解析+React组件映射
手动解析OpenAI返回的文本,将不同格式元素转换成对应React组件,完全规避dangerouslySetInnerHTML的安全风险。
核心实现步骤:
- 按换行符拆分文本,识别段落、无序列表项(以
-开头)、有序列表项(以数字.开头) - 用正则匹配URL,替换为带安全属性的React
<a>组件,并支持长URL自动缩短 - 分段渲染对应组件:普通段落用
<p>包裹,列表项收集后用<ul>/<ol>+<li>包裹
示例代码:
import React from 'react'; const parseOpenAIText = (text) => { const lines = text.split('\n'); const elements = []; let currentListType = null; // 标记当前是'ul'还是'ol' let listItems = []; // 匹配URL的正则 const urlRegex = /https?:\/\/[^\s]+/g; // 处理单行内容,替换URL为<a>组件 const renderLineContent = (line) => { const textParts = line.split(urlRegex); const urls = line.match(urlRegex) || []; return textParts.map((part, index) => ( <React.Fragment key={index}> {part} {urls[index] && ( <a href={urls[index]} target="_blank" rel="noopener noreferrer" style={{ color: '#0066cc', textDecoration: 'underline' }} > {urls[index].length > 50 ? `${urls[index].slice(0, 47)}...` : urls[index]} </a> )} </React.Fragment> )); }; lines.forEach((line) => { const trimmedLine = line.trim(); if (!trimmedLine) { // 空行:结束当前列表(如果存在) if (currentListType) { elements.push( currentListType === 'ul' ? ( <ul key={elements.length}>{listItems.map((item, i) => <li key={i}>{item}</li>)}</ul> ) : ( <ol key={elements.length}>{listItems.map((item, i) => <li key={i}>{item}</li>)}</ol> ) ); currentListType = null; listItems = []; } elements.push(<br key={elements.length} />); return; } // 识别无序列表项 if (trimmedLine.startsWith('- ')) { currentListType = 'ul'; listItems.push(renderLineContent(trimmedLine.slice(2))); return; } // 识别有序列表项 if (/^\d+\. /.test(trimmedLine)) { currentListType = 'ol'; listItems.push(renderLineContent(trimmedLine.replace(/^\d+\. /, ''))); return; } // 普通段落:先结束未完成的列表 if (currentListType) { elements.push( currentListType === 'ul' ? ( <ul key={elements.length}>{listItems.map((item, i) => <li key={i}>{item}</li>)}</ul> ) : ( <ol key={elements.length}>{listItems.map((item, i) => <li key={i}>{item}</li>)}</ol> ) ); currentListType = null; listItems = []; } elements.push(<p key={elements.length}>{renderLineContent(trimmedLine)}</p>); }); // 处理最后剩余的列表 if (currentListType) { elements.push( currentListType === 'ul' ? ( <ul key={elements.length}>{listItems.map((item, i) => <li key={i}>{item}</li>)}</ul> ) : ( <ol key={elements.length}>{listItems.map((item, i) => <li key={i}>{item}</li>)}</ol> ) ); } return elements; }; // 组件使用示例 const ChatMessage = ({ content }) => { return <div className="chat-message">{parseOpenAIText(content)}</div>; }; export default ChatMessage;
2. 使用安全的Markdown渲染库
选择内置XSS防护的Markdown解析库,直接将OpenAI返回的Markdown文本转换成React组件,无需手动解析。
推荐方案:react-markdown
该库完全基于React组件渲染,无innerHTML风险,支持自定义组件样式和行为。
安装命令:
npm install react-markdown
示例代码:
import React from 'react'; import ReactMarkdown from 'react-markdown'; const ChatMessage = ({ content }) => { return ( <div className="chat-message"> <ReactMarkdown components={{ // 自定义链接组件:缩短长URL+添加安全属性 a: ({ href, children }) => { const displayText = href.length > 50 ? `${href.slice(0, 47)}...` : href; return ( <a href={href} target="_blank" rel="noopener noreferrer" style={{ color: '#0066cc', textDecoration: 'underline' }} > {displayText} </a> ); }, // 自定义基础样式 p: ({ children }) => <p style={{ margin: '8px 0' }}>{children}</p>, ul: ({ children }) => <ul style={{ margin: '8px 0', paddingLeft: '24px' }}>{children}</ul>, ol: ({ children }) => <ol style={{ margin: '8px 0', paddingLeft: '24px' }}>{children}</ol>, li: ({ children }) => <li style={{ margin: '4px 0' }}>{children}</li> }} > {content} </ReactMarkdown> </div> ); }; export default ChatMessage;
3. 预处理OpenAI返回内容(可选)
调用API时,在prompt中明确要求返回标准Markdown格式,比如添加:"请使用标准Markdown格式输出,包括段落分隔、有序/无序列表、链接使用显示文本格式",能大幅降低前端解析的复杂度。
内容的提问来源于stack exchange,提问作者nik
相关产品推荐
相关产品推荐

