react-markdown
一个用于渲染 Markdown 的 React 组件。
功能亮点
- 默认安全 (没有使用
dangerouslySetInnerHTML
或 XSS 攻击风险) - 组件 (传入你自己的组件来替代
<h2>
以渲染## hi
) - 插件(你可以选择使用众多插件)
- 兼容性(100% 兼容 CommonMark,使用插件后100% 兼容 GFM)
目录
- 这是什么?
- 什么时候应该使用它?
- 安装
- 使用
- API
- 示例
- 使用插件
- 使用带选项的插件
- 使用自定义组件(语法高亮)
- [使用 remark 和 rehype 插件(数学公式)](#使用 remark 和 rehype 插件(数学公式) "#use-remark-and-rehype-plugins-math")
- 插件
- 语法
- 类型
- 兼容性
- 架构
- [附录 A:Markdown 中的 HTML](#附录 A:Markdown 中的 HTML "#appendix-a-html-in-markdown")
- [附录 B:组件](#附录 B:组件 "#appendix-b-components")
- [附录 C:Markdown(和 JSX)中的换行符](#附录 C:Markdown(和 JSX)中的换行符 "#appendix-c-line-endings-in-markdown-and-jsx")
- 安全性
- 相关项目
- 贡献
- 许可证
这是什么?
这个包是一个 React 组件,可以接收一个 Markdown 字符串,并将其安全地渲染为 React 元素。 你可以传入插件来改变 Markdown 的转换方式,并传入组件来代替普通的 HTML 元素。
什么时候应该使用它?
市面上有其他在 React 中使用 Markdown 的方式,那么为什么要使用这个呢? 主要有三个原因是:其他方式通常依赖于 dangerouslySetInnerHTML
,处理 Markdown 时有错误,或不允许你将元素替换为组件。 react-markdown
构建了一个虚拟 DOM,因此 React 只替换变化的部分,从语法树开始。 得益于我们使用了 unified,特别是 remark 处理 Markdown 和 rehype 处理 HTML,这些是流行的插件化内容转换工具。
这个包专注于让初学者在 React 中安全地使用 Markdown。 当你熟悉 unified 时,可以手动使用基于 modern hooks 的替代方案 react-remark
或 rehype-react
。 如果你想在 Markdown 文件内部 使用 JavaScript 和 JSX,请使用 MDX。
安装
此包仅支持 ESM。 在 Node.js(版本 16+)中,使用 npm 安装:
sh
npm install react-markdown
在 Deno 中使用 esm.sh
:
js
import Markdown from 'https://esm.sh/react-markdown@9'
在浏览器中使用 esm.sh
:
html
<script type="module">
import Markdown from 'https://esm.sh/react-markdown@9?bundle'
</script>
使用
一个简单的 Hello World:
jsx
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
const markdown = '# Hi, *Pluto*!'
createRoot(document.body).render(<Markdown>{markdown}</Markdown>)
显示等效的 JSX
jsx
<h1>
Hi, <em>Pluto</em>!
</h1>
这是一个展示如何使用插件的例子(remark-gfm
,它增加了对脚注、删除线、表格、任务列表和直接 URL 的支持):
jsx
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
const markdown = `Just a link: www.nasa.gov.`
createRoot(document.body).render(
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
)
显示等效的 JSX
jsx
<p>
Just a link: <a href="http://www.nasa.gov">www.nasa.gov</a>.
</p>
API
此包输出以下标识符:defaultUrlTransform
。 默认导出为 Markdown
。
Markdown
用于渲染 Markdown 的组件。
参数
options
(Options
) --- 属性
返回值
React 元素(JSX.Element
)。
defaultUrlTransform(url)
使 URL 安全。
参数
url
(string
) --- URL
返回值
安全的 URL(string
)。
AllowElement
过滤元素的函数(TypeScript 类型)。
参数
node
(hast
中的Element
) --- 待检查的元素index
(number | undefined
) ---element
在parent
中的索引parent
(hast
中的Node
) ---element
的父元素
返回值
是否允许 element
(boolean
,可选)。
Components
将标签名映射到组件的映射(TypeScript 类型)。
类型
ts
import type {Element} from 'hast'
type Components = Partial<{
[TagName in keyof JSX.IntrinsicElements]:
// 类组件:
| (new (props: JSX.IntrinsicElements[TagName] & ExtraProps) => JSX.ElementClass)
// 函数组件:
| ((props: JSX.IntrinsicElements[TagName] & ExtraProps) => JSX.Element | string | null | undefined)
// 标签名:
| keyof JSX.IntrinsicElements
}>
ExtraProps
我们传递给组件的额外字段(TypeScript 类型)。
字段
node
(hast
中的Element
, 可选) --- 原始节点
Options
配置(TypeScript 类型)。
字段
allowElement
(AllowElement
, 可选) --- 过滤元素;先使用allowedElements
/disallowedElements
allowedElements
(Array<string>
, 默认:所有标签名)disallowedElements
(Array<string>
, 默认:[]
) --- 不允许的标签名;不能与allowedElements
同时使用children
(string
, 可选) --- Markdown 内容className
(string
, 可选) --- 用这个类名包裹在一个div
中components
(Components
, 可选) --- 将标签名映射到组件rehypePlugins
(Array<Plugin>
, 可选) --- 要使用的 rehype 插件 列表remarkPlugins
(Array<Plugin>
, 可选) --- 要使用的 remark 插件 列表remarkRehypeOptions
(remark-rehype
的Options
, 可选) --- 传递给remark-rehype
的选项skipHtml
(boolean
, 默认:false
) --- 完全忽略 Markdown 中的 HTMLunwrapDisallowed
(boolean
, 默认:false
) --- 提取(解包)不允许元素中的内容;通常情况下,比如说strong
不被允许时,它和它的子元素都会被丢弃,使用unwrapDisallowed
时,元素本身被其子元素替代urlTransform
(UrlTransform
, 默认:defaultUrlTransform
) --- 更改 URL
UrlTransform
转换 URLs 的函数(TypeScript 类型)。
参数
url
(string
) --- URLkey
(string
, 例如:'href'
) --- 属性名node
(hast
中的Element
) --- 待检查的元素
返回值
转换后的 URL(string
,可选)。
示例
使用插件
这个示例展示了如何使用一个 remark 插件。 这里的例子是 remark-gfm
,它增加了对删除线、表格、任务列表和直接 URL 的支持:
jsx
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
const markdown = `一个带有*强调*和**重要性**的段落。
> 一个带有 ~删除线~ 和 URL 的引用块:https://reactjs.org。
* 列表
* [ ] 待办
* [x] 完成
一个表格:
| a | b |
| - | - |
`
createRoot(document.body).render(
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
)
显示等效的 JSX
jsx
<>
<p>
一个带有<em>强调</em>和<strong>重要性</strong>的段落。
</p>
<blockquote>
<p>
一个带有<del>删除线</del>和 URL 的引用块:
<a href="https://reactjs.org">https://reactjs.org</a>。
</p>
</blockquote>
<ul className="contains-task-list">
<li>列表</li>
<li className="task-list-item">
<input type="checkbox" disabled /> 待办
</li>
<li className="task-list-item">
<input type="checkbox" disabled checked /> 完成
</li>
</ul>
<p>一个表格:</p>
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
</table>
</>
使用带选项的插件
这个示例展示了如何使用插件并给它传递选项。 要做到这一点,使用一个数组,插件在第一位,选项在第二位。 remark-gfm
有一个选项可以仅允许使用双波浪线来表示删除线:
jsx
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
const markdown = '这个 ~不是~ 删除线,但是 ~~这个是~~!'
createRoot(document.body).render(
<Markdown remarkPlugins={[[remarkGfm, {singleTilde: false}]]}>
{markdown}
</Markdown>
)
显示等效的 JSX
jsx
<p>
这个 ~不是~ 删除线,但是 <del>这个是</del>!
</p>
使用自定义组件(语法高亮)
这个示例展示了如何通过传递组件来覆盖元素的正常处理方式。 在这种情况下,我们使用了令人惊叹的 [react-syntax-highlighter
][
使用自定义组件(语法高亮)
这个示例展示如何通过传递组件来覆盖元素的正常处理方式。在这个例子中,我们通过令人惊艳的 react-syntax-highlighter
来应用语法高亮,作者是 @conorhastings:
jsx
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'
import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism'
// 你知道你可以在 markdown 中用波浪线代替反引号来表示代码吗?✨
const markdown = `这里有一些 JavaScript 代码:
~~~js
console.log('It works!')
~~~
`
createRoot(document.body).render(
<Markdown
children={markdown}
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<SyntaxHighlighter
style={dark}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
/>
)
显示等效的 JSX
jsx
<>
<p>这里有一些 JavaScript 代码:</p>
<pre>
<SyntaxHighlighter language="js" style={dark} PreTag="div">
console.log('It works!')
</SyntaxHighlighter>
</pre>
</>
使用 remark 和 rehype 插件(数学公式)
这个示例展示如何通过 remark-math
(一个语法扩展插件)来支持 Markdown 中的数学公式,以及如何使用 rehype-katex
(一个转换插件)来渲染这些数学公式。
jsx
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import 'katex/dist/katex.min.css' // `rehype-katex` 不会为你导入 CSS
const markdown = `升力系数($C_L$)是一个无量纲系数。`
createRoot(document.body).render(
<Markdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{markdown}
</Markdown>
)
显示等效的 JSX
jsx
<p>
升力系数 (
<span className="katex">
<span className="katex-mathml">
<math xmlns="http://www.w3.org/1998/Math/MathML">{/* ... */}</math>
</span>
<span className="katex-html" aria-hidden="true">
{/* ... */}
</span>
</span>
) 是一个无量纲系数。
</p>
插件
我们使用 unified,特别是 remark 处理 Markdown 和 rehype 处理 HTML,这些是通过插件转换内容的工具。 这里有三种好方法来查找插件:
awesome-remark
和awesome-rehype
--- 最棒的项目精选- remark 插件列表 和 rehype 插件列表 --- 所有插件的列表
remark-plugin
和rehype-plugin
主题 --- GitHub 上标记的任何仓库
语法
react-markdown
默认遵循 CommonMark,它标准化了 markdown 实现之间的差异。 通过插件支持某些语法扩展。
我们在后台使用 micromark
进行解析。 查看它的文档以获取更多关于 markdown、CommonMark 和扩展的信息。
类型
此包使用 TypeScript 完全类型化。 它导出了额外的类型 AllowElement
、ExtraProps
、Components
、Options
以及 UrlTransform
。
兼容性
由 unified 集团维护的项目与 Node.js 的维护版本兼容。
当我们发布一个新的主要版本时,我们会放弃对 Node.js 不再维护版本的支持。 这意味着我们尝试保持当前的发布线路,react-markdown@^9
,与 Node.js 16 兼容。
他们在所有现代浏览器中都能工作(本质上:所有非 IE 11 的浏览器)。 您可以在项目中使用打包工具(如 esbuild、webpack 或 Rollup),并使用其选项(或插件)为旧版浏览器添加支持。
架构
rust
react-markdown
+----------------------------------------------------------------------------------------------------------------+
| |
| +----------+ +----------------+ +---------------+ +----------------+ +------------+ |
| | | | | | | | | | | |
markdown-+->+ remark +-mdast->+ remark 插件 +-mdast->+ remark-rehype +-hast->+ rehype 插件 +-hast->+ 组件 +-+->react 元素
| | | | | | | | | | | |
| +----------+ +----------------+ +---------------+ +----------------+ +------------+ |
| |
+----------------------------------------------------------------------------------------------------------------+
要理解这个项目做了什么,首先了解 unified 是很重要的:请阅读 unifiedjs/unified
readme 文档(直到你遇到 API 部分为止是必读的)。
react-markdown
是一个封装好的 unified 管道,大多数人不需要直接与 unified 交互。 处理器会经历以下步骤:
- 解析 markdown 到 mdast(markdown 语法树)
- 通过 remark(markdown 生态系统)进行转换
- 将 mdast 转换为 hast(HTML 语法树)
- 通过 rehype(HTML 生态系统)进行转换
- 将 hast 通过组件渲染为 React
附录 A:Markdown 中的 HTML
react-markdown
通常会转义 HTML(或者在设置了 skipHtml
时忽略它),因为这样做是危险的,并且违背了这个库的目的。
然而,如果你处于一个可信的环境(你信任 Markdown),并且不介意增加包大小(大约增加 60kb minzipped),那么你可以使用 rehype-raw
:
jsx
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
const markdown = `<div class="note">
一些*强调*和<strong>加粗</strong>!
</div>`
createRoot(document.body).render(
<Markdown rehypePlugins={[rehypeRaw]}>{markdown}</Markdown>
)
显示等效的 JSX
jsx
<div className="note">
<p>
一些<em>强调</em>和<strong>加粗</strong>!
</p>
</div>
注意 :Markdown 中的 HTML 仍然受限于 CommonMark 中 HTML 的工作方式。 确保在包含 markdown 的块级 HTML 周围使用空行!
附录 B:组件
你还可以改变从 markdown 来的内容:
jsx
<Markdown
components={{
// 将 `h1` (`# heading`) 映射为使用 `h2`。
h1: 'h2',
// 将 `em`s (`*like so*`) 重写为红色前景色的 `i`。
em({node, ...rest}) {
return <i style={{color: 'red'}} {...rest} />
}
}}
/>
components 中的键是你用 markdown 写作时对应的 HTML 元素(例如 h1
对应 # heading
)。 通常在 markdown 中,这些是:a
, blockquote
, br
, code
, em
, h1
, h2
, h3
, h4
, h5
, h6
, hr
, img
, li
, ol
, p
, pre
, strong
, 和 ul
。 使用 remark-gfm
,你还可以使用 del
, input
, table
, tbody
, td
, th
, thead
, 以及 tr
。 其他的 remark 或 rehype 插件,增加对新构造的支持,也可以与 react-markdown
一起使用。
传递的属性是你可能期望的:一个 a
(链接)将获得 href
(和 title
)属性,一个 img
(图片)将获得 src
、alt
和 title
等。
每个组件将接收一个 node
属性。 这是被处理的原始 mdast 或 hast 节点。这可以用来访问未被传递为属性的信息。
如果你想完全控制所有 HTML 标签的表现,可以传递一个完整的组件映射。例如,你也许想要为所有的 h1
、h2
、h3
等设置相同的样式,或者将所有的 pre
和 code
转换为使用语法高亮的组件。
jsx
<Markdown
components={{
h1: ({node, ...props}) => <h2 {...props} />,
h2: ({node, ...props}) => <h2 {...props} />,
h3: ({node, ...props}) => <h2 {...props} />,
// ...
pre: ({node, ...props}) => <div {...props} />,
code: ({node, ...props}) => <CodeBlock {...props} />,
// ...
}}
/>
在这个例子中,所有的标题都会被渲染为 h2
,并且 pre
和 code
会被转换为一个自定义的 CodeBlock
组件,你可以在里面实现语法高亮等功能。
在实际应用中,你可能还需要处理一些更复杂的情况,例如嵌套的元素或者带有特定属性的元素。这时候,访问原始的 mdast 或 hast 节点就非常有用了,因为你可以使用这些信息来决定如何渲染这些组件。
例如,你可能想要根据 mdast 节点中的 align
属性来决定如何渲染一个表格:
jsx
<Markdown
components={{
// ...
table: ({node, ...props}) => {
const align = node.align || [];
// 根据 align 来定制化你的表格渲染
return <CustomTable align={align} {...props} />;
}
// ...
}}
/>
最终的输出会完全依赖于你提供的组件和它们如何使用传递给它们的属性和节点信息。
不要忘记,如果你提供了一个自定义的组件来覆盖默认行为,你需要确保它能正确处理所有可能的输入。这包括处理任何必要的儿童组件,属性,以及遵循 React 的最佳实践,以确保高性能和兼容性。