正则把约定格式转成 sup 上标 → 借助 rehype-raw 渲染 → 用 components 自定义渲染器接管点击事件
上一篇文章我们基于 react-markdown 并引用 remark-gfm、rehype-raw、github-markdown-css 来处理复杂 md 格式、处理 html 标签和美化样式。本篇章我们来实现自定义标签和事件触发。
举例:例如后端跟我们约定,他们流式返回数据,我们接到后拼装展示,但是有一些特殊标识,比如说引用,类似于参考文献,一般是右上角上标,点击会跳转详情或者其他操作。
这里我们就需要:自定义标签 + 触发事件。暂定我们约定后端返回的数据特殊格式如下,第一个是引用的顺序,第二个是引用的 id:
[[1,'引用1的id']]
我们需要判定此格式:
<sup> 标签,并自定义样式;以上规则,demo 代码及展示如下:
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css"
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
## 二级标题:Markdown 丰富示例 [[1,'我是1的id']]
### 三级标题:Markdown 丰富示例 [[1,'我是1的id']]
`;
const App = () => {
return (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{richMarkdownContent}
</ReactMarkdown>
</div>
);
};
export default App;
[[1,'我是1的id']] 则按照普通字符串展示。下面我们按需截取特殊约定字符。正则截取,我们将顺序回显,id 作为自定义属性添加到元素上,借助 rehype-raw 渲染出来自定义的标签。注意自定义属性 data-supid:
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css"
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
## 二级标题:Markdown 丰富示例 [[1,'我是1的id']]
### 三级标题:Markdown 丰富示例 [[1,'我是1的id']]
`;
const replaceReferences = (str) => {
// 定义正则表达式
const regex = /\[\[(\d+),'(.*?)'\]\]/g;
// 使用 replace 方法进行全局替换
return str.replace(regex, (match, num, id) => {
return `<sup className="text-active cursor-pointer" data-supid="${id}">[${num}]</sup>`;
});
}
const regStr = replaceReferences(richMarkdownContent)
const App = () => {
return (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{regStr}
</ReactMarkdown>
</div>
);
};
export default App;
对于创建的元素,可以使用 tailwindcss 修改样式,或者其他自定义样式均可,和普通 html 样式无异:
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>`;
});
}
text-blue-600 颜色设置,上标现在变成蓝色、可点击外观。借助 react-markdown 的 components 来自定义标签:
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css"
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
## 二级标题:Markdown 丰富示例 [[2,'我是2的id']]
### 三级标题:Markdown 丰富示例 [[3,'我是3的id']]
`;
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;
两个关键点:
...rest 传递其他属性,确保 data-supid 属性被正确传递;event.target.dataset.supid 获取自定义属性 data-supid 的值;打印获取到的值,你可以在这里进行其他操作,比如将内容传递给父组件等。这样,当你点击 sup 元素时,就可以获取到自定义属性 data-supid 的值了。
这篇文章我们基于自定义的 sup 标签,借助自定义 components,结合正则来实现了文章前期的需求。可以发现功能很强大,对于多数其他标签有更特殊的需求,可以自己来实现。一家之言,有需要的按需取舍(原文可下载项目,dev2 分支)。
| 步骤 | 做什么 | 用到的能力 |
|---|---|---|
| 1 | 后端约定特殊格式(如 [[序号,'id']]) | 协议约定 |
| 2 | 正则把约定格式替换成带自定义属性的 HTML | String.replace + 正则捕获组 |
| 3 | 让这段 HTML 真正渲染出来 | rehype-raw(放行原生 HTML) |
| 4 | 自定义样式 | Tailwind 类名(text-blue-600 等) |
| 5 | 接管该标签的渲染与点击 | components 自定义渲染器 + dataset 取自定义属性 |
| 术语 | 定义 |
|---|---|
| components | react-markdown 的组件映射对象,key 是标签名(如 sup),value 是自定义渲染组件 |
| rehype-raw | 允许 markdown 中的原始 HTML 被解析渲染,本方案的"HTML 落地"依赖它 |
| data-* 属性 | HTML5 自定义属性,可存任意字符串,前端通过 dataset 读取 |
| event.target.dataset | 读取元素上所有 data-* 自定义属性的 DOM API |
| 正则捕获组 | 正则中 () 分组匹配的内容,replace 回调里依次作为参数传入 |
| ...rest | JS 扩展运算符,把剩余 props 透传给子元素,保证 data-supid 等属性不丢 |
// ========== 安装依赖 ==========
// 基于上一篇:react-markdown + remark-gfm + rehype-raw + github-markdown-css
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css";
// 1. 后端约定的特殊格式:[[序号,'引用id']]
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
## 二级标题:Markdown 丰富示例 [[2,'我是2的id']]
### 三级标题:Markdown 丰富示例 [[3,'我是3的id']]
`;
// 2. 正则替换:把约定格式转成带 data-supid 的 sup 标签
const replaceReferences = (str) => {
const regex = /\[\[(\d+),'(.*?)'\]\]/g;
return str.replace(regex, (match, num, id) => {
return `<sup className="text-blue-600 cursor-pointer" data-supid="${id}">[${num}]</sup>`;
});
};
const regStr = replaceReferences(richMarkdownContent);
// 3. components 接管 sup 的渲染:挂 onClick,透传 ...rest
const components = {
sup: ({ children, ...rest }) => {
return (
<sup className="text-active" onClick={(event) => handleSupClick(event)} {...rest}>
{children}
</sup>
);
},
};
// 4. 点击时通过 dataset 取出自定义属性
const handleSupClick = (event) => {
const supid = event.target.dataset.supid;
console.log("Clicked sup data-supid:", supid);
// 可以在这里把 id 传给父组件 / 跳转详情等
};
const App = () => {
return (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={components}>
{regStr}
</ReactMarkdown>
</div>
);
};
export default App;
未完待续……下一篇(系列 3)我们学习如何渲染 echarts 等报表,由浅入深;第 4 篇输出代码及高亮展示。