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

相关推荐
biaov7 分钟前
Jenkins + Docker 搭建前端 CI:前端一键打包部署
前端·docker·jenkins
rich_Chan.9 分钟前
阿里云百炼联网搜索教程:web_search 工具开启方法、web_extractor 网页抓取与 json 检索轨迹
前端·阿里云·json
瓦瓦gogo18 分钟前
实习中接触前端开发后,我开始了解 React
前端·react.js·前端框架
梦曦i19 分钟前
uni-app x 新组件库v0.1.0发布,多端统一开发
前端·ui·uni-app x
Dovis(誓平步青云)22 分钟前
切换语言后按钮被截断,本地化审计怎样发现长度风险
开发语言·前端·javascript·人工智能·机器学习·华为·harmonyos
CappuccinoRose29 分钟前
拖拽交互体系
前端·javascript·css·html·交互·1024程序员节
开开心心就好31 分钟前
免安装PDF盖章工具,透明度和位置可调
运维·服务器·前端·javascript·pdf·ocr·1024程序员节
风骏时光牛马31 分钟前
AI服务线上推理异常故障
前端
king王一帅43 分钟前
做了三年 ProseMirror 后,我用四个月自研排版引擎实现 WPS / Google Docs 替代品,整套框架 MIT 开源
前端·vue.js·开源
এ慕ོ冬℘゜3 小时前
如何在 HTML 中嵌入音频和视频?
前端·html·音视频