1. 为什么我们需要一个专门的富文本渲染引擎?

如果你在小程序里做过内容展示,尤其是那种从后台获取的、带格式的文章,你肯定遇到过这个头疼的问题:小程序原生的 rich-text 组件,用起来真是处处是坑。我刚开始做内容类小程序时,也是被它折磨得够呛。比如,后台编辑了一篇漂亮的文章,有加粗、有列表、有表格,甚至还有代码块,通过接口拿到一串 HTML 或者 Markdown 字符串,你兴冲冲地塞给 rich-text,结果展示出来样式乱七八糟,表格对不齐,代码没有高亮,图片尺寸失控……那种感觉,就像你精心准备的礼物,被一个粗糙的包装盒给毁了。

这背后的核心原因是,小程序为了安全和性能,对动态渲染的 HTML 节点和样式有非常严格的限制。它不是一个完整的浏览器环境,很多 Web 上习以为常的标签和 CSS 属性在这里直接失效。更麻烦的是,如果你渲染的是 Markdown,还得先把它转换成小程序能识别的结构(也就是所谓的 JSON 节点树),这个转换过程本身就很复杂。

所以,一个能“理解”富文本格式,并把它完美适配到小程序视图层的“翻译官”就显得至关重要。这就是 Towxml 的价值所在。它不是一个简单的组件,而是一个完整的渲染引擎。你可以把它想象成一个超级转换器:你把原始的 Markdown 或 HTML 字符串喂给它,它经过一系列复杂的解析、解码、样式计算,最终输出一份小程序 rich-text 组件能够完美消化、并且样式高度还原的“营养餐”。

而 uni-app 作为跨端开发框架,让我们可以用 Vue 的语法一次开发,发布到多个小程序平台。把 Towxml 和 uni-app 结合起来,就等于为我们跨端的内容展示需求,找到了一个通用且强大的解决方案。接下来,我就带你从零开始,手把手把这个引擎搭建起来,让你以后再也不怕富文本渲染。

2. 前期准备:认识我们的核心工具 Towxml 3.0

在动手敲代码之前,我们得先搞清楚手里的“武器”。Towxml 目前已经迭代到了 3.0 版本,和之前的 2.0 相比,它有了很多值得称赞的改进,这也是我推荐使用新版本的原因。

首先,Towxml 3.0 是一个纯 JavaScript 库。这意味着它的核心工作——解析 Markdown/HTML 并生成节点树——是在逻辑层完成的,不依赖特定的视图框架。这为它在 uni-app 中的集成提供了很好的基础。它的工作流程非常清晰:输入字符串 -> 解析成抽象语法树(AST)-> 根据规则转换为小程序节点树 -> 附加样式和事件。这个节点树,就是一个符合小程序 rich-text 组件 nodes 属性要求的 JSON 对象。

