在 Refine 应用中集成 React Markdown 编辑器:从 @uiw/react-md-editor 到高级定制与安全防护
本篇文章聚焦如何在 Refine(一款用于构建内部工具、管理后台与 B2B 应用的 React 框架)应用中集成 @uiw/react-md-editor(下文简称 React markdown editor),完成从项目初始化、组件接入、工具栏定制、KaTeX / Mermaid 预览、Markdown 安全净化到性能优化的完整实战。读完本文,你将掌握在 Refine 的 create / edit 表单中嵌入所见即所得 Markdown 编辑器的标准姿势,并理解其底层渲染机制与可复用的高级定制方案。
背景:为什么选择 Markdown 编辑器
在 2004 年 John Gruber 发明 Markdown 之前,WYSIWYG(所见即所得)编辑器是网站内容编辑的主流。Markdown 以极简的纯文本语法替代了繁琐的 HTML 书写,今天已成为全球最流行的标记语言之一,被大量需要文本格式化能力的企业级 Web 应用广泛采用。
React markdown editor(uiw/react-md-editor)正是为 React 生态打造的一款简洁而强大的 Markdown 编辑库,由 UIW(React UI components)团队开发。它提供友好的、可定制的编辑界面,内置语法高亮、格式化工具栏、实时预览以及将 Markdown 渲染为 HTML 的能力。它区别于其他 React Markdown 库的最大特点是拥有独立的 preview 预览窗格,用户可以在编辑的同时即时看到内容效果。
项目初始化:用 Refine 脚手架搭建示例应用
本文示例将使用 Refine 创建 React 应用。Refine 作为 headless 企业级 Web 应用开发框架,可以配合多种 UI 库使用;我们使用它的预生成页面来演示如何把 Markdown 编辑器融入一个接近真实业务的应用(博客文章管理)中。
使用 npm create refine-app 交互式初始化项目,本教程不需要复杂配置,选择以下选项即可:
✔ Choose a project template · refine-react
✔ What would you like to name your project?: · refine-markdown
✔ Choose your backend service to connect: · REST API
✔ Do you want to use a UI Framework?: · Ant Design
✔ Do you want to add example pages?: · No
初始化完成后进入项目目录并启动:
npm install
npm run dev
开发服务器会自动在默认浏览器新标签页打开应用;如果没有自动打开,可以手动访问 http://localhost:5173。
仓库中与该教程一一对应的可运行示例位于 examples/blog-refine-markdown,其 package.json 展示了完整的依赖清单:@refinedev/antd、@refinedev/simple-rest(REST API 数据提供者)、antd、@uiw/react-md-editor(^4.0.8)等。应用入口 App.tsx 注册了 blog_posts 与 categories 两个资源,并通过 dataProvider={dataProvider("https://api.fake-rest.refine.dev")} 连接演示 REST API,表单页面、列表页面和详情页面分别对应 /blog-posts/create、/blog-posts/edit/:id、/blog-posts/show/:id 路由。
使用 MDEditor 组件
安装依赖
在你的项目中将 React markdown editor 作为依赖安装:
npm i @uiw/react-md-editor
在 create / edit 表单中接入
在示例中,我们把 MDEditor 用在应用的 create 与 edit 页面(即表单所在页面)。引入组件、移除原有 Textarea,并插入如下代码:
import MDEditor from "@uiw/react-md-editor";
...
<Form.Item
label={translate("blog_posts.fields.content")}
name="content"
rules={[
{
required: true,
},
]}
>
<MDEditor data-color-mode="light" />
</Form.Item>;
...
这会渲染一个原生 Textarea 元素,并附带 Markdown 编辑能力与预览窗格。
仓库中的完整实现可以直接参考 create.tsx 与 edit.tsx:两者都通过 useForm<IPost>()(来自 @refinedev/antd)驱动表单,MDEditor 被包裹在 Form.Item name="content" 中,与标题、状态、分类等字段一同参与表单的提交与回填。
value 与 onChange:受控与不受控两种用法
在多数场景下,以下两个 props 就足以渲染一个功能完整的 Markdown 编辑器:
value:指定 Markdown 内容的初始值或当前值;onChange:处理 Markdown 内容的变化。
但在上面的示例中我们并没有显式使用这两个 props,应用依然工作正常——这是因为 Ant Design 的 Form 组件与 React markdown editor 包做了无缝集成:表单会自动读写 Markdown 值,无需额外处理。
在普通的 React 应用中,则需要自己用 state 捕获并存储 Markdown 值,再把它赋给编辑器的 value 和 onChange props,以保证编辑器与内容同步:
import React from "react";
import MDEditor from "@uiw/react-md-editor";
export default function App() {
const [value, setValue] = React.useState("");
return (
<div className="container">
<MDEditor value={value} onChange={setValue} />
</div>
);
}
此外,该包还提供若干用于定制工具栏、扩展功能的 props,常用列举如下:
commandsextraCommandspreviewOptionsenableScrollpreview
详情页的 Markdown 渲染:MarkdownField
编辑之外,Refine 的 antd 包还内置了 MarkdownField 组件,用于在 show 详情页把 Markdown 内容渲染为 HTML。仓库实现位于 packages/antd/src/components/fields/markdown/index.tsx:
export const MarkdownField: React.FC<RefineFieldMarkdownProps> = ({
value = "",
}) => {
return (
<ReactMarkdown
remarkPlugins={[gfm] as unknown as ReactMarkdown.PluggableList}
>
{value}
</ReactMarkdown>
);
};
可以看到 MarkdownField 基于 react-markdown 构建,并内置了 remark-gfm 插件以支持 GitHub Flavored Markdown(GFM,如表格、删除线、任务列表等扩展语法)。对应测试 index.spec.tsx 复用 @refinedev/ui-tests 的 fieldMarkdownTests 来验证组件行为。示例应用中的 show.tsx 正是用 <MarkdownField value={record?.content} /> 来展示文章正文。这与 @uiw/react-md-editor 的预览窗格共同构成了“编辑—预览—展示”的完整闭环。
自定义工具栏
默认工具栏对于起步来说已经足够全面;如果需要进一步定制,可以使用 commands 与 extraCommands 两个 props,按需实现自定义功能、扩展编辑器能力。
commands prop
commands prop 用于定制工具栏中展示的命令,接收一个由命令对象组成的数组。一旦提供 commands,默认工具栏就会被完全替换。例如声明一个空数组即可清空所有默认命令:
<MDEditor commands={[]} data-color-mode="light" />
也可以把预定义命令以对象形式传入数组,实现只保留部分命令的效果:
<MDEditor commands={[commands.bold, commands.italic]} data-color-mode="light" />
以上代码只会渲染 bold 和 italic 两个命令。
还可以通过定义带有特定属性的对象来创建自定义命令。命令对象的主要属性包括:
name:命令名称;keyCommand:与命令关联的按键命令;buttonProps:为命令添加无障碍属性(如aria-label);Icon:为工具栏命令设置图标;execute:为命令绑定事件或动作。
例如,为工具栏添加一个 help 命令,点击后打开 Refine 文档:
const help = {
name: "help",
keyCommand: "help",
buttonProps: { "aria-label": "insert help" },
icon: (
<svg viewBox="0 0 16 16" width="12px" height="12px">
<path
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8Zm.9 13H7v-1.8h1.9V13Zm-.1-3.6v.5H7.1v-.6c.2-2.1 2-1.9 1.9-3.2.1-.7-.3-1.1-1-1.1-.8 0-1.2.7-1.2 1.6H5c0-1.7 1.2-3 2.9-3 2.3 0 3 1.4 3 2.3.1 2.3-1.9 2-2.1 3.5Z"
fill="currentColor"
/>
</svg>
),
execute: () => {
window.open("https://refine.dev/", "_blank");
},
};
return (
<Form.Item
label={translate("blog_posts.fields.content")}
name="content"
rules={[
{
required: true,
},
]}
>
<MDEditor
commands={[commands.bold, commands.italic, help]}
data-color-mode="light"
/>
</Form.Item>
);
这里创建了 help 对象,指定 name、keyCommand、buttonProps 与 icon,并在 execute 中绑定点击事件——在新窗口打开 Refine 文档:
execute: () => {
window.open("https://refine.dev/", "_blank");
},
最后把它加入 MDEditor 的 commands 数组:
<MDEditor
commands={[commands.bold, commands.italic, help]}
data-color-mode="light"
/>
extraCommands prop
extraCommands 与 commands 目的相同,都是定义工具栏命令的对象数组;区别在于它用于向工具栏追加命令,且这些命令位于工具栏右侧。工具栏上的 preview 与 fullscreen 命令就是预置的 extra commands。
可以采用与 commands 相同的方式添加自定义 extra commands:
<MDEditor
commands={[commands.bold, commands.italic, help]}
extraCommands={[
commands.title1,
commands.title2,
commands.codePreview,
commands.codeEdit,
]}
data-color-mode="light"
/>
同样,既可以赋值预定义命令对象,也可以创建自定义命令。下面示例来自官方文档,演示如何通过 EditorContext 把一个“切换预览/编辑”的复合按钮注入到 extraCommands:
import React, { useContext } from "react";
import MDEditor, { commands, EditorContext } from "@uiw/react-md-editor";
const Button = () => {
const { preview, dispatch }: { preview?: any; dispatch?: any } =
useContext(EditorContext);
const click = () => {
dispatch({
preview: preview === "edit" ? "preview" : "edit",
});
};
if (preview === "edit") {
return (
<svg width="12" height="12" viewBox="0 0 520 520" onClick={click} />
);
}
return (
<svg width="12" height="12" viewBox="0 0 520 520" onClick={click} />
);
};
const codePreview = {
name: "preview",
keyCommand: "preview",
value: "preview",
icon: <Button />,
};
return (
<Form.Item
label={translate("blog_posts.fields.content")}
name="content"
rules={[
{
required: true,
},
]}
>
<MDEditor
commands={[commands.bold, commands.italic, help]}
extraCommands={[codePreview]}
data-color-mode="light"
/>
</Form.Item>
);
这里通过 EditorContext 获取当前 preview 状态并 dispatch 动作,用条件判断把 preview 与 edit 两种预览功能合并到一个命令中:点击后编辑器会在 edit 与 preview 状态间来回切换。
添加自定义预览:KaTeX 与 Mermaid
Markdown 编辑器可以胜任复杂的计算型编辑任务,包括渲染 TeX 数学公式、由文本生成图表与流程图。React markdown editor 默认不含这些功能,但提供了与 kaTeX、mermaid preview 等库集成的途径。
安装 kaTeX 依赖:
npm install katex
KaTeX 预览
kaTeX 是一个用于在 Web 上渲染 TeX 数学表达式的 JavaScript 库。React markdown editor 将 kaTeX 作为插件来预览数学表达式。
首先在组件(本例中的 create 与 edit 文件)中导入包及其配套样式:
import katex from "katex";
import "katex/dist/katex.css";
然后通过给 MDEditor 赋 previewOptions prop,让编辑器把 kaTeX 表达式格式化并预览为数学公式:
<MDEditor
data-color-mode="light"
previewOptions={{
components: {
code: ({ inline, children = [], className, ...props }) => {
const txt = children[0] || "";
if (inline) {
if (typeof txt === "string" && /^\$\$(.*)\$\$/.test(txt)) {
const html = katex.renderToString(
txt.replace(/^\$\$(.*)\$\$/, "$1"),
{
throwOnError: false,
},
);
return <code dangerouslySetInnerHTML={{ __html: html }} />;
}
return <code>{txt}</code>;
}
const code =
props.node && props.node.children
? getCodeString(props.node.children)
: txt;
if (
typeof code === "string" &&
typeof className === "string" &&
/^language-katex/.test(className.toLocaleLowerCase())
) {
const html = katex.renderToString(code, {
throwOnError: false,
});
return (
<code
style={{ fontSize: "150%" }}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
return <code className={String(className)}>{txt}</code>;
},
},
}}
/>
这段代码定制了 code 组件在遇到行内或块级代码时的行为:如果行内代码包含 kaTeX 表达式(以 $$ 包裹),使用 kaTeX 渲染;如果块级代码带有 language-katex class,同样交给 kaTeX 渲染;其余情况按普通文本显示。
注意:
previewOptions的components覆盖机制,与 Refine antd 包中MarkdownField基于react-markdown的components定制(见 index.tsx)是同一套渲染扩展思路,可用于自定义任意 Markdown 元素的渲染结果。
Mermaid 预览库的集成方式类似,可按照其对应文档中的步骤完成。
高级定制
以下思路可以帮助我们对 Markdown 编辑器进行深度改造、添加高级定制能力。
语法高亮定制
引入 Prism.js 可以获得更精细的语法高亮:
import ReactMarkdown from "react-markdown";
import Prism from "prismjs";
import "prismjs/themes/prism-tomorrow.css";
function MarkdownEditor({ content }) {
return (
<ReactMarkdown
children={content}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
return !inline && match ? (
<pre className={className} style={{ backgroundColor: "#282c34" }}>
<code
dangerouslySetInnerHTML={{
__html: Prism.highlight(
children,
Prism.languages[match[1]],
match[1],
),
}}
/>
</pre>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
/>
);
}
这段代码使用 Prism.js 为 Markdown 编辑器中的代码块做语法高亮。
Markdown 语法扩展
使用 markdown-it 插件扩展 Markdown 语法支持(例如 emoji 与脚注):
import MarkdownIt from "markdown-it";
import emoji from "markdown-it-emoji";
import footnote from "markdown-it-footnote";
const md = new MarkdownIt().use(emoji).use(footnote);
const MarkdownWithExtensions = ({ content }) => (
<div dangerouslySetInnerHTML={{ __html: md.render(content) }} />
);
这样编辑器便获得了 emoji 与脚注的支持。
主题与自定义命令
还可以为 Markdown 编辑器添加新的工具栏命令或主题。下面的 MyCustomCommand 通过 api.replaceSelection 在光标处插入文本,实现“一键插入定制内容”:
import MDEditor, { commands } from "@uiw/react-md-editor";
const MyCustomCommand = {
name: "custom",
keyCommand: "custom",
buttonProps: { "aria-label": "Add Custom" },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<circle cx="10" cy="10" r="10" fill="currentColor" />
</svg>
),
execute: (state, api) => {
api.replaceSelection("**Custom Content**");
},
};
<MDEditor
commands={[...commands, MyCustomCommand]}
value={value}
onChange={setValue}
/>;
净化 Markdown:防范 XSS 攻击
Markdown 输入在浏览器中渲染前需要被解析为 HTML 元素。然而这个解析过程可能引入跨站脚本(XSS)攻击漏洞——恶意用户可以向网页注入客户端脚本,绕过同源策略等访问控制。
为降低风险,必须通过移除潜在危险的 HTML 标签与属性来净化 Markdown 文本,在保证用户输入被正确格式化的同时,不牺牲应用安全性。
rehype-sanitize 插件
为了防止恶意脚本进入输入区域,我们可以在渲染前净化解析出的 HTML。这正是 rehype-sanitize 的用途——它是 React markdown editor 包用于编辑器内安全净化的插件,提供可靠的 HTML 净化能力以降低安全风险。
安装:
npm install rehype-sanitize
在 create 与 edit 文件中导入:
import rehypeSanitize from "rehype-sanitize";
最后把它作为值传给 previewOptions 的 rehypePlugins 属性:
<MDEditor
data-color-mode="light"
previewOptions={{
rehypePlugins: [[rehypeSanitize]],
}}
/>
接入后,如果在编辑器中粘贴恶意代码,rehype-sanitize 会在内容预览前将其移除,从而保证渲染出的 HTML 是安全的。
优化 Markdown 编辑器性能
针对编辑器的性能优化,可以从减少加载时间、控制主包体积与降低渲染负担几个方向入手。
懒加载编辑器
在不需要立即使用 Markdown 编辑器的页面上,可以通过懒加载把编辑器的加载推迟到用户交互时,减少首屏加载时间:
import React, { Suspense, lazy } from "react";
const MDEditor = lazy(() => import("@uiw/react-md-editor"));
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading editor...</div>}>
<MDEditor />
</Suspense>
</div>
);
}
该脚本只在真正需要时、延后加载 Markdown 编辑器,从而节省页面初始加载时间。
代码分割与动态导入
进一步地,可以结合动态导入做代码分割,把代码拆成小块、按需加载:
const loadEditor = async () => {
import("@uiw/react-md-editor").then((MDEditor) => {
// 加载完成后设置编辑器或触发重新渲染以更新状态
});
};
按需动态导入 Markdown 编辑器,有助于保持主包体积小巧、整体提升性能。
大文档优化:虚拟滚动
对于大体积 Markdown 文档,可以让编辑器只渲染可见区域,例如借助虚拟滚动:
import { FixedSizeList as List } from "react-window";
const LargeDocument = ({ lines }) => {
return (
<List height={500} itemCount={lines.length} itemSize={20} width={"100%"}>
{({ index, style }) => <div style={style}>{lines[index]}</div>}
</List>
);
};
通过 react-window 只渲染大文档的可见行,可以显著降低浏览器在大文档上的渲染负担。
用户输入防抖
对大文档实现防抖(debounce),避免频繁更新,让编辑器更实时、更灵敏——本质是通过减少不必要的重新渲染来提升响应性:
import { useState, useCallback } from "react";
import debounce from "lodash.debounce";
function MarkdownEditor() {
const [content, setContent] = useState("");
const handleChange = useCallback(
debounce((value) => {
setContent(value);
}, 300),
[],
);
return <MDEditor value={content} onChange={handleChange} />;
}
这段代码会“节流”状态更新:用户停止输入 300ms 后才真正更新 state,从而避免无谓的重渲染、提升性能。
结论
本文系统梳理了使用 uiw/react-md-editor 在 Refine 应用中集成 Markdown 编辑器的完整路径:从脚手架初始化、MDEditor 接入 create / edit 表单、value/onChange 受控用法与 MarkdownField 展示组件,到 commands/extraCommands 工具栏定制、KaTeX / Mermaid 自定义预览、Prism.js 语法高亮、markdown-it 语法扩展,再到 rehype-sanitize 安全净化和懒加载、虚拟滚动、输入防抖等性能优化。
围绕 Markdown 编辑器还有很多值得探索的进阶特性。结合本仓库可进一步研读的参考材料包括:示例应用(含 create.tsx、edit.tsx、show.tsx)、MarkdownField 源码 及其 测试用例,以及在 App.tsx 中查看资源注册与路由配置。深入官方文档并动手运行示例,是掌握更多用法的最佳途径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00