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

ReactMarkdown SyntaxHighlighter处理流式字符串时失效问题

问题描述

流式获取数据并传递给ReactMarkdown组件时,语法高亮功能失效;但传递完整字符串时,语法高亮正常工作。推测ReactMarkdown无法解析不完整的流式字符串。

流式数据获取代码

const response = await fetch(
    `${publicRuntimeConfig.REACT_APP_API_URL}/api/v1/get_response_stream`,
    {
      method: "POST",
      body: JSON.stringify({
        user_input: user_input,
        topic: router.query.topic,
      }),

      headers: {
        "Content-Type": "application/json",

        authorization: "Bearer " + globaluser.token,
      },
    }
  );
  const reader = response.body.getReader();
  let first = false;
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      // 复制最终结果硬编码时,语法高亮正常
      console.log(chats, "Done");
      break;
    }
    const text = new TextDecoder().decode(value);
    let updated = text
      ?.replace(/data:/g, "")
      ?.replace(/data: /g, "")
      ?.replace(/\s+/g, " ");
    //?.replace(/\n/g, "<br>")
    //.trim();
    // console.log(updated);
    if (!first) {
      // 添加新消息
      setChats((d) => [...d, updated]);
      first = true;
    } else {
      // 更新最后一条消息
      setChats((d) => {
        let existingChats = [...d];
        let newValue = `${
          existingChats[existingChats.length - 1]
        }${updated}`;

        //console.log(newValue);
        existingChats[existingChats.length - 1] = newValue;
        return existingChats;
      });
      bottomRef.current?.scrollIntoView();
    }
  }

聊天内容渲染代码

{chats.map((chat, i) => 
       <Typography key={i}
          variant='subtitle1'
          sx={{
             whiteSpace: "break-spaces",
          }}
          className='editor'
        >
                          
                          <ReactMarkdown
                            children={chat
                              .replace("then user: ", "")
                              .replace("then system: ", "")
                              .replace("system : ", "")
                              .replace("system: ", "")
                              .replace("user: ", "")
                              .replace(/^(\d+)\.\s/gm, "$1\\. ")
                              .replace(/^\*\s/gm, "\\* ")}
                            components={{
                              code({
                                node,
                                inline,
                                className,
                                children,
                                ...props
                              }) {
                                const match = /language-(\w+)/.exec(
                                  className || ""
                                );
                                return !inline && match ? (
                                  <SyntaxHighlighter
                                    {...props}
                                    children={String(children).replace(
                                      /\n$/,
                                      ""
                                    )}
                                    style={dark}
                                    language={match[1]}
                                    PreTag='div'
                                  />
                                ) : (
                                  <code {...props} className={className}>
                                    {children}
                                  </code>
                                );
                              },
                            }}
                          />
      </Typography>
)}
解决方案

核心原因

流式传输时,代码块是逐步拼接的,ReactMarkdown每次渲染的是不完整的Markdown片段(比如只收到javascript`开头,还没到结尾的),此时无法识别为完整的代码块,自然不会触发语法高亮逻辑。

具体解决方法

1. 跟踪代码块闭合状态,延迟高亮

在流式拼接内容时,记录是否处于未闭合的代码块中,只有当代码块完整时,才启用语法高亮:

修改流式数据获取逻辑,添加代码块状态跟踪

const response = await fetch(
  `${publicRuntimeConfig.REACT_APP_API_URL}/api/v1/get_response_stream`,
  {
    method: "POST",
    body: JSON.stringify({
      user_input: user_input,
      topic: router.query.topic,
    }),
    headers: {
      "Content-Type": "application/json",
      authorization: "Bearer " + globaluser.token,
    },
  }
);
const reader = response.body.getReader();
let first = false;
let inUnclosedCodeBlock = false; // 标记是否存在未闭合的代码块

while (true) {
  const { done, value } = await reader.read();
  if (done) {
    // 处理未闭合的代码块(可选:补全闭合标记,或保持原样)
    if (inUnclosedCodeBlock) {
      setChats(prev => {
        const newChats = [...prev];
        newChats[newChats.length - 1] += '```';
        return newChats;
      });
    }
    console.log(chats, "Done");
    break;
  }
  const text = new TextDecoder().decode(value);
  let updated = text
    ?.replace(/data:/g, "")
    ?.replace(/data: /g, "")
    ?.replace(/\s+/g, " ");

  // 统计当前片段中的代码块标记数量,更新闭合状态
  const codeBlockMarkers = updated.match(/```/g) || [];
  codeBlockMarkers.forEach(() => {
    inUnclosedCodeBlock = !inUnclosedCodeBlock;
  });

  if (!first) {
    setChats((d) => [...d, updated]);
    first = true;
  } else {
    setChats((d) => {
      let existingChats = [...d];
      let newValue = `${existingChats[existingChats.length - 1]}${updated}`;
      existingChats[existingChats.length - 1] = newValue;
      return existingChats;
    });
    bottomRef.current?.scrollIntoView();
  }
}

修改渲染逻辑,根据代码块状态启用高亮

添加辅助函数判断内容是否包含完整代码块,再决定是否触发语法高亮:

// 辅助函数:检查内容中的代码块是否全部闭合
const hasCompleteCodeBlocks = (content) => {
  const markerCount = (content.match(/```/g) || []).length;
  return markerCount % 2 === 0;
};

// 渲染组件部分
{chats.map((chat, i) => {
  const processedChat = chat
    .replace("then user: ", "")
    .replace("then system: ", "")
    .replace("system : ", "")
    .replace("system: ", "")
    .replace("user: ", "")
    .replace(/^(\d+)\.\s/gm, "$1\\. ")
    .replace(/^\*\s/gm, "\\* ");
  
  const isCodeComplete = hasCompleteCodeBlocks(processedChat);

  return (
    <Typography key={i}
      variant='subtitle1'
      sx={{ whiteSpace: "break-spaces" }}
      className='editor'
    >
      <ReactMarkdown
        children={processedChat}
        components={{
          code({ node, inline, className, children, ...props }) {
            // 如果代码块不完整且不是行内代码,先渲染普通文本
            if (!isCodeComplete && !inline) {
              return <pre {...props}><code>{children}</code></pre>;
            }
            
            const match = /language-(\w+)/.exec(className || "");
            return !inline && match ? (
              <SyntaxHighlighter
                {...props}
                children={String(children).replace(/\n$/, "")}
                style={dark}
                language={match[1]}
                PreTag='div'
              />
            ) : (
              <code {...props} className={className}>
                {children}
              </code>
            );
          },
        }}
      />
    </Typography>
  );
})}

2. 折中方案:积累内容后再渲染

如果对实时性要求不高,可以将流式数据先拼接成完整字符串,再一次性传递给ReactMarkdown。但这种方式会失去流式输出的即时反馈,适合对高亮优先级高于实时性的场景。


内容的提问来源于stack exchange,提问作者Muhammad Sami

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 14:26:58