其次,3.0 版本在功能上更加完善。我实测下来,它对 GitHub Flavored Markdown(GFM)的支持度很高。这意味着:

  • 表格渲染:终于不再是难题了,可以正确解析表头、表格体,并生成对应的 table、tr、td 标签结构。
  • 代码高亮:这是程序员最爱的功能!Towxml 内置了高亮支持,可以将 ```javascript 这样的代码块,转换成带语法高亮样式的视图,阅读体验瞬间提升。
  • 数学公式(LaTeX):对于技术文档或学术内容,这是一个杀手级功能。虽然需要额外配置,但它提供了可能性。
  • 图表支持:甚至能解析简单的流程图、时序图定义(需注意,我们输出时不能使用 mermaid 代码块,但 Towxml 有其自己的处理方式)。
  • 样式隔离与可定制性:Towxml 生成的样式类名是可控的,我们可以很方便地在自己的小程序样式文件中覆盖或增强,实现主题定制。

最后,它的输出结构更适合 uni-app。生成的目标节点树,可以直接赋值给一个封装好的自定义组件,这个组件内部再去调用小程序的 rich-text。这样,我们在 Vue 单文件组件里使用起来,就和使用一个普通的 view 组件一样自然。

简单来说,选择 Towxml 3.0,就是选择了一个功能全面、社区活跃、文档相对清晰的解决方案。它能帮我们解决 90% 以上的富文本渲染需求,剩下的 10%,我们可以通过样式微调来搞定。

3. 第一步:获取并构建 Towxml 库

好了,理论部分先到这里,我们开始动手。第一步,我们需要把 Towxml 的源代码拿到本地,并把它构建成我们 uni-app 项目能用的形式。这个过程有点像买家具,我们拿到的是板材和说明书(源代码),需要自己组装成能用的柜子(构建后的库)。

首先,克隆仓库。 打开你的终端(命令行工具),进入你平时存放代码的目录。我习惯在 ~/Projects 下面操作。执行下面的命令:

git clone https://github.com/sbfkcel/towxml.git

这个命令会把 Towxml 的主干代码拉取到本地一个叫 towxml 的文件夹里。如果网络不太好,这个过程可能会慢一点,耐心等待即可。

接着,安装依赖并构建。 进入刚刚克隆下来的 towxml 目录:

cd towxml
npm install

npm install 会读取项目里的 package.json 文件,把 Towxml 运行和构建所需的所有第三方工具包(我们称之为依赖)下载到本地的 node_modules 文件夹。这个过程视网络情况而定,可能会花费几分钟。

依赖安装成功后,就可以执行构建命令了:

npm run build

这个 build 命令是定义在 package.json 脚本里的。它具体做了什么呢?我带你简单理解一下:它会启动一个构建流程,把源代码(主要是 src 目录下的那些 js 文件)进行打包、转换和优化。最终,它会在项目根目录下生成一个 dist 文件夹。这个 dist 文件夹里的内容,才是我们 uni-app 项目真正需要的东西,它包含了已经处理好的、可以直接引用的 JavaScript 文件和组件配置文件。

构建成功后,你会看到终端有相应的完成提示。现在,你的 towxml 目录里应该多出了一个 dist 文件夹。我们的“板材”已经加工成“标准件”了。

注意:有些同学可能会遇到构建错误,比如提示某个模块找不到。这通常是因为 Node.js 版本或 npm 版本不兼容。我建议使用 Node.js 的 LTS(长期支持版),比如 18.x 或 20.x,并且确保你的 npm 版本在 8 以上。如果遇到问题,可以去 Towxml 的 GitHub 仓库的 Issues 页面看看,大概率有人遇到过同样的问题并提供了解决方案。

4. 第二步:将 Towxml 集成到 Uni-app 项目

构建好的 dist 文件夹不能直接拿来就用,我们需要把它“搬”到我们的 uni-app 项目里,并放到一个正确的位置。小程序平台有一个特殊要求:凡是需要用到小程序自定义组件的,如果这个组件不是通过 npm 安装的,那么它必须放在项目特定的目录下。

1. 重命名与创建目录 首先,我们把刚才得到的 dist 文件夹改个名,就叫 towxml,这样更直观。然后,打开你的 uni-app 项目(假设你已经用 HBuilderX 或命令行创建好了一个 uni-app 项目)。

在 uni-app 项目的根目录下,我们需要新建一个名为 wxcomponents 的文件夹。这个文件夹的名字是固定的,小程序平台会在这里寻找自定义组件。接着,把刚刚改名后的 towxml 文件夹,整个复制到 wxcomponents 目录里。

现在你的目录结构应该是这样的:

你的uni-app项目/
├── pages/
├── static/
├── wxcomponents/       <-- 你新建的目录
│   └── towxml/         <-- 从dist改名并复制过来的文件夹
│       ├── decode.json
│       ├── decode.js
│       ├── audio-player/
│       └── ... (其他文件和子目录)
├── App.vue
├── main.js
└── pages.json

2. 关键一步:修改组件引用路径 复制过来之后,有一个非常关键且容易出错的步骤:修改配置文件。打开 wxcomponents/towxml/decode.json 这个文件。这个文件定义了 towxml 组件内部依赖了哪些子组件。

原始文件里的路径可能是相对于它原来仓库结构的。现在它被移动了,我们必须把路径改成相对于当前 decode.json 文件的新位置。通常需要修改成下面这样:

{
  "component": true,
  "usingComponents": {
    "decode": "./decode",
    "audio-player": "./audio-player/audio-player",
    "echarts": "./echarts/echarts",
    "latex": "./latex/latex",
    "table": "./table/table",
    "todogroup": "./todogroup/todogroup",
    "yuml": "./yuml/yuml",
    "img": "./img/img"
  }
}

注意看,所有的路径都变成了 "./xxx" 或 "./xxx/xxx" 这种相对路径格式。./ 表示当前目录(也就是 towxml 文件夹)。这一步至关重要,如果路径不对,小程序在运行时就会报“找不到组件”的错误。我当初就在这里卡了半小时,一直提示组件未定义,最后才发现是路径没改对。

5. 第三步:在 Uni-app 中配置与注册

现在,引擎的“零件”已经放好了,我们需要告诉 uni-app 和小程序平台:“我这儿有个自定义组件,名字叫 towxml,你们可以用它了。” 这个配置主要在两个地方进行。

1. 页面级配置 (pages.json) pages.json 文件管理着我们小程序的页面路径和全局样式。我们需要在需要使用 towxml 组件的页面里声明它。

假设我们要在首页 (pages/index/index) 使用。找到 pages.json 的 pages 数组,在首页对应的 style 配置项里,添加 usingComponents:

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "富文本展示",
        "usingComponents": {
          "towxml": "/wxcomponents/towxml/towxml"
        }
      }
    }
    // ... 其他页面
  ]
}

这里,"towxml": "/wxcomponents/towxml/towxml" 的意思就是,在这个页面里,我可以使用一个叫 <towxml> 的标签,它的实际组件路径是项目根目录下的 /wxcomponents/towxml/towxml。

2. 全局配置 (pages.json 的 globalStyle) 如果你在多个页面都要用到这个组件,在每个页面都配一遍就太麻烦了。我们可以把它配置到全局。在 pages.json 中找到(或添加)globalStyle 节点:

"globalStyle": {
  "navigationBarTextStyle": "black",
  "navigationBarTitleText": "我的小程序",
  "navigationBarBackgroundColor": "#F8F8F8",
  "backgroundColor": "#F8F8F8",
  "usingComponents": {
    "towxml": "/wxcomponents/towxml/towxml"
  }
},

这样配置后,所有的页面就都可以直接使用 <towxml> 标签了,无需再在每个页面单独声明。我个人更推荐这种方式,一劳永逸。

3. 在 Vue 中引入工具函数 (可选但推荐) Towxml 的核心是一个解析函数,我们需要在 Vue 页面里调用它。通常的做法是,在项目的 main.js 或一个单独的工具模块中,引入并挂载这个函数,方便在任何页面使用。

在 main.js 中,你可以这样操作:

// main.js
import App from './App'
import towxml from './wxcomponents/towxml/towxml'; // 引入核心解析库

// 将 towxml 函数挂载到 Vue 原型上,这样在每个 Vue 组件里都能用 this.towxml() 调用
Vue.prototype.towxml = towxml;

// 也可以挂载到全局变量,根据你的项目习惯来
// uni.towxml = towxml;

App.mpType = 'app'
const app = new Vue({
  ...App
})
app.$mount()

通过 Vue.prototype.towxml = towxml; 这行代码,我们就将一个名为 towxml 的解析函数,注入到了每一个 Vue 组件实例中。在组件的 methods 或者 mounted 生命周期里,你就可以通过 this.towxml() 来调用它了。

6. 第四步:在页面中实战使用与测试

配置全都搞定,终于到了最激动人心的环节:在页面里真正用起来,看看效果!我们创建一个测试页面,或者就在首页 index.vue 里操作。

1. 编写页面模板和脚本 打开你的页面组件文件,比如 pages/index/index.vue。

<template>
  <view class="content">
    <!-- 这里就是我们的富文本渲染器,`:nodes` 绑定解析后的数据 -->
    <towxml :nodes="articleNodes" />
  </view>
</template>

<script>
export default {
  data() {
    return {
      // 这个变量将存放解析后的节点树
      articleNodes: null,
      // 这是我们的原始 Markdown 字符串,实际项目中通常从网络接口获取
      rawMarkdown: `
