Skip to content

商品详情要显示一段 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-vuewebview,接近 H5支持支持原生
App-nvue原生渲染,无 DOM不支持不支持需原生组件

所以富文本解析组件干的事情是:把 HTML 解析成一棵节点树,再按当前端的能力重新渲染成对应的组件。

不是「显示 HTML」,是「把 HTML 翻译成这个端能理解的东西」。这就是那 242 处条件编译的来源——树上每一类节点,在每一个端都要单独决定怎么落地。

四类必踩的节点 ​

一、图片:小程序上必须自己算宽度 ​

H5 上 img { max-width: 100% } 就完事了。

小程序的 rich-text 不认 class,你只能把样式写成行内 style。而且图片加载完之前拿不到真实宽高,页面会先塌一下再撑开。

解析的时候给每个 <img> 补上行内样式:

js
// 解析到 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> 组件在外面渲染:

js
// 解析时把 video 摘出来
if (node.name === 'video') {
  this.videos.push({
    src: node.attrs.src,
    poster: node.attrs.poster
  })
  // 原地留一个占位,外层去渲染
  return { name: 'placeholder' }
}

然后模板里按端分开:

html

<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-textnvue 页面别放富文本,或改用 webview 渲染
页面先塌后撑,抖一下图片加载完才知道高度后台存图片时一并存宽高,渲染时先占位

最后 ​

能复用的是业务逻辑、接口调用、状态管理。渲染这一层,
端与端的能力差异是硬的,只能一个一个处理。

工程量不在写这些分支,在知道有这些分支存在——
H5 上跑通了不代表任何事情。

那 242 处条件编译里,没有一处是写它的人想写的。


这条链路上的其他几篇

签名-C

大粽子