用 React + Taro 实现 Markdown AST 渲染器
在微信小程序或者跨端项目中,我们经常需要把 Markdown 内容渲染成可交互的页面。在本文中,我分享一个基于 React + Taro 的 AST 渲染器实现方案,它可以把 Markdown AST 转化为可复制的文本、图片、地图等原生组件,同时支持列表、表格和行内样式。
功能概览
- 支持文本渲染:纯文本或带行内样式(粗体、斜体、链接)。
- 支持块级组件:如图片、地图、分隔线、代码块、标题、列表、表格。
- 行内与块级元素区分:优化纯文本渲染性能,减少不必要的 View 包裹。
- 可复制文本:Text 标签添加
userSelect="text",保证内容可长按复制。 - 适用于 Taro 跨端:既可在小程序渲染,也可适配 Web。
ASTRenderer 核心思路
1. 判断节点类型
通过函数判断节点是块级还是行内:
function isBlockNode(node: ASTNode) {
return [ 'map', 'image', 'code_block', 'fence', 'hr', 'heading', 'paragraph', 'bullet_list', 'ordered_list', 'list_item', 'table', 'thead', 'tbody', 'tr', 'th', 'td' ].includes(node.type);
}
function isAllTextNodes(nodes: ASTNode[]): boolean {
return nodes.every(n => !isBlockNode(n) && (!n.children || isAllTextNodes(n.children)));
}
- 块级节点:单独占行,需要
View包裹。 - 行内节点:文本、加粗、斜体、链接等,可以直接在
Text中渲染。
2. 优化纯文本渲染
如果整篇内容都是文本,我们可以直接用最外层 Text 渲染,减少层级:
if (isAllTextNodes(nodes)) {
return (
<Text userSelect="text">
{nodes.map((n, i) => renderInlineNode(n, `${keyPrefix}-${i}`))}
</Text>
);
}
3. 渲染块级节点
块级节点使用 renderNode 函数渲染,不同类型对应不同组件和样式:
function renderNode(node: ASTNode, key: string): React.ReactNode {
switch (node.type) {
case 'image':
return (
<View key={key} style={{ marginVertical: 4 }}>
<Image src={node.attrs?.src} style={{ width: 200, height: 200 }} />
</View>
);
case 'heading':
const level = node.attrs?.level || 1;
const fontSize = [0, 24, 20, 18, 16, 14, 12][level] || 16;
return (
<Text key={key} style={{ fontSize, fontWeight: 'bold', display: 'block', marginVertical: 4 }} userSelect="text">
{node.children?.map((c, i) => renderInlineNode(c, `${key}-${i}`))}
</Text>
);
case 'paragraph':
if (isParagraphAllText(node)) {
return (
<Text key={key} userSelect="text" style={{ display: 'block', lineHeight: 1.5, marginBottom: 8 }}>
{node.children?.map((c, i) => renderInlineNode(c, `${key}-${i}`))}
</Text>
);
} else {
return (
<View key={key} style={{ display: 'flex', flexDirection: 'column', marginBottom: 8 }}>
{node.children?.map((child, i) =>
isBlockNode(child) ? renderNode(child, `${key}-${i}`) : renderInlineNode(child, `${key}-${i}`)
)}
</View>
);
}
// 其他块级元素如列表、表格、地图等...
}
}
4. 渲染行内节点
行内节点主要包括文本、加粗、斜体、链接等:
function renderInlineNode(node: ASTNode, key: string): React.ReactNode {
switch (node.type) {
case 'text':
case 'code_inline':
return <Text key={key}>{node.text || ''}</Text>;
case 'strong':
return <Text key={key} style={{ fontWeight: 'bold' }}>{node.children?.map((c, i) => renderInlineNode(c, `${key}-${i}`))}</Text>;
case 'em':
return <Text key={key} style={{ fontStyle: 'italic' }}>{node.children?.map((c, i) => renderInlineNode(c, `${key}-${i}`))}</Text>;
case 'link':
return (
<Text
key={key}
style={{ color: '#1a0dab' }}
onClick={() => node.attrs?.href && wx.navigateTo({ url: node.attrs.href })}
>
{node.children?.map((c, i) => renderInlineNode(c, `${key}-${i}`))}
</Text>
);
default:
return <Text key={key}>{node.text || ''}</Text>;
}
}
亮点总结
- 层级优化:纯文本直接顶层
Text,提高性能。 - 完整 Markdown 支持:块级、行内、列表、表格、代码、地图、图片。
- 可复制文本:所有
Text标签加userSelect="text"。 - Taro 跨端适配:同时支持小程序和 Web。
- 易扩展:新的节点类型只需在
renderNode或renderInlineNode添加即可。
小结
通过这个 ASTRenderer,我们可以把 Markdown AST 转化为可交互、可复制、跨端的原生组件渲染器。相比直接渲染 HTML,这种方式:
- 更适合小程序场景。
- 更灵活,支持自定义组件。
- 更安全,不依赖
dangerouslySetInnerHTML。
如果你正在做小程序的 Markdown 渲染或者跨端文档阅读器,这个方案值得参考。