小程序富文本能力的深入研究与应用
前言
在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。
[图片]
现有方案
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 地址
总结
以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦!
[图片]