掘金文章归档 · 第 5 篇 react-markdown 系列 4/4 React / 代码高亮

基于 react-markdown 实现对大模型输出展示(四):输出代码及高亮展示

基础代码展示 → rehype-highlight 语法高亮 → react-syntax-highlighter 自定义复制 / 主题切换

原文作者:随意_(掘金) | juejin.cn/post/7478940561845452800 | 发布于 2025-03-07 | 阅读 802 · 约 2 分钟

0背景:系列收官,补上代码展示

我们实现了基础的 md 展示、自定义标签、自定义图表的展示,接下来我们学习下如何展示代码。这一篇是 react-markdown 系列的收官篇:md 展示 + 自定义标签 + 图表 + 代码,大模型输出的能力闭环。

1基础代码展示:md 代码围栏默认就能渲染

在 markdown 内容里直接放 ```javascript 代码围栏,react-markdown 默认就能渲染出代码块:

import React, { useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css";
import * as echarts from 'echarts';

// 添加大段代码示例
const codeExample = `
function add(a, b) {
    return a + b;
}

const result = add(5, 3);
console.log(result);
`;

const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]

## 二级标题:Markdown 丰富示例 [[2,'我是2的id']]

### 三级标题:Markdown 丰富示例 [[3,'我是3的id']]

### 代码示例
\`\`\`javascript
${codeExample}
\`\`\`
`;

const replaceReferences = (str) => {
    // 定义正则表达式
    const regex = /\[\[(\d+),'(.*?)'\]\]/g;

    // 使用 replace 方法进行全局替换
    return str.replace(regex, (match, num, id) => {
        return `<sup className="text-blue-600 cursor-pointer" data-supid="${id}">[${num}]</sup>`;
    });
}

const regStr = replaceReferences(richMarkdownContent);

// 自定义渲染器
const components = {
    sup: ({ children, ...rest }) => {
        return (
            <sup className="text-active" onClick={(event) => handleSupClick(event)} {...rest}>
                {children}
            </sup>
        );
    },
};

// 点击事件处理函数
const handleSupClick = (event) => {
    const supid = event.target.dataset.supid;
    console.log("Clicked sup data-supid:", supid);
    // 你可以在这里进行其他操作,比如将内容传递给父组件等
};

const App = () => {
    return (
        <div className="markdown-body">
            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={components}>
                {regStr}
            </ReactMarkdown>
        </div>
    );
};

export default App;
✅ 可见支持基本的代码展示(默认有深色代码块样式,来自 github-markdown-css)。
截图位置请对照原文:页面出现 add 函数代码块,底色为深色。

2大段代码高亮:highlight.js + rehype-highlight

2.1 安装

npm install highlight.js -S
npm install rehype-highlight -S

2.2 在 rehypePlugins 里追加 rehypeHighlight

import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeHighlight from 'rehype-highlight';
import "github-markdown-css";
import 'highlight.js/styles/github.css';

// 增加大段代码示例
const codeExample = `
// 定义一个复杂的 JavaScript 类
class ComplexNumber {
    constructor(real, imaginary) {
        this.real = real;
        this.imaginary = imaginary;
    }

    // 加法方法
    add(other) {
        return new ComplexNumber(
            this.real + other.real,
            this.imaginary + other.imaginary
        );
    }

    // 减法方法
    subtract(other) {
        return new ComplexNumber(
            this.real - other.real,
            this.imaginary - other.imaginary
        );
    }

    // 乘法方法
    multiply(other) {
        return new ComplexNumber(
            this.real * other.real - this.imaginary * other.imaginary,
            this.real * other.imaginary + this.imaginary * other.real
        );
    }
}
`;

const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
### 代码示例
\`\`\`javascript
${codeExample}
\`\`\`
`;

const replaceReferences = (str) => {
    // 定义正则表达式
    const regex = /\[\[(\d+),'(.*?)'\]\]/g;

    // 使用 replace 方法进行全局替换
    return str.replace(regex, (match, num, id) => {
        return `<sup className="text-blue-600 cursor-pointer" data-supid="${id}">[${num}]</sup>`;
    });
}

