评论

小程序富文本能力的深入研究与应用

富文本插件的开发历程介绍

前言

在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。

现有方案

WxParse

WxParse 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。

  • 格式不正确时标签会被原样显示
    很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在WxParse中都会被认为是文本内容而原样输出,例如:
    <span style="font-family:"宋体"">Hello World!</span>
    
    这是由于WxParse的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本
    //WxParse的匹配模式
    var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
      endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/,
      attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
    
    然而,html 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。
  • 超过限定层数时无法显示
    这也是一个让许多人十分苦恼的问题,WxParse 通过 template 迭代的方式进行显示,当节点的层数大于设定的 template 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 wxml 的渲染方式也需要改进。
  • 对于表格、列表等复杂内容支持性差
    WxParse 对于 tableolul 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染

rich-text

rich-text 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处

  • 一些常用标签不支持
    rich-text 支持的标签较少,一些常用的标签(比如 section)等都不支持,导致其很难直接用于显示富文本内容
    ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题
  • 不能实现图片和链接的点击
    rich-text 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验
  • 不支持音视频
    音频和视频作为富文本的重要内容,在 rich-text 中却不被支持,这也严重影响了使用体验

共同问题

  • 不支持解析 style 标签
    现有的方案中都不支持对 style 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确

方案构建

因此要解决上述问题,就得构建一个新的方案来实现

渲染方式

对于该节点下没有图片、视频、链接等的,直接使用 rich-text 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如:

对于迭代的方式,有以下两种方案:

  • 方案一
    WxParse 那样通过 template 进行迭代,对于小于 20 层的内容,通过 template 迭代的方式进行显示,超过 20 层时,用 rich-text 组件兜底,避免无法显示,这也是一开始采用的方案
    <!--超过20层直接使用rich-text-->
    <template name='rich-text-floor20'>
    <block wx:for='{{nodes}}' wx:key>
      <rich-text nodes="{{item}}" />
    </block>
    </template>
    
  • 方案二
    添加一个辅助组件 trees,通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 template 占用空间,也是最终采取的方案
    <!--继续递归-->
    <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" />
    

解析脚本

htmlparser2 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率

//不同状态各通过一个函数进行判断和状态跳转
for (; this._index < this._buffer.length; this._index++)
    this[this._state](this._buffer[this._index]);
  • 兼容 rich-text
    为了解析结果能同时在 rich-text 组件上显示,需要对一些 rich-text不支持的组件进行转换
    //以u标签为例
    case 'u':
        name = 'span';
        attrs.style = 'text-decoration:underline;' + attrs.style;
        break;
    
  • 适配渲染需要
    在渲染过程中,需要对节点下含有图片、视频、链接等不能由 rich-text直接显示的节点继续迭代,否则直接使用 rich-text 组件显示;因此需要在解析过程中进行标记,遇到 imgvideoa 等标签时,对其所有上级节点设置一个 continue 属性用于区分
    case 'a':
       attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style;
       element.continue = true;
       //冒泡:对上级节点设置continue属性
       this._bubbling();
       break;
    

处理style标签

解析方式

  • 方案一
    正则匹配
    var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g);
    
    缺陷:
    1. style 字符串较长时,可能出现栈溢出的问题
    2. 对于一些复杂的情况,可能出现匹配失败的问题
  • 方案二
    状态机的方式,类似于 html 字符串的处理方式,对于 css 的规则进行了调整和适配,也是目前采取的方案

匹配方式

  • 方案一
    style 标签解析为一个形如 {key:content} 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功
    if (this._style[name]) attrs.style += (';' + this._style[name]);
    if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]);
    if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]);
    
    优点:匹配效率高,适合前端对于时间和空间的要求
    缺点:对于多层选择器等复杂情况无法处理
    因此在前端组件包中采取的是这种方式进行匹配
  • 方案二
    style 标签解析为一个数组,每个元素是形如 {key,list,content,index} 的结构体,主要用于多层选择器的匹配,内置了一个数组 list 存储各个层级的选择器,index 用于记录当前的层数,匹配成功时,index++,匹配成功的标签出栈时,index--;通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。

