从零到一:基于uniapp与Towxml 3.0构建小程序富文本渲染引擎
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)。要使用它们,你需要:
- 确保在
decode.json中这些组件的路径配置正确。 - 这些功能可能需要额外的运行时库(比如 LaTeX 渲染引擎),体积会比较大。你需要评估是否真的需要,如果不需要,可以考虑在构建 Towxml 时进行裁剪,或者不引入这些组件,以减小小程序包体积。
样式定制是一个持续的过程,最好的方法就是对照着设计稿,在开发者工具里一边调整样式,一边实时预览效果。记住,小程序的 CSS 支持是子集,有些 Web 上的属性(如 position: fixed 在某些容器内)可能表现不一致,需要多测试。
8. 常见问题排查与性能优化
项目上线前,我们还得扫清障碍,确保稳定和流畅。下面是我在项目中踩过的一些坑,以及对应的解决办法。
1. 组件未定义错误
- 症状:小程序控制台报错
Component is not found in path “...”。 - 排查:
- 检查
wxcomponents/towxml目录是否存在且完整。 - 重点检查
wxcomponents/towxml/decode.json文件中的路径是否正确。所有路径必须是相对于该文件本身的正确相对路径。 - 检查
pages.json中usingComponents的路径配置是否正确,路径应该以/开头,指向项目根目录。 - 如果是第一次配置,尝试在微信开发者工具中点击
工具 -> 构建 npm(如果你用了 npm)以及编译 -> 重新编译项目。
- 检查
2. 解析失败或渲染空白
- 症状:数据传了,但页面一片空白,或者只渲染了一部分。
- 排查:
- 在
this.towxml()调用后,用console.log打印输出结果,看看articleNodes是否是有效的、非空的对象。 - 检查原始字符串格式。确保你传给
this.towxml()的第二个参数(‘markdown‘ 或 ‘html‘)与实际内容格式匹配。不要把 HTML 字符串用 ‘markdown‘ 模式去解析。 - 检查字符串中是否包含非常规或极端复杂的标记,可以尝试先用一段简单的 Markdown(如
# 测试)来验证基础功能。
- 在
3. 样式不生效
- 症状:渲染出了内容,但样式和预期不符,自定义的 CSS 没起作用。
- 排查:
- 打开开发者工具的
Wxml面板,找到渲染出来的节点,查看其实际的class名。你的 CSS 选择器必须和这个类名完全匹配。 - 检查 CSS 选择器的优先级。小程序的样式也有优先级规则,可能需要使用
!important来覆盖(谨慎使用)。 - 确保样式文件被正确引入并作用于当前页面。在
App.vue中的样式是全局生效的。
- 打开开发者工具的
4. 性能优化建议
富文本解析,特别是长篇文章,是一个计算密集型操作。如果直接在页面的主线程(onLoad 或 mounted 中)同步执行 this.towxml(),可能会造成页面短暂的卡顿或白屏。
- 优化解析时机:对于很长的内容,可以考虑在页面
onLoad时先展示一个加载态,然后使用setTimeout或uni.nextTick将解析操作放到下一个事件循环中执行,避免阻塞渲染。onLoad() { uni.showLoading({ title: ‘加载中...‘ }); setTimeout(() => { this.renderContent(); uni.hideLoading(); }, 50); } - 缓存解析结果:如果同一篇文章会被多次查看(比如详情页),可以将解析后的
articleNodes对象缓存起来,例如存入 Vuex 或本地存储,下次直接读取,避免重复解析。 - 列表渲染优化:如果在列表页需要渲染很多文章的摘要(富文本片段),要警惕性能问题。可以考虑只解析并渲染纯文本摘要,或者限制摘要的长度。进入详情页再完整解析。
- 注意包体积:
towxml库本身有一定体积。构建发布时,使用微信开发者工具的“代码依赖分析”,查看它对总包大小的贡献。如果过大,可以考虑按需引入,或者与服务端协商,能否在后台完成解析,前端直接接收节点树数据(但这增加了后台复杂度)。
最后,再分享一个我自己的经验:在真机上多做测试。模拟器环境和真机环境,特别是在低端安卓机上,有时表现会有差异。真机测试能帮你发现那些在模拟器上发现不了的滚动卡顿、图片加载慢等问题。
更多推荐
所有评论(0)