在 uni-app 里写微信小程序但凡内容稍微复杂一点迟早都会碰到同一个尴尬场景后端给你返回一篇 Markdown 格式的文章或者一段带p、pre、img、table的 HTML 片段而小程序原生组件rich-text要么不支持 Markdown要么对 HTML 标签的支持残缺不全尤其是table、code高亮、图片点击预览这些基本只能干瞪眼。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 展示。实际上它对标签的支持非常有限常用的p、span、img没问题但碰到table、iframe、video、code高亮这种就完全歇菜了。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: [] }渲染层通过name或tag去匹配 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项目中集成towxml2.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.jstowxml 库本身是基于原生小程序语法写的依赖Component、template等机制。在 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是包含theme、type、nodeList等字段的 JSON 结构模板需要根据这个结构做递归渲染所以页面里必须有一个专门解析节点树的组件。2.4 封装渲染组件我在实际项目中的做法是在components/下建一个towxml-render组件逻辑如下template view template v-ifnode.type node !-- 根据 node.name 动态判断渲染方式 -- view v-ifnode.name view template v-for(child, index) in node.children :keyindex towxml-render :nodechild / /template /view text v-else-ifnode.name text{{ node.text }}/text image v-else-ifnode.name image :srcnode.attr.src tappreviewImage(node.attr.src) / block v-else template v-for(child, index) in node.children :keyindex towxml-render :nodechild / /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 :contentarticle /这样可以直接省掉自己写模板的工作量我建议新手先用现成的后续有定制需求再动模板。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 :contentarticle /如果你用的现成封装组件渲染 Markdown 时发现标题、列表都正常但代码块没有高亮那通常是配置项没开启。towxml 的 Markdown 解析器内部集成了代码高亮库默认不一定打开这时候需要你传入配置项。不同版本的用法略有差异比较通用的一种做法是const jsonData Towxml(mdText, markdown, { base: /, theme: light, highlight: true });highlight置为true后代码块解析时会把关键词包成带hljs-前缀的文本节点配合自带的highlight样式高亮效果就出来了。3.2 渲染 HTML 标签格式的内容towxml 同样支持直接渲染 HTML 字符串。接口上只需要把第二个参数换成htmlconst jsonData Towxml(htmlString, html);这条路径会走html2json把类似p这是一段b加粗/b文本/p转成节点树。对p、span、a、img、ul、ol、table这些常用标签的支持是没问题的。我在实际项目里遇到最多的场景是微信公众号文章导入。公众号后台导出的 HTML 一般带有大量内联样式比如stylecolor:#333;font-size:16px;line-height:1.8;towxml 解析时会把这部分放在节点的attr.style里渲染时通过内联样式直接生效。但这里有个隐藏问题有些编辑器会导出classrich_media_content之类的样式类名而小程序里没有对应的外部 CSS 文件这些类不会起任何作用。所以如果你要承接公众号内容建议在后端做一次 HTML 清洗把无用类名和section嵌套去掉只保留基础标签和必要的内联样式体积和渲染速度都会好看很多。3.3 代码块和表格的渲染优化Markdown 里代码块是高频需求。towxml 对代码块支持两种模式一种是行内代码code一种是块级代码pre。块级代码渲染时如果highlight开启它会为每个关键词生成独立的text节点再套上span外壳最终在 WXML 里用text组件逐字渲染。这带来一个问题一段 200 行的代码解析后可能生成几千个节点渲染性能会明显下降。我的建议是后端在返回文章内容时尽可能按需裁剪不要一次性给整本电子书的全部正文。前端做分页或触底加载每次只解析一部分章节内容。如果单篇文章代码块特别多可以考虑在highlight关闭的情况下仅做基本展示或者用一种更轻量的“纯文本 selectable”的方案牺牲高亮换取滚动流畅。表格在微信小程序里也是个老大难。原生rich-text对table支持不可靠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-ifnode.name image :srcnode.attr.src :modenode.attr.mode || widthFix tappreviewImage(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字段指定常用的是light和dark。我实际用下来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 classarticle-container uni-towxml :contentarticle / /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组件加上loadinglazy属性。这个属性在微信小程序基础库 2.19.5 及以上支持效果是页面滚动到图片附近才开始加载。image v-ifnode.name image :srcnode.attr.src modewidthFix loadinglazy tappreviewImage(node.attr.src) /5.6 与video标签的兼容towxml 对video标签的处理在小程序端算是个短板。如果你要在文章内展示视频Markdown 里写video srcxxx 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 解析后的效果小程序端只负责渲染这样能避免大量线上返工。