开始前先说两句题外话。第一次在小程序项目里接到“把服务端下发的 Markdown/HTML 渲染成页面”这个需求时我的第一反应是这还不简单把字符串塞进 rich-text 不就行了。结果真上手才发现问题远比想象中多——HTML 里的表格、代码块、行内样式在 rich-text 里要么不支持要么样式全乱Markdown 更不用说了小程序根本不认。后来我把 towxml 引了进来才真正把这套链路跑通。这篇文章就汇总一下我在项目中用 towxml 解析 Markdown 和 HTML、渲染成 WXML 界面的完整经验。注意标题里的“渲染为 WXML 文本”严格说不太准确——towxml 并不是把 Markdown/HTML 变成一段 WXML 字符串而是把它解析成一棵节点树JSON再通过自定义组件递归渲染成界面。理解这一点后面所有配置和踩坑就都有了解释。1. 先从 WXML 的边界说起为什么这活儿不是字符串拼接能解决的很多人第一次接触这个需求时都会有一个惯性思维既然 WXML 是类似 XML 的模板语言那我是不是可以把 HTML 字符串里的标签替换成 WXML 标签然后动态拼接出来这个思路在小程序里走不通原因得从 WXML 的渲染机制讲起。1.1 WXML 与 HTML 不是一个“语言”WXML 是小程序自定义的一套标签体系它只认view、text、image、scroll-view这类组件不认div、span、table这些浏览器标签。你跟小程序说“给我渲染一个div”它不会报错但会把它当成普通文本原样输出。更关键的是小程序出于安全考虑不允许开发者在运行时动态编译 WXML 模板所有页面结构必须在开发者工具构建阶段就确定下来。那有没有可能直接把 HTML 转成wxsswxml的静态代码理论上可以问题是小程序包是提前编译的运行时拿到的服务端内容不可能走编译链路。所以把 Markdown/HTML 渲染出来的唯一可行路径是在 JS 层把它们解析成小程序能理解的数据结构然后用已有的 WXML 模板“翻译”渲染。这就是 towxml 存在的理由。1.2 rich-text 的局限以及 towxml 补上了什么小程序官方其实提供了一个rich-text组件能把 HTML 字符串直接渲染出来。那为什么还要用 towxml我实际对比过差距在细节上。rich-text对 HTML 标签的支持是有限的它跟浏览器的 HTML 解析器不是一回事大量修饰性标签、嵌套列表、任务列表、不同语言的代码块解析出来经常丢样式或结构错乱。而且rich-text里面不能自定义每个元素的渲染逻辑比如你想给代码块加复制按钮、让图片支持点击预览、或者给表格做长列表横向滚动它都搞不定。towxml 的思路完全不同。它在 JS 层内置了 Markdown 解析器和 HTML 解析器先把字符串变成抽象语法树再把语法树映射成小程序的节点树组件内部通过递归模板一层层渲染。你可以把它理解为“一个小程序环境里的 mini 浏览器渲染引擎”。这样做的好处是每个节点类型都可以单独定制代码高亮、LaTeX 公式、流程图、任务列表这些富文本元素都能精确控制。我整理了一个两者对比的表格方便你判断自己的场景对比项rich-texttowxml直接渲染 HTML支持部分标签支持解析更完整渲染 Markdown不支持需先转 HTML原生支持 Markdown 解析代码高亮不支持内置 highlight 插件公式/流程图不支持支持 LaTeX、mermaid 等内部样式定制受限可通过 wxss 覆盖组件体积官方内置零成本需要引入约几十到几百 KB如果你的需求只是展示一段简单的富文本比如新闻摘要、活动说明rich-text足够用。但如果你要做一个博客阅读器、帮助中心、或者产品详情页——这些场景基本都会遇到 Markdown 和复杂 HTMLtowxml 是更稳的选择。2. 环境准备towxml 的正确引入方式和目录结构towxml 的引入方式跟普通 npm 包不太一样这一步如果没搞对后面全部白搭。我见过很多人在这一步卡住报各种“组件未找到”或“module not found”的错误。2.1 拷贝 还是 npm 安装我推荐前者towxml 目前有多个版本在 github 和 npm 上并存用法有细微差别。我以最常用的 2.x 版本为例从仓库把towxml目录直接拷贝到你的小程序项目里。没错就这么原始因为它依赖大量内部文件引用npm 安装后还要处理构建路径反而容易出幺蛾子。具体操作分三步在项目根目录一般是miniprogram目录下新建一个towxml文件夹把仓库里的towxml目录内容复制进去。在你需要渲染的页面 json 文件里注册组件{ usingComponents: { towxml: /towxml/towxml } }在页面的 JS 里引入入口文件const Towxml require(../../towxml/main);注意注册组件时的路径要写对。如果项目根目录不是miniprogram就写实际相对于根目录的路径。拷贝完成后可以看一眼目录结构正常情况下会有这些关键文件towxml/ ├── main.js // 入口导出 Towxml 类 ├── towxml.js // 自定义组件逻辑 ├── towxml.json // 组件配置 ├── towxml.wxml // 组件模板递归渲染的核心 ├── towxml.wxss // 组件样式 └── libs/ // 解析器相关依赖 ├── html2json/ // HTML 转节点树 ├── markdown/ // Markdown 解析 ├── highlight/ // 代码高亮 └── ...如果你用的是 npm 版本README 里会写明导入路径和组件注册方式但整体思路一致都要注册组件、都要实例化 Towxml、都要把解析结果传给组件。2.2 按需裁剪插件把包体积压下来towxml 功能全随之而来的问题是包体积不小。对于主包体积敏感的小程序来说如果只需要 Markdown 渲染和代码高亮完全可以把用不到的插件剪掉。towxml 的可选插件通常在libs目录下常见的有插件目录功能我是否保留markdownMarkdown 解析核心保留html2jsonHTML 解析核心保留很多场景需要highlight代码高亮按需保留latex数学公式一般不用可删mermaid流程图一般不用可删table表格渲染建议保留yumlUML 图可删裁剪时有一个大坑光删目录不够main.js或towxml.js里可能还引用了被删插件的模块。如果你删完运行时报“module not found”就打开报错指向的文件把对应require或import注释掉同时删掉相关分支逻辑。说实话对于一个上线项目我建议先别急着裁剪等整个功能跑通、确认哪些插件用不到再动刀这样排查问题时不至于分不清是裁剪导致的问题还是解析逻辑的问题。3. 核心链路toJson 到底把你的文章变成了什么towxml 的使用核心就一个方法toJson。它接收两个参数——原始字符串和类型markdown或html返回一个节点树对象。这个对象交给towxml组件后页面就能渲染出来。3.1 最小可用代码从字符串到页面渲染我先把最简代码贴出来你直接抄就能跑// pages/article/article.js const Towxml require(../../towxml/main); Page({ data: { article: null }, onLoad() { const towxml new Towxml(); const markdownContent # 这是标题\n\n这是 **加粗** 文本这是一个 [链接](https://example.com); // 第二个参数 markdown 表示按 Markdown 解析 const article towxml.toJson(markdownContent, markdown); this.setData({ article: article }); } });页面模板view classpage towxml nodes{{article}} / /view页面 json{ usingComponents: { towxml: /towxml/towxml } }这里的article不是一个普通对象而是包含了nodes、bind等字段的节点树容器。towxml组件拿到后会在内部通过递归模板把节点树“翻译”成view、text、image等 WXML 组件。如果是异步请求把它放在回调里就行wx.request({ url: https://api.example.com/article, success: (res) { const articleData towxml.toJson(res.data.content, markdown); this.setData({ article: articleData }); } });这里有一个很容易被忽略的点toJson是个同步方法。如果你的文章内容非常大它会在主线程上卡一下体验上表现为页面跳转后短暂白屏。后面第 5 章会专门讲优化。3.2 节点树的内部结构长什么样理解了节点树你就理解 towxml 的渲染机制了。我简化一个示例Markdown 里有一行# 标题towxml 解析后生成的节点树大概是这样的{ nodes: [ { type: element, tag: view, attrs: { class: towxml-h1 }, children: [ { type: text, text: 标题 } ] } ] }type字段区分元素节点和文本节点。元素节点通过tag字段映射到小程序组件children存子节点text节点直接保存字符串。towxml 组件在 wxml 模板里会对不同类型的节点做不同处理比如tag是image的节点渲染成imagetag是view的节点渲染成view。知道这个结构有什么用第一排查问题时你能看懂console.log里到底解析出了什么第二如果你要做二次开发比如提取标题生成目录就是遍历这棵树的节点逐个看attrs.class是不是towxml-h1、towxml-h2。我后面会讲到这个玩法。3.3 markdown 和 html 两条入口的差异toJson(content, markdown)和toJson(content, html)的区别本质上是用不同解析器处理原始字符串。Markdown 入口先把#、**、[文本](url)这类语法转成中间结构再变成节点树HTML 入口则直接解析标签嵌套关系变成节点树。实际开发中很多人会犯一个错误后端已经返回了富文本编辑器生成的 HTML前端又莫名其妙先转成 Markdown 再丢给 towxml。完全没必要。你只需要判断服务端下发的原始格式是什么对应调用入口就可以// 根据接口约定动态选择解析方式 const parserType res.data.format html ? html : markdown; const articleData towxml.toJson(res.data.content, parserType);我建议后端明确一个字段比如content_type避免前端猜。否则你以为是 HTML实际内容是 Markdown解析出来就是一大片乱码。4. 渲染细节与事件处理让效果更接近网页towxml 默认渲染出来的效果比较朴素但它的样式体系是开放的你可以用自己的 wxss 覆盖。这一章讲几个能直接提升体验的细节。4.1 主题切换和样式覆盖towxml 组件支持theme属性用来切换浅色/深色主题。用法很简单towxml nodes{{article}} themedark /如果没有传默认走组件内置的浅色样式。主题变量定义在towxml.wxss里我实际项目中通常保留浅色主题然后在自己页面的 wxss 里覆盖关键类名比如标题字号、正文字号、行高、图片圆角/* pages/article/article.wxss */ .towxml-h1 { font-size: 40rpx; font-weight: 700; margin: 40rpx 0 20rpx; } .towxml-p { font-size: 30rpx; line-height: 1.8; color: #333; } .towxml-img { border-radius: 12rpx; margin: 20rpx 0; }问题来了towxml 组件的样式默认是在组件内部生效的微信小程序的样式隔离机制会让页面 wxss 无法直接穿透到自定义组件内部。想覆盖组件内部样式你得在towxml组件的options里开启styleIsolation: apply-shared不同版本配置方式略有不同或者在页面里用!important加全局样式强制覆盖。这里分享一个我摸索出来的稳妥做法如果只是个别页面用 towxml就在app.wxss里写覆盖样式并加上组件专属前缀。虽然粗暴但胜在不会出现“页面里改了没用、组件内部又找不到改哪儿”的尴尬。4.2 代码高亮与图片预览这些交互怎么接towxml 对代码块的处理比较聪明解析 Markdown 的围栏代码时它会识别语言类型比如javascript、python然后用内置高亮器给代码节点加上对应的高亮 class。你只需要在towxml.wxss里为这些 class 定义配色。如果你懒得自己配色towxml 自带了几套高亮主题直接改theme或相关变量就行。图片预览是富文本里最常见的交互。towxml 在解析图片节点时会把src属性保留在节点里。你要做的是在页面层监听图片点击事件拿到图片地址然后调用wx.previewImage。因为 towxml 的事件回调方式在不同版本里并不统一有的通过bind:click暴露有的需要你在组件内部自行处理。稳妥的做法是打开towxml.wxml看内部对图片节点绑定了什么事件然后对应在页面里写处理函数。我的实现大概是这样的towxml nodes{{article}} bind:taponTowxmlTap /onTowxmlTap(e) { const dataset e.currentTarget.dataset; if (dataset dataset.src) { wx.previewImage({ urls: [dataset.src], current: dataset.src }); } }这里的关键是dataset.src是否存在。如果 towxml 没把src暴露到>data: { article: { nodes: [ { type: element, tag: view, attrs: { class: test }, children: [{ type: text, text: hello }] } ] } }如果静态节点能渲染成“hello”说明组件没坏问题出在解析环节如果连静态节点都不渲染说明组件本身没注册成功或模板有兼容问题。换一个内容源测试。把接口数据替换成本地写死的 Markdown 字符串如果本地能渲染、接口数据不行那就是接口返回内容有问题比如不是纯字符串而是 JSON 字符串。我在实际项目中遇到过一个隐蔽情况接口返回的内容带了 BOM 头\uFEFF导致 toJson 第一行标题解析异常整个页面白屏。解决方法是解析前去掉 BOMconst cleanContent content.replace(/^\uFEFF/, ); const articleData towxml.toJson(cleanContent, markdown);这个坑很小但排查起来很费时间。5.2 事件回调失灵和样式被污染towxml 交互事件失灵先检查你是不是把 towxml 放在了自定义组件内部。小程序自定义组件之间的事件传递有隔离机制towxml 内部触发的tap事件默认不会冒泡到页面你需要在中间层组件里转发// components/article-box/article-box.js Component({ methods: { onTowxmlTap(e) { // 把内部事件转发给页面 this.triggerEvent(towxmltap, e.detail); } } });然后页面 XML 里监听article-box bind:towxmltaponArticleTap /样式被污染也常见。比如你在app.wxss里对全局view设置了box-sizing: border-box这通常没问题但如果设置了letter-spacing或line-height作用于所有节点towxml 内部的排版就会乱。解决办法是把 towxml 内容包在一个设置了独立样式的容器里同时避免给全局view、text写带侵入性的样式。还有一个代码块换行的坑。towxml 渲染出来的代码块默认不会自动换行长代码会溢出屏幕。解决方式是在towxml.wxss里给代码容器加上横向滚动.towxml-code { white-space: pre; overflow-x: auto; }而外层文字节点如果太长又不能用white-space: pre否则中文长句也不换行了。所以代码块样式要单独处理别图省事把全局文本都设成pre。5.3 大数据量渲染与 setData 体积警告towxml 把整篇文章解析成节点树后所有节点数据都要通过setData传到渲染层。小程序setData单次上报有大小限制约 1MB一篇几万字的 Markdown 文档解析出来的节点树很容易逼近甚至超过这个上限。实际表现是开发者工具里报“The data transfer size is too large”真机上则可能直接白屏或闪退。怎么优化几个方案按优先级排序后端压缩内容。比如只下发正文摘要或者把正文分包加载不要一次性灌给前端。前端截断。文章超过一定长度时用“阅读原文”或分页的方式把内容切成多段。towxml 是整棵节点树渲染没法做到流式加载所以分页是成本最低的方案。数据精简。检查toJson返回的节点树去掉用不到的多余字段。但 towxml 内部对返回结构有依赖贸然删字段可能引发渲染问题不太建议动。如果你只是渲染几千字的文章这些问题都不用担心。但如果你做的是长文阅读类的应用这一条值得提前规划。6. 基于 towxml 的扩展方向towxml 解析出来的节点树是结构化数据这意味着你能在渲染之外做很多事情。这里分享两个我在项目中用到的扩展思路。6.1 动态切换 Markdown 与 HTML 两种输入有时候运营希望通过富文本编辑器编辑内容保存的是 HTML而技术文档中心保存的是 Markdown。你的前端页面需要同时兼容这两种格式。实现思路很简单页面提供一个contentType数据字段根据字段决定调用哪个解析入口switch (res.data.contentType) { case html: article towxml.toJson(res.data.content, html); break; case markdown: default: article towxml.toJson(res.data.content, markdown); break; } this.setData({ article });注意同一次渲染过程中不要在中途更换contentType否则节点树结构突变会导致渲染错乱。我的做法是每次进入页面重新解析并重置整个节点树。6.2 用节点树生成文章目录towxml 会把 Markdown 的#、##、###标题解析成带特定 class 的 view 节点。既然有了节点树我就可以在 JS 里遍历它把所有标题节点提取出来生成一个目录列表extractToc(nodes) { const toc []; const walk (list) { list.forEach((item) { const cls item.attrs item.attrs.class; if (cls /towxml-h[1-3]/.test(cls)) { toc.push({ level: cls.replace(towxml-h, ), text: item.children.map(c c.text).join() }); } if (item.children item.children.length) { walk(item.children); } }); }; walk(nodes); return toc; }拿到目录后你可以把它渲染成侧边栏或顶部的折叠面板点击时通过scroll-view的滚动或pageScrollTo定位到对应标题位置。这个小功能很提升阅读体验而且完全不用依赖额外的解析库。towxml 不是万能的但对于 90% 需要在小程序里渲染 Markdown/HTML 的场景它是我目前试过性价比最高的方案。我的建议是先用官方仓库里的 Demo 跑通一遍再按需裁剪、定制样式最后再考虑二次开发。这样每一步都有据可依不会掉进“不知道是自己改坏了还是库本身的问题”这个深坑。