const regStr = replaceReferences(richMarkdownContent);

// 自定义渲染器
const components = {
    sup: ({ children, ...rest }) => {
        return (
            <sup className="text-active" onClick={(event) => handleSupClick(event)} {...rest}>
                {children}
            </sup>
        );
    },
};

// 点击事件处理函数
const handleSupClick = (event) => {
    const supid = event.target.dataset.supid;
    console.log("Clicked sup data-supid:", supid);
    // 你可以在这里进行其他操作,比如将内容传递给父组件等
};

const App = () => {
    return (
        <div className="markdown-body">
            <ReactMarkdown
                remarkPlugins={[remarkGfm]}
                rehypePlugins={[rehypeRaw, rehypeHighlight]}
                components={components}
            >
                {regStr}
            </ReactMarkdown>
        </div>
    );
};

export default App;
📌 关键变化:rehypePlugins={[rehypeRaw, rehypeHighlight]} 多了 rehypeHighlight;同时引入 highlight.js 的 github 风格 CSS(import 'highlight.js/styles/github.css')。rehypeHighlight 会自动识别代码块语言并加高亮 class。
截图位置请对照原文:ComplexNumber 类代码出现关键字/字符串/注释等彩色高亮。

3自定义复制、主题切换:react-syntax-highlighter 版

要支持"复制代码 + 亮暗主题切换",就得自定义 code 组件。借助 react-syntax-highlighter(Prism 版)+ antd + CopyToClipboard:

3.1 CodeBlock 组件

import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
  materialLight,
  materialOceanic
} from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Button, Popover, Space } from 'antd';
import { CopyOutlined, BulbOutlined, BulbFilled } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';

// 代码块组件
function CodeBlock({ code, language = '' }) {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div style={{ position: 'relative', margin: '16px 0' }}>
      <div style={{
        position: 'absolute',
        right: 8,
        top: 8,
        zIndex: 1,
        backgroundColor: 'rgba(255, 255, 255, 0.2)',
        borderRadius: 4,
        padding: 4
      }}>
        <Space>
          <Button
            type="text"
            size="small"
            icon={isDarkTheme ? <BulbFilled /> : <BulbOutlined />}
            onClick={() => setIsDarkTheme(!isDarkTheme)}
          />
          <Popover open={copied} content="已复制!" trigger={[]}>
            <CopyToClipboard text={code} onCopy={handleCopy}>
              <Button type="text" size="small" icon={<CopyOutlined />} />
            </CopyToClipboard>
          </Popover>
        </Space>
      </div>

      <SyntaxHighlighter
        language={language.toLowerCase()}
        style={isDarkTheme ? materialOceanic : materialLight}
        customStyle={{
          padding: '40px 20px 20px',
          borderRadius: 8,
          fontSize: 14,
          overflowX: 'auto'
        }}
        PreTag="div"
      >
        {code.trim()}
      </SyntaxHighlighter>
    </div>
  );
}

// Markdown 渲染器组件
export default function MarkdownCodeBlock({ content }) {
  const components = {
    code({ node, inline, className, children, ...props }) {
      const match = /language-(\w+)/.exec(className || '');
      return !inline && match ? (
        <CodeBlock
          code={String(children).replace(/\n$/, '')}
          language={match[1]}
        />
      ) : (
        <code className={className} {...props}>
          {children}
        </code>
      );
    }
  };

  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      components={components}
    >
      {content}
    </ReactMarkdown>
  );
}

3.2 使用示例

import MarkdownCodeBlock from './CodeBlock';

const App = () => {
    const markdown = `
  ## 示例代码
  \`\`\`js
  console.log('Hello World');
  \`\`\`

  \`\`\`css
  .container {
    padding: 20px;
  }
  \`\`\`
  `;

  return (
    <div className="doc-container" style={{ maxWidth: 800, margin: '0 auto' }}>
      <MarkdownCodeBlock content={markdown} />
    </div>
  );
};

