商品详情要显示一段 HTML,三个端各解释成各的
商品详情要显示一段 HTML。三个端各解释成各的。
在一套 UniApp 代码里数了一下条件编译:全项目 179 个文件带 #ifdef。
分叉最密集的不是支付,不是登录,是富文本解析器。两份解析器组件 + 它们的依赖,加起来 242 处条件编译。商品详情页本体自己还有 38 处。
一个「把 HTML 显示出来」的功能,比支付还费劲。
支付好歹是三家平台三套协议。富文本就一段 HTML,三个端自己解释成三个样。
商品详情是运营在后台用富文本编辑器排的:图片、文字、表格、有时候还有视频。
你要把这段 HTML 在小程序、H5、App 三端显示出来,而且要长得一样。
卡人的地方在于:这三个端渲染 HTML 的机制根本不是一回事。
踩错的代价
最耗时的踩法是:H5 上看着完全正常,所以以为没问题。
H5 就是浏览器,v-html 一挂,什么都对。图能显示,表格有边框,视频能播。
然后小程序上一看:图片撑破屏幕,表格塌成一坨,视频位置是空白。
因为小程序没有 DOM。它的 rich-text 组件只支持一个受限的标签白名单,不支持 <style> 标签,不支持 class,不执行任何脚本,<video> 直接被丢掉。
这时候你才发现要引一个富文本解析组件。引进来,小程序好了,App 又出问题——因为 App 有两种渲染模式(webview 和 nvue),nvue 那套连 rich-text 都没有。
三个端的机制到底差在哪
| 渲染机制 | 支持 style 标签 | 支持 class | 视频 | |
|---|---|---|---|---|
| H5 | 真 DOM | 支持 | 支持 | <video> 原生 |
| 小程序 | rich-text 白名单 | 不支持 | 不支持 | 被丢弃 |
| App-vue | webview,接近 H5 | 支持 | 支持 | 原生 |
| App-nvue | 原生渲染,无 DOM | 不支持 | 不支持 | 需原生组件 |
所以富文本解析组件干的事情是:把 HTML 解析成一棵节点树,再按当前端的能力重新渲染成对应的组件。
不是「显示 HTML」,是「把 HTML 翻译成这个端能理解的东西」。这就是那 242 处条件编译的来源——树上每一类节点,在每一个端都要单独决定怎么落地。
四类必踩的节点
一、图片:小程序上必须自己算宽度
H5 上 img { max-width: 100% } 就完事了。
小程序的 rich-text 不认 class,你只能把样式写成行内 style。而且图片加载完之前拿不到真实宽高,页面会先塌一下再撑开。
解析的时候给每个 <img> 补上行内样式:
// 解析到 img 节点时
node.attrs.style =
'max-width:100%;display:block;'
+ (node.attrs.style || '')顺带一提:运营贴进来的图片经常带 width="750" 这种固定宽度属性,得先把它删掉,否则行内样式压不过属性。
二、表格:border 属性会被丢掉
后台编辑器生成的表格通常靠 <table border="1"> 画边框。border 这个属性不在 rich-text 的白名单里,解析时被丢掉,
所以 H5 上有边框,小程序上是一堆没有分隔的文字。
解决办法是解析时把表格拆开重排,或者干脆在解析层把 table 转成一组 div。后者更稳,但会丢掉表头的视觉层级。
我的建议是别让运营在商品详情里放表格。这个决定比任何技术方案都省事——规格参数用商品属性字段,不要塞进富文本。
三、视频:小程序会直接丢掉
rich-text 的白名单里没有 <video>。解析器要做的是把 video 节点单独抽出来,用小程序的原生 <video> 组件在外面渲染:
// 解析时把 video 摘出来
if (node.name === 'video') {
this.videos.push({
src: node.attrs.src,
poster: node.attrs.poster
})
// 原地留一个占位,外层去渲染
return { name: 'placeholder' }
}然后模板里按端分开:
<rich-text :nodes="nodes" />
<video v-for="v in videos"
:src="v.src" :poster="v.poster" />
<div v-html="html"></div>四、链接:小程序里点了没反应
富文本里的 <a href="..."> 在小程序上是死的。要跳转得自己拦:解析时把 <a> 转成一个可点击的节点,绑一个事件,事件里判断是站内路径还是外链,站内 navigateTo,外链在小程序里只能复制到剪贴板提示用户去浏览器打开。
这条经常被漏,因为商品详情里的链接不多,测试也不一定点。
顺带看一眼后端怎么存它
前面全是前端的账。后端这边有个细节值得单独说,因为它决定了前端要渲染几份。
商品详情不是商品表里的一个字段,是单独一张表。
商品相关的表一共 19 张,其中有一张只干这一件事。它只有 5 个字段:
id 商品 ID 详情正文
基础类型 营销类型关键是后两个。主键不是「商品 ID」一个。
基础类型区分普通商品、积分商品、虚拟商品、视频号、云盘、卡密 6 种,
营销类型区分基础、秒杀、拼团 3 种。
也就是说:同一个商品,在这张表里可以有多行详情——
秒杀版一份、拼团版一份、积分兑换版一份。
这在业务上说得通(秒杀页的详情要写活动规则),但对前端意味着一件事:
你那套解析器面对的不是「一个商品一段 HTML」,是「一个商品一组 HTML」,
而且它们是不同人在不同时间用不同习惯排出来的。
上面数的 242 处条件编译,兜的就是这组 HTML 里参差不齐的那些写法。
顺带一个数:整个后端 private String content 这种装富文本的字段,
出现在 31 个类里——商品详情只是其中一处,社区笔记、文章、
短信模板、打印模板都有各自的一份。富文本从来不是只有商品详情页在用。
一个能省一半事的决定
上面这些都是「HTML 已经很脏了,怎么救」。
更省事的方向是在源头就限制:后台的富文本编辑器配置一个精简工具栏,只留加粗、图片、段落。表格、视频、字体颜色、自定义样式全部关掉。
这样解析器要处理的节点类型从几十种降到五六种,条件编译的分支跟着少一大半。
代价是运营会觉得不够用。但商品详情本来就该是图片为主——真正影响转化的是图,不是排版花样。
这个决定要在项目早期做。 等运营已经排了三千个商品的花式详情页,再想收工具栏,那些历史数据你收不回来。
常见报错对照表
| 现象 | 真正的原因 | 怎么查 |
|---|---|---|
| H5 正常,小程序上图片撑破屏幕 | rich-text 不认 class,max-width 没生效 | 解析时补行内 style |
| 补了行内 style 还是撑破 | 图片带 width="750" 属性,属性压过样式 | 解析时删掉 width / height 属性 |
| 小程序上视频位置一片空白 | <video> 不在 rich-text 白名单里 | 解析时摘出来,用原生组件渲染 |
| 表格没有边框 | <table border> 属性不在 rich-text 白名单里,被丢掉 | 转成 div,或者禁止富文本里放表格 |
| 富文本里的链接点了没反应 | 小程序里 <a> 不跳转 | 解析成可点击节点,自己绑事件 |
| nvue 页面上整块富文本不显示 | nvue 没有 rich-text | nvue 页面别放富文本,或改用 webview 渲染 |
| 页面先塌后撑,抖一下 | 图片加载完才知道高度 | 后台存图片时一并存宽高,渲染时先占位 |
最后
能复用的是业务逻辑、接口调用、状态管理。渲染这一层,
端与端的能力差异是硬的,只能一个一个处理。
工程量不在写这些分支,在知道有这些分支存在——
H5 上跑通了不代表任何事情。
那 242 处条件编译里,没有一处是写它的人想写的。
这条链路上的其他几篇
- 1021 深 小程序调通了,打包成 H5 一条路直接断
- 1026 深 登录页就一个按钮,库里有三条不同的身份
