你们用react-markdown做聊天的时候,内容是流不断返回的,如果有图片的话。会造成页面闪烁,因为contten不断变化,重新渲染了。怎么解决的?

import { FC, memo, useState } from 'react';
// @ts-ignore
import ReactMarkdown, { Options } from 'react-markdown';
import rehypeMathjax from 'rehype-mathjax';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { CodeBlock } from './CodeBlock';

export const MemoizedReactMarkdown: FC<Options> = memo(
  ReactMarkdown,
  (prevProps, nextProps) => prevProps.children === nextProps.children,
);

const Markdown = ({
  content,
  inStreaming,
  pending,
  className,
}: {
  content: string;
  inStreaming?: boolean;
  pending?: boolean;
  className?: string;
}) => {
  const [showModal, setShowModal] = useState(false); // 控制模态框显示的状态
  const [modalImage, setModalImage] = useState(''); // 存储模态框中要显示的图片的 URL

  // 图片预览模态框的 JSX 结构
  const ImageModal = () => (
    <div
      className="fixed top-0 bottom-0 left-0 right-0 z-[9999] flex items-center justify-center bg-black bg-opacity-50"
      style={{ display: showModal ? 'flex' : 'none' }}
      onClick={() => setShowModal(false)}
    >
      <div
        className="relative max-h-full overflow-y-auto max-w-[95%] lg:max-w-[85%]"
        onClick={(event) => event.stopPropagation()} // 阻止事件冒泡
      >
        <img src={modalImage} alt="Preview" className="max-w-full" />
      </div>
      <span
        className="absolute m-2 text-white top-10 right-10 text-[36px] cursor-pointer"
        onClick={() => setShowModal(false)}
      >
        ×
      </span>
    </div>
  );

  return (
    <div>
      <MemoizedReactMarkdown
        className={className}
        remarkPlugins={[remarkGfm, remarkMath]}
        rehypePlugins={[rehypeMathjax]}
        components={{
          code({ node, inline, className, children, ...props }) {
            if (children.length) {
              if (children[0] === '▍') {
                return (
                  <div className="inline-block">
                    {inStreaming && !pending ? (
                      <span className="mt-1 cursor-default">▍</span>
                    ) : null}
                  </div>
                );
              }

              children[0] = (children[0] as string).replace('`▍`', '▍');
            }

            const match = /language-(\w+)/.exec(className || '');

            return !inline ? (
              <CodeBlock
                key={Math.random()}
                language={(match && match[1]) || ''}
                value={String(children).replace(/\n$/, '')}
                {...props}
              />
            ) : (
              <code className={className} {...props}>
                {children}
              </code>
            );
          },
          table({ children }) {
            return (
              <table className="px-3 py-1 border border-collapse border-black dark:border-white">
                {children}
              </table>
            );
          },
          th({ children }) {
            return (
              <th className="px-3 py-1 text-white break-words bg-gray-500 border border-black dark:border-white">
                {children}
              </th>
            );
          },
          td({ children }) {
            return (
              <td className="px-3 py-1 break-words border border-black dark:border-white">
                {children}
              </td>
            );
          },

          p({ children }) {
            return <>{children}</>;
          },
          dl({ children }) {
            // 去重children 数组中的空格
            const newChildren = children.filter((item) => {
              return item !== '\n';
            });
            return <dl className="">{newChildren}</dl>;
          },
          ol({ children }) {
            // 去重children 数组中的空格
            const newChildren = children.filter((item) => {
              return item !== '\n';
            });
            return <ol className="">{newChildren}</ol>;
          },
          ul({ children }) {
            // console.log('ul children', children);
            // 去重children 数组中的空格
            const newChildren = children?.filter((item) => {
              return item !== '\n';
            });
            // console.log('ul newChildren', newChildren);
            return <ul className="">{newChildren}</ul>;
          },
          li({ children }) {
            // console.log('li children', children);
            // 去重children 数组中的空格
            const newChildren = children?.filter((item) => {
              return item !== '\n';
            });
            // console.log('li newChildren', newChildren);
            return <li>{newChildren}</li>;
          },
          a({ children, ...props }) {
            return (
              <>
                {/* 1.处理 链接中的空格*/}
                {children[0] === '详细' ? null : (
                  <a
                    className="text-blue-500 underline cursor-pointer hover:text-blue-700 "
                    href={props.href}
                    target="_blank"
                    rel="noreferrer"
                  >
                    {children}
                  </a>
                )}
              </>
            );
          },
          img({ src, alt }) {
            // img 拼接url
            return (
              <img
                src={src}
                alt={alt}
                className="max-w-full h-auto lg:max-w-[800px] cursor-zoom-in pt-5 pb-5"
                onClick={(event) => {
                  event.preventDefault();
                  setModalImage(src!);
                  setShowModal(true);
                }}
              />
            );
          },
        }}
      >
        {content}
      </MemoizedReactMarkdown>
      <ImageModal /> {/* 渲染图片预览模态框 */}
    </div>
  );
};

export default Markdown;
阅读 221
2 个回答

目测大家好像都放任页面抖动。因为输出的时候限于流式输出,没法做到准确预测内容布局,只能实时修改页面。

不过你可以适当缩小重绘的范围,让页面保持固定,只有当前组件重绘,应该会好一些。

