uni-app微信小程序富文本渲染:towxml集成与Markdown/HTML解析实践
2026/9/13 17:44:52 网站建设 项目流程

在 uni-app 里写微信小程序,但凡内容稍微复杂一点,迟早都会碰到同一个尴尬场景:后端给你返回一篇 Markdown 格式的文章,或者一段带ppreimgtable的 HTML 片段,而小程序原生组件rich-text要么不支持 Markdown,要么对 HTML 标签的支持残缺不全,尤其是tablecode高亮、图片点击预览这些,基本只能干瞪眼。

towxml 就是为解决这个问题而生的。它是一个开源的、专门面向小程序环境的 Markdown 和 HTML 渲染组件,早期主要服务于微信原生小程序,后来因为 uni-app 生态的流行,社区里也沉淀了大量集成方案。核心功能是:把 Markdown 文本解析成 JSON 结构的小程序节点树,再基于wxml递归渲染出来,同时支持代码高亮、表格、图片预览、数学公式(Latex)等复杂类型支持。对于 HTML 字符串也能做一套类似的清洗和解析,最终输出为小程序可识别的节点结构。如果你正在做资讯类小程序、博客客户端、帮助中心,或者任何需要展示富文本内容的 uni-app 微信小程序项目,这篇内容基本可以帮你一次搞定。

先说清楚一个底层概念:小程序的 WXML 不支持动态 HTML,所以业界渲染 Markdown 或 HTML 主要有三条路线。

  • rich-text:能渲染部分 HTML,但只支持有限标签,且无法对节点做事件绑定,代码高亮、图片预览这种交互就别想了。
  • web-view:整个页面用 H5 渲染,但又重又绕,通信繁琐,动静很大。
  • 解析成节点树递归渲染:towxml 走的就是这条路线,把 Markdown/HTML 转成 JSON 节点数据,然后通过 WXML 递归模板渲染,性能和交互能力都够用。

我要重点讲的,就是第三条路线。本篇文章我会分四块推进:先带你拆解 towxml 的设计思路和选型背景,再说清楚如何集成到 uni-app 微信小程序项目里,接着直接上渲染 Markdown 和 HTML 标签的完整可用代码,最后把我实际踩过的坑、排查过的问题整理成一份实操清单分享给你。

1. 整体设计与思路拆解

1.1 为什么小程序原生能力不够

微信小程序的rich-text组件刚出来的时候,很多人以为它能替代 HTML 展示。实际上它对标签的支持非常有限,常用的pspanimg没问题,但碰到tableiframevideocode高亮这种就完全歇菜了。rich-text还有一个致命问题:它内部渲染的节点无法绑定bindtap等交互事件,这就导致你没办法实现“点击图片放大预览”“点击链接跳转”“长按复制代码”这类基础功能,而这些恰恰是内容型小程序的基本操作。

web-view如果内容量级不大,勉强可以,但问题也不小:加载 H5 页面有明显白屏时间、通信复杂、加载第三方网页还要配业务域名。对于一个“原生小程序为主、局部展示富文本”的场景来说,引入web-view意味着整页切换,体验非常割裂。

towxml 的做法是中间路线:不去依赖小程序不能做的事情,而是自己做解析和渲染。它先把 Markdown 按规则解析成一个树形 JSON,然后在 WXML 里用template递归渲染这棵树。因为小程序对template递归是支持的,所以理论上节点可以随意嵌套,标签类型由数据里的type字段决定,不同的type对应不同的 WXML 模板片段。

1.2 towxml 的解析链路

towxml 的静态解析流程其实是一个典型三段式管道:

输入文本 -> 词法分析/切割 -> 语法树生成 -> JSON节点树 -> WXML递归渲染

我简化说,它把 Markdown 的标题、段落、列表、引用、代码块、表格等语法逐条解析,最终每一个节点都有统一的结构:

{ "type": "node", "name": "h2", "tag": "h2", "attr": {}, "children": [] }

渲染层通过nametag去匹配 WXML 里的模板片段,children则继续递归渲染。这种结构的好处是扩展性极强——你想新增一个自定义容器,只要在 JSON 结构里加一种name,再在 WXML 模板里补一个对应template即可,不需要动整个渲染框架。

1.3 备选方案对比

我在最初选型的时候并不是直接锁定 towxml 的,而是同时考察了几个主流方案。对比之后才决定用 towxml,原因很简单。