# 欢迎使用 Towxml 3.0

这是一个在 uni-app 小程序中渲染 Markdown 的完整示例。

## 功能展示

### 1. 标题层级
如上所示,H1到H6的标题都能正确渲染。

### 2. 列表
- **无序列表项一**
- *无序列表项二*
- 无序列表项三

1. **有序列表第一项**
2. *有序列表第二项*
3. 有序列表第三项

### 3. 引用与强调
> 这是一段引用文字。富文本渲染让内容层次更清晰。

你可以使用**加粗**、*斜体*或者***粗斜体***来强调内容。

### 4. 表格
| 功能 | 是否支持 | 备注 |
| :--- | :---: | --- |
| 表格 | 是 | 支持对齐方式 |
| 代码块 | 是 | 支持语法高亮 |
| 图片 | 是 | 需配置域名 |

### 5. 代码块
下面是一段 JavaScript 代码:
\`\`\`javascript
// 这是一个示例
function helloTowxml() {
  const parser = this.towxml;
  const nodes = parser(‘# Hello‘, ‘markdown‘);
  console.log(‘解析完成‘, nodes);
  return nodes;
}
\`\`\`

---
渲染完成!如果内容很长,记得给外层容器加上适当的高度和滚动。
`
    };
  },
  onLoad() {
    // 在页面加载时,调用解析函数
    this.renderContent();
  },
  methods: {
    renderContent() {
      // 调用挂载在 Vue 原型上的 towxml 函数
      // 第一个参数:要解析的字符串
      // 第二个参数:字符串类型,‘markdown‘ 或 ‘html‘
      this.articleNodes = this.towxml(this.rawMarkdown, ‘markdown‘);

      // 解析完成后,articleNodes 就是一个可以直接给 <towxml> 组件的 nodes 属性使用的对象
      console.log(‘解析结果:‘, this.articleNodes);
    }
  }
};
</script>

