- [打怪升级]小程序自定义头部导航栏“完美”解决方案
[图片] 为什么要做这个? 主要是在项目中,智酷君发现的一些问题 一些页面是通过扫码和订阅消息访问后,没有直接可以点击去首页的,需要添加一个home链接 需要添加自定义搜索功能 需要自定义一些功能按钮 [图片] 其实,第一个问题,在最近的微信版本更新中已经优化了,通过 小程序模板消息 过来的,系统会自动加上home按钮,但对于其他的访问方式则没有支持~ 一个不大不小的问题:两边ICON不对齐问题 [图片] 智酷君之前尝试了各种解决方法,发现有一个问题,就是现在手机屏幕太多种多样,有 传统头部、宽/窄刘海屏、水滴屏等等,无法八门,很多解决方案都无法解决特殊头部,系统**“胶囊按钮”** 和 自定义按钮在Android屏幕可能有 几像素不对齐 的问题(强迫症的噩梦)。 下面分享下一个相对比较完善的解决方案: [图片] 小程序代码段DEMO Link: https://developers.weixin.qq.com/s/cuUaCimT72cH ID: cuUaCimT72cH 智酷君做了一个demo代码段,方便大家直接用IDE工具查看源码~ [图片] 页面配置 1、页面JSON配置 [代码]{ "usingComponents": { "NavComponent": "/components/nav/common" //以插件的方式引入 }, "navigationStyle": "custom" //自定义头部需要设置 } [代码] 如果需要自定义头部,需要设置navigationStyle为 “custom” 2、页面代码 [代码]<!-- home 类型的菜单 --> <NavComponent v-title="自定义头部" bind:commonNavAttr="commonNavAttr"></NavComponent> <!-- 搜索菜单 --> <NavComponent is-search="true" bind:commonNavAttr="commonNavAttr"></NavComponent> [代码] 可以在自定义导航标签上添加属性配置来设置功能,具体按照实际需要来 3、目录结构 [代码]│ ├─components │ └─nav │ common.js │ common.json │ common.wxml │ common.wxss │ ├─images │ back.png │ home.png │ └─index index.js index.json index.wxml index.wxss search.js search.json search.wxml search.wxss [代码] 仅供参考 插件对应的JS部分 components/nav/common.js部分 [代码]const app = getApp(); Component({ properties: { vTitle: { type: String, value: "" }, isSearch:{ type: Boolean, value: false } }, data: { haveBack: true, // 是否有返回按钮,true 有 false 没有 若从分享页进入则没有返回按钮 statusBarHeight: 0, // 状态栏高度 navbarHeight: 0, // 顶部导航栏高度 navbarBtn: { // 胶囊位置信息 height: 0, width: 0, top: 0, bottom: 0, right: 0 }, cusnavH: 0, //title高度 }, // 微信7.0.0支持wx.getMenuButtonBoundingClientRect()获得胶囊按钮高度 attached: function () { if (!app.globalData.systeminfo) { app.globalData.systeminfo = wx.getSystemInfoSync(); } if (!app.globalData.headerBtnPosi) app.globalData.headerBtnPosi = wx.getMenuButtonBoundingClientRect(); console.log(app.globalData) let statusBarHeight = app.globalData.systeminfo.statusBarHeight // 状态栏高度 let headerPosi = app.globalData.headerBtnPosi // 胶囊位置信息 console.log(statusBarHeight) console.log(headerPosi) let btnPosi = { // 胶囊实际位置,坐标信息不是左上角原点 height: headerPosi.height, width: headerPosi.width, top: headerPosi.top - statusBarHeight, // 胶囊top - 状态栏高度 bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, // 胶囊bottom - 胶囊height - 状态栏height (胶囊实际bottom 为距离导航栏底部的长度) right: app.globalData.systeminfo.windowWidth - headerPosi.right // 这里不能获取 屏幕宽度,PC端打开小程序会有BUG,要获取窗口高度 - 胶囊right } let haveBack; if (getCurrentPages().length != 1) { // 当只有一个页面时,并且是从分享页进入 haveBack = false; } else { haveBack = true; } var cusnavH = btnPosi.height + btnPosi.top + btnPosi.bottom // 导航高度 console.log( app.globalData.systeminfo.windowWidth, headerPosi.width) this.setData({ haveBack: haveBack, // 获取是否是通过分享进入的小程序 statusBarHeight: statusBarHeight, navbarHeight: headerPosi.bottom + btnPosi.bottom, // 胶囊bottom + 胶囊实际bottom navbarBtn: btnPosi, cusnavH: cusnavH }); //将实际nav高度传给父类页面 this.triggerEvent('commonNavAttr',{ height: headerPosi.bottom + btnPosi.bottom }); }, methods: { _goBack: function () { wx.navigateBack({ delta: 1 }); }, bindKeyInput:function(e){ console.log(e.detail.value); } } }) [代码] 解决不同屏幕头部不对齐问题的终极办法是 wx.getMenuButtonBoundingClientRect() 这个方法从微信7.0.0开始支持,通过这个方法我们可以获取到右边系统胶囊的top、height、right等属性,这样无论是水滴屏、刘海屏、异形屏,都能完美对齐右边系统默认的胶囊bar,完美治愈强迫症~ APP.js 部分 [代码]//app.js App({ /** * 加载页面 * @param {*} options */ onShow: function (options) { }, onLaunch: async function () { let self = this; //设置默认分享 this.globalData.shareData = { title: "智酷方程式" } // this.getSysInfo(); }, globalData: { //默认分享文案 shareData: {}, qrCodeScene: false, //二维码扫码进入传参 systeminfo: false, //系统信息 headerBtnPosi: false, //头部菜单高度 } }); [代码] 将获取的参数存储在一个全局变量globalData中,可以减少反复调用的性能消耗。 插件HTML部分 [代码]<view class="custom_nav" style="height:{{navbarHeight}}px;"> <view class="custom_nav_box" style="height:{{navbarHeight}}px;"> <view class="custom_nav_bar" style="top:{{statusBarHeight}}px; height:{{cusnavH}}px;"> <!-- 搜索部分--> <block wx:if="{{isSearch}}"> <input class="navSearch" style="height:{{navbarBtn.height-2}}px;line-height:{{navbarBtn.height-4}}px; top:{{navbarBtn.top+1}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;" maxlength="10" bindinput="bindKeyInput" placeholder="输入文字搜索" /> </block> <!-- HOME 部分--> <block wx:else> <view class="custom_nav_icon {{!haveBack||'borderLine'}}" style="height:{{navbarBtn.height}}px;line-height:{{navbarBtn.height-2}}px; top:{{navbarBtn.top}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;"> <view wx:if="{{haveBack}}" class="icon-back" bindtap='_goBack'> <image src='/images/back.png' mode='aspectFill' class='back-pre'></image> </view> <view wx:if="{{haveBack}}" class='navbar-v-line'></view> <view class="icon-home"> <navigator class="home_a" url="/pages/home/index" open-type="switchTab"> <image src='/images/home.png' mode='aspectFill' class='back-home'></image> </navigator> </view> </view> <view class="nav_title" style="height:{{cusnavH}}px; line-height:{{cusnavH}}px;"> {{vTitle}} </view> </block> </view> </view> </view> [代码] 主要是对几种状态的判断和定位的计算。 插件CSS部分 [代码]/* components/nav/test.wxss */ .custom_nav { width: 100%; background: #3a7dd7; position: relative; z-index: 99999; } .custom_nav_box { position: fixed; width: 100%; background: #3a7dd7; z-index: 99999; border-bottom: 1rpx solid rgba(255, 255, 255, 0.3); } .custom_nav_bar { position: relative; z-index: 9; } .custom_nav_box .nav_title { font-size: 28rpx; color: #fff; text-align: center; position: absolute; max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; top: 0; left: 0; right: 0; bottom: 0; margin: auto; z-index: 1; } .custom_nav_box .custom_nav_icon { position:absolute; z-index: 2; display: inline-block; border-radius: 50%; vertical-align: top; font-size:0; box-sizing: border-box; } .custom_nav_box .custom_nav_icon.borderLine { border: 1rpx solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.1); } .navbar-v-line { width: 1px; margin-top: 14rpx; height: 32rpx; background-color: rgba(255, 255, 255, 0.3); display: inline-block; vertical-align: top; } .icon-back { display: inline-block; width: 74rpx; padding-left: 20rpx; vertical-align: top; /* margin-top: 12rpx; vertical-align: top; */ height: 100%; } .icon-home { /* margin-top: 8rpx; vertical-align: top; */ display: inline-block; width: 80rpx; text-align: center; vertical-align: top; height: 100%; } .icon-home .home_a { height: 100%; display: inline-block; vertical-align: top; width: 35rpx; } .custom_nav_box .back-pre, .custom_nav_box .back-home { width: 35rpx; height: 35rpx; vertical-align: middle; } .navSearch { width: 200px; background: #fff; font-size: 14px; position: absolute; padding: 0 20rpx; z-index: 9; } [代码] 总结: 通过微信API: getMenuButtonBoundingClientRect(),结果各类手机屏幕的适配问题 将算好的参数存储在全局变量中,一次计算全局使用,爽YY~ 往期回顾: [填坑手册]小程序PC版来了,如何做PC端的兼容?! [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 小程序实现看一看视频滑动切换
最终效果 由于很多人不知道看一看还有个视频功能,所以这里先让大家看下我们最终要完成的效果。 它的入口在 发现 -> 看一看 -> 精选 -> 随便找个视频点进去即可。 [图片] 初步想法 由效果可以看出,其实就是需要监听视频的滚动,当超出可视区范围多少px,就切换到下一个视频。 要实现这个功能,大多数人的想法都是:监听scroll 事件后,在调用目标元素的getBoundingClientRect()方法,得到它对应于视图的坐标,再判断是否在可视区域之内,然后切换视频。 缺点 但是这样做的缺点是:调用目标元素的getBoundingClientRect 是会触发重排的,尤其是元素一多起来,调用所有元素的getBoundingClientRect得到信息在进行判断所以很容易造成性能问题。而且这种切换计算的逻辑会写的非常复杂,可以自行脑补一下。 所以我们要换个思维,不要通过监听scoll事件去计算目标元素距离顶部或者底部距离。而应该是直接监听当前目标元素是否还在可视区域内。当离开可视区域的时候,切换到下一个。 整理完大致思路之后,终于要开搞了。 [图片] 实现 前面已经分析了要通过监听当前目标元素是否还在可视区域内来做切换的动作,那么有什么API是可以用来做这件事的呢? 答案是: IntersectionObserver API 这个API是用来观察目标元素与指定元素交集的变化。当交集 < 0 的时候,说明不在指定元素区域内。当目标元素进入或者退出指定元素的时候,会执行相应的回调函数。所以我们可以通过这个API注册一个回调函数用于切换视频。 在小程序里,同样提供了这个API,是IntersectionObserver。 有了这个API,就可以开始干活了。由于我们这个区域是一个滚动区域,所以我用了scoll-view。 index.wxml 文件 [代码]<scroll-view> <view wx:for="{{ videos }}" wx:for-index="idx" wx:for-item="videoItem"> <!-- <view class="{{ currentPlayVideoIndex === idx ? 'active test' : 'test'}}" data-index="{{ idx }}" id="{{ videoItem.video_id }}"> {{ idx }}dddd</view> --> <span class="{{ currentPlayVideoIndex === idx ? 'active' : ''}}">{{ idx }}ddddddd </span> <video id="{{ videoItem.video_id }}" data-index="{{ idx }}" preload src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" class='video-item' muted controls> </video> </view> </scroll-view> [代码] index.wxss [代码].video-item { height: 450px; } .test { width: 100%; height: 450px; border: 1px solid red; padding: 30px; } .active { color: pink; } [代码] [代码]Page({ /** * 页面的初始数据 */ data: { videos: [{ video_id: 'mpVideo0', url: 'http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400', }, { video_id: 'mpVideo1', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo2', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo3', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo4', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo5', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo6', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo7', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }], currentPlayVideoIndex: 0, isActive: true }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { // onLoad 的时候立刻调用handleVideoScroll // 对视频进行监听 this.handleVideoScroll(); // cgi请求,用于获取videos 的数据,由于是demo演示,我直接写死了videos... }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, controlVideos: function (res) { console.log('调用controvideos', res); }, handleVideoScroll: function () { const currentId = this.data.videos[this.data.currentPlayVideoIndex].video_id; // 关键代码 // relativeToViewport 这里指定对比的就是viewport,viewport的意思就是document中的可视区域 this.observerObj = wx.createIntersectionObserver().relativeToViewport(); console.log('listen ' + currentId); // 监听目标视频跟viewport相交区域的变化 this.observerObj.observe(`#${currentId}`, this.controlVideos); } }) [代码] copy上面的代码进入小程序,你就会看到这样一个界面。 [图片] 其中外面的一圈表示的是viewPort,里面一层就是我们现在正在监听的视频,我用右上角的粉色字体来标记了,它的回调函数是controlVideos。当目标视频进入或者退出viewport的时候,controlVideos就会执行。 onLoad的时候执行了handleVideoScroll,这时候开始对目标视频进行监听,此时目标元素在viewport内,所以会调用controVideos,打印出相关信息。 [图片] 其他的字段先不说,其中的intersectionRatio表示了他们相交的比例。其中1表示完全在viewport内,0表示不在viewport内。 如果我持续去滚动第一个视频,直到它看不到了,就会看到控制台打印出 [图片] 这时候的intersectionRatio = 0,代表已经不在viewport内了,所以我们就可以将currentPlayIndex 切换到下一个了。 切换代码如下: [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放 // 并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 切换到下一个视频 this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 注意切换完成之后,还需要在调用handleVideoScroll 来对下一个视频进行绑定 this.handleVideoScroll(); }); } }, [代码] 到这一步,应该就可以看到这样的向下切换的效果了。 [图片] 但是,我们现在只是做下向下滚动的切换。那么向上的呢?要做向上滑动的切换,首先要知道视频是在向下还是向上滑动。这里有个字段可以帮助我们识别:boundingClientRect 。 它表示的是目标元素相对与viewport的节点信息。当视频向上滚动的时候,它距离viewport的top值为负,向下滚动的时候,为正值。 [图片] 有了这个字段,我们就可以通过判断向上还是向下的滚动,来切换视频了。 [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放,并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 当top < 0的时候,说明是在向上滑动,这时候currentPlayVideoIndex 需要加1 if (res.boundingClientRect.top < 0) { if (currentPlayVideoIndex < this.data.videos.length - 1) { this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 同时解绑第一个视频,保证同一个时间只监听一个视频 this.observerObj.disconnect(); this.handleVideoScroll(); }); } } else { // 当top > 0的时候,说明是在向下滑动,这时候currentPlayVideoIndex 需要减1 if (currentPlayVideoIndex - 1 < 0) { return; } this.setData({ 'currentPlayVideoIndex': this.data.currentPlayVideoIndex - 1 }, () => { this.observerObj.disconnect(); this.handleVideoScroll(); }) } }, [代码] 但是我们的产品在体验的过程中,会提出并不是完全看不见了才去切换,可能想要还剩个150px就切换了,所以我这里要对viewport调整一下 [代码] this.observerObj = wx.createIntersectionObserver().relativeToViewport({ top: -300, bottom: -300 }); [代码] 完成之后,你就可以缓缓的滑动你的视频,实现视频切换的效果了。可以看到当视频差不多被遮住不到一半,就开始切换了。 [图片] 总结 整个过程其实就是好好利用了IntersectionObserver这个API而已。当然现在只是一个非常简单的实现,性能问题,以及快读滑动的情况都无法应对,我们下一篇在接着~。
2019-08-15 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 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 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 截图组件(修正版,上篇删了,有bug)
刚才发的截图时发生错位,现在重新发 https://developers.weixin.qq.com/s/hzVM3BmU7Paz 以后有时间会出一整套原生的框架,开源 开源 开源,希望大神指点一二
2019-08-14 - 利用wxs文件实现touch事件拖动
[图片] 图一:wxml文件中的touch事件绑定在js文件中,js文件中的对应的事件做出处理,然后修改x,y等相关的位置信息,并且渲染出来,问题在于:卡顿,原因:比如说js文件和wxml文件之间时通过一条管道进行通信的,那么在wxml文件触发touch事件时,开始向js文件传递数据,js接受到数据后进行处理,然后再传相关数据给wxml文件,这样一次touch事件就在这条管道中进行了两次传输,而touchmove事件的触发过于频繁,导致了管道堵塞,(塞车)。所以卡顿了 图二:通过微信给出的相关api,把wxml文件中的touch事件绑定到wxs文件中,然后再wxs文件中做出相应的修改,(修改样式),文档链接 这样做可以解决卡顿的问题,但是有个问题是,再wxs文件中修改的样式等级是最高的,也就是说通过wxs文件修改样式后,其他途径修改样式就行不通了,如果不实际情况允许的话,用这种方法做也是可以的。 图三:图三是把js,wxml,wxs三个文件形成一个圆环,wxml触发touch事件,把事件传递给wxs文件,wxs文件做相关处理,然后把结果传给js文件,js文件接收到数据后,再对wxml文件做相关的样式修改(通常都是修改x,y等值),这样既可以避免图一的卡顿,也可以避开图二的样式等级的问题,如果再在wxs文件中做相关节流的话,效果应该会更好,相关的操作,用到的api请点击这里 开发文档里面都有,就不11说明了 另外对于movable-view,我就不说, 一开始就是用的movable-view,问题多多,最后改用了图三的做法 第一次写文章,有问题的地方请多指教,纯属分享个人开发经验
2019-07-31 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28