- 【插件已经停止维护】【微信小程序-插件开发实战】学校选择器 11.9更新
插件已经停止维护 插件使用效果图: [图片] 学校选择器,起源于我个人开发的项目中的一个实际需求:从列表中选择高校。本质上只是一个选择列表,有很多种呈现方法,而我想让这一环节呈现的更舒适一点(就是带图片咯~),于是开发了这个组件。但我觉得应该有其他开发者会用到同样的需求,那何不分享出来?把它插件化并开源。 一、插件的配置 首先新建一个插件,而不是小程序,如下图。 [图片] 微信提供的插件模板分为两个模块,一个是miniprogram,是用于模拟业务环境的,你可以在这里模拟下使用插件的小程序业务页面;另一个模块是plugin,这就是插件的实际开发环境了,最新的基础库已经支持插件里面有自定义组件和多个page页面。 [图片] 改动改动一些模板的变量名,如plugin.json的参数(如要插件的页面、自定义组件等)后,后面就可以开始在plugin里面写插件代码! { "pages": { "chooseSchool": "pages/chooseSchool" }, "main": "index.js" } 在插件的配置方面,官方文档介绍的非常详细,我就不再赘述了:https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/development.html#%E5%88%9B%E5%BB%BA%E6%8F%92%E4%BB%B6%E9%A1%B9%E7%9B%AE 二、学校选择器 下面开始写插件的业务代码。 学校选择器主要由三部分组成:一个固定的搜索框、懒加载的列表、搜索失败的提示框。 1、固定的搜索框 input配置了bindinput,让每次键入字符时,都将包含该字符的学校名加入渲染的列表里 /** * 搜索学校 */ search_school(e){ var value = e.detail.value var schoolList = this.data.schoolList var schoolList_now = [] for(var i = 0; i < schoolList.length; i++){ if(schoolList[i].name.indexOf(value) != -1){ schoolList_now.push(schoolList[i]) } } this.setData({ schoolList_now:schoolList_now }) } 2、懒加载的列表 {{item.name}} 985 211 其中的image配置了lazy-load以允许懒加载,这样可以加快渲染速度。 3.搜索失败的提示框 没有搜索到结果 可能是尚未收录该学校 你可以自己输入学校名字 配置了wx:if,当待渲染列表长度为0时就渲染出现。 三、插件的调用 如何调用插件呢(这是你必须要告知插件使用者的一件事),微信小程序提供的插件模板已经为你配置好了:你只需要在小程序端的app.json中配置plugin的值如下:"choose-school"是插件的名字,provider填写插件的appId,version是版本号(开发时使用的是dev,但正式版需要正式的版本号) "plugins": { "choose-school": { "version": "dev", "provider": "wxddd52601ccb94739" } } 如果插件中还使用了自定义组件,小程序端的对应页面的json中也需要配置对应的usingComponents,我这个插件是没有用到自定义组件的。 小程序端(也就是插件的使用者)该如何进入插件页面呢?进入插件有两种方法,一种是wxml中的navigator组件,模板中自带,你只需要改动一下跳转的路径即可,如下图: Go to Plugin page 另一种方法是使用wx.navigateTo,我比较推荐这一种方法,因为我是用这种方法才能拿到回传。如下所示,你只要把navigateTo绑定在任意一个组件的事件上就行,它需要配置两个参数,url需要按插件的规则配置:plugin: // + 插件名 + 插件页面。events需要配置acceptDataFromOpenedPage这个参数以获取返回值。 wx.navigateTo({ url: 'plugin://choose-school/chooseSchool', events: { // 监听返回数据 acceptDataFromOpenedPage: function(res) { console.log('回调成功:',res) that.setData({ school:res.data }) }, }, }) 到这一步,插件的调用就结束了,下面是获取它的返回值。 四、插件的返回 当插件的业务逻辑和调用都准备就绪时,这个时候最关键的一步来了,小程序调用你的插件后,总是要获取返回值的,那如何传递这个值呢? 微信官方给我们提供了EventChannel这个路由方法,它是页面间事件通信通道。我们在插件页面的学校view上绑定一个点击事件choose,点击后获取该学校的数据并返回给上一个页面,如下图所示: choose(e){ var item = e.currentTarget.dataset.item var school = this.data.school school.name = item.name school.is985 = item.is985 school.is211 = item.is211 this.setData({ school:school }) const eventChannel = this.getOpenerEventChannel() eventChannel.emit('acceptDataFromOpenedPage', {data: school}); wx.navigateBack({ delta: 1, }) }, 想要将值返回,就新建一个通信信道eventChannel,并且用它发射(emit)一次数据给小程序端开放的信道监控事件acceptDataFromOpenedPage,并把数据school作为data的值传入。然后再调用navigateBack返回小程序端,传值完毕。 五、本插件的实机调用 11.8号我的插件就通过审核啦,我用另一款我已上线的小程序作为例子,给大家分享一下如何调用“学校选择器”。 第一步:登录小程序后台——设置——第三方设置 [图片] 第二步:点击添加插件,搜索“高校选择器”,点击添加,即可发送申请[图片] 第三步:等待申请审核通过(是我审核,如果我没有及时通过,你可以直接私信我) [图片] 第四步:点击“详情”——开发文档,查看插件的使用说明 [图片] 第五步:插件申请通过后,在小程序app.json里面如下配置:(version需要填写1.0.1)(provider一定是填我的插件id:wxddd52601ccb94739,不是小程序的appId),插件名字写“choose-school”(我甚至忘了是在哪里设置这个名字的,以至于我都改不了) "plugins": { "choose-school": { "version": "1.0.1", "provider": "wxddd52601ccb94739" } }, [图片] 第六步:打开开发者工具的详情,查看插件信息里是否有“高校选择器”,如果没有就重启一下开发者工具 [图片] 第七步:在任意地方绑定如图函数,函数触发后就会跳转到插件,选择学校后数据可回调到res里。 wx.navigateTo({ url: 'plugin://choose-school/chooseSchool', events: { // 监听返回数据 acceptDataFromOpenedPage: function(res) { console.log('回调成功:',res) }, }, }) [图片] 第八步:测试是否能拿到回调。 [图片] [图片] 第九步:上线使用~,有任何建议都可以在任何联系到我的地方反馈,我会及时回复! 六、项目源码 组件的源码放在了Gitee上,欢迎下载。 https://gitee.com/cao-mengliang98/school/tree/master 七、其他 如果你想用自己的呈现方式,甚至不使用在微信小程序上,你可以通过这个url来获得学校的logo:http://www.ming13.cn:5000/schoolImages/学校的名字.jpg 不要漏掉.jpg,这是我个人的服务器,个人开发者可以直接使用。以及从源码仓库中获取学校名单.xlxs。 (logo和名字均暂时只有985211高校,后续会补充完整国内高校)
07-02 - bindinput和bindblur的问题
bindinput 的事件中,如果是手写输入,在最后的时候如果直接点击触发按钮事件,比如查询或者搜索。最后那个字符是不能获取到的。 bindblur 的事件,如果直接在输入的状态下直接点击小程序上面的按钮触发其他事件,这个时候bindblur事件其实并没有拿到input里面的值。请问这些有什么办法可以解决??
2018-07-17 - [干货]新一轮用户授权改版来了,肝起来吧!
废话不多说,直奔主题。感谢TX团队赞助,不然没机会通宵!以下思路,仅供参考。 公告开山 [图片] 改造原有授权逻辑 原来通过getUserProfile接口拿到userinfo数据,仍然不变。仅对返回结果加一次判断; [代码]wx.getUserProfile && wx.getUserProfile({ lang: 'zh_CN', desc: "desc", success: ({ userInfo }) => { if (userInfo?.nickName === '微信用户') { // 跳转至新建的资料修改页面 return } // 同步用户资料信息 api.updateUserinfo(userInfo) }, fail: (error) => { // 跳转至新建的资料修改页面 } }) [代码] 资料修改页面 页面 此处需要注意的是, 头像组件有最低版本兼容要求。而昵称填写则可以忽略版本兼容性。此处不赘述昵称填写 兼容性处理 [代码]data: { supportAvatarInput: compareVersion(version, '2.21.2') >= 0, } [代码] [代码]<!-- 代码有省略,见下 --> <button wx:if="{{supportAvatarInput}}" open-type="chooseAvatar"></button> <button wx:else></button> [代码] 页面美化 由于官方没有直接提供头像选择效果的组件可供直接使用,而是以button的形式。因此需要我们自行处理,不过也很简单position + opacity即可; 先上效果图: [图片] [代码]<view class="__input-wrapper"> <image class="avatar" src="{{avatarUrl}}"></image> <button wx:if="{{supportAvatarInput}}" class="opacity-button" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar"></button> <button wx:else class="opacity-button" catchtap="handleChooseImage"></button> </view> [代码] [代码].__input-wrapper{ position: relative; top: 0; left: 0; } .__input-wrapper .avatar { width: 120rpx; height: 120rpx; border-radius: 50%; } .__input-wrapper .opacity-button{ position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 9; opacity: 0; padding: 0 !important; margin: 0 !important; } [代码] 到此,界面完工。接下来开始处理头像上传问题。 业务逻辑 先来处理头像上传组件本身的事件, 此处有个坑。官方的头像上传不支持裁剪,因此需要用到更新的cropImage这个API。 所以我们先来实现图片裁剪功能, 依然是注意兼容性。因为目前是从2.26.0才开始支持。 [代码] cropAvatar(url){ return new Promise((resolve, reject) => { try { if (wx.cropImage) { wx.cropImage({ src: url, cropScale: "1:1", success({ tempFilePath }) { if (!tempFilePath) return reject(Error('处理失败,未获取到路径')) resolve(tempFilePath) }, fail(error) { reject(Error(error.errmsg || error.errMsg)) } }) return } // 低版本兼容 // 此处可弹窗提醒用户手动裁剪为1:1 if (wx.editImage) return wx.editImage({ src: url, success({ tempFilePath }) { if (!tempFilePath) return reject(Error('处理失败,未获取到路径')) resolve(tempFilePath) }, fail(error) { reject(Error(error.errmsg || error.errMsg)) } }) // 其他情况返回原url resolve(url) } catch (error) { reject(error) } }) } [代码] 考虑到用户上传的图片可能会很大。也可以继续设计一个压缩图片的方法。 此处又是一个坑,官方的compressImage API模拟器上会返回错误的tempFilePath方法(真机目前没有) [代码]compressImage(url){ return new Promise(async (resolve, reject) => { // 指定压缩为480px * 480px const COMPRESS_SIZE = 480 try { const { width, height } = await this.getImageInfo(url) const size = Math.ceil(Math.sqrt(width * height)) if (size <= COMPRESS_SIZE) throw Error(`当前图片尺寸为 ${width} * ${height} (${size}) , 已符合压缩规则,忽略压缩`) const quality = Math.ceil(COMPRESS_SIZE * 100 / size) if (!wx.compressImage) throw Error("当前基础库版本太低,请升级微信客户端") wx.showLoading({ title: '压缩中', }) wx.compressImage({ src: url, quality: quality, // 以下高版本支持,低版本仍考虑 quality compressedWidth: COMPRESS_SIZE, compressHeight: COMPRESS_SIZE, success({ tempFilePath }) { if (!tempFilePath) return reject(Error('处理失败,未获取到路径')) // 检查后缀是否存在, 应对模拟器返回错误的tempFilePath问题 if (!/^.*\.(.+)$/.test(tempFilePath)) return reject(Error('SDK错误,压缩路径不正确')) resolve(tempFilePath) }, fail(error) { reject(Error(error.errmsg || error.errMsg)) }, complete() { wx.hideLoading({}) } }) } catch (error) { reject(error) } }) } [代码] 到此准备工作已就绪。 新坑来袭 又一个坑, 此处通过头像上传的url并非和之前一样。这次的是个临时链接,不能直接存储至数据库使用。因此需要服务端提供头像上传的接口。前端调用wx.uploadFile API上传。 此处不赘述 处理头像组件的回调 [代码]async onChooseAvatar({ detail }) { let url = detail.avatarUrl // 裁剪 url = await this.cropAvatar(url) // 压缩 try { url = await this.compressImage(url) } catch (error) { console.error('头像压缩失败:', error) } try { // 此处是上传接口调用。 根据自己需要改造即可 const { avatarUrl } = await app.$http.post({ url: '/upload', header: { 'content-type': 'multipart/form-data' }, data: { name: 'picture', filePath: url, }, timeout: 30 * 1000, loading: '上传中' }) this.setData({ avatarUrl: avatarUrl }) app.$Toast("上传成功") } catch (error) { wx.showModal({ content: `${error.message || "上传失败,请稍后再试"}`, showCancel: false }) } } [代码] 别忘了还有个低版本的适配, 这个简单。 不过这个还是有低版本兼容处理 [代码]handleChooseImage() { try { let self = this if (wx.chooseMedia) { return wx.chooseMedia({ count: 1, mediaType: 'image', async success(res) { const tempFilePath = res?.tempFiles[0]?.tempFilePath if (!tempFilePath) return app.$Toast("选取失败,请稍后再试") // 此处复用之前的头像上传回调 await self.onChooseAvatar({ detail: { avatarUrl: tempFilePath } }) }, fail() { app.$Toast("头像修改失败 请稍后再试") } }) } wx.chooseImage({ count: 1, async success(res) { const tempFilePath = res?.tempFilePaths[0] if (!tempFilePath) return app.$Toast("选取失败,请稍后再试") await self.onChooseAvatar({ detail: { avatarUrl: tempFilePath } }) }, fail() { app.$Toast("头像修改失败 请稍后再试") } }) } catch (error) { app.$Toast("当前版本太低,建议您先升级微信客户端") } }, [代码] 总结 希望这是最后一次 低版本兼容性要注意, 狂放的小伙伴可以设置最低版本库,一劳永逸 头像和昵称组件,自带了安全识别功能。因此会比较慢,需要吐槽的是头像图片的鉴黄识别有点拉跨,经常误报。 如果你看到提示”当前头像不可用“之类的提示,不要担心是代码问题 图片裁剪API才出, 所以在此之前官方是没有考虑到的。吐血 官方其他的bug也该修修了,就比如compressImage里模拟器返回的路径 写的仓促,如有不对,请多指正! 一点小宣传 已经第一时间接入cropImage了,很香。 欢迎体验!! 附小程序码: [图片] 体验路径 : 我的 -> 左上角”设置“ -> 个人信息修改 大家还有什么想说的! 欢迎评论区交流
2023-02-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 - 小程序用户头像昵称获取规则调整公告
更新时间:2022年11月9日由于 PC/macOS 平台「头像昵称填写能力」存在兼容性问题,对于来自低于2.27.1版本的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称。 更新时间:2022年9月28日考虑到近期开发者对小程序用户头像昵称获取规则调整的相关反馈,平台将接口回收的截止时间由2022年10月25日延期至2022年11月8日24时。 调整背景在小程序内,开发者可以通过 wx.login 接口直接获取用户的 openId 与 unionId 信息,实现微信身份登录,支持开发者在多个小程序或其它应用间匿名关联同一用户。 同时,为了满足部分小程序业务中需要创建用户的昵称与头像的诉求,平台提供了 wx.getUserProfile 接口,支持在用户授权的前提下,快速使用自己的微信昵称头像。 但实践中发现有部分小程序,在用户刚打开小程序时就要求收集用户的微信昵称头像,或者在支付前等不合理路径上要求授权。如果用户拒绝授权,则无法使用小程序或相关功能。在已经获取用户的 openId 与 unionId 信息情况下,用户的微信昵称与头像并不是用户使用小程序的必要条件。为减少此类不合理的强迫授权情况,作出如下调整。 调整说明自 2022 年 10 月 25 日 24 时后(以下统称 “生效期” ),用户头像昵称获取规则将进行如下调整: 自生效期起,小程序 wx.getUserProfile 接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。自生效期起,插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回:生效期后发布的插件新版本,通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响,但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称:如业务需获取用户头像昵称,可以使用「头像昵称填写能力」(基础库 2.21.2 版本开始支持,覆盖iOS与安卓微信 8.0.16 以上版本),具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求:对于来自低版本的基础库与微信客户端的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称,开发者可继续使用以上能力做向下兼容。对于上述 3,wx.getUserProfile 接口、wx.getUserInfo 接口、头像昵称填写能力的基础库版本支持能力详细对比见下表: [图片] *针对低版本基础库,兼容处理可参考 兼容文档 请已使用 wx.getUserProfile 接口的小程序开发者和已使用 wx.getUserInfo 接口的插件开发者尽快适配。小游戏不受本次调整影响。 最佳实践小程序可在个人中心或设置等页面使用头像昵称填写能力让用户完善个人资料: [图片] 微信团队 2022年5月9日
2023-09-26 - video-swiper短视频轮播,解决方案2(增加动态加载数据)
实现短视频小程序指定到某个视频开始轮播,方案1已经解析过,这里就不多说了,不懂的可以请移步到这里查看:https://developers.weixin.qq.com/community/develop/article/doc/000c2e0afc8cc8b2a96b36d665b413 这里主要讲的是,如何进行动态加载数据问题 第一步,在获取数据列表中加个条件判断,如果超过你设置的长度就算二次获取数据,进行数据切割加到将要预览的数组里面,代码如下(主要看条件判断,这里以10个数据为例): videoList: { type: Array, value: [], observer: function observer() { var newVal = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; if (newVal.length) { newVal.map((item, index) => { return item.idxKey = index + 1; }); if (newVal.length<=10) { this._videoListChanged(newVal); } else { // 重点是这里的条件判断 // 防止当前数组被污染 let arr = JSON.parse(JSON.stringify(newVal)); // 去掉已有的数据 let nextArr = arr.splice(this.data.total); this.data.nextQueue.push(...nextArr); } this.setData({total: newVal.length}) } } } 第二步,在每次滑动视频时,判断下当前视频总数和剩下视频个数,满足条件即可请求加载数据,代码如下: // 判断总数据是否大于等于10,并且下滑剩下4个视频开始请求接口拿数据;这里大小可以根据自己需求修改 if (total>=10 && nextQueue.length < 5) { this.triggerEvent('UpdataVideo', {}); } 就加这两步,轻松完成一个短视频,从定位到某个视频开始播放,到数据没有时进行预加载视频。 有什么问题,欢迎随时咨询。 完整版代码片段:https://developers.weixin.qq.com/s/Ikk98ymm7tph
2021-04-14 - 微信小程序前端开发踩坑——引入weui组件库
前言 今天在写微信小程序前端页面,想引入weui组件库来完成开发。结果按着官方文档来遇到了一堆问题,最后靠着不断百度查资料才最终解决。所以将过程记录一下,避免后面再遇到这类坑。 注意:本文默认读者已知道怎么使用npm 1. 初始化 以管理员身份运行命令行窗口(cmd),在cmd中进入项目的根目录。然后输入以下命令: [代码]npm init [代码] 后面一路按回车健即可,最终会在项目的根目录中创建出一个名为package.json的文件。 2. 安装weui组件库 在cmd中紧接着输入以下命令: [代码]npm install weui-miniprogram [代码] 命令执行完毕后会多出来一个node_modules文件夹,里面包含了weui组件库。 3. 构建npm 在微信开发者工具中,选择“工具”->“构建npm”。如无意外会出现类似“没有找到可以构建的NPM包……”这样的报错。 这时就需要在项目根目录找到package.config.json文件,修改相关的配置如下: [代码]{ ... "setting": { ... "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./" } ] } } [代码] 继续在开发者工具中的“详情”->“本地设置”里检查是否勾选上“使用npm模块”选项,若没勾选则勾选上。 完成上述配置后,重新构建npm,即可构建完成。 4. 重启项目 在开发者工具中“项目”->“重新打开此项目”,完成对项目的重启。 注意:这一步非常重要!!!否则引入组件会提示找不到文件!!! 5. 引入wxss 在app.wxss中,引入weui库的wxss文件 [代码]@import 'miniprogram_npm/weui-miniprogram/weui-wxss/dist/style/weui.wxss'; [代码] 引入时要根据实际情况调整路径,但最长后缀均为 [代码]/weui-miniprogram/weui-wxss/dist/style/weui.wxss [代码] 6. 引入组件 在想要使用组件的页面对应的js文件中,对组件进行的引入。一定要注意自己项目的目录结构!!! [图片] 而官方文档的写法是 [图片] 如果直接照搬官方文档的写法,则忽略了目录结构,会报错!! 接着在要使用组件的页面对应的wxml文件中使用该组件即可 [代码]<mp-dialog title="test" show="{{true}}" bindbuttontap="tapDialogButton" buttons="{{[{text: '取消'}, {text: '确认'}]}}"> <view>test content</view> </mp-dialog> [代码] 效果如下: [图片] 后记 不得不说,前端开发的坑实在是太多了,上面记录的过程我摸索了一个多小时。看来平时一定要多注意总结才行,不然真的非常消耗时间!!! 创作不易,觉得有用麻烦点个赞,谢谢~~~
2022-01-11 - 【改进版】如何从零实现上拉无限加载瀑布流组件
前言 之前分享过一篇瀑布流如何实现的文章,经过时间的证明,之前的做法并不好,性能上会有问题,所以还是不投机取巧了,老老实实的实现。 回顾: 通过grid-auto-rows的特性实现 item通过grid-row设置高度 js获取节点高度计算span的值 通过wxs设置css的变量实现修改样式 痛点: grid-auto-rows数值越大,span计算准确度越低。 谷歌浏览器、微信开发工具,如果界面高度超过[代码]1000 * grid-auto-rows[代码]的高度,那么后面的内容就不会显示了,谷歌解释说是为了不过渡消耗性能。 因为性能问题,超过1000的item就不会显示了,全会挤压在最下面,导致页面非常卡,开发工具能直接卡崩溃,手机上还没发现这个问题,之前也忽视了这个问题,后面调试的时候就非常恼火,开发工具跟真机上效果不一致。 为了保证span计算的准确度高,grid-auto-rows一般设置成1-10px,1px准确就等于view的高度,但是超过1000px就卡没了。 实现思路 通过selectAllComponents获取所有的子节点 通过getComputedStyle获取节点的高度 简单的排序算法计算节点位置 设置节点的样式 通过wxs的[代码]getState[代码]储存每屏节点渲染的数据 触发[代码]image[代码]组件的[代码]load[代码]事件重新计算并渲染节点位置 创建组件 需要开启抽象节点 [代码]// waterfall/index.json { "componentGenerics": { "selectable": true } } [代码] 利用wxs响应事件获取页面的节点 [代码]<view class="waterfall" views="{{ views.length }}" data-option="{{ {span} }}" change:views="{{ wxs.init }}" > <!-- 嵌套遍历views二维数组 --> <block wx:for="{{ views }}" wx:key="item" wx:for-index="i" > <selectable class="item view-{{ i }}" wx:for="{{ item }}" wx:key="item" value="{{ item }}" /> </block> </view> [代码] 创建item的x,y边距变量 [代码]--span[代码] [代码].waterfall { --span: 5px; width: 100%; position: relative; .item { width: calc(50% - var(--span)); position: absolute; } } [代码] 创建 [代码]index.wxs[代码],核心业务代码都写在这里 [代码]// 当views被setData的时候会被触发 module.export = { init: function(newValue, oldValue, ownerInstance, instance) { console.log(newValue, oldValue, ownerInstance, instance) } } [代码] 业务逻辑 步骤一:获取所有节点 [代码]function init(length, oldValue, ownerInstance, instance) { // 加个判断,避免views长度为0时,或者长度为发生变化时也会执行业务代码 // 只有当views被push新的内容才会执行下面的业务 if (!length || length === oldValue) return // index 其实就是views的长度减一,就等于当前的数组下标 var index = length - 1 var views = ownerInstance.selectAllComponents('.view-' + index) console.log(JSON.stringify(views)) } [代码] [图片] [图片] 步骤二:遍历views获取节点的高度 [代码]views.forEach(function(v, k){ var viewStyle = v.getComputedStyle(['width', 'height']) // 获取高度 var height = viewStyle.height console.log(viewStyle) // [WXS Runtime info] {"width":"182.5px", "height":"242px"} }) [代码] 步骤三:计算view的位置信息 [代码]var LH = 0 var RH = 0 views.forEach(function (v, k) { var viewStyle = v.getComputedStyle(['width', 'height']) // 格式化高度,将px去掉 var height = fixUnit(viewStyle.height) var style = {} if (LH <= RH) { style = { left: 0, top: LH + 'px' } LH += height } else if (RH < LH) { style = { right: 0, top: RH + 'px' } RH += height } // 设置view的样式 v.setStyle(style) }) [代码] 此时,页面的节点会根据position自动排列好 [图片] 步骤四:储存LH,RH到局部变量 [代码]function init(length, oldValue, ownerInstance, instance) { if (!length || length === oldValue) return // 获取局部变量 var state = ownerInstance.getState() // 获取当前节点的dataset var dataset = instance.getDataset() var index = length - 1 state.option = dataset.option state.page = length // 创建并生成记录左侧、右侧高度 // 用二维数组来记录 if (!state.heights) { state.heights = [[0, 0]] } // 记录初次渲染时间戳 if (!state.timeouts) { state.timeouts = [] } // 获取时间戳,并且加上3000毫秒,用于后面计算图片loaded完是否超时 state.timeouts[index] = getDate().getTime() + 3000 refreshViews(index, ownerInstance, state) } function refreshViews(index, ownerInstance, state) { var views = ownerInstance.selectAllComponents('.view-' + index) var span = state.option.span var LH = state.heights[index][0] // 左侧 var RH = state.heights[index][1] // 右侧 views.forEach(function (v, k) { var viewStyle = v.getComputedStyle(['width', 'height']) var height = fixUnit(viewStyle.height) var style = {} if (LH <= RH) { style = { left: 0, top: LH + 'px' } LH += height + span[0] } else if (RH < LH) { style = { right: 0, top: RH + 'px' } RH += height + span[0] } v.setStyle(style) // 保存LH, RH的值到state.heights // 当前的LH,RH其实就是下屏开始的坐标 state.heights[index + 1] = [LH, RH] console.log('渲染', index, k) }) } [代码] 步骤五:图片加载完重新计算位置 [代码]// waterfall/index.js Component({ properties: { views: Array, span: { type: Array, value: [10, 10], }, }, methods: { onLoaded({ detail: { width, height, pIndex, index } }) { this.setData({ [`views[${pIndex}][${index}].loaded`]: { width, height }, }) }, }, }) [代码] [代码]function loaded(value, oldValue, ownerInstance, instance) { if (!value.item.loaded || !oldValue) return // 获取局部变量 var state = ownerInstance.getState() // 判断加载是否超时,如果超时则不触发计算渲染事件 // 让该节点保持当前的位置及高度 var timeout = state.timeouts[value.pIndex] if (timeout < getDate().getTime()) { console.log('加载超时') return } var view = instance.selectComponent('.loaded-view') var viewWidth = view.getComputedStyle(['width']).width // 设置虚拟节点card组件里的loaded-view高度 view.setStyle({ height: computedHeight( viewWidth, value.item.loaded.width, value.item.loaded.height ) + 'px', }) // 加个函数防抖,因为图片加载快的情况下,会并发触发事件 // 尽可能的少触发计算,渲染事件,保证性能 ownerInstance.clearTimeout(timer) timer = ownerInstance.setTimeout(function () { // 渲染当前图片加载完后面的所有views // for循环处理当前图片所在的views,以及后面所有的views // 因为有些图片过大,可能会加载5s左右,但是用户如果上拉又加载了 // 一屏内容并且也通过计算渲染了,这时候上一屏又触发了计算渲染 // 那么可能位置信息就会发生变化,导致被遮挡,或者有空白,这时候只能 // 计算触发事件的图片以及后面的图片,保证位置信息是正确的 for (var i = value.pIndex; i < state.page; i++) { console.log('需要渲染', i) refreshViews(i, ownerInstance, state) } }, 300) } [代码] [图片] 优化 瀑布流最好后台会返回图片的尺寸信息,然后初次渲染的时候就计算好节点的长宽比例,这样就不用监听图片loaded事件了,瀑布流组件代码也不会频繁触发计算渲染,性能也好,方法也简单。 [代码]<image src="xxxx" style="{{ wxs.computed({width, height}) }}" /> [代码] [代码]// wxs function computed(option) { // 节点宽度自己去计算 var viewWidth = 375 / 2 var width = option.width var height = option.height return (viewWidth / width) * height + 'px } [代码] 完整代码 打开代码片段https://developers.weixin.qq.com/s/SO5q6UmF7doL,可直接运行。 https://github.com/liziwork/li-ui github 如果打不开,请切换到码云,gitee.com,代码同步更新的,觉得有用动动您的小手点个Star。 扫码查看更多组件 [图片]
2021-03-19 - 使用可视化组件,movable-area的子view点击事件不响应?
组件:movable-area和movable-view 基础库:2.8.3 对可移动控件进行组件化,使用<slot></slot> 插入其他视图,但子view的点击事件不响应,请问如何解决? [代码]<[代码][代码]movable-area[代码] [代码]class[代码][代码]=[代码][代码]"custom-class"[代码] [代码]style[代码][代码]=[代码][代码]"pointer-events: none;height: {{moveViewHeight}};width: 100%;left:0px;top:0px;"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]""[代码] [代码]style[代码][代码]=[代码][代码]"height:100%;overflow-y:scroll;"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]movable-view[代码] [代码]wx:if[代码][代码]=[代码][代码]"{{show}}"[代码] [代码]direction[代码][代码]=[代码][代码]"all"[代码] [代码]x[代码][代码]=[代码][代码]"{{moveViewX}}"[代码] [代码]y[代码][代码]=[代码][代码]"{{moveViewY}}"[代码] [代码]animation[代码][代码]=[代码][代码]"{{false}}"[代码] [代码]style[代码][代码]=[代码][代码]"pointer-events: auto; width: 40px;height:56px;z-index: 999;"[代码] [代码]bindtap[代码][代码]=[代码][代码]"onHome"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"img-view"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]src[代码][代码]=[代码][代码]'/images/home.png'[代码] [代码]class[代码][代码]=[代码][代码]"home-img"[代码][代码]></[代码][代码]image[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"home-txt"[代码][代码]>返回首页</[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]movable-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]slot[代码][代码]></[代码][代码]slot[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]movable-area[代码][代码]>[代码][代码]<[代码][代码]movable-custom-view[代码] [代码]show[代码][代码]=[代码][代码]"{{true}}"[代码] [代码]moveViewHeight[代码][代码]=[代码][代码]"100%"[代码] [代码]moveViewX[代码][代码]=[代码][代码]"{{moveViewLeft}}"[代码] [代码]moveViewY[代码][代码]=[代码][代码]"{{moveViewTop}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"content-view"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]src[代码][代码]=[代码][代码]"{{imgSrc}}"[代码] [代码]mode[代码][代码]=[代码][代码]"widthFix"[代码] [代码]bindtap[代码][代码]=[代码][代码]"onPreviewImage"[代码] [代码]data-value[代码][代码]=[代码][代码]"{{imgSrc}}"[代码][代码]></[代码][代码]image[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]movable-custom-view[代码][代码]>[代码] 代码片段:https://developers.weixin.qq.com/s/JdkCxHml7mbR
2019-09-27