用 React + Taro 实现 Markdown AST 渲染器 支持长按复制

45 阅读2分钟

用 React + Taro 实现 Markdown AST 渲染器

在微信小程序或者跨端项目中,我们经常需要把 Markdown 内容渲染成可交互的页面。在本文中,我分享一个基于 React + Taro 的 AST 渲染器实现方案,它可以把 Markdown AST 转化为可复制的文本、图片、地图等原生组件,同时支持列表、表格和行内样式。

功能概览

  1. 支持文本渲染:纯文本或带行内样式(粗体、斜体、链接)。
  2. 支持块级组件:如图片、地图、分隔线、代码块、标题、列表、表格。
  3. 行内与块级元素区分:优化纯文本渲染性能,减少不必要的 View 包裹。
  4. 可复制文本:Text 标签添加 userSelect="text",保证内容可长按复制。
  5. 适用于 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>;
  }
}

亮点总结

  1. 层级优化:纯文本直接顶层 Text,提高性能。
  2. 完整 Markdown 支持:块级、行内、列表、表格、代码、地图、图片。
  3. 可复制文本:所有 Text 标签加 userSelect="text"
  4. Taro 跨端适配:同时支持小程序和 Web。
  5. 易扩展:新的节点类型只需在 renderNoderenderInlineNode 添加即可。

小结

通过这个 ASTRenderer,我们可以把 Markdown AST 转化为可交互、可复制、跨端的原生组件渲染器。相比直接渲染 HTML,这种方式:

  • 更适合小程序场景。
  • 更灵活,支持自定义组件。
  • 更安全,不依赖 dangerouslySetInnerHTML

如果你正在做小程序的 Markdown 渲染或者跨端文档阅读器,这个方案值得参考。