方案Markdown 支持HTML 标签支持图片预览代码高亮体积与维护性
rich-text不支持有限,无交互不支持不支持原生,轻量
web-view依赖H5完整需H5实现需H5实现重,通信复杂
towxml完整大多数常用自带自带(支持highlight.js)适中,社区活跃
自研解析器需造轮子需造轮子需开发需集成成本高

如果只是展示几段固定样式、无交互的纯文本,rich-text足够了。但只要是面向用户的内容型页面,图片预览、代码复制、链接跳转这些刚需,选 towxml 基本是最合适的选择。

2. 在uni-app项目中集成towxml

2.1 获取towxml库

towxml 的源码托管在 GitHub 上,搜索towxml即可找到。这里给出一个最稳定的获取方式:直接 clone 完整仓库。仓库里会包含towxml/主目录以及示例项目,示例项目是原生小程序,不过不用担心,这个主目录是可以直接拿到 uni-app 里用的。

如果你的网络环境访问 GitHub 比较慢,也可以找国内 Gitee 上的一些镜像仓库,通常也能搜到完整代码。下载后,把towxml文件夹复制到你 uni-app 项目的common/components/目录下,我自己的项目放在common/towxml,后续引用比较清晰。注意不要放到static/目录,因为static/下的文件不会被uni-app编译处理,而 towxml 里面有js文件需要通过import引入。

2.2 uni-app项目中的目录规划

放好之后,你的目录结构大致是这样:

project-root/ ├── common/ │ └── towxml/ │ ├── towxml.js │ ├── main.js │ ├── html2json.js │ ├── md2json.js │ ├── lib/ │ └── templates/ │ ├── ... ├── pages/ │ └── article/ │ └── article.vue ├── App.vue └── main.js

towxml 库本身是基于原生小程序语法写的,依赖Componenttemplate等机制。在 uni-app 中使用时,最关键的技巧是利用mixins或直接将其封装为easycom组件。但 towxml 内部有较多template递归调用,如果直接封装成vue组件会有样式和递归的坑。我建议的稳定做法是:把 towxml 的 WXML 模板改造为uni-app可识别的template,但这对新手难度偏高。

其实更常见的做法是:在pages/article/article.vue页面中,直接通过import { towxml } from '@/common/towxml/towxml.js'引入解析函数,然后使用rich-text的替代方案,把解析后的 JSON 数据交给一个自定义组件渲染。这里我推荐直接借助官方示例改造一个页面组件。

2.3 页面配置与引入

第一步先要在pages/article/article.vue里引入。

import Md2Html from '@/common/towxml/main.js';

这里注意:main.js在 towxml 里是核心入口。不同版本的 towxml 入口文件可能叫法不一样,早期版本是towxml.js,后来统一成main.js。你下载下来之后先看仓库根目录说明,确认具体入口。

然后在script里声明:

