首页
/ 在 Refine 应用中集成 React Markdown 编辑器:从 @uiw/react-md-editor 到高级定制与安全防护

在 Refine 应用中集成 React Markdown 编辑器:从 @uiw/react-md-editor 到高级定制与安全防护

2026-09-09 15:05:20作者:江焘钦

本篇文章聚焦如何在 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_postscategories 两个资源,并通过 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 用在应用的 createedit 页面(即表单所在页面)。引入组件、移除原有 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.tsxedit.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 值,再把它赋给编辑器的 valueonChange 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,常用列举如下:

  • commands
  • extraCommands
  • previewOptions
  • enableScroll
  • preview

详情页的 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-testsfieldMarkdownTests 来验证组件行为。示例应用中的 show.tsx 正是用 <MarkdownField value={record?.content} /> 来展示文章正文。这与 @uiw/react-md-editor 的预览窗格共同构成了“编辑—预览—展示”的完整闭环。

自定义工具栏

默认工具栏对于起步来说已经足够全面;如果需要进一步定制,可以使用 commandsextraCommands 两个 props,按需实现自定义功能、扩展编辑器能力。

commands prop

commands prop 用于定制工具栏中展示的命令,接收一个由命令对象组成的数组。一旦提供 commands,默认工具栏就会被完全替换。例如声明一个空数组即可清空所有默认命令:

<MDEditor commands={[]} data-color-mode="light" />

也可以把预定义命令以对象形式传入数组,实现只保留部分命令的效果:

<MDEditor commands={[commands.bold, commands.italic]} data-color-mode="light" />

以上代码只会渲染 bolditalic 两个命令。

还可以通过定义带有特定属性的对象来创建自定义命令。命令对象的主要属性包括:

  • 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 对象,指定 namekeyCommandbuttonPropsicon,并在 execute 中绑定点击事件——在新窗口打开 Refine 文档:

execute: () => {
  window.open("https://refine.dev/", "_blank");
},

最后把它加入 MDEditorcommands 数组:

<MDEditor
  commands={[commands.bold, commands.italic, help]}
  data-color-mode="light"
/>

extraCommands prop

extraCommandscommands 目的相同,都是定义工具栏命令的对象数组;区别在于它用于向工具栏追加命令,且这些命令位于工具栏右侧。工具栏上的 previewfullscreen 命令就是预置的 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 动作,用条件判断把 previewedit 两种预览功能合并到一个命令中:点击后编辑器会在 editpreview 状态间来回切换。

添加自定义预览:KaTeX 与 Mermaid

Markdown 编辑器可以胜任复杂的计算型编辑任务,包括渲染 TeX 数学公式、由文本生成图表与流程图。React markdown editor 默认不含这些功能,但提供了与 kaTeXmermaid preview 等库集成的途径。

安装 kaTeX 依赖:

npm install katex

KaTeX 预览

kaTeX 是一个用于在 Web 上渲染 TeX 数学表达式的 JavaScript 库。React markdown editor 将 kaTeX 作为插件来预览数学表达式。

首先在组件(本例中的 createedit 文件)中导入包及其配套样式:

import katex from "katex";
import "katex/dist/katex.css";

然后通过给 MDEditorpreviewOptions 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 渲染;其余情况按普通文本显示。

注意:previewOptionscomponents 覆盖机制,与 Refine antd 包中 MarkdownField 基于 react-markdowncomponents 定制(见 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

createedit 文件中导入:

import rehypeSanitize from "rehype-sanitize";

最后把它作为值传给 previewOptionsrehypePlugins 属性:

<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.tsxedit.tsxshow.tsx)、MarkdownField 源码 及其 测试用例,以及在 App.tsx 中查看资源注册与路由配置。深入官方文档并动手运行示例,是掌握更多用法的最佳途径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395