在 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 小程序中使用。

相关推荐
李鸿耀1 小时前
主题换肤指南:设计到开发的完整实践
前端
带娃的IT创业者6 小时前
TypeScript + React + Ant Design 前端架构入门:搭建一个 Flask 个人博客前端
前端·react.js·typescript
非凡ghost7 小时前
MPC-BE视频播放器(强大视频播放器) 中文绿色版
前端·windows·音视频·软件需求
Stanford_11067 小时前
React前端框架有哪些?
前端·微信小程序·前端框架·微信公众平台·twitter·微信开放平台
洛可可白7 小时前
把 Vue2 项目“黑盒”嵌进 Vue3:qiankun 微前端实战笔记
前端·vue.js·笔记
学习同学8 小时前
从0到1制作一个go语言游戏服务器(二)web服务搭建
服务器·前端·golang
-D调定义之崽崽8 小时前
【初学】调试 MCP Server
前端·mcp
四月_h8 小时前
vue2动态实现多Y轴echarts图表,及节点点击事件
前端·javascript·vue.js·echarts
文心快码BaiduComate9 小时前
用Zulu轻松搭建国庆旅行4行诗网站
前端·javascript·后端
行者..................10 小时前
手动编译 OpenCV 4.1.0 源码,生成 ARM64 动态库 (.so),然后在 Petalinux 中打包使用。
前端·webpack·node.js