背景
RichText 是显示富文本的重要组件,但是在小程序中会屏蔽所有节点的事件。最经常遇到的问题就是点击富文本的图片无法进行预览。
现在线上有很多很好的解决方案,如 Parser,但是这些解决方案里,基本都是把提供的富文本内容,根据节点类型进行转义,转义为独立的元素,比如;<p> => <view>/<text>
。
这种方式问题在于,当需要显示一篇文章,文章的内容偏长,通过转义产生新的组件会较多,产生了更多的渲染成本。
<!-- Parser -->
<!--trees 递归显示组件-->
<wxs module="Handler" src="./handler.wxs" />
<block wx:for="{{nodes}}" wx:key="index">
<block wx:if="{{Handler.notContinue(item)}}">
<!--图片-->
<rich-text wx:if="{{item.name=='img'}}" id="{{item.attrs.id}}" class="img" style="text-indent:0;{{Handler.getStyle(item.attrs.style,'inline-block')}}" nodes="{{Handler.setImgStyle(item,controls.imgMode,imgLoad)}}" data-attrs="{{item.attrs}}" bindtap="previewEvent" />
<!--文本-->
<block wx:elif="{{item.type=='text'}}">
<text wx:if="{{!item.decode}}" decode>{{item.text}}</text>
<rich-text wx:else style="display:inline-block" nodes="{{[item]}}" />
</block>
<text wx:elif="{{item.name=='br'}}">\n</text>
<!--视频-->
<block wx:elif="{{item.name=='video'}}">
<view wx:if="{{item.lazyLoad && !controls[item.attrs.id].play}}" id="{{item.attrs.id}}" class="_video {{item.attrs.class||''}}" style="{{item.attrs.style||''}}" bindtap="loadVideo" />
<video wx:else src="{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}" id="{{item.attrs.id}}" loop="{{item.attrs.loop}}" controls="{{item.attrs.controls}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}"
unit-id="{{item.attrs['unit-id']}}" class="{{item.attrs.class}}" muted="{{item.attrs.muted}}" style="{{item.attrs.style||''}}" data-source="{{item.attrs.source}}" bindplay="playEvent" binderror="videoError" />
</block>
<!--音频-->
<audio wx:elif="{{item.name=='audio'}}" id="{{item.attrs.id}}" class="{{item.attrs.class}}" src="{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}" loop="{{item.attrs.loop}}" controls="{{item.attrs.controls}}"
poster="{{item.attrs.poster}}" name="{{item.attrs.name}}" author="{{item.attrs.author}}" style="{{item.attrs.style||''}}" data-source="{{item.attrs.source}}" binderror="audioError" />
<!--链接-->
<view wx:elif="{{item.name=='a'}}" class="_a {{item.attrs.class||''}}" style="{{item.attrs.style||''}}" data-attrs="{{item.attrs}}" hover-class="navigator-hover" hover-start-time="25" hover-stay-time="300" bindtap="tapEvent">
<trees class="_node" nodes="{{item.children}}" />
</view>
<!--广告-->
<ad wx:elif="{{item.name=='ad'}}" unit-id="{{item.attrs['unit-id']}}" class="{{item.attrs.class}}" style="{{item.attrs.style||''}}" binderror="adError" />
<!--富文本-->
<rich-text wx:else id="{{item.attrs.id}}" class="__{{item.name}}" style="{{Handler.getStyle(item.attrs.style,'block')}}" nodes="{{Handler.setStyle(item)}}" />
</block>
<!--继续递归-->
<trees wx:else id="{{item.attrs.id}}" class="_node _{{item.name+' '+(item.attrs.class||'')}}" style="{{item.attrs.style||''}}" nodes="{{item.children}}" controls="{{controls}}" />
</block>
曾经做过测试,在一篇77张图片,138个段落(P标签)的文章,使用本地数据的前提下,iOS需要170ms渲染完成,Android需要325ms。当改用为Rich-Text后,iOS需要30ms渲染完成,Android需要55ms。
所提及的测试结果仅作参考,不过单独依赖Rich-Text解决富文本的渲染成本是最小的,比较不需要创建过多的组件元素。
尝试点击
由于Rich-Text在官方已提及,已屏蔽所有节点的事件,所以是没办法在正常的html标签写入任何onclick ontap之类的响应事件。
但是在Rich-Text本身却提供了点击事件,所以可以在rich-text标签直接绑定一个tap事件,获取点击的属性。

<rich-text nodes="{{content}}" bindtap="clickedRichText"></rich-text>
clickedRichText(e){
console.log(e.detail) // {x: 165.515625, y: 353.671875}
}
点击事件里,可以获取的内容是点击的坐标(x,y),这个x和y是rich-text实际的坐标,而不是屏幕上的坐标。简单来说,已经可以知道用户点击的具体位置。
找到点击的图片

因为知道了用户点击Rich-Text的位置,接下来需要找到图片渲染后所在的位置。可以通过createSelectorQuery()进行查询。
swan.createSelectorQuery().selectAll('.rich-text-img').fields({rect: true}).exec((res)=> {
console.log(res)
})

这些查询结果就是图片所在位置,在我们的小程序里业务场景默认图片是full width,所以只关心查询结果的top和bottom元素即可。

假设图片是在屏幕距离顶部是212px,图片底部距离顶部为375px,很直观能看到,只要用户点击的区域在这个范围内,就是点击了该图片。
用户操作中
现在可以知道用户点击了哪里,同时也能找到图片所在的位置。
但是有两个问题需要解决
1.因为图片需要渲染成功后,查询图片位置的坐标才精准,因为在加载中图片的高度会有所变化。
2.在计算的时候,若用户进行了滑动操作,那么图片距离顶部的位置会产生变化。
首先,图片在渲染成功后,我们没办法得到这个响应,因为Rich-Text组件或里面需渲染的元素不提供任何回调事件。所以我们只能通过简单的轮询进行更新。
我在业务代码上使用Math.pow()
产生每次轮询的时间,原因是因为渲染时间很快,在前期快速查询多次后,后期的查询可以当做图片坐标数据的修正行为。另一方面,查询元素节点属性这个行为,会消耗额外的资源,毕竟Model和View是隔离的,需要产生查询事件的消费。
比如,我设置了setTimeout的时间里,底数是6(ms),指数从1开始,总查询6次。从设置好rich-text的nodes开始执行。
所以总共执行6次,时间分别为:6ms、36ms、216ms、1296ms、7776ms、46656ms
假设文章的图片加载平均在150ms已经完成,那么前3次的查询已经可以记录好图片具体位置。
对于第二个问题,可以结合scrollTop属性进行解决
在计算图片过程中,用户进行了滑动的操作。直接会影响到图片对于屏幕顶部的距离。在查询图片成功后,可以使用scrollTop进行补正,scrollTop在onPageScroll中可以获取。
const finalResult = []
// selectResult为createSelectorQuery的结果
selectResult.forEach((r) => {
finalResult.push({
top: r.top + scrollTop,
bottom: r.bottom + scrollTop
})
})
this.imagesPosition = finalResult
扩展
到现在,我们能知道图片的位置和用户点击的位置,那么在点击后如何启动previewImage就不写了,具体下面也有demo代码
既然在百度小程序里面,可以查询rich-text组件里面的元素,那么可以做到的东西也多了。比如点击某个图片,启动播放视频;点击某段文字,可以实现复制等等,重点是已经清楚用户点击了什么。
另外,我也测试了一下微信小程序,是不支持这种实现方式,打扰了。
Demo
百度小程序代码片段
swanide://fragment/029aa6cab8754c9ab62b688298a210ca1577777948620