前言
在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。
现有方案
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
对于table
、ol
、ul
等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染
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
组件显示;因此需要在解析过程中进行标记,遇到img
、video
、a
等标签时,对其所有上级节点设置一个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);
- 当
style
字符串较长时,可能出现栈溢出的问题 - 对于一些复杂的情况,可能出现匹配失败的问题
- 当
- 方案二
状态机的方式,类似于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
本身进行设置的情况下,无法实现行内元素的效果,类似的还有float
、width
(设置为百分比时)等情况
解决方案- 方案一
用一个view
包裹在rich-text
外面,替代最外层的标签
缺陷:当该标签为<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view>
table
、ol
等功能性标签时,会导致错误 - 方案二
对rich-text
组件使用最外层标签的样式
缺陷:当该标签的<rich-text nodes="{{item}}" style="{{item.attrs.style}}" />
style
中含有margin
、padding
等内容时会被缩进两次 - 方案三
通过wxs
脚本将顶层标签的display
、float
、width
等样式提取出来放在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 地址
总结
以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦!
请教个问题,清除富文本内容,为啥还会一个‹p›‹/p›标签存在
这个可以实现小程序的bindtap事件吗?就比如我div里有click,解析后,在小程序就变成bindtap
可以关闭img 的预览效果吗?
标点符号排在句首的问题能不能解决?
想问一下,长按图片出现如下效果是否可以去掉:
<wxs module="Handler" src="./handler.wxs" /><block wx:for='{{nodes}}' wx:key><block wx:if="{{Handler.isContinue(item)}}"><rich-text wx:if="{{item.name=='img'}}" class="img" style="text-indent:0;{{Handler.getStyle(item.attrs.style,'inline-block')}}" nodes='{{Handler.setImgStyle(item,controls.imgMode,imgLoad)}}' data-ignore='{{item.attrs.ignore}}' data-src='{{item.attrs.src}}' 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]}}"></rich-text></block><text wx:elif="{{item.name=='br'}}">\n</text><block wx:elif="{{item.name=='video'}}"><view wx:if="{{item.attrs.id>'video3'&&!controls[item.attrs.id].play}}" class="pvideo {{item.attrs.class||''}}" style="{{item.attrs.style}}" data-id="{{item.attrs.id}}" bindtap="_loadVideo"><view class="video-triangle" /></view><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="v {{item.attrs.class||''}}" muted="{{item.attrs.muted}}" style="{{item.attrs.style}}" data-id="{{item.attrs.id}}" data-source="{{item.attrs.source}}" bindplay='playEvent' binderror="videoError" /></block><audio wx:elif="{{item.name=='audio'}}" class="{{item.attrs.class||''}}" 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}}' poster='{{item.attrs.poster}}' name='{{item.attrs.name}}' author='{{item.attrs.author}}' style="{{item.attrs.style}}" data-id="{{item.attrs.id}}" data-source="{{item.attrs.source}}" binderror="audioError" /><navigator wx:elif="{{item.name=='a'}}" url="{{item.attrs.href}}" class="{{item.attrs.class||''}}" style="{{item.attrs.style}}" data-href='{{item.attrs.href}}' hover-class="navigator-hover" hover-start-time="25" hover-stay-time="300" bindtap="tapEvent"><trees id="node" nodes="{{item.children}}" controls="{{controls}}" lazy-load="{{lazyLoad}}"/></navigator><ad wx:elif="{{item.name=='ad'}}" unit-id="{{item.attrs['unit-id']}}" class="{{item.attrs.class||''}}" style="{{item.attrs.style}}" binderror="adError"></ad><rich-text wx:else class="{{item.name}}" style="{{Handler.getStyle(item.attrs.style,'block')}}" nodes="{{Handler.setStyle(item)}}" /></block><trees wx:else id="node" class="{{item.name+' '+(item.attrs.class||'')}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" lazy-load="{{lazyLoad}}"/></block>
楼主你好,我想请问一下,解析出来的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>
<parser tag-style="{{tagStyle}}" />
data:{
tagStyle:{
table:"width:auto"
}
}
目前不支持iframe是吗?现在需要插入腾讯视频。。。
楼主,问个问题
<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> 金晓伟</strong></p>
</section>
</section>
</section>
这段代码在富文本里的效果是这样的,
在小程序中是这样的:
这种支持多张图片在一行吗?还是说传小一点的图片就可以了。还有就是有的我在富文本里预览都是显示一行的,但是在小程序就不行了。
我这边需要做个限制,只显示一屏的内容
<parser html="{{html}}" style="height:100vh;overflow:hidden"></parser>