百度智能小程序 富文本RichText 组件无痛支持点击图片预览

2,124 阅读5分钟

背景

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