ARTICLE · 1107747
我比较挑剔 Uniapp 为了完美适配 一个功能我写了三份
商品详情要显示一段 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 都没有。
三个端的机制到底差在哪
H5
渲染机制:真 DOM 支持 style 标签:支持 支持 class:支持 视频: <video>原生
小程序
渲染机制: rich-text白名单支持 style 标签:不支持 支持 class:不支持 视频:被丢弃
App-vue
渲染机制:webview,接近 H5 支持 style 标签:支持 支持 class:支持 视频:原生
App-nvue
渲染机制:原生渲染,无 DOM 支持 style 标签:不支持 支持 class:不支持 视频:需原生组件
所以富文本解析组件干的事情是:把 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 深 登录页就一个按钮,库里有三条不同的身份