微信小程序 AI 对话页的 Markdown 渲染实践:轻量解析、流式输出与两个样式坑
最近在做一个 AI 提示词助手类微信小程序,其中对话页(分镜 / 视频提示词生成)有个绕不开的需求:AI 的回复是 Markdown 格式的,有标题、列表、代码块和表格,如果只当纯文本显示,可读性很差。这篇文章记录一下我在小程序里落地 Markdown 渲染的方案,以及过程中踩的两个很隐蔽的样式坑。
为什么自研一个轻量解析器
小程序对包体积敏感,现成的 Markdown 库要么体积偏大,要么依赖 DOM,而小程序的渲染层并不提供真正的 DOM。实际观察 AI 回复里用到的语法,其实很集中:标题、有序/无序列表、引用、代码块、表格、分割线,再加上行内的加粗、斜体、删除线、行内代码和链接。
所以我写了一个无依赖的轻量解析器(一个单独的 JS 文件),只做这批语法:
- 块级解析按行扫描,识别标题、列表、引用、围栏代码块、表格和分割线;
- 行内只做单层解析,不追求嵌套格式的完整还原,避免复杂度失控;
- 表格的对齐标记统一按左对齐处理,这是刻意的取舍,移动端窄屏下差异并不明显;
- 代码块单独带复制按钮,链接在小程序里不能直接打开,就做成点按复制。
解析结果是一组带位置 key 的结构化块,页面再用模板把块渲染出来。key 按位置生成有一个好处:流式输出时,除末尾正在增长的块之外,前面块的 key 保持稳定,节点不会被整体重建。
流式输出:80ms 增量解析
AI 回复是流式返回的,一个字一个字往外吐。如果每来一个片段就全量重新解析、全量 setData,手机上很快就会卡。我的做法是把解析做节流:流式期间每 80ms 做一次增量解析,只更新当前消息对应的数据。
还有一个容易忽略的点:解析必须有兜底。流式到一半时,Markdown 往往是不完整的,比如围栏代码块还没闭合、表格只写了一行。如果解析器在这种半成品输入上抛异常,整个流式渲染就会卡死。后来我给渲染入口包了一层 try/catch,解析异常时直接退回纯文本显示,等内容完整了自然又能解析成富文本,用户侧无感。
踩坑一:行内代码被从单词中间截断
样式调完后我截了张图检查效果,发现行内代码断行很怪:update:modelValue 被断成了 update:modelV 和 alue,export default 也被从中间劈开。
原因在容器上的一行 CSS:word-break: break-all。它允许在任意两个字符之间断行,对中文没问题,对英文标识符就是灾难。改法是换成:
.md {
overflow-wrap: break-word;
}
这样只在空格、标点处优先换行,实在放不下的超长字符串才会断开,行内代码恢复正常。
踩坑二:表格列被压扁,横向滚动失效
第二个坑在表格。最初单元格用了 flex: 1,想着让各列均分宽度,结果内容再长也被挤进屏幕宽度里,外层明明写了横向滚动,却根本滚不起来,列全被压扁了。
后来改成让列宽由内容决定,并给单个单元格加一个上限:
.md-table-cell {
flex: none;
max-width: 560rpx;
}
flex: none 后,窄表格照常显示,超宽表格可以左右滑动;max-width 则防止某一格里有一段超长文本,把整张表撑得过宽,超过上限就在格内换行。
滚动跟随的一点教训
流式输出时视图要自动跟随到底部,但用户一旦手动上滑看历史,就不该再把他拽回去。这件事不能靠猜滚动事件去反推用户意图,更稳的做法是直接跟踪触摸状态:记录用户是否正在触摸、是否在拖动,再配合一个短暂的惯性窗口和程序滚动后的锁定时间,决定要不要继续跟随。另外还有个很现实的教训:排查这类问题时,先确认自己测的是不是最新构建——我曾经对着旧包调了好几轮,最后发现新代码根本没跑起来。
小结
在小程序里做 Markdown 渲染,我的体会是三点:第一,按实际用到的语法做减法,轻量解析器完全够用,包体积和可控性都更好;第二,流式场景下一定要考虑「内容不完整」这个中间状态,解析要有兜底,渲染要有节流;第三,移动端的样式问题往往不在复杂逻辑,而在一行 word-break、一个 flex: 1 这种小地方,截图逐项对照检查是最快的定位方式。
- 点赞
- 收藏
- 关注作者
评论(0)