遇到的问题

  • rich-text 组件整体的显示问题
    在显示过程中,需要把 rich-text 作为整体的一部分,在一些情况下会出现问题,例如:

    Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/>
    

    在这种情况下,虽然对 rich-text 中的顶层 div 设置了 display:inline-block,但没有对 rich-text 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 floatwidth(设置为百分比时)等情况
    解决方案

    • 方案一
      用一个 view 包裹在 rich-text 外面,替代最外层的标签
      <view style="{{item.attrs.style}}">
        <rich-text nodes="{{item.children}}" />
      </view>
      
      缺陷:当该标签为 tableol 等功能性标签时,会导致错误
    • 方案二
      rich-text 组件使用最外层标签的样式
      <rich-text nodes="{{item}}" style="{{item.attrs.style}}" />
      
      缺陷:当该标签的 style 中含有 marginpadding 等内容时会被缩进两次
    • 方案三
      通过 wxs 脚本将顶层标签的 displayfloatwidth 等样式提取出来放在 rich-text 组件的 style 中,最终解决了这个问题
      var res = "";
      var reg = getRegExp("float\s*:\s*[^;]*", "i");
      if (reg.test(style)) res += reg.exec(style)[0];
      reg = getRegExp("display\s*:\s*([^;]*)", "i");
      if (reg.test(style)) {
        var info = reg.exec(style);
        res += (';' + info[0]);
        display = info[1];
      } else res += (';display:' + display);
      reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig");
      var width = reg.exec(style);
      while (width) {
        res += (';' + width[0]);
        width = reg.exec(style);
      }
      return res;
      
  • 图片显示的问题
    html 中,若 img 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 image 组件模拟,需要通过 bindload 来获取图片宽高,再进行 setData,当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制
    解决方案
    rich-text 中的 img 替代 image 组件,实现更加贴近 html 的方式 ;对 img 组件设置默认的效果 max-width:100%;

  • 视频显示的问题
    当一个页面出现过多的视频时,同时进行加载可能导致页面卡死
    解决方案
    在解析过程中进行计数,若视频数量超过3个,则用一个 wxss 绘制的图片替代 video 组件,当受到点击时,再切换到 video 组件并设置 autoplay 以模拟正常效果,实现了一个类似懒加载的功能

    <!--视频-->
    <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo">
      <view class="triangle_border_right"></view>
    </view>
    <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" />
    
  • 文本复制的问题
    小程序中只有 text 组件可以通过设置 selectable 属性来实现长按复制,在富文本组件中实现这一功能就存在困难
    解决方案
    在顶层标签上加上 user-select:text;-webkit-user-select

实现更加丰富的功能

在此基础上,还可以实现更多有用的功能

  • 自动设置页面标题
    在浏览器中,会将 title 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能
    if (res.title) {
      wx.setNavigationBarTitle({
        title: res.title
      })
    }
    
  • 多资源加载
    由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 source 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能
    errorEvent(e) {
        //尝试加载其他源
        if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) {
          this.data.controls[e.currentTarget.dataset.id] = {
            play: false,
            index: 1
          }
        } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) {
          this.data.controls[e.currentTarget.dataset.id].index++;
        }
        this.setData({
          controls: this.data.controls
        })
        this.triggerEvent('error', {
          target: e.currentTarget,
          message: e.detail.errMsg
        }, {
          bubbles: true,
          composed: true
        });
    },
    
  • 添加加载提示
    可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 slot 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。

最终效果

经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验

github 地址
npm 地址

总结

以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦!

最后一次编辑于  2020-12-27  
点赞 59
收藏
评论

