资讯动态

uni-app小程序用towxml渲染Markdown与HTML的完整实践

发布时间:2026/9/15 5:36:25 来源:尧图企业网站定制
上周后台同事跟我说他们现在文章有两种来源一种是运营在公众号后台直接粘贴的富文本导出来全是带内联样式的 HTML另一种是技术同学提的 MD 文档需要原样展示。而小程序这边uni-app 编译到微信小程序的内容页没法像浏览器一样直接把 HTML 往 div 里一扔——微信小程序的 WXML 不是 DOMrich-text组件虽然能解析一部分 HTML但对 MD 完全没辙样式和交互也处处受限。我当时的想法就是找个现成解析库。towxml 就是专门干这个的它能同时把 Markdown 和 HTML 标签解析成一套结构化的节点数据再由小程序自定义组件递归渲染出来。标题里说的“使用towxml去渲染md格式和html标签格式的内容”其实核心就三步拿内容、调解析、传节点。这篇文章我把在 uni-app 里从零到一跑通的全过程写一遍包括版本选择、目录放置、组件注册、数据传递以及我最想分享的那一堆踩坑记录。适合谁看正在用 uni-app 做微信小程序、又恰好需要展示文章详情/帮助文档/用户协议/公告这类内容的前端同学。已经用了 towxml 但遇到样式、图片、事件问题的可以直接跳到第 5 节。1. 为什么非要用towxml小程序渲染Markdown和HTML的先天限制1.1 rich-text看起来能用实际上处处是坑先说一个很多人走过的弯路微信小程序里好歹有个rich-text组件能直接把 HTML 字符串渲染出来那是不是就不用 towxml 了理论上确实能渲染一部分但放到真实业务里你会发现它只是“看起来能用”。第一个问题是样式。rich-text对标签的解析走的是小程序内部的虚拟节点渲染页面style里写的类名基本管不到它内部的节点它更认内联样式。富文本编辑器导出时通常会把大部分样式都内联到style里所以排版看着还行。但如果你拿到的是经过服务端模板拼接的 HTML大量依赖 class 选择器那在rich-text里就是一片裸奔的纯文本。第二个问题是 MD 根本没戏。Markdown 源文本不是 HTMLrich-text只吃节点数组或者 HTML 字符串你拿# 标题这种文本进去就是普通文字。想要用rich-text渲染 MD还得先找个库把 MD 转 HTML转完又是一堆样式问题等于把问题绕了一圈又带了回来。第三个问题最致命交互。业务里图片要点击预览、链接要跳转、代码块要支持复制这些需求rich-text一概不给你接口。它对外只暴露了一个整体容器的点击事件你想知道用户点在哪个图片、哪个链接上只能靠坐标和正则去猜既不优雅也不可靠。1.2 towxml的核心思路先把内容解析成节点树towxml 解决的思路和rich-text完全不一样。它不试图“模拟 HTML”而是先把 Markdown 或 HTML 解析成一棵 JSON 节点树然后自己在 WXML 里递归渲染每一个节点。这棵节点树长什么样大概是这样{ name: view, attrs: { class: towxml--p }, children: [ { name: text, attrs: {}, children: [这是一段文字] } ] }每个节点对应一个小程序原生组件view、text、image这些。因为渲染层是真实的小程序组件拼出来的所以每个节点都可以单独加样式、绑定事件。这也就是为什么 towxml 能在点击链接、预览图片这些交互需求上比rich-text强很多。实际用起来你并不需要直接加工节点树towxml 内部已经帮你把解析和递归渲染全部封装好了。你要做的只是把原始内容丢进去然后把返回的节点对象传给组件。这是 towxml 最省心的地方。1.3 适合与不适合的场景用了一段时间后我总结了一下 towxml 的适用范围。适合的场景非常明确文章详情页、帮助文档、用户协议、公告、版本更新说明以及一切“后台存的是 MD 或富文本 HTML前端只需要排版展示”的内容型页面。不太适合的场景也有比如内容中含大量自定义交互组件、需要像 PPT 一样复杂布局、或者对包体积极度敏感的项目。towxml 的源码加插件不算小塞进小程序包之后体积会涨。如果你的需求只是一小段说明文字完全没必要上它。2. 接入姿势版本选择与uni-app适配关系2.1 别急着npm install先把源码放进static目录网上很多教程一上来就让你npm install towxml然后在页面里 import。这个思路在纯小程序项目里可能没问题但在 uni-app 里容易翻车。uni-app 的编译链路是先通过 HBuilderX 或 CLI 处理再交给微信开发者工具原生小程序自定义组件和 npm 包的兼容性并不总是那么完美。我见过不少同学卡在“明明安装了却报Component is not found”的问题上。所以最稳妥的方式是从 towxml 的 release 或者插件市场下载源码把整个towxml文件夹直接丢进static目录。static目录里的文件会被 uni-app 原样拷贝到小程序包中不存在编译转换问题路径清晰排查也方便。2.2 建议的目录结构放好之后项目结构大概是这样的src/static/towxml/ ├── towxml.js ├── towxml.json ├── towxml.wxml ├── towxml.wxss ├── plugins/ │ ├── ... ├── themes/ │ ├── ...注意towxml 不是一个单纯的小程序组件它分为两部分。towxml.js是解析器负责把 MD/HTML 转成节点树towxml.wxml那套是渲染组件负责把节点树画出来。你页面里要同时用到这两部分。2.3 先确认你下载版本的调用方式towxml 迭代了好几个大版本不同版本的调用接口并不完全一致。有些版本导出的就是一个单例函数直接towxml(content)就能解析有些版本导出的是构造函数需要new Towxml()还有的版本把init方法改了名。最靠谱的办法是拿到源码后先打开towxml.js搜索module.exports看它到底导出了什么。如果导出的是对象就找对象上的方法如果是函数直接调用即可。大部分版本的核心方法是这样的const parser new Towxml(); const article parser.init(md, content, { theme: light, base: });如果new Towxml()报错说不存在那就检查下module.exports是不是直接指向的init函数。2.4 锁定版本别乱升级towxml 的init参数里的theme、base、highlight等字段在不同版本间有过调整。我个人建议项目里固定使用一个版本不要因为看到新版本发布就顺手升一下。你永远不知道某个options字段换了名字之后线上内容会渲染成什么样。版本锁定了问题就好排查。3. 从零到一把towxml集成进uni-app页面3.1 页面JSON里注册组件把 towxml 源码放进static目录之后需要在具体页面的json配置里注册组件。如果你用的是 uni-app 的 vue 页面入口文件会编译生成对应的 json通常直接在页面目录下建一个同名的.json文件就能生效{ usingComponents: { towxml: /static/towxml/towxml } }路径里的/static/towxml/towxml指向的就是刚才放进 static 目录的组件文件。注意这里不需要写.wxml、.js后缀小程序会自动补全。如果你多个页面都要用 towxml也可以直接在pages.json里按页面维度去加usingComponents但更推荐的做法是封装成一个 vue 组件第 6 节会展开讲。3.2 页面逻辑解析内容并塞给组件组件注册好之后页面逻辑里最关键的一步是调用 towxml 解析器把原始内容变成节点数据。下面这段是完整示例script import Towxml from /static/towxml/towxml.js; export default { data() { return { article: {}, loading: true }; }, async onLoad(query) { const content await this.fetchContent(query.id); this.article this.parseContent(content); this.loading false; }, methods: { fetchContent(id) { // 这里换成你的接口请求 return new Promise((resolve) { setTimeout(() { resolve(# 标题\n\n这是一段**markdown**内容); }, 200); }); }, parseContent(content) { const parser new Towxml(); return parser.init(md, content, { theme: light, base: https://your-cdn.com }); } } }; /script这里有一个 uni-app 特有的点在 vue 页面里可以直接用this.article this.parseContent(...)赋值。towxml 返回的节点树通常不小你如果习惯用this.setData也没问题但要注意大数据量setData可能会触发性能警告。后面第 5 节讲长文档时再细说。3.3 模板里渲染组件页面模板中只需要把节点树通过nodes属性传给 towxml 组件即可view classarticle-container towxml :nodesarticle / /view如果页面需要 loading 状态包一层条件渲染view v-ifloading加载中.../view towxml v-else :nodesarticle /这里有个小细节nodes的初始值一定不能是null或者空字符串最好给个空对象{}。不然组件首次渲染时拿不到数据可能会在控制台报一堆看不懂的告警。3.4 第一次跑通后先做整体样式检查跑通之后先别急着接真实接口用一小段覆盖了标题、代码块、表格、引用块、图片的样本文档先测一轮。重点看三件事标题层级是否明显、是否加粗代码块是否有底色和高亮表格是否溢出屏幕这三样是最容易出问题的。如果哪一项不对基本都是第 5 节里的某个坑直接对应去排查。4. 同时兼容MD和HTML类型判断与细节处理4.1 type参数决定了解析路径towxml 的init方法第一个参数就是内容类型常见取值是md和html。这个参数不能省略它直接决定内部走的是 Markdown 解析器还是 HTML 解析器。所以你接入时必须有办法知道当前内容是 MD 还是 HTML。最靠谱的方案让后端在接口里直接返回content_type字段。不要相信前端去嗅探因为内容是动态的格式可能千奇百怪。但如果后端一时改不了前端也需要有兜底判断。4.2 没有content_type时的兜底判断我写了一个简单的判断函数function detectType(content) { if (!content || typeof content ! string) { return md; } const trimmed content.trim(); // 以 开头或者以常见html标签开头大概率是html if (trimmed.startsWith() || /^(p|div|section|article|html|h[1-6]|ul|ol|table)[^]*/i.test(trimmed)) { return html; } return md; }这个判断在绝大多数场景下够用。唯一需要注意的情况是Markdown 本身允许嵌 HTML 块如果一段 MD 里第一行就是div那这里会误判成 HTML。所以再次强调后端字段才是正解前端嗅探只是兜底。4.3 HTML内容最好先做一轮轻量清洗富文本编辑器导出的 HTML 里经常混着大量垃圾内容比如无用的style标签、>function cleanHtml(html) { return html // 去掉style标签和里面的内容 .replace(/style[\s\S]*?\/style/gi, ) // 去掉注释 .replace(/!--[\s\S]*?--/g, ) // 去掉字体标签 .replace(/\/?font[^]*/gi, ) // 去掉常见的无语义属性 .replace(/\s(data-v-[a-z0-9-]|class|id)[^]*/gi, ); }注意这个清洗非常克制只删明显的垃圾不要做太复杂的 DOM 操作。小程序端没有一个真正的 DOM你自己写复杂解析反而容易误伤内容结构。4.4 同一页面动态切换类型时节点树可能不更新如果你在一个页面里先是渲染了 HTML然后用户切换又加载了一段 MD会发现 towxml 组件可能还保留着上一次的内容。这是因为原生自定义组件对nodes属性的变化检查并不总是可靠。解决办法很简单给 towxml 加一个key切换内容时把key加一强制重建组件towxml :nodesarticle :keyrenderKey clickhandleClick /this.renderKey 1; this.article this.parseContent(newContent);这个方法同样适用于从空内容到有内容的切换建议一上来就加上。5. 真实项目里踩过的坑从空白到样式再到交互5.1 坑一明明有数据页面却一片空白这个坑我见得太多了排查链路基本是固定的第一步先在解析完成后打印article确认它是不是一个包含children字段的对象。如果打印出来是undefined说明你的towxml.init调用方式不对回到第 2.3 节检查版本。第二步确认组件路径。刷新后如果控制台出现Component is not found或者usingComponents找不到去页面 json 里检查路径和static目录里的大小写。小程序对路径大小写敏感Towxml和towxml是两回事。第三步确认 towxml 组件内部渲染是否报错。微信开发者工具会抛类似Cannot read property xx of undefined的错比如Cannot read property children of undefined那说明传进去的nodes结构没被组件识别大概率你传成了原始字符串而不是 init 之后的节点树。第四步检查容器样式。towxml 组件根节点如果被父容器的height: 0或者overflow: hidden盖住也会表现为白屏。这时候打开调试器的 WXML 面板看节点树是否存在节点存在但高度为 0那问题就在样式上。5.2 坑二样式完全不对标题没有加粗代码块没有底色towxml 组件作为原生自定义组件在小程序里存在样式隔离。具体表现是你在页面style里写的.article-title { font-weight: bold }这种样式进不了 towxml 组件内部towxml 组件自己的towxml.wxss又可能因为编译顺序或写法问题没有被完全加载。我推荐的处理方式是直接用towxml.wxss作为样式补丁的主战场。打开这个文件找到对应 class比如.towxml--h1、.towxml--code、.towxml--table想调整什么直接改。因为 towxml 渲染出来的节点自带这些 class改这里最直接、最可控。如果你需要在页面级别微调某个特定页面可以在 towxml 外层包一个带id的 view然后在页面style里用后代选择器去尝试覆盖。但说实话微信小程序的自定义组件样式隔离加上 uni-app 的编译这套覆盖链路过长我实际用下来成功率并不高。真需要差异化样式我更建议给不同的theme传不同的配置而不是硬覆盖。5.3 坑三图片不显示、图片溢出图片问题在内容型页面里几乎必现。第一个子问题是图片跨域或域名没配置。小程序里访问远程图片如果图片域名不在微信公众平台后台的“downloadFile 合法域名”列表里开发工具偶尔能显示因为本机没限制真机上一律黑屏。这个一定要提前把 CDN 域名配好。第二个子问题是相对路径。富文本或者 MD 里的图片可能写的是./uploads/1.png这种相对路径。towxml 的init方法里有base参数把图片基础路径传进去它会在解析时尝试拼接parser.init(md, content, { base: https://your-cdn.com });传了之后还是不行那就需要在清洗 HTML 时自己做一轮src替换把相对路径改成绝对路径。第三个子问题是宽度溢出。towxml 默认的图片样式在不同版本里并不一致有些版本没有限制max-width。解决方式是在towxml.wxss里增加.towxml--img { max-width: 100%; height: auto; }5.4 坑四代码高亮失效或者冷门语言没有高亮代码块是 MD 渲染里的重头戏。towxml 内置了 highlight.js但它为了控制包体积默认并没有把所有语言全打包进去。如果你文章里的代码是常见的js、python、java、c基本没问题如果是rust、kotlin、sql这种较冷门的有可能渲染出来就是一片白底黑字没有任何高亮。处理方法有两种第一种在 towxml 源码的插件目录里找到 highlight 相关配置看看有没有对应语言包有就补进去。第二种干脆关闭自动高亮自己在解析后的节点树上附加样式或者接受纯文本风格。另外还有一个容易被忽略的点代码块的语言标识必须正确。比如 Markdown 里写的是javascripttowxml 才能正确识别如果写成js或者干脆不写语言高亮效果可能就差很多。5.5 坑五点击事件怎么接尤其是链接跳转和图片预览towxml 组件内部其实已经处理了一部分点击逻辑它会把用户点击的节点信息通过自定义事件抛出来。但不同版本的事件名和参数结构不一样这也是网上教程互相矛盾的原因。我的做法很笨但很有效先在页面上绑一个事件然后打印出来看结构。towxml :nodesarticle clickhandleTowxmlClick /handleTowxmlClick(e) { console.log(e.detail, e.currentTarget.dataset); }打开控制台点一下图片看detail里有没有src、url、tagName这些字段。不同版本字段名可能从url变成href但大体都在。拿到以后链接跳转可以这样处理handleTowxmlClick(e) { const detail e.detail || {}; if (detail.tagName a || detail.href) { const url detail.href || detail.url; if (url.startsWith(/pages/)) { uni.navigateTo({ url }); } else if (url.startsWith(http)) { uni.setClipboardData({ data: url, success: () uni.showToast({ title: 链接已复制 }) }); } } if (detail.tagName img) { wx.previewImage({ current: detail.src, urls: this.imageList }); } }如果事件名不是click就在towxml.js里搜triggerEvent看看组件向外抛的事件到底叫什么名字。这个方法适用于任何版本比我直接告诉你“应该叫 click”要靠谱得多。5.6 坑六超长MD导致setData数据量过大页面卡顿towxml 解析出来的节点树是嵌套结构的一篇 1 万字的文章生成的节点对象可能有几百 KB。一次性塞给setData在小程序里会直接触发性能警告甚至在某些低端安卓机上白屏闪退。我实际项目里的做法是分块渲染先把文章按一级标题拆成几段首屏只加载前两段等用户滚动到接近底部时再继续追加下一段。虽然实现起来要维护一个“已加载段数”的状态但对长文体验的提升非常明显。如果你的文章长度比较稳定也可以更粗暴一点把 towxml 组件放在scroll-view外面先隐藏等解析和setData完成后再显示至少保证不白屏。6. 进阶封装成公共组件与备选方案6.1 把towxml封装成一个业务无关的ArticleRenderer组件直接在业务页面里用 towxml代码会变得很啰嗦而且每个页面都得重复处理类型判断、加载状态、事件转发。我在项目里习惯再封装一层 vue 组件对外只暴露最简单的内容输入接口。template view classarticle-renderer view v-ifloading classarticle-renderer__loading加载中.../view towxml v-else :nodesnodes :keyrenderKey clickhandleClick / /view /template script import Towxml from /static/towxml/towxml.js; function detectType(content) { if (!content || typeof content ! string) return md; const trimmed content.trim(); if (trimmed.startsWith() || /^(p|div|section|article|html|h[1-6]|ul|ol|table)[^]*/i.test(trimmed)) { return html; } return md; } export default { name: ArticleRenderer, props: { content: { type: String, default: }, type: { type: String, default: auto }, theme: { type: String, default: light }, base: { type: String, default: } }, data() { return { nodes: {}, renderKey: 0, loading: false }; }, watch: { content() { this.rebuild(); }, type() { this.rebuild(); } }, created() { this.rebuild(); }, methods: { rebuild() { if (!this.content) { this.nodes {}; return; } this.loading true; this.renderKey 1; const parser new Towxml(); const type this.type auto ? detectType(this.content) : this.type; this.nodes parser.init(type, this.content, { theme: this.theme, base: this.base }); this.loading false; }, handleClick(e) { const detail e.detail || {}; this.$emit(click, detail); } } }; /script封装之后业务方接入就变成了一行代码article-renderer :contentarticleContent /团队成员不需要知道 towxml 的存在也不容易用错。这是我觉得最值得做的工程化改造。6.2 主题定制深色模式和品牌色towxml 的init方法支持传theme参数很多版本内置了light和dark两套主题。如果你的 App 本身有深色模式需求直接把theme字段和全局的主题状态绑定就行this.theme isDark ? dark : light;如果默认主题的颜色和品牌不搭最快的办法还是改towxml.wxss里的 CSS 变量。注意改完之后要回归测试一下 MD 和 HTML 两种解析模式的页面因为某些版本的 HTML 解析会额外插入一些自带 class主题切换时可能漏改。6.3 什么情况下我会考虑换成mp-htmltowxml 不是唯一选择mp-html也是一个口碑不错的库。我在评估是否切换时主要看三点维度towxmlmp-htmlMarkdown解析强原生支持弱需要先转htmlHTML解析中等依赖源码版本强标签覆盖更全uni-app适配需手动放源码官方支持接入更顺交互事件有但字段因版本而异文档更清晰包体积较大也不小如果项目里 MD 是主要输入我会继续用 towxml。如果 HTML 场景更多、并且需要处理很多复杂标签mp-html可能更合适。当然迁移本身有成本真要换的话建议在独立分支里先跑通 Demo 再决定。就目前来说towxml 在我负责的项目里稳定跑了两个版本迭代后台只需要告诉我content和type前端组件负责解析、渲染、事件上报整个链路已经不需要我再关心底层细节了。最后给你一个实在的建议不管你用什么版本先把 towxml 仓库里的 demo 完整跑一遍再往业务里接很多人翻车都是跳过了 demo 直接改然后分不清到底是配置问题还是库本身的问题。真正跑通一次之后上面这些坑你基本都能一眼定位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价