<style>
.content {
  padding: 20rpx 30rpx;
  box-sizing: border-box;
}
/* 这里可以添加一些全局样式来美化 towxml 的渲染结果,比如段落间距、字体等 */
</style>

2. 关键点解析

  • :nodes="articleNodes":这是单向绑定,将我们脚本中解析好的 articleNodes 数据传递给 towxml 组件。
  • this.towxml(this.rawMarkdown, ‘markdown‘):这就是核心解析调用。它接收原始字符串和类型标识,返回节点树。务必确保第二个参数类型正确,解析 Markdown 就用 ‘markdown‘,解析 HTML 就用 ‘html‘。
  • 异步数据:实际项目中,rawMarkdown 大概率是从服务器 API 获取的。你需要在接口请求成功后的回调函数里(例如 then 或 async/await 之后)再执行 this.towxml() 解析,并将结果赋值给 articleNodes。

保存文件,运行你的 uni-app 项目到微信开发者工具。如果一切配置正确,你应该能在模拟器上看到一篇格式清晰、带有标题、列表、表格、代码高亮的完整文章了!第一次看到自己渲染出这么复杂的富文本时,那种成就感真的很棒。

7. 第五步:深度定制与样式优化

基础渲染跑通,只是成功了第一步。要让渲染效果真正贴合你的产品设计,还需要进行样式定制。Towxml 生成的节点都带有特定的类名(class),这给我们提供了定制样式的钩子。

