评论

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

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

前言

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

现有方案

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  
点赞 39
收藏
评论

24 个评论

  • 学习使我快乐
    学习使我快乐
    2019-07-08

    现在的在校生都那么强了吗。。。我这个loser感觉快被淘汰了,可怕

    2019-07-08
    赞同 7
    回复
  • 莫克
    莫克
    02-19

    又是技术又幽默,给同志点赞、!

    02-19
    赞同 1
    回复
  • 白居易
    白居易
    2020-05-10

    标点符号排在句首的问题能不能解决?

    2020-05-10
    赞同
    回复
  • 潇潇雨歇
    潇潇雨歇
    2020-03-30

    想问一下,长按图片出现如下效果是否可以去掉:

    2020-03-30
    赞同
    回复 7
    • 金煜峰
      金煜峰
      2020-03-30
      trees.wxss里image的show-menu-by-longpress去掉应该就没了
      2020-03-30
      回复
    • 潇潇雨歇
      潇潇雨歇
      2020-03-30回复金煜峰
      好的,我试试
      2020-03-30
      回复
    • 潇潇雨歇
      潇潇雨歇
      2020-03-30
      没找到
      2020-03-30
      回复
    • 金煜峰
      金煜峰
      2020-03-30
      wxml
      2020-03-30
      回复
    • 潇潇雨歇
      潇潇雨歇
      2020-03-31回复金煜峰
      我的文件是这样的,data-ignore去掉会有啥影响吗
      2020-03-31
      回复
    查看更多(2)
  • 草莓好甜
    草莓好甜
    2020-01-08

    楼主你好,我想请问一下,解析出来的table无法自适应改怎么解决呢?也没有表格线。

    HTML为:<TABLE style='WORD-BREAK: break-all' border=0 cellSpacing=0 width=650><TBODY><TR><TD>猴大哥、猴二哥各摘了多少个花生?</TD></TR><TR><TD><div align=center><IMG style='VERTICAL-ALIGN: middle' border=0 src='http://img2.shangxueba.com/daanimg/20141209/11/4EA11BC78EBA4D9A6E925B069AFDD7AA.gif'></div></TD></TR></TBODY></TABLE>



    2020-01-08
    赞同
    回复 6
    • 金煜峰
      金煜峰
      2020-01-08
      table设置了 width=650,所以宽度是650px超出屏幕宽度,去掉就好了,没有表格线是因为设置了 border=0
      2020-01-08
      回复
    • 草莓好甜
      草莓好甜
      2020-01-08回复金煜峰
      所以是需要后台改数据的是吗?
      2020-01-08
      回复
    • 金煜峰
      金煜峰
      2020-01-08
      办法很多吧,后台改数据也可以,或者通过 tag-style 属性给 table 设置默认的 width:auto 的样式(加个style标签也可以),在 bindparse 回调中进行修改应该也可以
      2020-01-08
      回复
    • 草莓好甜
      草莓好甜
      2020-01-08回复金煜峰
      好的,谢谢!我用replace替换掉了。然后想请问一下,这个有对应的头条小程序版本吗?
      2020-01-08
      回复
    • 金煜峰
      金煜峰
      2020-01-08
      没有,不过用uni-app的话可以编译到头条
      2020-01-08
      回复
    查看更多(1)
  • 潇潇雨歇
    潇潇雨歇
    2019-12-11

    目前不支持iframe是吗?现在需要插入腾讯视频。。。

    2019-12-11
    赞同
    回复 1
    • 金煜峰
      金煜峰
      2019-12-11
      iframe 视频是没办法支持的(因为里面涉及到动态 js 脚本),要播放腾讯视频可以用腾讯视频插件替代原插件里的 video
      2019-12-11
      回复
  • 潇潇雨歇
    潇潇雨歇
    2019-12-09

    楼主,问个问题

    <section style="margin: 10px 0%; position: static;">

    <section style="display: inline-block; width: 100%; border-left: 3px solid #c88b3f; border-top-style: solid; border-right-style: solid; border-bottom-style: solid; border-right-color: #c88b3f; padding-right: 5px; padding-left: 5px; box-shadow #000000 0px 0px 0px;">

    <section style="margin-right: 0%; margin-bottom: 8px; margin-left: 0%; transform: translate3d(-5px, 0px, 0px); text-align: left; position: static;">

    <section style="display: inline-block; height: 2em; padding: 0.3em 0.5em; vertical-align: top; background-color: #c88b3f; color: #ffffff;">

    <p style="margin-top: 0px; margin-bottom: 0px;"><strong>对话摄影师</strong></p>

    </section>

    <section style="width: 0.5em; display: inline-block; height: 2em; vertical-align: top; border-bottom: 1em solid #c88b3f; border-top: 1em solid #c88b3f; border-right: 1em solid transparent !important;"></section>

    </section>

    <section style="font-size: 18px; color: #c88b3f;">

    <p style="margin-top: 0px; margin-bottom: 0px;"><strong>易享+ <span style="color: #f96e57;"><span style="color: #3e3e3e;">×</span></span>&nbsp;金晓伟</strong></p>

    </section>

    </section>

    </section>

    这段代码在富文本里的效果是这样的,


    在小程序中是这样的:


    2019-12-09
    赞同
    回复 3
    • 潇潇雨歇
      潇潇雨歇
      2019-12-09
      不对,这段代码的效果就是这样,是从秀米直接复制粘贴,富文本给出的样式有问题
      2019-12-09
      回复
    • 潇潇雨歇
      潇潇雨歇
      2019-12-09
      打扰了
      2019-12-09
      回复
    • 潇潇雨歇
      潇潇雨歇
      2019-12-09
      这富文本坑好多啊
      2019-12-09
      回复
  • 潇潇雨歇
    潇潇雨歇
    2019-12-05

    这种支持多张图片在一行吗?还是说传小一点的图片就可以了。还有就是有的我在富文本里预览都是显示一行的,但是在小程序就不行了。

    2019-12-05
    赞同
    回复 6
    • 潇潇雨歇
      潇潇雨歇
      2019-12-05
      直接从秀米复制过来的样式
      2019-12-05
      回复
    • 金煜峰
      金煜峰
      2019-12-06回复潇潇雨歇
      提供一下html
      2019-12-06
      回复
    • 潇潇雨歇
      潇潇雨歇
      2019-12-06回复金煜峰
      客户传的,现在改成其他样式了
      2019-12-06
      回复
    • 潇潇雨歇
      潇潇雨歇
      2019-12-06
      两张图片并列排布,只要图片大小合适就行吧?
      2019-12-06
      回复
    • 金煜峰
      金煜峰
      2019-12-06回复潇潇雨歇
      理论上是的,没有具体html代码也不好说
      2019-12-06
      回复
    查看更多(1)
  • 潇潇雨歇
    潇潇雨歇
    2019-11-29

    我这边需要做个限制,只显示一屏的内容

    2019-11-29
    赞同
    回复 2
    • 金煜峰
      金煜峰
      2019-11-29
      限制高度了吗,像下面这样应该可以的
      2019-11-29
      回复
    • 潇潇雨歇
      潇潇雨歇
      2019-11-30回复金煜峰
      不好意思,我开始放在外面一层了,其实是可以的,已经弄好了
      2019-11-30
      回复
  • 潇潇雨歇
    潇潇雨歇
    2019-11-29

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

    2019-11-29
    赞同
    回复

正在加载...

登录 后发表内容