Web应用的富文本内容大部分是以HTML字符串的形式存储的,所以通过HTML文档显示HTML内容是没有问题的。但是,在微信小程序(以下简称“小程序”)中,这部分内容应该如何渲染呢?解决方案wxParse小程序刚推出时,无法直接渲染HTML内容,于是诞生了一个名为“wxParse”的库。它的原理是将HTML代码解析成树状结构的数据,然后通过小程序的模板渲染数据。rich-text后来,小程序增加了一个“rich-text”组件来显示富文本内容。但是这个组件有一个很大的局限性:所有节点的事件都被屏蔽在组件内部。也就是说,在这个组件中,连“图片预览”这样简单的功能都无法实现。在web-view之后,小程序允许通过“web-view”组件嵌套网页,通过网页显示HTML内容是最兼容的解决方案。但是,由于多加载了一页,性能很差。当“WePY”遇上“wxParse”,基于用户体验和功能交互的考虑,我们抛弃了“rich-text”和“web-view”这两个原生组件,选择了“wxParse”。但是,使用之后发现“wxParse”并不能很好的满足需求:我们的小程序是基于“WePY”框架开发的,而“wxParse”是基于原生小程序编写的。为了使两者兼容,必须修改“wxParse”的源代码。“wxParse”只是简单的通过图像组件显示和预览原始img元素的图像。在实际使用中,可以通过云存储的接口来缩小图片,从而达到“小图显示,原图预览”的目的。“wxParse”直接使用小程序的视频组件来显示视频,但是视频组件的图层问题经常导致UI异常(比如挡住某个固定位置的元素)。另外,查看“wxParse”的代码仓库,可以发现已经两年没有迭代了。于是萌生了基于“WePY”组件模型重写一个富文本组件的想法,结果就是“WePYHTML”项目。解析HTML的实现过程仍然是将HTML字符串解析成树状结构的数据。我用的是“特殊字符分离法”。HTML中的特殊字符是“<”和“>”,前者是起始符,后者是结束符。?如果要解析的内容以起始字符开头,则截取起始字符和结束字符之间的内容作为节点进行解析。?如果要解析的内容不以起始符开头,则截取从起始符到起始符之前的内容(如果起始符不存在则截取结尾),并作为纯文本解析。?剩余内容进入下一轮解析,直到没有剩余内容为止。如下图:为了形成树形结构,在解析过程中需要维护一个上下文节点(默认为根节点):?如果截取的内容是开始标签,根据匹配到的标签名和属性,在当前上下文节点下创建一个子节点。如果标签不是自闭标签(br、img等),则将上下文节点设置为新节点。?如果拦截的内容是结束标签,则根据标签名关闭当前上下文节点(将上下文节点设为其父节点)。?如果是纯文本,则在当前上下文节点下创建一个文本节点,上下文节点不变。流程如下表所示:经过以上流程,HTML字符串被解析成节点树。对比将上述算法与其他类似的解析算法进行对比(性能以“解析10000长HTML代码”来衡量):可以看出,在不考虑容错性(产生错误结果而不是抛出异常)的情况下,与其他算法相比二、该组件的算法具有压倒性优势,满足小程序“小而快”的需求。一般来说,富文本编辑器生成的代码不会有语法错误。所以即使它的容错性较低,也没什么大不了的(但这是需要改进的地方)。模板渲染树结构的渲染,必然会涉及子节点的递归处理。但是小程序的模板不支持递归,好像掉了个大坑。我看了一下“wxParse”模板的实现,它用一种简单粗暴的方式解决了这个问题:通过13个几乎相同的模板进行嵌套调用(1调用2,2调用3,...,12calls13),也就是说最多可以支持12个嵌套。一般来说,这个深度也足够了。由于“WePY”框架本身就有构建机制,因此无需手工编写几十个几乎相同的模板,只需通过内置的插件生成即可。下面是一个需要重复嵌套的模板(简化版)。在代码首尾插入特殊注释,用于标识,另外一个特殊注释("")Logo:{{item.text}}以下是对应的构建代码(需要安装“wepy-plugin-replace”)://wepy.config.js{plugins:{replace:{filter:/\.wxml$/,config:{find:/<\!--wepyhtml-repeatstart-->([\W\w]+?)<\!--wepyhtml-repeatend-->/,replace(match,tpl){letresult='';//反正没钱,直接写个20层嵌套for(leti=0;我<=20;i++){结果+='\n'+tpl.replace('wepyhtml-0','wepyhtml-'+i).replace(/<\!--下一个模板-->/g,()=>{returni===20?'':`模板>`;});}返回结果;}}}}}但是运行后发现,第二层和更深层次的节点没有渲染出来,说明嵌套失败了。查看dist目录下生成的wxml文件,可以发现变量名和组件源码不一样:“WePY”在生成组件代码时,为了避免组件数据与页面数据的变量名冲突,会在变量名前加上前缀根据一定的规则(例如“上面代码中的$htmlContent)$wepyHtml$”)。所以在生成嵌套模板时,也必须使用带前缀的变量名。首先在组件代码中添加一个变量“thisIsMe”来标识前缀:{{thisIsMe}}{{item.text}}!--wepyhtml-repeatend-->然后修改构建代码:replace(match,tpl){letresult='';让前缀='';//匹配thisIsMe的前缀tpl=tpl.replace(/\{\{\s*(\$.*?\$)thisIsMe\s*\}\}/,(match,p)=>{prefix=p;返回'';});for(leti=0;i<=20;i++){result+='\n'+tpl.replace('wepyhtml-0','wepyhtml-'+i).replace(/<\!--下一步模板-->/g,()=>{返回i===20?'':`模板>`;});}returnresult;}至此,渲染问题就解决了。为了节省流量和提高加载速度,在展示富文本内容时,一般会将里面的图片按照需要的大小进行缩小。单击小预览图像时仅显示原始图像。主要涉及修改节点属性:?将图片的原始路径(src属性值)保存到自定义属性(如“data-src”)中,添加到预览图片数组中。?将图片的src属性值改为缩小后的图片的URL(一般云服务商都会提供这样的URL规则)。?单击图像时使用自定义属性的值进行预览。为了实现这个需求,这个组件在解析节点时提供了一个钩子(onNodeCreate):onNodeCreate(name,attrs){if(name==='img'){attrs['data-src']=attrs.src;//预览数组this.previewImgs.push(attrs.src);//缩略图attrs.src=resizeImg(attrs.src,640);}}对应的模板和事件处理逻辑如下:}"data-src="{{elem.attrs['data-src']||elem.attrs.src}}"@tap="imgTap">//点击小的image看大图imgTap(e){wepy.previewImage({current:e.currentTarget.dataset.src,urls:this.previewImgs});}Video小程序中,视频组件的层级较高(和不能降低)。如果页面设计中有可能遮挡视频的元素,则需要一些技巧来处理:?隐藏视频组件并使用图像组件(视频封面)代替。?当您点击图片时,让视频全屏播放;?如果退出全屏,请暂停播放。相关代码如下:{//点击封面图片播放视频videoTap(e){constnodeId=e.currentTarget.dataset.nodeid;constcontext=wepy.createVideoContext('wepyhtml-video-'+nodeId);context.play();//Android微信下,如果视频不可见,调用play()无法播放//需要调用全屏方法if(wepy.getSystemInfoSync().platform==='android'){context.requestFullScreen();}},//视频层较高,为了防止其他特殊定位元素被遮挡,导致界面异常,//强制全屏播放videoPlay(e){wepy.createVideoContext(e.currentTarget.id).requestFullScreen();},//暂停videoFullscreenChange(e){if(!e.detail.fullScreen){wepy.createVideoContext(e.currentTarget.id).暂停();}}}本文分享到此结束