1. 理解 Towxml 的样式类名 Towxml 在解析时,会给不同的元素打上类名。比如:

  • 段落可能用 .p
  • 链接用 .a
  • 代码块容器用 .code
  • 表格用 .table, .tr, .td
  • 引用块用 .blockquote

你需要打开小程序开发者工具的调试器,在 Wxml 面板中查看实际渲染出来的节点结构及其类名,这是最准确的方式。

2. 编写全局样式文件 我推荐在 App.vue 的 <style> 标签中,或者在一个全局引入的样式文件(如 common/theme.css)里,编写针对这些类名的样式。因为 towxml 组件最终会渲染在小程序页面的节点树里,全局样式对其是有效的。

例如,在 App.vue 中增强样式:

<style>
/* App.vue 中的样式对所有页面生效 */
page {
  font-family: -apple-system, BlinkMacSystemFont, ‘Helvetica Neue‘, Helvetica, sans-serif;
  color: #333;
  line-height: 1.6;
}

/* 定制 Towxml 渲染的样式 */
/* 1. 标题 */
.t-p {
  margin: 1.5em 0 0.8em;
  font-weight: bold;
}
.t-h1 { font-size: 2em; }
.t-h2 { font-size: 1.5em; }
.t-h3 { font-size: 1.17em; }

/* 2. 列表 */
.t-ul, .t-ol {
  padding-left: 2em;
  margin: 1em 0;
}
.t-li {
  margin: 0.3em 0;
}

/* 3. 表格 - 这是一个常见痛点,需要仔细调整 */
.t-table {
  width: 100%;
  border-collapse: collapse;
  margin: 1em 0;
  font-size: 0.9em;
  overflow-x: auto; /* 小程序中可能需要外层容器滚动 */
  display: block;
}
.t-tr {
  border-bottom: 1px solid #eaeaea;
}
.t-th, .t-td {
  padding: 12rpx 16rpx;
  text-align: left;
  border: 1px solid #ddd;
}
.t-th {
  background-color: #f5f5f5;
  font-weight: bold;
}

/* 4. 代码块 */
.t-code {
  background-color: #f6f8fa;
  border-radius: 6px;
  padding: 1em;
  margin: 1em 0;
  overflow-x: auto;
  font-family: ‘Menlo‘, ‘Monaco‘, ‘Courier New‘, monospace;
  font-size: 0.9em;
}
.t-code-inline {
  background-color: #f0f0f0;
  padding: 0.2em 0.4em;
  border-radius: 3px;
  font-family: monospace;
}

/* 5. 引用块 */
.t-blockquote {
  border-left: 4px solid #ddd;
  padding-left: 1em;
  margin: 1em 0;
  color: #666;
  font-style: italic;
}

/* 6. 图片自适应 */
.t-img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 1em auto;
  border-radius: 4px;
}
</style>

3. 处理图片自适应与预览 图片是富文本中的另一个大头。Towxml 会将 img 标签解析出来,但默认可能没有宽度限制。上面的 .t-img 样式通过 max-width: 100% 确保了图片不会撑破容器。更进一步的,你可能需要实现图片预览功能。这需要给图片绑定点击事件,Towxml 组件通常支持事件传递,你需要查阅其文档,看如何监听图片的 tap 事件,然后调用小程序本身的 wx.previewImage API 来实现预览。

4. 关于 LaTeX 和图表 如果你的内容涉及数学公式或复杂图表,Towxml 3.0 提供了对应的子组件(如 latex, echarts, yuml)。要使用它们,你需要:

  1. 确保在 decode.json 中这些组件的路径配置正确。
  2. 这些功能可能需要额外的运行时库(比如 LaTeX 渲染引擎),体积会比较大。你需要评估是否真的需要,如果不需要,可以考虑在构建 Towxml 时进行裁剪,或者不引入这些组件,以减小小程序包体积。