可以使用 useMemo 来缓存图片组件
我把你代码修改了一下
修改主要集中在使用 useMemo 缓存图片组件 MemoizedImage,并在 ReactMarkdown 的 components 中使用这个缓存的组件。

import { FC, memo, useState, useMemo } from 'react';
// @ts-ignore
import ReactMarkdown, { Options } from 'react-markdown';
import rehypeMathjax from 'rehype-mathjax';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { CodeBlock } from './CodeBlock';

export const MemoizedReactMarkdown: FC<Options> = memo(
  ReactMarkdown,
  (prevProps, nextProps) => prevProps.children === nextProps.children,
);

const Markdown = ({
  content,
  inStreaming,
  pending,
  className,
}: {
  content: string;
  inStreaming?: boolean;
  pending?: boolean;
  className?: string;
}) => {
  const [showModal, setShowModal] = useState(false); // 控制模态框显示的状态
  const [modalImage, setModalImage] = useState(''); // 存储模态框中要显示的图片的 URL

  // 图片预览模态框的 JSX 结构
  const ImageModal = () => (
    <div
      className="fixed top-0 bottom-0 left-0 right-0 z-[9999] flex items-center justify-center bg-black bg-opacity-50"
      style={{ display: showModal ? 'flex' : 'none' }}
      onClick={() => setShowModal(false)}
    >
      <div
        className="relative max-h-full overflow-y-auto max-w-[95%] lg:max-w-[85%]"
        onClick={(event) => event.stopPropagation()} // 阻止事件冒泡
      >
        <img src={modalImage} alt="Preview" className="max-w-full" />
      </div>
      <span
        className="absolute m-2 text-white top-10 right-10 text-[36px] cursor-pointer"
        onClick={() => setShowModal(false)}
      >
        ×
      </span>
    </div>
  );

  const MemoizedImage = useMemo(() => {
    return ({ src, alt }) => (
      <img
        src={src}
        alt={alt}
        className="max-w-full h-auto lg:max-w-[800px] cursor-zoom-in pt-5 pb-5"
        onClick={(event) => {
          event.preventDefault();
          setModalImage(src!);
          setShowModal(true);
        }}
      />
    );
  }, []);

  return (
    <div>
      <MemoizedReactMarkdown
        className={className}
        remarkPlugins={[remarkGfm, remarkMath]}
        rehypePlugins={[rehypeMathjax]}
        components={{
          code({ node, inline, className, children, ...props }) {
            if (children.length) {
              if (children[0] === '▍') {
                return (
                  <div className="inline-block">
                    {inStreaming && !pending ? (
                      <span className="mt-1 cursor-default">▍</span>
                    ) : null}
                  </div>
                );
              }

              children[0] = (children[0] as string).replace('`▍`', '▍');
            }

            const match = /language-(\w+)/.exec(className || '');

            return !inline ? (
              <CodeBlock
                key={Math.random()}
                language={(match && match[1]) || ''}
                value={String(children).replace(/\n$/, '')}
                {...props}
              />
            ) : (
              <code className={className} {...props}>
                {children}
              </code>
            );
          },
          table({ children }) {
            return (
              <table className="px-3 py-1 border border-collapse border-black dark:border-white">
                {children}
              </table>
            );
          },
          th({ children }) {
            return (
              <th className="px-3 py-1 text-white break-words bg-gray-500 border border-black dark:border-white">
                {children}
              </th>
            );
          },
          td({ children }) {
            return (
              <td className="px-3 py-1 break-words border border-black dark:border-white">
                {children}
              </td>
            );
          },

          p({ children }) {
            return <>{children}</>;
          },
          dl({ children }) {
            // 去重children 数组中的空格
            const newChildren = children.filter((item) => {
              return item !== '\n';
            });
            return <dl className="">{newChildren}</dl>;
          },
          ol({ children }) {
            // 去重children 数组中的空格
            const newChildren = children.filter((item) => {
              return item !== '\n';
            });
            return <ol className="">{newChildren}</ol>;
          },
          ul({ children }) {
            // console.log('ul children', children);
            // 去重children 数组中的空格
            const newChildren = children?.filter((item) => {
              return item !== '\n';
            });
            // console.log('ul newChildren', newChildren);
            return <ul className="">{newChildren}</ul>;
          },
          li({ children }) {
            // console.log('li children', children);
            // 去重children 数组中的空格
            const newChildren = children?.filter((item) => {
              return item !== '\n';
            });
            // console.log('li newChildren', newChildren);
            return <li>{newChildren}</li>;
          },
          a({ children, ...props }) {
            return (
              <>
                {/* 1.处理 链接中的空格*/}
                {children[0] === '详细' ? null : (
                  <a
                    className="text-blue-500 underline cursor-pointer hover:text-blue-700 "
                    href={props.href}
                    target="_blank"
                    rel="noreferrer"
                  >
                    {children}
                  </a>
                )}
              </>
            );
          },
          img({ src, alt }) {
            return <MemoizedImage src={src} alt={alt} />;
          },
        }}
      >
        {content}
      </MemoizedReactMarkdown>
      <ImageModal /> {/* 渲染图片预览模态框 */}
    </div>
  );
};

export default Markdown;
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