export default { data() { return { article: '', content: '' } }, onLoad(options) { this.content = decodeURIComponent(options.content || ''); this.renderContent(); }, methods: { renderContent() { // 将 Markdown 文本转换为 towxml 可渲染的 json 结构 this.article = Md2Html(this.content, 'markdown'); } } }

但这里还没完,因为article是包含themetypenodeList等字段的 JSON 结构,模板需要根据这个结构做递归渲染,所以页面里必须有一个专门解析节点树的组件。

2.4 封装渲染组件

我在实际项目中的做法是,在components/下建一个towxml-render组件,逻辑如下:

<template> <view> <template v-if="node.type === 'node'"> <!-- 根据 node.name 动态判断渲染方式 --> <view v-if="node.name === 'view'"> <template v-for="(child, index) in node.children" :key="index"> <towxml-render :node="child" /> </template> </view> <text v-else-if="node.name === 'text'">{{ node.text }}</text> <image v-else-if="node.name === 'image'" :src="node.attr.src" @tap="previewImage(node.attr.src)" /> <block v-else> <template v-for="(child, index) in node.children" :key="index"> <towxml-render :node="child" /> </template> </block> </template> </view> </template> <script> export default { name: 'TowxmlRender', props: { node: { type: Object, default: () => ({}) } }, methods: { previewImage(src) { uni.previewImage({ urls: [src] }); } } } </script>

看到这里你可能觉得有点繁琐,是的,towxml 的渲染本质就是做递归组件。如果你不想自己维护这一层,可以直接在 github 上找社区已经封装好的uni-towxml组件,下载后放到components/uni-towxml,页面里直接用:

<uni-towxml :content="article" />

这样可以直接省掉自己写模板的工作量,我建议新手先用现成的,后续有定制需求再动模板。

3. 核心渲染实现细节与实操要点

3.1 渲染 Markdown 格式的内容

先看渲染 Markdown 的核心调用。我强烈建议把markdown文本在页面onLoad时就解析完成,存成 JSON 结构,不要放在模板实时解析。原因很简单:解析过程有性能开销,放到渲染层会导致每次数据更新都重新解析一遍,页面会出现明显卡顿。

实际代码:

import Towxml from '@/common/towxml/main.js'; methods: { parseMarkdown(mdText) { const jsonData = Towxml(mdText, 'markdown'); this.article = jsonData; } }

这里Towxml的第二个参数是'markdown',它会走 Markdown 解析器。解析完成后,模板里直接:

<uni-towxml :content="article" />

如果你用的现成封装组件渲染 Markdown 时发现标题、列表都正常,但代码块没有高亮,那通常是配置项没开启。towxml 的 Markdown 解析器内部集成了代码高亮库,默认不一定打开,这时候需要你传入配置项。不同版本的用法略有差异,比较通用的一种做法是:

const jsonData = Towxml(mdText, 'markdown', { base: '/', theme: 'light', highlight: true });

highlight置为true后,代码块解析时会把关键词包成带hljs-前缀的文本节点,配合自带的highlight样式,高亮效果就出来了。

3.2 渲染 HTML 标签格式的内容

towxml 同样支持直接渲染 HTML 字符串。接口上只需要把第二个参数换成'html'

const jsonData = Towxml(htmlString, 'html');

这条路径会走html2json,把类似<p>这是一段<b>加粗</b>文本</p>转成节点树。对pspanaimguloltable这些常用标签的支持是没问题的。

我在实际项目里遇到最多的场景是微信公众号文章导入。公众号后台导出的 HTML 一般带有大量内联样式,比如style="color:#333;font-size:16px;line-height:1.8;",towxml 解析时会把这部分放在节点的attr.style里,渲染时通过内联样式直接生效。但这里有个隐藏问题:有些编辑器会导出class="rich_media_content"之类的样式类名,而小程序里没有对应的外部 CSS 文件,这些类不会起任何作用。所以如果你要承接公众号内容,建议在后端做一次 HTML 清洗,把无用类名和section嵌套去掉,只保留基础标签和必要的内联样式,体积和渲染速度都会好看很多。

3.3 代码块和表格的渲染优化

Markdown 里代码块是高频需求。towxml 对代码块支持两种模式:一种是行内代码code,一种是块级代码pre。块级代码渲染时,如果highlight开启,它会为每个关键词生成独立的text节点,再套上span外壳,最终在 WXML 里用text组件逐字渲染。这带来一个问题:一段 200 行的代码,解析后可能生成几千个节点,渲染性能会明显下降。

我的建议是:

  • 后端在返回文章内容时,尽可能按需裁剪,不要一次性给整本电子书的全部正文。
  • 前端做分页或触底加载,每次只解析一部分章节内容。
  • 如果单篇文章代码块特别多,可以考虑在highlight关闭的情况下仅做基本展示,或者用一种更轻量的“纯文本 +selectable”的方案,牺牲高亮换取滚动流畅。

表格在微信小程序里也是个老大难。原生rich-texttable支持不可靠,towxml 的处理方式是把table解析成多层view嵌套,模拟表格布局。这种方案渲染上没问题,但列数一多就容易溢出屏幕,所以记得在全局样式里给表格容器加上overflow-x: auto,或者限制最小列宽。渲染表格的样式可以这样补:

.towxml-table { width: 100%; overflow-x: auto; } .towxml-table .table-row { display: flex; border-bottom: 1px solid #eee; } .towxml-table .table-cell { flex: 1; padding: 8px 10px; word-break: break-all; }

3.4 图片点击预览的实现

towxml 解析出来的image节点,在 WXML 模板里其实就是一个image组件。

<image v-if="node.name === 'image'" :src="node.attr.src" :mode="node.attr.mode || 'widthFix'" @tap="previewImage(node.attr.src)" />

mode属性建议默认用widthFix,因为 Markdown 里的图片往往只写了宽高比不固定的原图,用widthFix可以保证宽度撑满容器,高度自适应,避免图片被拉伸变形。

previewImage方法:

previewImage(current) { uni.previewImage({ current, urls: this.imageList }); }

注意imageList最好在解析完成后从 JSON 节点树里提前提取,而不是预览时再遍历整棵树。我写了一个简单递归提取的方法:

extractImages(node, result = []) { if (!node) return result; if (node.name === 'image' && node.attr && node.attr.src) { result.push(node.attr.src); } if (node.children && node.children.length) { node.children.forEach(child => this.extractImages(child, result)); } return result; }

3.5 主题和样式定制

towxml 自带几套主题,一般在配置项里用theme字段指定,常用的是lightdark。我实际用下来,light主题的代码高亮配色更适合大多数资讯站点,dark适合阅读类、极简风的 App。

如果你需要高度定制,比如标题颜色、正文字号、行间距,不要尝试去改库内模板样式,因为库更新后你的改动会被覆盖。正确做法是:在页面或者全局样式中,利用scoped样式对 towxml 暴露的容器类名做覆盖。不过要注意,小程序里scoped可能失效,所以我一般会把定制样式写在page级别的非 scoped style 里。

4. 实操过程:从零搭建一个文章详情页

4.1 环境准备

在动手之前,先确认环境:

  • HBuilderX 3.4.0 以上。
  • 项目类型为 uni-app,编译目标选择微信小程序。
  • 已在微信开发者工具里配置好 AppID,能正常预览。

4.2 完整步骤

第一步:下载 towxml。

我建议直接下载官方 release 包,避免克隆整个仓库连带一堆示例代码。

第二步:复制到项目。

towxml文件夹复制到项目的common/目录下。如果你用的封装组件叫uni-towxml,则放到components/目录。

第三步:配置 easycom。

pages.json里增加 easycom 规则:

{ "easycom": { "autoscan": true, "custom": { "^uni-towxml(.*)": "@/components/uni-towxml/uni-towxml.vue" } } }

配置好之后,页面里不需要手动import,直接写标签即可。

第四步:创建页面。

pages/article/article.vue中:

<template> <view class="article-container"> <uni-towxml :content="article" /> </view> </template> <script> import Towxml from '@/common/towxml/main.js'; export default { data() { return { article: {} }; }, onLoad(options) { const mdText = decodeURIComponent(options.md || ''); this.article = Towxml(mdText, 'markdown', { highlight: true }); } }; </script> <style> .article-container { padding: 30rpx; font-size: 30rpx; line-height: 1.8; } </style>

这样一个基本的 Markdown 文章详情页就跑通了。

第五步:渲染 HTML 内容。

如果接口返回的是 HTML,而不是 Markdown,只需调整解析参数:

this.article = Towxml(htmlText, 'html', { highlight: true });

我个人建议后端接口尽量返回 Markdown,因为 Markdown 数据体积更小、解析更稳定、样式还原度更统一。HTML 来源不可控,标签样式五花八门,解析出错概率更高。

4.3 数据预处理的细节

在实际业务中,你拿到的 Markdown 文本未必是干净的。比如从数据库里取出来的内容可能包含 HTML 实体字符&amp;&lt;,或者包含锚点跳转、自定义容器。towxml 对标准 Markdown 解析没问题,但遇到非标准扩展语法可能会出现部分内容显示异常。

我的建议是在解析前做一次预处理,把不兼容的语法做兼容替换,例如:

  • ::: tip类型的容器语法替换为普通blockquote
  • <!-- more -->这类注释标记直接移除。
  • <br/>标签保留,因为 towxml 支持它。

4.4 页面加载体验优化

towxml的解析过程是同步的,也就是说,解析大文本时,页面会短暂卡住。为了优化体验,我给文章详情页做的处理是:先展示一个 loading 骨架屏,等解析完成后再渲染uni-towxml

onLoad(options) { this.loading = true; this.parseContent(options.md); } methods: { parseContent(md) { setTimeout(() => { const article = Towxml(decodeURIComponent(md), 'markdown', { highlight: true }); this.article = article; this.loading = false; }, 0); } }

setTimeout只是为了把解析操作放到下一个事件循环,避免阻塞首次渲染。如果你的文章很长,这个方案依然会卡,更彻底的办法是后端直接返回解析好的 towxml JSON,前端省去解析环节。

5. 常见问题与排查技巧实录

5.1 渲染出来是空白或白屏

这个问题我遇到太多次了。大部分原因是article初始值是空对象,而在onLoad中同步解析时还没有解析完成,模板里递归组件可能已经把空对象当成有效节点处理了,导致渲染中断。

排查思路:

  • 在模板里加v-if判断article.nodeList是否存在。
  • 确认Towxml返回的对象不是undefined
  • 检查是不是main.js引入路径写错了,import Towxml from '@/common/towxml/main.js'这种路径一旦拼错,解析函数就是undefined,页面自然空白。

5.2 表格渲染出来挤成一团

这个问题的根源在于,towxml 解析表格后,每一行的单元格默认都用view实现了flex布局,但外层容器没有加overflow-x: auto。表格一旦超过屏幕宽度,就会被压缩成一根面条。

解决办法是在样式表加:

.towxml-table { display: block; width: 100%; overflow-x: auto; }

5.3 代码高亮无效

代码高亮不生效,先看配置里highlight是否设置为true。有些版本的封装组件把highlight默认关掉了,因为高亮会导致节点数量成倍增加。如果你确认highlight已经开启还是不生效,检查代码块的语言标识是否写清楚了,比如:

```javascript const a = 1; ```

上面 ``` 后面如果没写语言标识,部分版本的高亮插件会无法识别语言,直接当成纯文本输出。

还有一个容易被忽略的点:主题配置必须与高亮样式配套。如果你把theme设置成dark,但组件里引入的还是light主题的代码高亮 CSS,那么代码高亮虽然“开启了”,但因为背景色和文字色不匹配,肉眼看起来像没开。

5.4 点击事件失效

如果你在自定义封装的组件里给image节点绑定了@tap,但发现点击图片没反应。大概率是因为你用了rich-text来渲染解析结果。rich-text内部的所有节点都不接受外部事件绑定,这是组件的固有机制。只要改用树形组件递归渲染,事件才能正常触发。

5.5 图片懒加载与高清图性能

小程序里图片组件默认没有懒加载,towxml 渲染大量图片时,页面加载会明显变慢。正确的做法是在模板中给image组件加上loading="lazy"属性。这个属性在微信小程序基础库 2.19.5 及以上支持,效果是页面滚动到图片附近才开始加载。

<image v-if="node.name === 'image'" :src="node.attr.src" mode="widthFix" loading="lazy" @tap="previewImage(node.attr.src)" />

5.6 与video标签的兼容

towxml 对video标签的处理在小程序端算是个短板。如果你要在文章内展示视频,Markdown 里写<video src="xxx" controls></video>,解析后可能渲染成rich-text里无法播放的节点。

我的建议是:在预处理阶段把video标签整体替换成自定义的封面图片 + 播放按钮,点击后跳转到视频播放页面,或者用uni.createVideoContext动态创建播放器。这就不依赖 towxml 了,可控性和体验都更好。

6. 我整理的一份避坑速查表

我把使用过程中总结的常见问题做成了一张表,方便你开发时随时查阅。这张表比较适合做团队协作时的自查清单,放在项目文档里也很实用。

问题原因解决方案
白屏main.js路径错误或article初始值为空修正路径;加v-if控制渲染时机
样式错乱不同主题的高亮 CSS 混用统一主题与高亮样式
代码不高亮highlight配置未开启配置项设为true,检查语言标识
表格挤压缺少横向滚动容器给表格容器加overflow-x: auto
图片点击无反应使用了rich-text渲染改用递归组件渲染节点树
长文卡顿解析生成海量节点后端返回 JSON、分页加载、关闭代码高亮
富文本类名不生效后端 HTML 带了class类名后端清洗或前端过滤无用类名
视频不播放小程序环境不支持原生 video 标签内嵌替换为封面图跳转播放页

最后说点实际的建议

如果你正要在一个 uni-app 微信小程序项目里引入 towxml,我的核心建议是:先跑通一个最简单的页面,再考虑样式和扩展功能。不要在第一步就想着把所有 Markdown 扩展语法都支持,因为 towxml 对标准 Markdown 的覆盖已经完全够用了,常见的坑基本集中在代码高亮、表格、图片交互这几块。

我个人实际用下来的体会是,towxml 最大的翻车点反而不是解析能力,而是开发者在集成时绕开了模板递归的原理,导致渲染层级出问题。只要理解了“解析成 JSON 节点树,再递归渲染”这个核心逻辑,后续遇到任何渲染异常,你都能快速定位是解析层的问题还是模板层的问题。另外,如果你做的是一个内容管理后台 + 小程序端的组合,强烈建议把解析和渲染分组拆开:管理后台直接预览 towxml 解析后的效果,小程序端只负责渲染,这样能避免大量线上返工。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询