使用react-markdown实现可折叠区块遇渲染异常求助
解决方案
问题根源
remark-collapse插件会生成原生的<details>和<summary>HTML标签,但react-markdown默认会转义所有HTML内容,导致这些标签被当作文本渲染。rehype-raw虽然能解析HTML,但会放行所有未过滤的HTML,存在XSS风险。
下面提供两种安全的替代方案:
方案一:用rehype-sanitize放行特定HTML标签(简单高效)
利用rehype-sanitize过滤恶意HTML,只允许<details>和<summary>标签,既保留折叠功能,又避免XSS风险。
步骤:
- 安装依赖:
npm install rehype-sanitize
- 配置
react-markdown:
import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkCollapse from 'remark-collapse'; import rehypeSanitize from 'rehype-sanitize'; import { defaultSchema } from 'rehype-sanitize'; // 扩展默认安全规则,允许details和summary标签 const sanitizeSchema = { ...defaultSchema, tags: [...defaultSchema.tags, 'details', 'summary'], attributes: { ...defaultSchema.attributes, details: ['open'] // 允许控制展开状态的open属性 } }; export function MarkdownViewer({ content }) { return ( <ReactMarkdown remarkPlugins={[ remarkGfm, // 配置remark-collapse匹配含"Collapse"的标题 [remarkCollapse, { test: /^Collapse/ }] ]} rehypePlugins={[ [rehypeSanitize, sanitizeSchema] ]} > {content} </ReactMarkdown> ); }
- 对应的Markdown写法:
## Collapse: 我的折叠区块 这里是折叠内容,支持**加粗**、列表等Markdown格式: - 列表项1 - 列表项2
方案二:自定义Remark插件+React组件(高度可控)
如果需要定制折叠组件的样式或行为(比如自定义展开/收起按钮),可以通过自定义Remark插件将折叠区块转换为自定义AST节点,再用React组件渲染。
步骤:
- 安装AST遍历工具:
npm install unist-util-visit
- 编写自定义Remark插件:
import { visit } from 'unist-util-visit'; // 识别含"Collapse:"前缀的标题,将其与后续内容打包为自定义节点 export function remarkCustomCollapse() { return (tree) => { let currentSection = null; const sections = []; visit(tree, 'heading', (node, index, parent) => { const title = node.children.map(c => c.value).join('').trim(); // 匹配前缀为"Collapse:"的标题 if (title.startsWith('Collapse:')) { const sectionTitle = title.replace(/^Collapse:\s*/, ''); currentSection = { type: 'collapsableSection', title: sectionTitle, children: [], data: { hName: 'div' } }; sections.push({ index, section: currentSection }); return visit.SKIP; } // 将后续内容加入当前折叠区块,直到遇到同级标题 if (currentSection && node.depth <= parent.children[sections.at(-1).index].depth) { currentSection = null; } else if (currentSection) { currentSection.children.push(node); parent.children.splice(index, 1); return visit.SKIP; } }); // 将自定义节点替换回原AST sections.forEach(({ index, section }) => { tree.children.splice(index, 1, section); }); }; }
- 配置
react-markdown并编写折叠组件:
import React, { useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { remarkCustomCollapse } from './remarkCustomCollapse'; // 自定义折叠组件 function CollapsableSection({ title, children }) { const [isOpen, setIsOpen] = useState(false); return ( <div className="collapse-section"> <button className="collapse-toggle" onClick={() => setIsOpen(!isOpen)} aria-expanded={isOpen} > {isOpen ? '▼' : '▶'} {title} </button> {isOpen && <div className="collapse-content">{children}</div>} </div> ); } export function MarkdownViewer({ content }) { return ( <ReactMarkdown remarkPlugins={[remarkGfm, remarkCustomCollapse]} components={{ // 将自定义AST节点映射到React组件 collapsableSection: CollapsableSection }} > {content} </ReactMarkdown> ); }
为什么这两种方案安全?
- 方案一通过
rehype-sanitize严格过滤HTML,只允许指定的安全标签,恶意脚本会被自动移除。 - 方案二完全基于React组件渲染,所有内容都经过
react-markdown的安全解析,不存在HTML注入风险。
内容的提问来源于stack exchange,提问作者Gonzalo Gallotti
相关产品推荐
相关产品推荐