样式定制是一个持续的过程,最好的方法就是对照着设计稿,在开发者工具里一边调整样式,一边实时预览效果。记住,小程序的 CSS 支持是子集,有些 Web 上的属性(如 position: fixed 在某些容器内)可能表现不一致,需要多测试。

8. 常见问题排查与性能优化

项目上线前,我们还得扫清障碍,确保稳定和流畅。下面是我在项目中踩过的一些坑,以及对应的解决办法。

1. 组件未定义错误

  • 症状:小程序控制台报错 Component is not found in path “...”。
  • 排查:
    1. 检查 wxcomponents/towxml 目录是否存在且完整。
    2. 重点检查 wxcomponents/towxml/decode.json 文件中的路径是否正确。所有路径必须是相对于该文件本身的正确相对路径。
    3. 检查 pages.json 中 usingComponents 的路径配置是否正确,路径应该以 / 开头,指向项目根目录。
    4. 如果是第一次配置,尝试在微信开发者工具中点击 工具 -> 构建 npm(如果你用了 npm)以及 编译 -> 重新编译项目。

2. 解析失败或渲染空白

  • 症状:数据传了,但页面一片空白,或者只渲染了一部分。
  • 排查:
    1. 在 this.towxml() 调用后,用 console.log 打印输出结果,看看 articleNodes 是否是有效的、非空的对象。
    2. 检查原始字符串格式。确保你传给 this.towxml() 的第二个参数(‘markdown‘ 或 ‘html‘)与实际内容格式匹配。不要把 HTML 字符串用 ‘markdown‘ 模式去解析。
    3. 检查字符串中是否包含非常规或极端复杂的标记,可以尝试先用一段简单的 Markdown(如 # 测试)来验证基础功能。

3. 样式不生效

  • 症状:渲染出了内容,但样式和预期不符,自定义的 CSS 没起作用。
  • 排查:
    1. 打开开发者工具的 Wxml 面板,找到渲染出来的节点,查看其实际的 class 名。你的 CSS 选择器必须和这个类名完全匹配。
    2. 检查 CSS 选择器的优先级。小程序的样式也有优先级规则,可能需要使用 !important 来覆盖(谨慎使用)。
    3. 确保样式文件被正确引入并作用于当前页面。在 App.vue 中的样式是全局生效的。

4. 性能优化建议 富文本解析,特别是长篇文章,是一个计算密集型操作。如果直接在页面的主线程(onLoad 或 mounted 中)同步执行 this.towxml(),可能会造成页面短暂的卡顿或白屏。

  • 优化解析时机:对于很长的内容,可以考虑在页面 onLoad 时先展示一个加载态,然后使用 setTimeout 或 uni.nextTick 将解析操作放到下一个事件循环中执行,避免阻塞渲染。
    onLoad() {
      uni.showLoading({ title: ‘加载中...‘ });
      setTimeout(() => {
        this.renderContent();
        uni.hideLoading();
      }, 50);
    }
    
  • 缓存解析结果:如果同一篇文章会被多次查看(比如详情页),可以将解析后的 articleNodes 对象缓存起来,例如存入 Vuex 或本地存储,下次直接读取,避免重复解析。
  • 列表渲染优化:如果在列表页需要渲染很多文章的摘要(富文本片段),要警惕性能问题。可以考虑只解析并渲染纯文本摘要,或者限制摘要的长度。进入详情页再完整解析。
  • 注意包体积:towxml 库本身有一定体积。构建发布时,使用微信开发者工具的“代码依赖分析”,查看它对总包大小的贡献。如果过大,可以考虑按需引入,或者与服务端协商,能否在后台完成解析,前端直接接收节点树数据(但这增加了后台复杂度)。

最后,再分享一个我自己的经验:在真机上多做测试。模拟器环境和真机环境,特别是在低端安卓机上,有时表现会有差异。真机测试能帮你发现那些在模拟器上发现不了的滚动卡顿、图片加载慢等问题。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