35 个评论

  • 潇潇雨歇
    潇潇雨歇
    2019-11-29

    大佬,我使用了这个插件,overflow:hidden失效了

    2019-11-29
    赞同
    回复
  • 李白
    李白
    2019-11-17

    最近测试发现一个问题,经常的点开内容是空白的,要在空白处随便点那么几下比如点到一个图片了,图片预览出现了,然后再返回内容就显示了,这是咋回事

    2019-11-17
    赞同
    回复 1
    • 金煜峰
      金煜峰
      2019-11-17
      空白的时候在Wxml面板上看一下是啥情况?
      2019-11-17
      回复
  • 郝sir
    郝sir
    2019-11-10

    请教一个问题,如何把word式排版“段首空2个字符”统一改成新媒体式排版“段首顶格,段与段之前空一行”呢?

    2019-11-10
    赞同
    回复 3
    • 金煜峰
      金煜峰
      2019-11-10
      这个有点难,可以大概通过这样的正则替换实现
      2019-11-10
      回复
    • 郝sir
      郝sir
      2019-11-10回复金煜峰
      感谢,我试试。这一段是加载parser下哪个文件呢?
      2019-11-10
      回复
    • 金煜峰
      金煜峰
      2019-11-10回复郝sir
      先替换完再送给parser的html属性
      2019-11-10
      回复
  • Seldom🍂 🍂
    Seldom🍂 🍂
    2019-11-06

    那么问题来了,怎么设置图片的边距呢

    2019-11-06
    赞同
    回复 9
    • 金煜峰
      金煜峰
      2019-11-06
      什么意思?margin?
      2019-11-06
      回复
    • Seldom🍂 🍂
      Seldom🍂 🍂
      2019-11-06回复金煜峰
      嗯嗯
      2019-11-06
      回复
    • Seldom🍂 🍂
      Seldom🍂 🍂
      2019-11-06
      wxParser可以设置margin初始化
      2019-11-06
      回复
    • 金煜峰
      金煜峰
      2019-11-06回复金煜峰
      可以通过tag-style属性设置各标签的默认样式,例如:
      2019-11-06
      回复
    • Seldom🍂 🍂
      Seldom🍂 🍂
      2019-11-08回复金煜峰
      还有一个问题,想请教一下,就是在某一个页面,我不想让图片可以被预览,请问可以设置吗,是重写方法吗
      2019-11-08
      回复
    查看更多(4)
  • 2019-11-02
    render() {
      let { current, tabList, live_brief, hqHeight, num } =this.state
      return (
        <View>
          <AtTabs swipeable sroll height='100%' className='navlist' current={current} tabList={tabList} onClick={this.handleClick.bind(this)}>
            <AtTabsPane className='items' current={current} index={0}>
              <Chatmod />
            </AtTabsPane>
            <AtTabsPane className='items' current={current} index={1}>
              <ScrollView
                scrollY
                scrollWithAnimation
                style={{ height: hqHeight +'rpx' }}
              >
                <ParserRichText html={live_brief}></ParserRichText>
              </ScrollView>
            </AtTabsPane>
            {tabList.map((item, index) => {
              let jsonData = JSON.stringify(tabList);// 转成JSON格式
              let result = JSON.parse(jsonData);// 转成JSON对象
      

              return (
                <AtTabsPane className='items' key={item} current={current} index={index + 2}>
                  <ScrollView
                    scrollY
                    scrollWithAnimation
                    style={{ height: hqHeight +'rpx' }}
                  >
                    <View className='txt'>C{result[index + 2].data}</View>
                    {/* <RichText nodes='{{tabList[index+2].data}}' bindtap='tap'></RichText> */}
                    <ParserRichText html={result[2].data} id={result[index + 2].title}></ParserRichText>
                  </ScrollView>
                </AtTabsPane>
              )
            })
            }
     
     
     
    ParserRichText 的html={result[2].data}  如果改成 {result[index+2].data} 就失效,只要加index 就报错,但是 它里面的id 可以正常打印各个数据,小程序源生的RichText 可以正常遍历出数据,就是 ParserRichText  不可以,写个2是因为,它只能固定,不能动态传值进去否则报错。我就想像源生小程序那样写获取数据,就是报错。


    2019-11-02
    赞同
    回复 10
    • 金煜峰
      金煜峰
      2019-11-02
      这是taro吧,这我也不太了解,之前是一个老哥帮忙适配的
      确定是在完全相同的情况和写法时RichText可以正常显示,但是ParserRichText会报错?
      2019-11-02
      回复
    • 2019-11-02回复金煜峰
      2019-11-02
      回复
    • 2019-11-02回复金煜峰
      我怀疑是这个组件的BUG, Taro的封装它时是不是 代码里有错误,我看了下源码,不会改,o(╥﹏╥)o,但我很需要它
      2019-11-02
      回复
    • 金煜峰
      金煜峰
      2019-11-03
      2019-11-03
      回复
    • 2019-11-04回复金煜峰

      不行,html的index 还是不可以直接写 还是报错越界,o(╥﹏╥)o
      2019-11-04
      回复
    查看更多(5)
  • 2019-11-01

    前面一堆神逻辑,最后那个 实现更加丰富的功能解决了一切 o(╥﹏╥)o

    2019-11-01
    赞同
    回复
  • jin
    jin
    2019-10-27

    特此登录点赞 wxparse 图片无宽高的话会被小程序显示默认320px 240px。这个bug困扰了我2天,用了wxparser 这个bug就没有了 哈哈 作者辛苦啦

    2019-10-27
    赞同
    回复 10
    • 金煜峰
      金煜峰
      2019-10-27
      哈哈哈
      2019-10-27
      回复
    • jin
      jin
      2019-10-27回复金煜峰
      大佬好,我突然发现个bug。这个问题在wxparse没有,原因我也弄清楚了。bug:相同的2张以上的图片,只能加载第一张,其他图片会显示裂开。因为图片url后面被添加了?index=索引。请问跟源码有关吗?
      2019-10-27
      回复
    • jin
      jin
      2019-10-27回复jin
      嘻嘻 ,把DomHandler.js中的if(this.imgList.indexOf(attrs.src)!=-1){attrs.src=attrs.src+"?index="+this.imgIndex++}注释掉就好了!不会引发其他问题吧
      2019-10-27
      回复
    • 金煜峰
      金煜峰
      2019-10-27回复jin
      加了一个?index=索引就会裂开吗?应该加一个参数是不影响的吧,加上那个是通过这样的方式保证所有图片的url不同,否则都是相同的图片的时候在wx.previewImg的时不管点哪张都会预览第一张
      2019-10-27
      回复
    • 金煜峰
      金煜峰
      2019-10-27回复金煜峰
      好像有些图片确实会,先把那几行去了吧,我再想想办法
      2019-10-27
      回复
    查看更多(5)
  • 2019-10-11


    请问大佬我这个换行符该怎么清除

    2019-10-11
    赞同
    回复 2
    • 金煜峰
      金煜峰
      2019-10-12
      正则替换一下就可以啦
      html=html.replace(/换行符(这里打不出来)/g,'')
      2019-10-12
      回复
    • 金煜峰
      金煜峰
      2019-11-06回复Seldom🍂 🍂
      这里是说清除,替换成''就行了;要换成换行符只能替换为br标签
      2019-11-06
      回复
  • 杉杉爱吃鱼🐳
    杉杉爱吃鱼🐳
    2019-08-06

    您好大神,我看过您的文章之后就应用到我的小程序中了,确实比wxparse 好用的多!现在又一个问题。就是video,图片资源过长的时候,小程序会报错,我单独在小程序video 标签中加的时候没有问题。

    附上图片;



    第一张图片中有两段视频,第一个视频为api 文档举例视频地址,测试没问题。第二个是供应商让加的视频地址和控制台报错信息。(https://ugcws.video.gtimg.com/uwMROfz2r5zEIaQXGdGnC2dfDmafRkP9ujxgqKjuATzMrE-2/g0909h823xk.mp4?sdtfrom=v1010&guid=57c19fe14bc09565996d9963c3e0a8fb&vkey=701BA7278A7C3447399055E804F7B272D5BE76651F07518AFEFE48FE89BE17282B79BD807D47F1522290626DC7B3AA527EBCF1579990DF30730A3C2831A3821B746D295A8E020AA4150BAB3C6062A8E14C83A8A910653D6375F4F3C5C214C7BFFF12DA9A2490FA70CD340C2A95CADFEAE8B9BA681A076F84E55FCFE3C37678A9




    第二张图片,为通过插件转译后在wxml 中的表现。

    第三张是我在wxml文件中 主动添加video表现有问题视频的路径,测试无问题。

    第四张 ,没啥用,就是正常转译后的wxml表现,和第二张图片差不多。

    请求后台富文本编辑器里面的内容为:

    <p class="d-text" data-v-a64cc490="" data-v-5f128736=""><video controls="controls" width="300" height="150">
    <source src="https://ugcws.video.gtimg.com/uwMROfz2r5zEIaQXGdGnC2dfDmafRkP9ujxgqKjuATzMrE-2/g0909h823xk.mp4?sdtfrom=v1010&amp;guid=57c19fe14bc09565996d9963c3e0a8fb&amp;vkey=701BA7278A7C3447399055E804F7B272D5BE76651F07518AFEFE48FE89BE17282B79BD807D47F1522290626DC7B3AA527EBCF1579990DF30730A3C2831A3821B746D295A8E020AA4150BAB3C6062A8E14C83A8A910653D6375F4F3C5C214C7BFFF12DA9A2490FA70CD340C2A95CADFEAE8B9BA681A076F84E55FCFE3C37678A9" /></video></p>


    请问这个问题怎么解决?

    2019-08-06
    赞同
    回复 2
    • 金煜峰
      金煜峰
      2019-08-07
      确定这个链接没有问题吗?我这里直接放在video组件中或者在浏览器中访问都是报405错误
      2019-08-07
      回复
    • 杉杉爱吃鱼🐳
      杉杉爱吃鱼🐳
      2019-08-08回复金煜峰
      您好,这个链接不太稳定,有时候可以有时候不可以,我也发现了。
      2019-08-08
      回复
  • 漂泊
    漂泊
    2019-07-01

    看了下果然收藏 了

    2019-07-01
    赞同
    回复

正在加载...

登录 后发表内容