export default App;
📌 核心分发逻辑:code 渲染函数里用 /language-(\w+)/ 从 className 里取语言,块级代码(!inline && match)走 CodeBlock 富组件,行内代码走默认 <code>。这就是上一篇说过的"CodeNode 分发器"思想的代码块版。
截图位置请对照原文:代码块右上角出现"亮/暗主题切换 + 复制"按钮,点复制出现"已复制!"提示。

4总结:大模型输出的四件套

基于大模型的输出,几乎也就这么点功能,大差不差:md 基础展示 → 样式美化 → 自定义标签 / 事件 → 图表 → 代码及高亮。有更好的实现方式可以分享(原文源码可下载,dev4 分支)。

篇目能力关键插件 / 组件
第 1 篇md 渲染 + 美化 + htmlreact-markdown + github-markdown-css + remark-gfm + rehype-raw
第 2 篇自定义标签 + 事件正则替换 + components 自定义渲染器
第 3 篇echarts 报表md 里嵌容器 div + useEffect + echarts.init
第 4 篇(本文)代码 + 高亮 + 复制/主题rehype-highlight 或 react-syntax-highlighter + antd
⚠️ 两套高亮方案按需选:rehype-highlight 轻量、零组件成本,适合"够用就好";react-syntax-highlighter 可控性高,适合要"复制按钮 / 主题切换 / 自定义样式"的场景。

5术语表 & 附录:完整可运行代码

术语表

术语定义
rehype-highlight基于 highlight.js 的 rehype 插件,自动为代码块加语法高亮 class
highlight.js轻量语法高亮库,提供多种主题 CSS
react-syntax-highlighterReact 代码高亮组件(Prism / hljs 双版本),支持主题与自定义容器
inline / block code行内代码(`x`)vs 块级代码(``` 围栏),渲染路径不同
language-(\w+)从代码块 className 中提取语言名的正则,react-markdown 的语言标记格式
CopyToClipboard复制到剪贴板的 React 组件(react-copy-to-clipboard)

附录 A:完整可运行代码(高亮 + 复制 + 主题切换)

// ========== 安装依赖 ==========
// npm i react-syntax-highlighter -S
// npm i antd @ant-design/icons -S
// npm i react-copy-to-clipboard -S
// 已有:react-markdown + remark-gfm

import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { materialLight, materialOceanic } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Button, Popover, Space } from 'antd';
import { CopyOutlined, BulbOutlined, BulbFilled } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';

function CodeBlock({ code, language = '' }) {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div style={{ position: 'relative', margin: '16px 0' }}>
      {/* 右上角工具栏:主题切换 + 复制 */}
      <div style={{
        position: 'absolute', right: 8, top: 8, zIndex: 1,
        backgroundColor: 'rgba(255, 255, 255, 0.2)', borderRadius: 4, padding: 4
      }}>
        <Space>
          <Button type="text" size="small"
            icon={isDarkTheme ? <BulbFilled /> : <BulbOutlined />}
            onClick={() => setIsDarkTheme(!isDarkTheme)} />
          <Popover open={copied} content="已复制!" trigger={[]}>
            <CopyToClipboard text={code} onCopy={handleCopy}>
              <Button type="text" size="small" icon={<CopyOutlined />} />
            </CopyToClipboard>
          </Popover>
        </Space>
      </div>

      <SyntaxHighlighter
        language={language.toLowerCase()}
        style={isDarkTheme ? materialOceanic : materialLight}
        customStyle={{ padding: '40px 20px 20px', borderRadius: 8, fontSize: 14, overflowX: 'auto' }}
        PreTag="div"
      >
        {code.trim()}
      </SyntaxHighlighter>
    </div>
  );
}

export default function MarkdownCodeBlock({ content }) {
  const components = {
    code({ node, inline, className, children, ...props }) {
      const match = /language-(\w+)/.exec(className || '');
      return !inline && match ? (
        <CodeBlock code={String(children).replace(/\n$/, '')} language={match[1]} />
      ) : (
        <code className={className} {...props}>{children}</code>
      );
    }
  };

  return (
    <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
      {content}
    </ReactMarkdown>
  );
}