Markdown 里直接放 echarts 容器 div → rehype-raw 渲染成 DOM → useEffect 里 echarts.init 画图
上一篇文章我们学习了如何借助自定义 components 来实现自定义的标签和事件,这篇文章我们更进一步,学习下如何渲染 echarts 等报表,由浅入深。
直接给出代码了,相信能看懂,下面有代码解释:
import React, { useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css";
import * as echarts from 'echarts';
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
## 二级标题:Markdown 丰富示例 [[2,'我是2的id']]
### 三级标题:Markdown 丰富示例 [[3,'我是3的id']]
<div id="echarts-container" style="width: 600px; height: 400px;"></div>
`;
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 = () => {
const chartRef = useRef(null);
useEffect(() => {
// 获取 ECharts 容器元素
const chartContainer = document.getElementById('echarts-container');
if (chartContainer) {
// 初始化 ECharts 实例
const myChart = echarts.init(chartContainer);
// 指定图表的配置项和数据
const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line'
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
// 组件卸载时销毁 ECharts 实例
return () => {
myChart.dispose();
};
}
}, []);
return (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={components}>
{regStr}
</ReactMarkdown>
</div>
);
};
export default App;
<div id="echarts-container"> 被 rehype-raw 真实渲染成 DOM;useEffect 在组件挂载后能获取到这个节点,并用 echarts.init + setOption 画出折线图。一个 md 里放两个容器,用 flex 排布,两个 useEffect 各初始化一个图表(折线图 + 柱状图):
import React, { useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css";
import * as echarts from 'echarts';
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
## 二级标题:Markdown 丰富示例 [[2,'我是2的id']]
### 三级标题:Markdown 丰富示例 [[3,'我是3的id']]
<div style="display:flex">
<div id="echarts-container-1" style="width: 600px; height: 400px;"></div>
<div id="echarts-container-2" style="width: 600px; height: 400px;"></div>
</div>
`;
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 = () => {
const chartRef1 = useRef(null);
const chartRef2 = useRef(null);
useEffect(() => {
// 获取第一个 ECharts 容器元素
const chartContainer1 = document.getElementById('echarts-container-1');
if (chartContainer1) {
// 初始化第一个 ECharts 实例
const myChart1 = echarts.init(chartContainer1);
// 第一个图表的配置项和数据
const option1 = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line'
}]
};
// 使用刚指定的配置项和数据显示第一个图表。
myChart1.setOption(option1);
// 组件卸载时销毁第一个 ECharts 实例
return () => {
myChart1.dispose();
};
}
}, []);
useEffect(() => {
// 获取第二个 ECharts 容器元素
const chartContainer2 = document.getElementById('echarts-container-2');
if (chartContainer2) {
// 初始化第二个 ECharts 实例
const myChart2 = echarts.init(chartContainer2);
// 第二个图表的配置项和数据
const option2 = {
xAxis: {
type: 'category',
data: ['A', 'B', 'C', 'D', 'E']
},
yAxis: {
type: 'value'
},
series: [{
data: [300, 400, 200, 500, 100],
type: 'bar'
}]
};
// 使用刚指定的配置项和数据显示第二个图表。
myChart2.setOption(option2);
// 组件卸载时销毁第二个 ECharts 实例
return () => {
myChart2.dispose();
};
}
}, []);
return (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={components}>
{regStr}
</ReactMarkdown>
</div>
);
};
export default App;
我们可以基于自定义标签来实现报表,可以借此实现特殊的交互,对于多数其他标签有更特殊的需求,可以自己来实现。一家之言,有需要的按需取舍(原文可下载项目,dev3 分支)。
| 上一篇(系列 2) | 本篇(系列 3) |
|---|---|
| 自定义 sup 标签 + 点击事件 | 自定义 div 容器 + 图表渲染 |
| components 接管标签渲染 | rehype-raw 渲染容器 + useEffect 初始化 |
| event.target.dataset 取自定义属性 | document.getElementById 取容器节点 |
| 术语 | 定义 |
|---|---|
| echarts.init(dom) | 在指定 DOM 容器上初始化一个 ECharts 实例 |
| setOption(option) | 给图表实例设置配置项和数据,完成绘制 |
| dispose() | 销毁 ECharts 实例,释放资源(组件卸载时必做) |
| useEffect | React 副作用钩子,适合在 DOM 挂载后操作真实节点 |
| chartRef | useRef 创建的引用(本篇实际用 id 查找容器,ref 为辅助写法) |
| rehype-raw | 放行 markdown 中原始 HTML 的 rehype 插件,图表容器的"落地前提" |
// ========== 安装依赖 ==========
// npm i echarts -S(在上一篇依赖基础上)
// 已有:react-markdown + remark-gfm + rehype-raw + github-markdown-css
import React, { useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css";
import * as echarts from 'echarts';
// 1. md 内容里"埋"一个图表容器 div(rehype-raw 会把它渲染成真实 DOM)
const markdownContent = `
# 一周数据趋势
<div id="echarts-container" style="width: 600px; height: 400px;"></div>
`;
const App = () => {
useEffect(() => {
// 2. DOM 挂载后:找到容器 → 初始化 → setOption
const chartContainer = document.getElementById('echarts-container');
if (!chartContainer) return;
const myChart = echarts.init(chartContainer);
myChart.setOption({
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
yAxis: { type: 'value' },
series: [{ data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line' }]
});
// 3. 组件卸载时销毁实例,防止内存泄漏
return () => myChart.dispose();
}, []);
return (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{markdownContent}
</ReactMarkdown>
</div>
);
};
export default App;
未完待续……下一篇(系列 4)我们学习如何展示代码及高亮。