You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用react-markdown实现可折叠区块遇渲染异常求助

解决方案

问题根源

remark-collapse插件会生成原生的<details>和<summary>HTML标签,但react-markdown默认会转义所有HTML内容,导致这些标签被当作文本渲染。rehype-raw虽然能解析HTML,但会放行所有未过滤的HTML,存在XSS风险。

下面提供两种安全的替代方案:


方案一:用rehype-sanitize放行特定HTML标签(简单高效)

利用rehype-sanitize过滤恶意HTML,只允许<details>和<summary>标签,既保留折叠功能,又避免XSS风险。

步骤:

  1. 安装依赖:
npm install rehype-sanitize
  1. 配置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>
  );
}
  1. 对应的Markdown写法:
## Collapse: 我的折叠区块
这里是折叠内容,支持**加粗**、列表等Markdown格式:
- 列表项1
- 列表项2

方案二:自定义Remark插件+React组件(高度可控)

如果需要定制折叠组件的样式或行为(比如自定义展开/收起按钮),可以通过自定义Remark插件将折叠区块转换为自定义AST节点,再用React组件渲染。

步骤:

  1. 安装AST遍历工具:
npm install unist-util-visit
  1. 编写自定义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);
    });
  };
}
  1. 配置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 06:17:40