<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>React 的 Markdown 渲染 · Cytrogen 的个人博客</title><meta name="description" content="本文是一篇在 React 中实现 Markdown 渲染的实战教程,重点解决了新版 react-markdown 库中代码高亮的难题。文章首先介绍了如何使用 react-markdown 和 remark-gfm 插件来支持 GitHub 风格的 Markdown(如表格、删除线)。核心部分针对 react-markdown 新版废弃 inline 属性导致旧教程失效的问题,提供了一个创新的解决方案:通过自定义 pre 组件来准确识别并使用 react-syntax-highlighter 渲染代码块。本文为希望在 React 项目中实现现代化 Markdown 渲染的开发者提供了一套与时俱进的实用方法。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/c0fc.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/c0fc.html">永久链接</a><div class="p-summary visually-hidden"><p>项目需求中有一个功能是支持 Markdown 渲染,尽量仿照 ChatGPT、Claude 的效果。</p>
<p>该文章的目的是记录我在实现这个功能时遇到的问题和解决方案。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/JavaScript/">JavaScript</a><a class="p-category" href="../tags/React-js/">React.js</a></div><h1 class="post-title p-name">React 的 Markdown 渲染</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-02-21T04:11:13.000Z">2/20/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:55.001Z"></time></div><div class="post-content e-content"><html><head></head><body><p>项目需求中有一个功能是支持 Markdown 渲染,尽量仿照 ChatGPT、Claude 的效果。</p>
<p>该文章的目的是记录我在实现这个功能时遇到的问题和解决方案。</p>
<span id="more"></span>
<h2 id="react-markdown"><a class="markdownIt-Anchor" href="#react-markdown"></a> <code>react-markdown</code></h2>
<p>先安装 <code>react-markdown</code>。</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install react-markdown</span><br></pre></td></tr></tbody></table></figure>
<p>此处运用 <code>react-markdown</code> 库的官方示例中的文本来进行测试:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br></pre></td><td class="code"><pre><span class="line"># A demo of `react-markdown`</span><br><span class="line"></span><br><span class="line">`react-markdown` is a markdown component for React.</span><br><span class="line"></span><br><span class="line">👉 Changes are re-rendered as you type.</span><br><span class="line"></span><br><span class="line">👈 Try writing some markdown on the left.</span><br><span class="line"></span><br><span class="line">## Overview</span><br><span class="line"></span><br><span class="line">* Follows [CommonMark](https://commonmark.org)</span><br><span class="line">* Optionally follows [GitHub Flavored Markdown](https://github.github.com/gfm/)</span><br><span class="line">* Renders actual React elements instead of using `dangerouslySetInnerHTML`</span><br><span class="line">* Lets you define your own components (to render `MyHeading` instead of `'h1'`)</span><br><span class="line">* Has a lot of plugins</span><br><span class="line"></span><br><span class="line">## Contents</span><br><span class="line"></span><br><span class="line">Here is an example of a plugin in action</span><br><span class="line">([`remark-toc`](https://github.com/remarkjs/remark-toc)).</span><br><span class="line">**This section is replaced by an actual table of contents**.</span><br><span class="line"></span><br><span class="line">## Syntax highlighting</span><br><span class="line"></span><br><span class="line">Here is an example of a plugin to highlight code:</span><br><span class="line">[`rehype-highlight`](https://github.com/rehypejs/rehype-highlight).</span><br><span class="line"></span><br><span class="line">```js</span><br><span class="line">import React from 'react'</span><br><span class="line">import ReactDOM from 'react-dom'</span><br><span class="line">import Markdown from 'react-markdown'</span><br><span class="line">import rehypeHighlight from 'rehype-highlight'</span><br><span class="line"></span><br><span class="line">const markdown = `</span><br><span class="line"># Your markdown here</span><br><span class="line">`</span><br><span class="line"></span><br><span class="line">ReactDOM.render(</span><br><span class="line"> <Markdown rehypePlugins={[rehypeHighlight]}>{markdown}</Markdown>,</span><br><span class="line"> document.querySelector('#content')</span><br><span class="line">)</span><br><span class="line">\```</span><br><span class="line"></span><br><span class="line">> Pretty neat, eh?</span><br><span class="line"></span><br><span class="line">## GitHub flavored markdown (GFM)</span><br><span class="line"></span><br><span class="line">For GFM, you can *also* use a plugin:</span><br><span class="line">[`remark-gfm`](https://github.com/remarkjs/react-markdown#use).</span><br><span class="line">It adds support for GitHub-specific extensions to the language:</span><br><span class="line">tables, strikethrough, tasklists, and literal URLs.</span><br><span class="line"></span><br><span class="line">These features **do not work by default**.</span><br><span class="line">👆 Use the toggle above to add the plugin.</span><br><span class="line"></span><br><span class="line">| Feature | Support |</span><br><span class="line">| ---------: | :------------------- |</span><br><span class="line">| CommonMark | 100% |</span><br><span class="line">| GFM | 100% w/ `remark-gfm` |</span><br><span class="line"></span><br><span class="line">~~strikethrough~~</span><br><span class="line"></span><br><span class="line">* [ ] task list</span><br><span class="line">* [x] checked item</span><br><span class="line"></span><br><span class="line">https://example.com</span><br><span class="line"></span><br><span class="line">## HTML in markdown</span><br><span class="line"></span><br><span class="line">⚠️ HTML in markdown is quite unsafe, but if you want to support it, you can</span><br><span class="line">use [`rehype-raw`](https://github.com/rehypejs/rehype-raw).</span><br><span class="line">You should probably combine it with</span><br><span class="line">[`rehype-sanitize`](https://github.com/rehypejs/rehype-sanitize).</span><br><span class="line"></span><br><span class="line"><blockquote></span><br><span class="line"> 👆 Use the toggle above to add the plugin.</span><br><span class="line"></blockquote></span><br><span class="line"></span><br><span class="line">## Components</span><br><span class="line"></span><br><span class="line">You can pass components to change things:</span><br><span class="line"></span><br><span class="line">```markdown</span><br><span class="line">import React from 'react'</span><br><span class="line">import ReactDOM from 'react-dom'</span><br><span class="line">import Markdown from 'react-markdown'</span><br><span class="line">import MyFancyRule from './components/my-fancy-rule.js'</span><br><span class="line"></span><br><span class="line">const markdown = `</span><br><span class="line"># Your markdown here</span><br><span class="line">`</span><br><span class="line"></span><br><span class="line">ReactDOM.render(</span><br><span class="line"> <Markdown</span><br><span class="line"> components={{</span><br><span class="line"> // Use h2s instead of h1s</span><br><span class="line"> h1: 'h2',</span><br><span class="line"> // Use a component instead of hrs</span><br><span class="line"> hr(props) {</span><br><span class="line"> const {node, ...rest} = props</span><br><span class="line"> return <MyFancyRule {...rest} /></span><br><span class="line"> }</span><br><span class="line"> }}</span><br><span class="line"> ></span><br><span class="line"> {markdown}</span><br><span class="line"> </Markdown>,</span><br><span class="line"> document.querySelector('#content')</span><br><span class="line">)</span><br><span class="line">/```</span><br></pre></td></tr></tbody></table></figure>
<div class="danger">
<p>测试文本中也有代码块,但是因为我用的是 Hexo 写的博客,这些原本应该在(我自己设置的)代码块内的文本都被渲染成了代码块。</p>
<p>因此我在每个代码块的后面都加了个斜杠,以使其不被渲染成代码块。你们在测试时可以 <strong>将这些斜杠去掉</strong>。</p>
</div>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">ReactMarkdown</span> <span class="keyword">from</span> <span class="string">"react-markdown"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> markdownContent = <span class="string">`{刚才说的Markdown测试文本}`</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">ReactMarkdown</span>></span>{markdownContent}<span class="tag"></<span class="name">ReactMarkdown</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></></span></span></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>显示出来的效果(记得将 <code>markdownContent</code> 的值替换掉):</p>
<p><img src="/posts/c0fc/1.png" alt="img.png"></p>
<p>可以看出和 Typora 或者 GitHub 的渲染效果有一些差异,比方说代码块和普通文字的样式过于贴近,我们还是更适用于有着明显背景色以及代码高亮的代码块。</p>
<p>并且像是表格、任务列表、删除线等特殊样式也没有被渲染出来。</p>
<h2 id="remark-gfm"><a class="markdownIt-Anchor" href="#remark-gfm"></a> <code>remark-gfm</code></h2>
<p>其实测试文本都告诉了我们为什么:那些是 GFM,也就是 GitHub Flavored Markdown 的特性,而 <code>react-markdown</code> 默认是不支持 GFM 的。</p>
<p>所以我们需要安装 <code>remark-gfm</code> 插件来支持 GFM:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install remark-gfm</span><br></pre></td></tr></tbody></table></figure>
<p><code>react-markdown</code> 引用插件的方式也很简单:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> remarkGfm <span class="keyword">from</span> <span class="string">"remark-gfm"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag"><<span class="name">ReactMarkdown</span> <span class="attr">remarkPlugins</span>=<span class="string">{[remarkGfm]}</span>></span>{markdownContent}<span class="tag"></<span class="name">ReactMarkdown</span>></span></span></span><br></pre></td></tr></tbody></table></figure>
<p>展现出来的效果:</p>
<p><img src="/posts/c0fc/2.png" alt="img.png"></p>
<p>看到这里,你肯定很是疑惑:表格的边框呢??</p>
<p>如果我们用开发者工具查看这个表格的样式,会发现表格确确实实被渲染成 <code>table</code> 标签,但是 User Agent Stylesheet 中的样式对 <code>table</code> 标签做了一些处理。 所以我们需要自己写一些样式来覆盖这些默认样式:</p>
<figure class="highlight css"><figcaption><span>index.CSS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">table</span> {</span><br><span class="line"> <span class="attribute">border-spacing</span>: <span class="number">0</span> <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">border-collapse</span>: collapse <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">border-color</span>: inherit <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">display</span>: block <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">width</span>: max-content <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">max-width</span>: <span class="number">100%</span> <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">overflow</span>: auto <span class="meta">!important</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">tbody</span>, <span class="selector-tag">td</span>, <span class="selector-tag">tfoot</span>, <span class="selector-tag">th</span>, <span class="selector-tag">thead</span>, <span class="selector-tag">tr</span> {</span><br><span class="line"> <span class="attribute">border-color</span>: inherit <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">border-style</span>: solid <span class="meta">!important</span>;</span><br><span class="line"> <span class="attribute">border-width</span>: <span class="number">2px</span> <span class="meta">!important</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>把所有的样式统统加上 <code>!important</code>,这样就可以覆盖掉 User Agent Stylesheet 中的样式了。</p>
<p>再次展现出来的效果:</p>
<p><img src="/posts/c0fc/3.png" alt="img.png"></p>
<div class="danger">
<p>HTML 支持因为安全性考虑,我决定不使用,所以这里就不再展示了。</p>
</div>
<p>这样我们就完成了一个简单的 Markdown 渲染功能。不过在这个需求中有一个最为重要的功能:代码块的高亮。</p>
<h2 id="代码块的高亮"><a class="markdownIt-Anchor" href="#代码块的高亮"></a> 代码块的高亮</h2>
<p>因为项目的要求,代码块必须是突显出来、语句高亮的,这对行内代码块也一样。</p>
<p>这里我们可以使用 <code>react-syntax-highlighter</code>。</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install react-syntax-highlighter</span><br></pre></td></tr></tbody></table></figure>
<p><code>react-syntax-highlighter</code> 具有两个引擎:<code>prism</code> 和 <code>highlight.js</code>。两者的区别详细可以直接去网上搜索。</p>
<p>这里我们使用 <code>prism</code> 引擎以及 <code>oneDark</code> 主题:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Prism</span> <span class="keyword">as</span> <span class="title class_">SyntaxHighlighter</span> } <span class="keyword">from</span> <span class="string">"react-syntax-highlighter"</span>;</span><br><span class="line"><span class="keyword">import</span> { oneDark } <span class="keyword">from</span> <span class="string">"react-syntax-highlighter/dist/esm/styles/prism"</span>;</span><br></pre></td></tr></tbody></table></figure>
<p>在 <code>ReactMarkdown</code> 的 <code>components</code> 属性中,我们可以自定义代码块的渲染方式。如果你看过网上的其他教程,近乎清一色的都是这样写的:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><<span class="title class_">ReactMarkdown</span></span><br><span class="line"> remarkPlugins={[remarkGfm]}</span><br><span class="line"> components={{</span><br><span class="line"> <span class="title function_">code</span>(<span class="params">{ node, inline, className, children, ...props }</span>) {</span><br><span class="line"> <span class="keyword">const</span> match = <span class="regexp">/language-(\w+)/</span>.<span class="title function_">exec</span>(className || <span class="string">''</span>)</span><br><span class="line"> <span class="keyword">return</span> !inline && match ? (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">SyntaxHighlighter</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{nightOwl}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">language</span>=<span class="string">{match[1]}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">PreTag</span>=<span class="string">"div"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">children</span>=<span class="string">{String(children).replace(/\n$/,</span> '')}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> {<span class="attr">...props</span>}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> /></span></span></span><br><span class="line"> ) : (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">code</span> <span class="attr">className</span>=<span class="string">{className}</span> {<span class="attr">...props</span>} <span class="attr">children</span>=<span class="string">{children}</span> /></span></span></span><br><span class="line"> )</span><br><span class="line"> }</span><br><span class="line"> }}</span><br><span class="line">></span><br><span class="line"> {markdownContent}</span><br><span class="line"></<span class="title class_">ReactMarkdown</span>></span><br></pre></td></tr></tbody></table></figure>
<p>这里我们自定义了 <code>code</code> 组件的渲染行为。</p>
<blockquote>
<p><code>code({ node, inline, className, children, ...props }) {}</code> 中的参数们分别表示了:</p>
<ul>
<li><code>node</code>:当前节点</li>
<li><code>inline</code>:是否是行内代码块</li>
<li><code>className</code>:类名</li>
<li><code>children</code>:子节点(代码块中的内容)</li>
<li><code>...props</code>:其他属性</li>
</ul>
</blockquote>
<p>接着我们使用正则表达式来匹配 <code>className</code> 中的 <code>language-xxx</code>,如果匹配到并且不是行内代码块,就使用 <code>SyntaxHighlighter</code> 来渲染代码块,否则使用默认的 <code>code</code> 标签。</p>
<p><em><strong>但是!!!!</strong></em></p>
<p><code>inline</code> 属性在新版本的 <code>react-markdown</code> 中已经被废弃、不会作为参数传入了!!!</p>
<p>这让当时的我非常头疼,网上的资料翻了翻也没有找到解决方案,一时选择去解决另一个问题:没有定义语言的代码块的渲染。</p>
<p>代码块被渲染后,和行内代码块最大的区别是外面贴了一套 <code>pre</code> 标签。由于 <code>code</code> 标签作为子标签无法从 <code>props</code> 中获取到父标签的样式,但是反过来想,<code>pre</code> 标签是可以获取到 <code>code</code> 标签的样式的呀!</p>
<p>于是我嘎嘎乱写:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><<span class="title class_">Markdown</span></span><br><span class="line"> remarkPlugins={[remarkGfm]}</span><br><span class="line"> components={{</span><br><span class="line"> <span class="title function_">pre</span>(<span class="params">{node, className, children, ...props}</span>) {</span><br><span class="line"> <span class="keyword">if</span> (children[<span class="string">"type"</span>] === <span class="string">"code"</span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> match = children[<span class="string">"props"</span>][<span class="string">"className"</span>].<span class="title function_">match</span>(<span class="regexp">/language-(\w+)/</span>)</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">pre</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">SyntaxHighlighter</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{oneDark}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">language</span>=<span class="string">{match[1]}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">PreTag</span>=<span class="string">"div"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">showLineNumbers</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">wrapLongLines</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">children</span>=<span class="string">{String(children[</span>"<span class="attr">props</span>"]["<span class="attr">children</span>"])<span class="attr">.replace</span>(/\<span class="attr">n</span>$/, '')}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">pre</span>></span></span></span><br><span class="line"> )</span><br><span class="line"> } <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">pre</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">SyntaxHighlighter</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">style</span>=<span class="string">{oneDark}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">language</span>=<span class="string">"python"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">PreTag</span>=<span class="string">"div"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">showLineNumbers</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">wrapLongLines</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> <span class="attr">children</span>=<span class="string">{String(children[</span>"<span class="attr">props</span>"]["<span class="attr">children</span>"])<span class="attr">.replace</span>(/\<span class="attr">n</span>$/, '')}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml"> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">pre</span>></span></span></span><br><span class="line"> )</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }}</span><br><span class="line">></span><br><span class="line"> {markdownContent}</span><br><span class="line"></<span class="title class_">Markdown</span>></span><br></pre></td></tr></tbody></table></figure>
<p>直接在 <code>pre</code> 标签中判断 <code>children</code> 的类型,如果是 <code>code</code> 那就肯定是代码块了,然后再去匹配 <code>className</code> 中的语言类型。</p>
<p>如果匹配不到就会导致错误,所以我临时加了一个 <code>try...catch</code> 语句,匹配不到的话就默认渲染成 <code>python</code> 语言的代码块吧。</p>
<p>效果还是不错的:</p>
<p><img src="/posts/c0fc/4.png" alt="img.png"></p>
<h2 id="后话"><a class="markdownIt-Anchor" href="#后话"></a> 后话</h2>
<p>这个需求的实现还是有一些问题的,比方说行内代码块的渲染就没有解决、 <code>try...catch</code> 语句并不是一个好的解决方案,以及没有定义语言就默认渲染成 <code>python</code> 语言的代码块的技术债太鬼畜了!</p>
<p>但是由于项目的时间紧迫,我也没有再去深究这个问题。或许以后时间充裕了我会再去解决这个问题。以后再说吧。</p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="de2c.html">上一篇</a><a class="next" href="14d5.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/c0fc.html" data-full-url="https://cytrogen.icu/posts/c0fc.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>暂无 Webmentions</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>