- 如何监听小程序中的手势事件(缩放、双击、长按、滑动、拖拽)
mina-touch [图片] [代码]mina-touch[代码],一个方便、轻量的 小程序 手势事件监听库 事件库部分逻辑参考[代码]alloyFinger[代码],在此做出声明和感谢 change log: 2019.03.10 优化监听和绘制逻辑,动画不卡顿 2019.03.12 修复第二次之后缩放闪烁的 bug,pinch 添加 singleZoom 参数 2020.12.13 更名 mina-touch 2020.12.27 上传 npm 库;优化使用方式;优化 README 支持的事件 支持 pinch 缩放 支持 rotate 旋转 支持 pressMove 拖拽 支持 doubleTap 双击 支持 swipe 滑动 支持 longTap 长按 支持 tap 按 支持 singleTap 单击 扫码体验 [图片] demo 展示 demo1:监听 pressMove 拖拽 手势 查看 demo 代码 [图片] [图片] demo2: 监听 pinch 缩放 和 rotate 旋转 手势 (已优化动画卡顿 bug) 查看 demo 代码 [图片] [图片] demo3: 测试监听双击事件 查看 demo 代码 [图片] [图片] demo4: 测试监听长按事件 查看 demo 代码 [图片] [图片] demo 代码 demo 代码地址 mina-tools-client/mina-touch 使用方法 大致可以分为 4 步: npm 安装 mina-touch,开发工具构建 npm 引入 mina-touch onload 实例化 mina-touch wxml 绑定实例 命令行 [代码]npm install mina-touch[代码] 安装完成后,开发工具构建 npm *.js [代码]import MinaTouch from 'mina-touch'; // 1. 引入mina-touch Page({ onLoad: function (options) { // 2. onload实例化mina-touch //会创建this.touch1指向实例对象 new MinaTouch(this, 'touch1', { // 监听事件的回调:multipointStart,doubleTap,longTap,pinch,pressMove,swipe等等 // 具体使用和参数请查看github-README(底部有github地址 }); }, }); [代码] NOTE: 多类型事件监听触发 setData 时,建议把数据合并,在 touchMove 中一起进行 setData ,以减少短时内多次 setData 引起的动画延迟和卡顿(参考 demo2) *.wxml 在 view 上绑定事件并对应: [代码]<view catchtouchstart="touch1.start" catchtouchmove="touch1.move" catchtouchend="touch1.end" catchtouchcancel="touch1.cancel" > </view> <!-- touchstart -> 实例对象名.start touchmove -> 实例对象名.move touchend -> 实例对象名.end touchcancel -> 实例对象名.cancel --> [代码] NOTE: 如果不影响业务,建议使用 catch 捕获事件,否则易造成监听动画卡顿(参考 demo2) 以上简单几步即可使用 mina-touch 手势库 😊😊😊 具体使用和参数请查看Github https://github.com/Yrobot/mina-touch 如果喜欢mina-touch的话,记得在github点个start哦!🌟🌟🌟
2021-06-24 - 腾讯课堂小程序详情页开发总结
状态管理 一开始为了借鉴和复用课堂H5详情页的状态管理,引入 redux ,但由于 reducer 总是返回一个新的更新后的对象,这意味着每次 setData 时会传递全量的数据,而在小程序双线程界面渲染的数据通信模型下,传输数据量与性能正相关,因此对于数据量比较大的详情页来说,每次 action 操作都比较耗性能,体验不好。 于是改用腾讯开源的小程序状态管理方案 westore, 它利用小程序 setData 函数支持以数据路径的形式传递数据的特点,通过 update 函数先进行 diff 得到最小更新的数据路径集合,然后再调用 setData 函数传递变化的数据以达到更优的性能。 可是 westore 是基于页面路径来同步数据的,如果同时存在两个相同路径的页面,则只有最新的页面会更新;例如当前页面 A (pages/course?cid=A)打开相同路径的页面 B (pages/course?cid=B)时,由于 store 数据是共享的,这时页面 B 持有页面 A 的数据,同时页面 A、B 路径(pages/course)相同,此时 westore 已经丢掉页面 A 的引用,当 westore 更新数据时只会影响到页面 B ,页面 B 返回页面 A 后,已经无法再更新页面 A 了。 对于这个问题,只要增加一个栈来记录页面路径实例,新开页面时,重置 westore 数据,页面返回时,将旧页面实例的数据同步到 westore 即可。 [图片] 除此之外,H5详情页中很多复合的状态逻辑都放在嵌套较深的自定义组件中,可在小程序环境下就有点力不从心了,所以必须要将这部分常变状态和遍历逻辑提前计算,以便 westore diff 局部更新。 富文本 [图片] 课堂详情页中需要展示由富文本编辑器 CKEditor 生成的课程详情,里面可能包含视频,但小程序提供的 rich-text 组件无法支持 video 标签,因此用到 wxParse 来将 HTML 文本解析成 JSON 树,然后通过 view + css 来模拟 HTML 元素进行渲染。 可是 wxParse 已经很久没有更新了,在使用过程中发现它有很多问题和局限性,以下是踩坑改造优化经验: 缺少解码、解析和渲染完成等钩子:由于后台 CGI 返回的 HTML 文本存在二次编码的情况,只经过 wxParse 的一次解码后仍有部分字串没有被正确解析,同时针对某些解析后的 HTML 标签需要扩展其属性等等。 因此只能修改源码增加 beforeDiscode、afterDiscode、parsedStartTag、parsedEndTag、parsed 和 complete 等钩子来提高其灵活性。 含有较多复杂属性的 HTML 标签无法解析出来:主要是 wxParse 中 startTag 的正则表达式不够全面导致的。 [图片] 上图无法解析出第一个 p 标签。 修改一下 startTag 的正则表达式即可。 [图片] 12个相同 template:wxParse 定义了 wxParse0 到 wxParse11 共 12 个 template,这 12 个 template 除了子结构调用不同的 wxParseXX template 之外其余代码都是一样,究其原因是因为小程序 template 不能递归引用,当然这种变通的处理方式有个局限性,就是它处理不了超过 12 层的结构,超过以后就解析不了,再加上小程序的机制,这样是不会报错的,导致查 bug 很困难。要解决这个问题,除了官方支持 template 递归,可以将 wxParse 改为自定义组件(暂未尝试),或者尽可能的合并 HTML 结构。例如 [图片] wxParse 解析渲染后的结果 [图片] 这里可以发现每个 wxParse-inline 元素的样式完全可以合并,同时形如 wxParse-s 等元素是通过 css 来模拟 HTML 元素的,因此对于这样嵌套的行内元素,可以进行合并 [图片] a 标签作为块元素:由于 a 标签允许包裹其中没有交互内容的块元素,wxParse 把 a 标签视为块级元素,导致解析 a 时将其前一个行内元素提前闭合了,造成显示错误。解决办法是将 a 标签从块级元素中剔除。 不支持腾讯视频 vid:小程序 video 仅支持视频地址和云文件 ID,但课程详情会包含腾讯视频,而腾讯视频播放路径需要通过腾讯视频 SDK 将视频 vid 转换出来,由于已经引入腾讯视频组件,VID 转换这一步可以省略交给腾讯视频组件,只需要将 wxParse template 中 video 标签改为 txv-video,同时在 wxParse 解析出 video 数据时计算出 authExt ,连同 vid 等必要字段一并提供给 txv-video 即可播放视频。考虑到课程详情中的视频播放频次不高,没必要详情展示时就生成腾讯视频组件,因此使用封面 + 播放按钮来替代,等待用户点击封面时才生成。 wxParse 样式污染全局:定义了 view 样式,但没有限定在 .wxParse 作用域下生效,导致影响了页面全局。 标签内文本含有 < 则解析结束标签有误 setData含有较多与界面渲染无关的数据 … WXML WXML(WeiXin Markup Language)是小程序视图层的一套标签语言,它与 Vue 的模板语法很相似,但在实际开发过程中经常会遇到一些问题与限制。 数据绑定中的数据处理 在 WXML 中,数据绑定只支持简单的 js 表达式,不能调用方法。例如保留数据的小数点后两位 [代码]<view>{{num.toFixed(2)}}</view> [代码] 这种写法是不会生效的,为了弥补 WXML 中数据处理的短板,小程序提供了 WXS(WeiXin Script)脚本,可以这么做 [代码]<view>{{tools2.toFixed(num, 2)}}</view> <wxs module="tools2"> function toFixed(num, len) { return num.toFixed(len); } module.exports = { toFixed: toFixed } </wxs> [代码] 但要注意的是 wxs 与 javascript 是不同的语言,有自己的语法,并不和 javascript 一致,更不能使用 es6 语法。 wxs 的运行环境和其他 javascript 代码是隔离的,wxs 中不能调用其他 javascript 文件中定义的函数,也不能调用小程序提供的API。 wxs 函数不能作为组件的事件回调。 wxs 目前共有以下几种数据类型:number,string,boolean,object,function,array,date,regexp template 的 data 传参 如果只看 官方文档 template 说明,你可能不知道 template 的 data 传参有三种方式: 格式一:data="{{ …value1,…value2,… }}",value 前面的 [代码]...[代码] 是扩展运算符。 格式二:data="{{ value1,value2,… }}"。 格式三:data="{{ key1: value1,key2: value2,… }}"。 value 可以是 boolean、number、string、null、object、array。 例如 [代码]value = { a: 1, b: 2, c: 3}[代码],那么在 template 中的使用如下: [代码]<!-- 格式一 --> <template name="example1"> <view>{{a}}: {{b}}: {{c}}</view> </template> <template is="example1" data="{{...value}}" /> <!-- 格式二 --> <template name="example2"> <view>{{value.a}}: {{value.b}}: {{value.c}}</view> </template> <template is="example2" data="{{value}}" /> <!-- 格式三 --> <template name="example3"> <view>{{k.a}}: {{k.b}}: {{k.c}}</view> </template> <template is="example3" data="{{k:value}}" /> [代码] 如果在列表渲染时,想要将列表的索引 index 在 template 中使用,可以这样做 [代码]<template name="example4"> <view>{{index}}: {{msg}}: {{time}}</view> </template> <template is="example4" wx:for="{{items}}" data="{{index, ...item}}" /> [代码] 除此之外还可以结合 wxs [代码]<template name="example5"> <view>{{msg}}: {{time}}</view> </template> <template is="example5" data="{{...tools.getLast(items)}}" /> <wxs module="tools"> function getLast(items) { return items[items.length - 1]; } module.exports = { getLast: getLast }; </wxs> [代码] 数据缓存与自定义组件和 wx:if 在做页面数据缓存时,由于页面数据字段比较多且嵌套深,有时图方便,我们会省略嵌套深的字段定义同时将缓存赋给 data,然后直接在 wxml 中使用 [图片] 如果 wxml 中刚好使用了 wx:if 和自定义组件,那么在小程序基础库 2.4.0 及以下,从第二次进入该页面时就会报错 [代码]Expect FLOW_CREATE_NODE but get another[代码],对于这个问题,有几种解决办法: _list 列出所有字段定义。 data 中 list 不直接赋值 _list,改在 onLoad 时通过 setData 传递。 wxml 中 wx:if 改为 hidden 处理,或者不适用自定义组件。 上述问题出现的条件比较特殊,很大部分是编码问题,但从小程序基础库 2.4.1 开始就不会出现。对比了不同版本基础库在 onLoad 阶段输出的 data 信息,发现 2.4.1 及以上 data 的初始值不再等于当前缓存的 _list 值。 2.4.0 及以下第一次和第二次进入该页面时的 data 值,第二次进入已有缓存 [图片] 2.4.1 及以上第一次和第二次进入该页面时的 data 值相同 [图片] 其他 template 模板与 component 组件 template 模块与 component 组件是小程序中组件化的方式。二者的区别: template 模块主要是展示,交互需要在使用 template 的页面中定义。 component 组件拥有自己的数据处理与交互逻辑,类似一个 page 页面。 在需要频繁更新的场景下或者在列表中涉及到列表子项独立的操作时,使用自定义组件可以只在组件内部进行更新,即实现页面局部更新,而不受页面其他部分内容的影响。 onPageScroll 与 IntersectionObserver 在做图片懒加载、元素曝光上报和元素吸顶展示时,离不开元素位置与页面滚动位置的判断,与之相关的事件或API有: onPageScroll:page 中监听用户滑动页面的事件。自定义组件无法使用,只能通过传参或事件总线来获取变化状态。 IntersectionObserver:监听某些节点与参照物边界相交状态的对象。参照物可以是指定一个节点或者页面显示区域。 从触发回调频次来看,onPageScroll 远远高于 IntersectionObserver,而且每一次事件回调都是一次视图到逻辑的通信过程。因此应该只在必要的时候才使用 onPageScroll,其他情况使用 IntersectionObserver 替代较好。
2019-05-07 - 刚刚起步的小程序-SaUi
作为一个前端开发者来说,还做了这么多年,一直想做一套属于自己的ui框架。 前期也做了很多准备,虽然是移动跟pc的,小程序的代码虽然跟它们有所不一样,但是很多都是大同小异。这次想借这个小程序,完完全全的整理出来。定期的发布,就是为了更好的去监督自己。 先简单的介绍下我的小程序: 以下是我小程序的目录 [图片] 样式我是用stylus写的,里面也封装了各种方法。目前我的想法是通过配置,及方法去更换。 [图片] [图片] 首页 [图片] Button [图片] Form [图片] List [图片] Tab(scroll) [图片] Tag [图片] 小程序入口: [图片]
2019-05-15 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20