在 Taro 小程序中实现完整 Markdown 渲染器的实践

在 Taro 小程序中实现完整 Markdown 渲染器的实践

在小程序开发中,展示 Markdown 内容是一项常见需求。特别是当 Markdown 内容既包含纯文本、行内格式(加粗、斜体、链接等),又包含块级元素(图片、地图、表格、代码块、列表等)时,渲染顺序、布局和文本复制问题就变得复杂。本文分享一个完整的 Taro Markdown 渲染器实现方案。


一、功能概览

本方案实现了以下功能:

  1. Markdown → AST 转换

    • 使用 markdown-it 解析 Markdown,生成 Token
    • 转换为 AST(抽象语法树),每个节点包含类型、属性和子节点
  2. 完整 Markdown 渲染

    • 支持段落、标题、列表、表格、代码块、图片、Map
    • 支持行内格式:加粗、斜体、链接、行内代码、软换行/硬换行
  3. 文本优化

    • 段落内纯文本 → 单个 <Text userSelect="text"> 包裹
    • 整篇 Markdown 全是纯文本 → 顶层直接 <Text> 渲染
    • 支持长按复制
  4. 块级元素正确布局

    • Map 自动映射为小程序 <Map>
    • 图片、表格、代码块、列表按顺序渲染
    • 避免显示错位

二、实现思路

1. 将 Markdown 转为 AST

ini 复制代码
import MarkdownIt from 'markdown-it';
import { mdToAST } from './mdToAST';

const md = new MarkdownIt();
const tokens = md.parse(markdownString, {});
const ast = mdToAST(tokens);

说明

  • Token → AST 转换时,保留节点类型、属性和子节点
  • 支持常见 Markdown 语法,包括行内和块级元素

2. AST 渲染原则

  1. 块级元素 → 使用 <View> 或对应组件(如 <Map><Image>)包裹
  2. 行内元素 → 使用 <Text> 渲染
  3. 段落优化 → 段落内部全是行内元素 → 用单 <Text>,减少嵌套
  4. 顶层纯文本优化 → 整篇 Markdown 都是文本 → 顶层直接 <Text>

3. 判断块级和行内元素

scss 复制代码
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)));
}

4. AST 渲染器核心逻辑

typescript 复制代码
import React from 'react';
import { Text, View, Image, Map } from '@tarojs/components';
import { ASTNode } from './mdToAST';

interface RendererProps { nodes: ASTNode[]; keyPrefix?: string; }

export const ASTRenderer: React.FC<RendererProps> = ({ nodes, keyPrefix = 'node' }) => {
  if (isAllTextNodes(nodes)) {
    // 顶层纯文本
    return <Text userSelect="text">{nodes.map((n, i) => renderInlineNode(n, `${keyPrefix}-${i}`))}</Text>;
  }
  return <>{nodes.map((n, i) => renderNode(n, `${keyPrefix}-${i}`))}</>;
};

function renderNode(node: ASTNode, key: string): React.ReactNode {
  switch (node.type) {
    case 'map':
      return <Map key={key} latitude={+node.attrs?.latitude} longitude={+node.attrs?.longitude} style={{ width: '100%', height: 200 }} />;
    case 'image':
      return <Image key={key} src={node.attrs?.src} style={{ width: 200, height: 200 }} />;
    case 'paragraph':
      if (node.children.every(c => !isBlockNode(c))) {
        return <Text key={key} userSelect="text">{node.children.map((c,i) => renderInlineNode(c, `${key}-${i}`))}</Text>;
      } else {
        return <View key={key}>{node.children.map((c,i) => isBlockNode(c) ? renderNode(c, `${key}-${i}`) : renderInlineNode(c, `${key}-${i}`))}</View>;
      }
    default:
      return node.text ? <Text key={key}>{node.text}</Text> : null;
  }
}

function renderInlineNode(node: ASTNode, key: string): React.ReactNode {
  switch(node.type){
    case 'text': 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 'softbreak':
    case 'hardbreak': return <Text key={key}>{'\n'}</Text>;
    default: return node.text ? <Text key={key}>{node.text}</Text> : null;
  }
}

5. 核心亮点

  1. 顶层纯文本优化

    • 整篇 Markdown 直接 <Text> 渲染,轻量且可复制
  2. 段落内部纯文本优化

    • 减少 <View> 包裹,段落轻量化
  3. 块级元素顺序保持

    • Map、图片、表格、列表、代码块按 AST 顺序渲染
    • 避免显示错位
  4. 支持文本长按复制

    • 使用 userSelect="text" 替代 selectable
  5. 避免 React #130 错误

    • 顶层渲染逻辑合理,段落和文本分离

六、使用示例

ini 复制代码
const markdownAST = mdToAST(md.parse(markdownString, {}));

<ASTRenderer nodes={markdownAST} />
  • 整篇纯文本 → 顶层 <Text>
  • 段落内含 Map / 图片<View> 按顺序渲染
  • 文本可长按复制
  • Map 自动渲染为小程序 Map 组件

七、总结

通过将 Markdown 转为 AST,再递归渲染为 Taro 组件,本方案实现了:

  • 完整 Markdown 支持(文本、行内格式、块级元素)
  • 纯文本段落与整篇文本优化
  • 块级元素顺序保持,避免错位
  • 小程序文本可长按复制

这是一套既通用又实用的 Markdown 渲染方案,非常适合在 Taro 小程序中使用。

相关推荐
恋猫de小郭2 小时前
Flutter Riverpod 3.0 发布,大规模重构下的全新状态管理框架
android·前端·flutter
wordbaby2 小时前
用 window.matchMedia 实现高级响应式开发:API 全面解析与实战技巧
前端·javascript
薄雾晚晴2 小时前
Rspack 实战,构建流程升级:自动版本管理 + 命令行美化 + dist 压缩,一键输出生产包
前端·javascript
晚星star2 小时前
在 Web 前端实现流式 TTS 播放
前端·vue.js
huabuyu2 小时前
基于 Taro 的 Markdown AST 渲染器实现
前端
薄雾晚晴2 小时前
Rspack 性能优化实战:JS/CSS 压缩 + 代码分割,让产物体积直降 40%
前端·javascript
本末倒置1832 小时前
前端面试高频题:18个经典技术难点深度解析与解决方案
前端·vue.js·面试
狗头大军之江苏分军3 小时前
Meta万人裁员亲历者自述:小扎尝到了降本的甜头
前端·后端·github
秃顶老男孩.3 小时前
web中的循环遍历
开发语言·前端·javascript