- 开启websocket服务端口,调试微信websocket接口方法
使用node环境,在vscode 工具中,创建 app.js 文件 ,代码如下,记得安装 npm install websocket 和 npm install http 模块 . 开启 websocket接口服务后,就可以去封装 官方提供的 wx.sendSocketMessage 等接口了。 [代码]const http = require("http"); const WebSocketServer = require("websocket").server; const httpServer = http.createServer((request, response) => { console.log("[" + new Date() + "] Received request for " + request.url); response.writeHead(404); response.end(); }); const wsServer = new WebSocketServer({ httpServer, autoAcceptConnections: true }); wsServer.on("connect", connection => { connection .on("message", message => { if (message.type === "utf8") { console.log(">> message content from client: " + message.utf8Data); connection.sendUTF(message.utf8Data); // 输出内容返回给前端接口调用 } }) .on("close", (reasonCode, description) => { console.log( "[" + new Date() + "] Peer " + connection.remoteAddress + " disconnected." ); }); }); httpServer.listen(8080, () => { console.log("[" + new Date() + "] Serveris listening on port 8080"); }); #小程序页面示例代码,请参考文档 const socketOpen = false const socketMsgQueue = [] wx.connectSocket({ // url: 'test.php', url :“ws://localhost:8080/” }) wx.onSocketOpen(function (res) { socketOpen = true for (let i = 0; i < socketMsgQueue.length; i++) { sendSocketMessage(socketMsgQueue[i]) } socketMsgQueue = [] }) function sendSocketMessage(msg) { if (socketOpen) { wx.sendSocketMessage({ data: msg }) } else { socketMsgQueue.push(msg) } }[代码]
2019-04-18 - 2020-08-16
- 小程序仿instagram交互效果实现(附长列表优化处理)
需求 最近几天在忙着搞公司项目的一个新的需求,原因是这样的:公司准备开发一个偏向于社交娱乐项的小程序,其中首页是可以看到用户发的话题帖子之类的,每个帖子都至少包含一张图片或者一个视频, 然后产品那边希望首页可以实现instagram的交互效果,效果图如下(本来应该是显示图片,奈何我的gif图片大小超过两兆,不能上传,所以我就用表情包替换了): [图片] 嗯,大致上这个就是需求的背景,然后就是每个帖子的高度是不确定的,高度大概在500~600px之间。 实现思路 一开始接到这个需求,其实我心里还是有点慌的,毕竟有一段时间不怎么接触小程序,也不知道小程序更新到什么程度,文档更新到什么程度。仔细分析一下项目需求,大致上可以归类为两个:交互 和 性能优化。 性能优化 因为首页是一个长列表,众所周知,页面一旦渲染的节点过多,就会卡顿,更何况是小程序,并且小程序是分为逻辑层和渲染层,两者通过setData链接,所以处理的时候需要注意两点: setData的数据量不能太大,记得好像是有个大小限制,,忘了是多少,也懒得找:clown_face:,你们可以自己在官方文档上找一下; 页面能够渲染的帖子数量是有限的,在这里,我是控制为最多渲染25个帖子。 处理 针对于长列表的优化,官方也有相应的组件-recycle-view,但是貌似并不符合项目需求,所以被我pass掉了。 虽然没用官方的组件,但是在组件的文档里面把对于长列表得性能优化解释一遍,这里摘抄一下重点: 核心的思路就是只渲染显示在屏幕的数据,基本实现就是监听 scroll 事件,并且重新计算需要渲染的数据,不需要渲染的数据留一个空的 div 占位元素。 其实也就是设置一个变量控制该数据是否可以渲染,如果是不能够渲染得话,那我们就用一个空的view取代它,需要注意的失败的是:空的框架高度需要设置为帖子的高度,这样子才不会闪屏。 针对这种思路,我们就可以确定其中一种长列表的性能优化的解决思路: 将数据分为二维数组,这样子就可以限制每次setData的数据量,等待数据渲染完成之后,获取每组数据所占用的总高度,这里的高度是为了在改组数据不渲染时设置占位框的高度。 [代码]/** * 获取 有渲染,但是高度还没获取到的分组 的高度 */ _getGroupListHeight() { this.data.list.forEach((item, index) => { if (item.show && !item.height) { const id = 'XXXXXXXX' // 组的id let query = wx.createSelectorQuery() query.select(id).boundingClientRect(rect => { this.data.list[index].height = rect.height }).exec(); this.getTopicHeight(item.data, index) // 获取列表中每个话题的高度,用于计算滑动时要滚动的距离 } }) } [代码] 上面就是一个简单获取每组的高度的代码实例,当该组数据有被渲染但是高度不明的情况下,就会去获取,加一步判断是为了防止重获获取组的数据,造成不必要的浪费。在获取每组数据的高度时,还会对应去 获取该组的每个帖子的高度,这样子是为了后面实现 仿instagram 交互做准备。 获取到了每组数据的高度,接下来,我们就可以监听页面滚动的高度,从而控制需要渲染的数据,需要注意的一点是,我们需要在该组的数据基础上,多渲染上两组和下两组数据,目的是防止用户快速滑动的时候出现白屏的不友好体验。当然也可以根据自己的需要多渲染几组。 [代码]/** * 页面滚动 * @param e */ onPageScroll(e) { // android页面滑动处理(非仿instagram版本) if (!this.data.isIos && !this.data.scrollBoxInfo.canUseScrollBox) { // 1. 处理当前页面正在播放的视频 if (this.data.currentPlayingId && Math.abs(e.scrollTop - this.data.scrollTop) > 100) { this.selectComponent(this.data.currentPlayingId).pauseVideo() this.data.currentPlayingId = '' } // 2. 处理 Andorid 渲染的分组数据 this._dealAndroidScroll(e) // 3. 处理视频自动播放 if (this.timer) { clearTimeout(this.timer) } this.timer = setTimeout(() => { this.data.scrollTop = e.scrollTop // 记录下当前的滚动距离,滑动暂停视频播放的时候需要用到 this.handleAutoPlay(e) // 视频自动播放 }, 300) } } /** * Android 监听滚动,动态设置分组 * @param {Object} e */ _dealAndroidScroll(e) { let max_height = 0 // 最大高度 for (let i = 0; i <= this.data.topicScroll.show_index; i++) { max_height += this.data.list[i].height } let min_height = max_height - this.data.list[this.data.topicScroll.show_index].height // 最小高度 // 超过,+1 if (e.scrollTop > max_height && this.data.topicScroll.show_index < this.data.list.length - 1) { ++this.data.topicScroll.show_index this._dealListShow(this.data.topicScroll.show_index) } // 小于,-1 if (e.scrollTop < min_height) { --this.data.topicScroll.show_index this._dealListShow(this.data.topicScroll.show_index) } } [代码] 这里没有对代码进行过滤,代码实现的效果就是监听滚动到的位置,并且设置渲染分组数据,因为有些是视频帖子,所以需要在滚动完成之后实现自动播放视频的功能。这段代码只是处理anroid平滑滚动的情况,因为代码涉及到 instagram 交互的实现。 对于长列表性能优化的思路大致上就这样:数据分为二位数组,渲染指定分组的数据,减少渲染的数据量和需要渲染的节点数量, 不需要渲染的数据就用指定高度的空的view替代,指定高度是为了防止闪屏。 仿 instagram 交互实现 从上面的 instagram 交互视频可以看出来,我们需要监听用户的手势滑动从而控制帖子的切换,并且每次切换只是切换一个帖子。知道了交互的详情,我们就可以展开想象了,大致上可以给个基本的实现思路: 监听手指点击和手指离开的事件,记录下手指点击的高度 && 手指离开的高度,用来判断用户滑动的距离和方向;记录下手指点击 和 手指离开的时间,可以粗略用来判断用户当前的滑动行为是快滑还是慢滑。根据上面的得到的信息,我们基本上实现滑动切换帖子的操作: [代码]/** * 监听手指点击操作 * @param e */ touchStart(e) { this.data.topicScroll.startTimeStamp = new Date().getTime() // 记录下当前手指点击事件 this.data.topicScroll.startPosition = e.changedTouches[0].clientY // 记录下手指开始点击的位置 }, /** * 手指离开屏幕 * @param e */ touchEnd(e) { const diffTime = new Date().getTime() - this.data.topicScroll.startTimeStamp // 手指离开的时候的时间戳 const clientY = e.changedTouches[0].clientY // 手指离开屏幕的位置 const diffY = Math.abs(clientY - this.data.topicScroll.startPosition) // 手指滑动的距离 const direction = this.data.topicScroll.startPosition - clientY > 0 // 手指滑动的方向,true为向上滑,false为向下滑 const scrollInfo = this.data.topicScroll // 1. 第一个节点手指向下滑动 && 最后一个节点手指向上滑动 不做操作 if (!scrollInfo.parent_index && !scrollInfo.child_index && !direction) { return } // 第一个节点向下滑动不做操作 if (scrollInfo.parent_index === (this.data.list.length - 1) && scrollInfo.child_index === (this.data.list[scrollInfo.parent_index].data.length - 1) && direction) { return } // 最后一个节点向上滑动不做操作 // 2. 根据滑动的方向,判断需要滚动到哪个节点下 const can_move = (diffTime < 100 && diffY > 50) || (diffTime >= 100 && diffY > 80) // 是否可以滑动,手势滑动判断依据 if (can_move) { if (direction) { if (scrollInfo.child_index === 4) { ++scrollInfo.parent_index scrollInfo.child_index = 0 } else { ++scrollInfo.child_index } } else { if (scrollInfo.child_index === 0) { --scrollInfo.parent_index scrollInfo.child_index = 4 } else { --scrollInfo.child_index } } } // 3. 处理滚动 this._dealScroll(can_move ? pausePlayId : '', can_move) } [代码] 根据监听到信息,可以判断是否切换以及切换到那个帖子的操作,所以我们只需要根据面已经获取到的帖子高度就可以计算出来要滚动的高度。 2. 根据第一点的操作,我们就可以实现 仿Instagram 交互效果,但是忽略了致命的一点:页面的惯性滚动。也正是因为这个所谓的“惯性滚动”,我多花了几天的时间去研究交互的实现💥 众所周知,为了使得页面的滑动更加流畅,当我们滑动停止的时候,页面就像会产生惯性一般,自动的滑动一定距离才停下。 安卓下默认有惯性滚动,而在 iOS 下需要额外设置-webkit-overflow-scrolling: touch的样式 而第一点方案实现的大前提是页面不能拥有惯性滚动,否则的话页面无法准确滚动到指定位置。 解决方案 ios的解决方案比较简单,我们只需要设置 -webkit-overflow-scrolling: auto 的样式即可。 比较麻烦的是android的实现,一开始我是上网找了不少的资料去实现取消页面的惯性滚动,毕竟页面滚动的性能是最好的。不过很可惜,我没有找到可行的方案,所以我选择退而求其次,模拟手指的滑动。经过对小程序文档的浏览,我选择可两种比较可能实现的方案:①使用wxs实现手指滑动的效果;②使用scroll-view的fast-deceleration 属性。显而易见,使用wxs方案实现比较复杂,所以我一开始选择scroll-view这个方案。 android实现思路 scroll-view方案实现的思路与ios类似,比较不同的是页面的滚动,因为ios是可以直接使用页面滚动的,即wx.pageScrollTo,而scroll-view则是使用scroll-into-view 或者 scroll-top,为了减少操作,我是直接使用了scroll-top这个属性。正当我信心满满的时候,做出来的效果却是打了我的脸:每次滚动到指定位置的时候都会抖动一下,虽然没有仔细去查明原因,不过 我猜想是页面滚动依然存在一定的惯性滚动,当我们设置了scroll-top的时候,惯性滚动的存在会使得列表偏移位置,最后在偏移回来,这样子看起来就像是抖动了一下。 正当我准备放弃scroll-view这个方案的时候,无意中让我发现了scroll-view的api接口,本着想试一下新接口的心态,我尝试一下使用官方的api接口控制scroll-view滚动,神奇的是,成了!!!! (此处为效果图) 既然scroll-view做出来的效果勉强还可以,我就直接pass后面的wxs方案,毕竟那一块的逻辑应该会比较复杂。 [代码] 因为时使用了scroll-view的api接口,支持的版本库比较高,是2.14.4,虽然大部分的用户可以支持,但还是要兼容少部分的用户,所以android部分我是搞了一个scroll-view版本和平滑滚动版本 fast-deceleration这个特性在开发者工具那里貌似不生效,但是手机预览却是可以,不知道是开发工具的问题还是说只是兼容了部分手机(我这边测试了小米和华为这两款,没更多的手机测试了:clown_face:) [代码] demo 说了这么多,也不一定有人看得下去,demo呈上 结尾 在调试这个效果的时候,遇到不少的坑,其中有一个坑印象巨深,在此记录一下: touch 期间 touchstart 的目标节点被移除,则对应的 touchend 事件会因为没有目标节点而缺失。 遇到这个坑是因为小程序同个页面不允许存在多个视频,所以需要将没有播放的视频使用图片替换,需要播放的视频的时候就替换回来,这样子,就会出现上面的情况,导致无法监听到touchend事件,整个列表停在原地,没有滚动到指定位置。 原文链接 传送门
2021-11-14 - weapp-qrcode-canvas-2d在微信小程序中生成二维码,新版canvas-2d接口
weapp-qrcode-canvas-2d weapp-qrcode-canvas-2d 是使用新版canvas-2d接口在微信小程序中生成二维码(外部二维码)的js包。canvas 2d 接口支持同层渲染且性能更佳,建议切换使用,可大幅提升生成图片的速度。 仓库地址 weapp-qrcode-canvas-2d【码云gitee】 weapp-qrcode-canvas-2d【github】 [图片] 测试环境 微信小程序基础库版本:2.10.4 开发者工具版本:Stable 1.03.2101150 Usage 先在 wxml 文件中,创建绘制的 [代码]canvas[代码],并定义好 [代码]width[代码], [代码]height[代码], [代码]id[代码] , [代码]type[代码] ,其中type的值必须为[代码]2d[代码] [代码]<canvas type="2d" style="width: 260px; height: 260px;" id="myQrcode"></canvas> [代码] 安装方法1:直接引入 js 文件 直接引入 js 文件,使用 [代码]drawQrcode()[代码] 绘制二维码 [代码]// 将 dist 目录下,weapp.qrcode.esm.js 复制到项目中。路径根据实际引用的页面路径自行改变 import drawQrcode from '../../utils/weapp.qrcode.esm.js' [代码] 安装方法2:npm安装 [代码]npm install weapp-qrcode-canvas-2d --save [代码] // 然后需要在小程序开发者工具中:构建npm [代码]import drawQrcode from 'weapp-qrcode-canvas-2d' [代码] 安装完成后调用 例子1:没有使用叠加图片 [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node // 调用方法drawQrcode生成二维码 drawQrcode({ canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, background: '#ffffff', foreground: '#000000', text: 'abc', }) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ canvasId: 'myQrcode', canvas: canvas, x: 0, y: 0, width: 260, height: 260, destWidth: 260, destHeight: 260, success(res) { console.log('二维码临时路径:', res.tempFilePath) }, fail(res) { console.error(res) } }) }) [代码] 例子2:使用叠加图片(在二维码中加logo) [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node var img = canvas.createImage(); img.src = "/image/logo.png" img.onload = function () { // img.onload完成后才能调用 drawQrcode方法 var options = { canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, paddingColor: '#fff', background: '#fff', foreground: '#000000', text: '123456789', image: { imageResource: img, width: 80, // 建议不要设置过大,以免影响扫码 height: 80, // 建议不要设置过大,以免影响扫码 round: true // Logo图片是否为圆形 } } drawQrcode(options) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ x: 0, y: 0, width: 260, height: 260, destWidth: 600, destHeight: 600, canvasId: 'myQrcode', canvas: canvas, success(res) { console.log('二维码临时路径为:', res.tempFilePath) }, fail(res) { console.error(res) } }) }; }) [代码] API drawQrcode([options]) options Type: Object 参数 必须 说明 示例 canvas 必须 画布标识,传入 canvas 组件实例 canvasId 非 绘制的[代码]canvasId[代码] [代码]'myQrcode'[代码] text 必须 二维码内容 ‘123456789’ width 非 二维码宽度,与[代码]canvas[代码]的[代码]width[代码]保持一致 260 padding 非 空白内边距 20 paddingColor 非 内边距颜色 默认与background一致 background 非 二维码背景颜色,默认值白色 [代码]'#ffffff'[代码] foreground 非 二维码前景色,默认值黑色 [代码]'#000000'[代码] typeNumber 非 二维码的计算模式,默认值-1 8 correctLevel 非 二维码纠错级别,默认值为高级,取值:[代码]{ L: 1, M: 0, Q: 3, H: 2 }[代码] 1 image 非 在 canvas 上绘制图片,层级高于二维码,v1.1.1+版本支持。具体使用见:例子2 [代码]{imageResource: '', width:80, height: 80, round: true}[代码]
2023-04-02 - 微信小程序 -- 基于 movable-view 实现拖拽排序
微信小程序 – 基于 movable-view 实现拖拽排序 项目基于[代码]colorui[代码]样式组件 ColorUI组件库 (color-ui.com) 1.实现效果 [图片] 2. 设计思路 movable-view 绑定块移动事件的 块[代码]ID[代码] ,块移动的坐标 移动结束后触发[代码]moveEnd[代码]事件,根据[代码]Y[代码]坐标对对象数组进行排序 根据排序结果重置块位置 3.实现代码 代码已经进行了最简化处理 图中效果实现需引入[代码]colorui[代码]的[代码]main.wxss[代码]样式部分。 wxml [代码]<movable-area class="padding text-center bg-grey" style="width:100%;height:500px;" > <movable-view class="radius shadow bg-white" style="width:80%;height:80px;z-index:{{index==moveId?2:1}}" wx:for="{{tabList}}" wx:key="index" x="{{item.x}}" y="{{item.y}}" direction="all" bindchange="moveStatus" bindtouchend='moveEnd' data-moveid="{{index}}"> {{item.name}}</movable-view> </movable-area> [代码] js [代码]var compare = function (obj1, obj2) { var val1 = obj1.y; var val2 = obj2.y; if (val1 < val2) { return -1; } else if (val1 >= val2) { return 1; } else { return 0; } } Page({ /** * 页面的初始数据 */ data: { branchid:'', appdocid:'', tabList:[ { name:'十步杀一人' }, { name:'千里不留行' }, { name:'事了拂衣去' }, { name:'深藏身与名' } ], //移动的是哪个元素块 moveId:null, //最终停止的位置 endX:0, endY:0 }, initMove(){ let tabList = this.data.tabList; var tarr = [] tabList.forEach(function(ele,index){ let obj = ele obj.id = index obj.x = 30 obj.y = 100*index +20 tarr.push(obj) }) console.log(tarr) this.setData({ tabList:tarr }) }, moveEnd(e){ console.log(e) var that = this; that.setData({ ["tabList["+that.data.moveId+"].x"]:that.data.endX, ["tabList["+that.data.moveId+"].y"]:that.data.endY },()=>{ let tabList = this.data.tabList; tabList = tabList.sort(compare); that.setData({ tabList },()=>{ setTimeout(function(){ that.initMove(); },500) }) }) //计算位置 }, moveStatus(e){ // console.log(e) //移动的块ID var moveid = e.currentTarget.dataset.moveid; //最终坐标 let x = e.detail.x let y = e.detail.y this.setData({ moveId:moveid, endX:x, endY:y }) }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { this.initMove(); } }) [代码] 4.参考文档 movable-view | 微信开放文档 (qq.com)
2021-06-17 - 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加catchtouchmove,但是发现部分场景无效果,那么就不再赘述了。 改变底层:既然是顶层影响了底层,要是底层不会滚动,那就没这个问题了。 如何改变底层解决该问题呢? 不成熟方案: 底部页面最外层view设置position: fixed;页面不可滚动,但是这个时候会导致页面回到顶部。 滚动时监听滚动距离,弹窗时记录滚动位置,关闭弹窗后使用wx.pageScrollTo回滚到记录的位置。 成熟方案 使用page-meta组件,通过该组件我们可以操作Page的style样式,类似于h5里body设置overflow: hidden; 控制页面不可滚动。文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/page-meta.html 使用wx.setPageStyle设置overflow: hidden, 也可以实现给Page组件设置样式。) page-meta组件: 通过该组件我们可以直接操作[代码]Page[代码]组件 ,我们给它的wxss样式overflow动态设置[代码]hidden[代码]or[代码]visible[代码]or[代码]auto[代码] 就可以控制整个页面是否可以滚动。 [图片] wx.setPageStyle方法: 调用这个api,动态设置它为hidden/auto,用于控制页面是否可滚动,主要用于页面组件内使用,比如封装好的弹窗组件,就不用单独写page-meta组件了。。 [代码]wx.setPageStyle({ style: { overflow: 'hidden' // ‘auto’ } }) [代码] 老规矩,结尾放代码片段: https://developers.weixin.qq.com/s/U6ItgQmP7upQ 拓展 支付宝小程序虽然存在page-meta组件,但是由于内核为69版本,给page设置overflow: hidden 也无法控制底部元素不可滚动,目前已联系支付宝的底层开发同学提供API控制页面disableScroll,目前正在封装Appx,近期开放。 [代码] my.setPageScrollable({ scrollable: true, success: res => { console.log(res); }, fail: err => { console.log(err); }, complete: res => { console.log(res); } }) [代码] 20250618. API已开放,支付宝小程序测试时发现bug,安卓设置禁止滚动后,弹窗内的可滚动区域也会被禁止,IOS正常,且该问题暂时无法解决。 原因: 由于系统实现层面的差异,安卓与 iOS 对于滚动禁止的层级控制存在区别: 安卓端采用 Webview 级滚动限制(全页面锁定),生效时界面及所有弹层均不可滚动; iOS 端采用组件级滚动限制(局部锁定),当弹层激活时会智能区分层级,仅限制底层页面滚动而保持弹层可滚动。
2天前 - 通俗易懂的五星评价代码——微信小程序如何展示像淘宝天猫那样五星好评?
项目背景 2020年起始,微信官方宣布支持小程序直播。陆续有很多的商家与开发者参与到这场微信生态直播大潮中,作为电商小程序评价内容必不可少的,官方没有提供评价组件只能自己动手丰衣足食。网上看了很多资料写的很复杂,找了些开源,我简化下代码,还上了注释,希望对于新晋开发者有所帮助。 效果图 [图片] .wxml [代码]<!-- 评价管理 --> <view class="star-title">1、品质效果</view> <view class="star-pos"> <image class='stars' bindtap="changeColor" data-index='0' data-no="{{item}}" wx:for="{{stardata}}" wx:key="index" src="{{flag[0]>=item? '/asset/img/icon/star-red.png': '/asset/img/icon/star-empty.png'}}" /> <view style='margin-left: 30rpx;'>{{startext[0]}}</view> </view> <view class="star-title">2、服务质量</view> <view class="star-pos"> <image class='stars' bindtap="changeColor" data-index='1' data-no="{{item}}" wx:for="{{stardata}}" wx:key="index" src="{{flag[1]>=item? '/asset/img/icon/star-red.png': '/asset/img/icon/star-empty.png'}}" /> <view style='margin-left: 30rpx;'>{{startext[1]}}</view> </view> <view class="star-title">3、综合管理</view> <view class="star-pos"> <image class='stars' bindtap="changeColor" data-index='2' data-no="{{item}}" wx:for="{{stardata}}" wx:key="index" src="{{flag[2]>=item? '/asset/img/icon/star-red.png': '/asset/img/icon/star-empty.png'}}" /> <view style='margin-left: 30rpx;'>{{startext[2]}}</view> </view> [代码] .js [代码]Page({ /** * 页面的初始数据 */ data: { flag:[0, 0, 0], //每行标记变量 改变显示的五角星 startext: ['', '', ''], //各个下标对应每行显示 5个评价中的一个 非常不满意/不满意 stardata: [1, 2, 3, 4, 5], //每颗星星对应的数值 决定了渲染出几颗五角星 }, // 五星评价事件 changeColor: function (e) { var index = e.currentTarget.dataset.index; //判断选中了哪个评价 在前端页面写死的索引 var num = e.currentTarget.dataset.no; //前端页面的 item 传来选择了多少个 console.log(num) var a = 'flag[' + index + ']'; var b = 'startext[' + index + ']'; console.log(a,b) var that = this; if(num == 1) { that.setData({ [a]: 1, [b]: '非常不满意' }); } else if (num == 2){ that.setData({ [a]: 2, [b]: '不满意' }); } else if (num == 3) { that.setData({ [a]: 3, [b]: '一般' }); } else if (num == 4) { that.setData({ [a]: 4, [b]: '满意' }); } else if (num == 5) { that.setData({ [a]: 5, [b]: '非常满意' }); } }, }) [代码] .wxss [代码]/* 评价 */ .star-pos { margin: 10rpx; display: flex; flex-direction: row; } .stars{ width: 40rpx; height: 40rpx; margin-left: 30rpx; } [代码] 总结 未接触开发这个评价的时候,以为很难很复杂。网上查阅了很多资料,也显示的非常复杂,后来仔细思考清晰了下思路,无非就是图标的变换,数值的改变,以及循环显示五角星,最后整理成上文的代码。 我将陆续发布微信小程序云开发电商系统的文章,欢迎大家关注。也欢迎大家上我开发的“百熟优质果”微信小程序购买我们家的水果哈~ 系统使用过程中,有任何需要优化的地方欢迎给我建议~ [图片]
2020-03-22 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化api等功能。 页面的跳转存在哪些问题呢? 与接口的调用一样面临url的管理问题; 传递参数的方式不太友好,只能拼装url; 参数类型单一,只支持string。 alias 第一个问题很好解决,我们做一个集中管理,比如新建一个[代码]router/routes.js[代码]文件来实现alias: [代码]// routes.js module.exports = { // 主页 home: '/pages/index/index', // 个人中心 uc: '/pages/user_center/index', }; [代码] 然后使用的时候变成这样: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { wx.navigateTo({ url: routes.uc, }); }, }); [代码] query 第二个问题,我们先来看个例子,假如我们跳转[代码]pages/user_center/index[代码]页面的同时还要传[代码]userId[代码]过去,正常情况下是这么来操作的: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { const userId = '123456'; wx.navigateTo({ url: `${routes.uc}?userId=${userId}`, }); }, }); [代码] 这样确实不好看,我能不能把参数部分单独拿出来,不用拼接到url上呢? 可以,我们试着实现一个[代码]navigateTo[代码]函数: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, query }) { const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&'); wx.navigateTo({ url: `${url}?${queryStr}`, }); } Page({ onReady() { const userId = '123456'; navigateTo({ url: routes.uc, query: { userId, }, }); }, }); [代码] 嗯,这样貌似舒服一点。 参数保真 第三个问题的情况是,当我们传递的参数argument不是[代码]string[代码],而是[代码]number[代码]或者[代码]boolean[代码]时,也只能在下个页面得到一个[代码]string[代码]值: [代码]// pages/index/index.js Page({ onReady() { navigateTo({ url: routes.uc, query: { isActive: true, }, }); }, }); // pages/user_center/index.js Page({ onLoad(options) { console.log(options.isActive); // => "true" console.log(typeof options.isActive); // => "string" console.log(options.isActive === true); // => false }, }); [代码] 上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。 有什么办法可以让数据变成字符串之后,还能还原成原来的类型? 好熟悉,这不就是json吗?我们把要传的数据转成json字符串([代码]JSON.stringify[代码]),然后在下个页面把它转回json数据([代码]JSON.parse[代码])不就好了嘛! 我们试着修改原来的[代码]navigateTo[代码]: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, data }) { const dataStr = JSON.stringify(data); wx.navigateTo({ url: `${url}?jsonStr=${dataStr}`, }); } Page({ onReady() { navigateTo({ url: routes.uc, data: { isActive: true, }, }); }, }); [代码] 这样我们在页面中接受json字符串并转换它: [代码]// pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(options.jsonStr); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这里其实隐藏了一个问题,那就是url的转义,假如json字符串中包含了类似[代码]?[代码]、[代码]&[代码]之类的符号,可能导致我们参数解析出错,所以我们要把json字符串encode一下: [代码]function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } // pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(decodeURIComponent(options.encodedData)); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这样使用起来不方便,我们封装一下,新建文件[代码]router/index.js[代码]: [代码]const routes = require('./routes.js'); function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { routes, navigateTo, extract, }; [代码] 页面中我们这样来使用: [代码]const router = require('../../router/index.js'); // page home Page({ onLoad(options) { router.navigateTo({ url: router.routes.uc, data: { isActive: true, }, }); }, }); // page uc Page({ onLoad(options) { const json = router.extract(options); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] route name 这样貌似还不错,但是[代码]router.navigateTo[代码]不太好记,[代码]router.routes.uc[代码]有点冗长,我们考虑把[代码]navigateTo[代码]换成简单的[代码]push[代码],至于路由,我们可以使用[代码]name[代码]的方式来替换原来[代码]url[代码]参数: [代码]const routes = require('./routes.js'); function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const url = routes[name]; wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { push, extract, }; [代码] 在页面中使用: [代码]const router = require('../../router/index.js'); Page({ onLoad(options) { router.push({ name: 'uc', data: { isActive: true, }, }); }, }); [代码] navigateTo or switchTab 页面跳转除了navigateTo之外还有switchTab,我们是不是可以把这个差异抹掉?答案是肯定的,如果我们在配置routes的时候就已经指定是普通页面还是tab页面,那么程序完全可以切换到对应的跳转方式。 我们修改一下[代码]router/routes.js[代码],假设home是一个tab页面: [代码]module.exports = { // 主页 home: { type: 'tab', path: '/pages/index/index', }, uc: { path: '/pages/a/index', }, }; [代码] 然后修改[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; if (route.type === 'tab') { wx.switchTab({ url: `${route.path}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${route.path}?encodedData=${dataStr}`, }); } [代码] 搞定,这样我们一个[代码]router.push[代码]就能自动切换两种跳转方式了,而且之后一旦页面类型有变动,我们也只需要修改[代码]route[代码]的定义就可以了。 直接寻址 alias用着很不错,但是有一点挺麻烦得就是每新建一个页面都要写一个alias,即使没有别名的需要,我们是不是可以处理一下,如果在alias没命中,那就直接把name转化成url?这也是阔以的。 [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : name; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 在页面中使用: [代码]Page({ onLoad(options) { router.push({ name: 'pages/user_center/a/index', data: { isActive: true, }, }); }, }); [代码] 注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如[代码]pages/index[代码]下面会存放[代码]pages/index/index.js[代码]、[代码]pages/index/index.wxml[代码]、[代码]pages/index/index.wxss[代码]、[代码]pages/index/index.json[代码],这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如[代码]pages/index/pageB/index.js[代码]、[代码]pages/index/pageB/index.wxml[代码]、[代码]pages/index/pageB/index.wxss[代码]、[代码]pages/index/pageB/index.json[代码]。 这样是能实现功能,但是这个name怎么看都跟alias风格差太多,我们试着定义一套转化规则,让直接寻址的name与alias风格统一一些,[代码]pages[代码]和[代码]index[代码]其实我们可以省略掉,[代码]/[代码]我们可以用[代码].[代码]来替换,那么原来的name就变成了[代码]user_center.a[代码]: [代码]Page({ onLoad(options) { router.push({ name: 'user_center.a', data: { isActive: true, }, }); }, }); [代码] 我们再来改进[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : `pages/${name.replace(/\./g, '/')}/index`; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 这样一来,由于支持直接寻址,跳转home和uc还可以写成这样: [代码]router.push({ name: 'index', // => /pages/index/index }); router.push({ name: 'user_center', // => /pages/user_center/index }); [代码] 这样一来,除了一些tab页面以及特定的路由需要写alias之外,我们也不需要新增一个页面就写一条alias这么麻烦了。 其他 除了上面介绍的navigateTo和switchTab外,其实还有[代码]wx.redirectTo[代码]、[代码]wx.navigateBack[代码]以及[代码]wx.reLaunch[代码]等,我们也可以做一层封装,过程雷同,所以我们就不再一个个介绍,这里贴一下最终简化后的api以及原生api的映射关系: [代码]router.push => wx.navigateTo router.replace => wx.redirectTo router.pop => wx.navigateBack router.relaunch => wx.reLaunch [代码] 最终实现已经在发布在github上,感兴趣的朋友可以移步了解:mp-router。
2019-04-26 - 微信小程序使用自定义目录(文件路径)进行下载/保存 案例(fail permission denied 解决方案)
场景描述 最近项目中有一个需要把网络文件下载下来保存到本地,然后对下载的文件进行读取,待文件不再使用后把文件进行删除的需求。当然也类似的需求还有很多,比如把小程序中的临时图片/文件永久保存下来等等,都是对文件操作的典型场景。 常见问题 在以上场景的实现过程中可能会遇到各式各样的问题,以下是比较常见的几个: 不清楚文件应该保存到哪个目录下。 fail permission denied 文件权限问题。 使用同步函数不清楚怎么获取执行结果。 API提炼 提到文件操作我们会自然而然地想到了API中的FileSystemManager相关的API,我这里用到的函数有以下几个: 下载函数 wx.downloadFile(Object object) 异步函数: FileSystemManager.access(Object object) FileSystemManager.mkdir(Object object) 同步函数: FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 我对同样的业务逻辑分别分别尝试了异步和同步的两种不同的方案,下面我们用一个最简单的下载保存到本地的案例来切入正题。 案例实践 一:获取正确的文件目录路径 当然在保存文件之前我们先要解决一个小问题,那就是我们要保存到哪里?也就是我们自定义的目录。这里我们简单命名其为 [代码]//自定义缓存文件根路径 var rootPath = "......"; [代码] (当然你也可以命名成其他名字) 变量名字可以随便写,不过自定义路径可不能随便写,也不可以在下载的时候直接给一个path路径,否者就会抛出没有权限或找不到文件的异常。所以开发者并不是可以随意的决定自定义文件的路径,这里就不卖关子了,小程序API中有一个很容易被忽略的API, wx.env.USER_DATA_PATH是专门获取文件系统中的用户目录路径的常量值。 这就是我们在小程序中合法的可操作文件的根目录路径: [代码]rootPath = wx.env.USER_DATA_PATH; [代码] 好了到目前为止我们已经知道了我们该往里存储文件了。 定义一下我们下载文件的缓存目录 [代码]var cachePath = rootPath+"/cache"; [代码] 也就是说之后我们下载的文件都会保存到 /cache 目录下,我们在之后的代码中用到的目录路径均为此路径。 二:异步函数实现方案 我们先来用异步函数来实现一下下载保存的流程。 1 下载文件之前我们首先要判断当前目录是否存在,如果目录不存在我们就直接下载文件到该目录下就会抛出 fail permission denied [图片] 这是判断目录存在的代码 [代码] access() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.access({ path: cachePath, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 2 如果目录真实存在那我们当然可以直接使用,如果目录不存在则需要开发者自己创建目录。 [代码] mkdir(){ return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.mkdir({ dirPath: cachePath, recursive: true, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 代码执行完之后我可以验证一下目录是否如我们所愿被创建出来。 开发工具右上角的详情–>基本信息–>文件系统–>当前小程序文件系统根目录 [图片] 点击usr文件夹进入根目录 [图片] 执行完上面代码之后我们可以看一下 cache 文件夹确实已经存在了 [图片] 3 目录也存在了,万事具备只欠下载了 [代码] downloadFile() { let fileUrl = 'https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg'; wx.downloadFile({ url: fileUrl, filePath: cachePath + '/temp.png', success: function(res) { console.log('downloadFile success', res); }, fail: function(err) { console.log('downloadFile fail', err); } }); }, [代码] 那执行完下载的代码之后我们再来看看cache目录 [图片] 这就是我们刚刚下载的图片。 ok,异步的整个下载和文件创建流程就走完了。接下来我们来瞅瞅同步流程中有哪些需要我们注意的。 三: 同步方案 同步方案和异步方案的流程大体一致,都是先判断文件目录是否存在,若不存在则创建目录,存在则执行下载逻辑。 使用同步函数需要特别注意的是怎么去判断函数的执行结果,由于 FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 这两个同步函数没有直接给出任何的返回结果。那我们怎么知道目录是否存在、目录是否被创建成功了呢? 这里我们需要剑走偏锋一下,即利用同步函数抛出的异常来判断结果。 我们直接来看代码 [代码] accessSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.accessSync(cachePath); resolve(); } catch (err) { resolve(err); } }); }, mkdirSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.mkdirSync(cachePath, true); resolve(); } catch (err) { resolve(err); } }); }, [代码] 可以看到我们的代码中多了 try catch 的代码结构,因为同步方法中没有直接返回给我们可用的信息,那我们可以认为同步函数正常执行完的结果为true或success,而进入 catch 后则结果为false或fail亦或根据具体异常具体处理。我们利用同步函数来走一遍下载保存的流程。 [代码] // 同步函数流程 this.accessSync().then(function (err) { if (err) { return that.mkdirSync(); } }).then(function (err) { if (!err) { that.downloadFile() } }); [代码] 可以看到我们利用同步函数下载的图片。 [图片] 总结时刻 我们分别使用异步和同步函数完成了目录的创建和文件的下载等流程。在这个过程中我们特别需要注意几点: 操作文件的根目录是以 wx.env.USER_DATA_PATH 开头的。 使用自定义目录时一定主要不可直接使用,需要增加 判断目录存在、创建目录 两个步骤。 使用同步函数时的执行结果是通过抓取同步函数抛出的异常来进行判断的。在没有给出直接结果的时候要学会利用异常信息来达到目的。 在创建目录时如果该目录存在同样会抛出异常(fail file already exists),这时按照success逻辑继续往下执行即可。 异步方案中介绍了目录查看的步骤和方法,可自行验证。其中usr目录是开发者自定义目录根节点,tmp目录是小程序默认的缓存根节点。 结尾 叙述若有不对或不严谨之处还请不吝指正。
2019-10-31 - 微信小程序定位授权,获取经纬度并转换为实际地址
一、准备工作 参考 https://lbs.qq.com/miniProgram/jsSdk/jsSdkGuide/jsSdkOverview 1.1注册腾讯位置服务账号 腾讯位置服务为微信小程序提供了基础的标点能力、线和圆的绘制接口等地图组件和位置展示、地图选点等地图API位置服务能力支持,使得开发者可以自由地实现自己的微信小程序产品。 在此基础上,腾讯位置服务微信小程序JavaScript SDK是专为小程序开发者提供的LBS数据服务工具包,可以在小程序中调用腾讯位置服务的POI检索、关键词输入提示、地址解析、逆地址解析、行政区划和距离计算等数据服务,让您的小程序更强大! 本文要介绍的是其中的逆地址解析 https://lbs.qq.com/ 1.2.申请开发者密–钥 [图片] 1.3.开通webserviceAPI服务 [图片] 控制台 -> key管理 -> 设置(使用该功能的key)-> 勾选webserviceAPI -> 保存 (小程序SDK需要用到webserviceAPI的部分服务,所以使用该功能的KEY需要具备相应的权限) 1.4.下载微信小程序JavaScriptSDK http://3gimg.qq.com/lightmap/xcx/jssdk/qqmap-wx-jssdk1.2.zip 1.5.安全域名设置 微信公众平台登录你的小程序->开发->开发设置->服务器域名->将[代码]https://apis.map.qq.com[代码]填入request合法域名 这样在微信开发者工具就可以看到了:[图片] 腾讯位置服务是有免费额度的,每个key的每个服务接口的调用量如下: 日调用量:1万次 / Key 并发数:5次 / key / 秒 用来学习足够了。 至此,准备工作已经全部完成。 二、实践 2.1加入JavaScriptSDK 理论上可以随便放入一个文件夹。但是程序员做事应该有条理一点。 创建一个工具类文件夹 untils,将qqmap-wx-jssdk.js放入。 [代码]//在要使用服务的页面 var QQMapWX = require('../../untils/qqmap-wx-jssdk.js'); var qqmapsdk; Page({ onLoad: function () { // 实例化API核心类 qqmapsdk = new QQMapWX({ key: '你在腾讯位置服务申请的key' }); }, [代码] 2.2.获取用户定位授权 wx.authorize(Object object) https://developers.weixin.qq.com/miniprogram/dev/api/open-api/authorize/wx.authorize.html 提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。更多用法详见 用户授权。 [代码]function () { var that = this wx.authorize({ scope: 'scope.userLocation',//发起定位授权 success: function () { console.log('有定位授权') //授权成功,此处调用获取定位函数 }, fail() { //如果用户拒绝授权,则要告诉用户不授权就不能使用,引导用户前往设置页面。 console.log('没有定位授权') wx.showModal({ cancelColor: 'cancelColor', title: '没有授权无法获取位置信息', content: '是否前往设置页面手动开启', success: function (res) { if (res.confirm) { wx.openSetting({ withSubscriptions: true, }) } else { wx.showToast({ icon: 'none', title: '您取消了定位授权', }) } }, fail: function (e) { console.log(e) } }) } }) } [代码] 2.3. 获取定位信息并进行逆地址解析 如果用户同意了授权,就可以获取定位信息了,调用wx.getLocation(Object object) 调用成功就会返回位置信息: [图片] 然后调用SDK的reverseGeocoder(options:Object)进行逆地址解析:[图片] 代码如下: [代码]//此函数在用户定位授权成功后调用 function () { wx.getLocation({//获取地址 type: 'gcj02', success(res) { const latitude = res.latitude const longitude = res.longitude const speed = res.speed const accuracy = res.accuracy console.log(latitude, longitude, speed, accuracy) qqmapsdk.reverseGeocoder({//SDK调用 location: { latitude, longitude }, success: function (res) { console.log(res) } }) } }) } [代码] 完成。 水平有限,欢迎交流。 觉得有用请点个赞。
2020-11-12 - 南苑导览 · 如何做好一款地图导览类小程序?
[图片] 南苑导览是一款由学生独立开发的以地图为载体,提供中山大学南方学院(南苑)具体地点的位置信息、导航、校园历史及文化介绍的小程序。旨在解决校园导航标识不到位、地图形式低效单一、信息设计不够好等问题,为来南苑新人和游客提供更加完美的出行体验。 仅需修改地图配置文件,即可适配任意场景(校园、景区)的小程序个性化地图定制。 原生小程序 + TypeScript + gulp + vantUI + 云开发能力 GitHub:https://github.com/Observer-L/NFU-Guide-Map 2019微信小程序高校大赛 · 华南赛区二等奖
2019-09-24 - 组件封装的思考
本文是我的小程序开发日记的其中一篇, GitHub 原文地址 欢迎star,感谢万分! 前言 在小程序开发的早期,是没有 自定义组件(component),仅有 自定义模板(template) 的。最早接触到组件开发还是在使用 [代码]React[代码]、[代码]Vue[代码] 框架的时候,熟悉以上两个框架的读者,对小程序的组件应该会有熟悉的感觉,机制和写法差不多 为什么要有组件? 对于这个问题,很多人的第一反应也许是:代码复用 的确,代码复用是组件的核心职责,但它还有更大的使命:性能 因为通过组件封装,可以将页面拆分成多个组件,因此较大粒度的页面就被拆分成粒度较小的组件。当一些数据发生变更导致页面变化时,就只需要重新渲染包含该数据的组件即可,而不用渲染整个页面,从而达到了提高渲染性能的效果 [图片] 生命周期 在 [代码]Vue[代码] 中,每个页面是一个 [代码]Vue[代码] 实例,而组件又是可复用的 [代码]Vue[代码] 实例,因此可以理解成,页面和组件是相同的生命周期 而小程序就将页面和组件拆分成两个类:[代码]Page[代码] 和 [代码]Component[代码],因此接收的生命周期函数也是不一样的。比如,[代码]Page[代码] 接收的是:[代码]onLoad[代码]、[代码]onShow[代码]、[代码]onReady[代码]等函数,而 [代码]Component[代码] 则接收 [代码]created[代码]、[代码]attached[代码]、[代码]ready[代码] 等函数 命名风格都不一致,真是让人头大 [图片] 数据传递 Vue [代码]Vue[代码] 的组件间数据传递的机制是这样的:父组件通过[代码]property[代码]传递数据给子组件,而子组件通过事件通知的形式传递数据给父组件 在页面包含的组件结构还比较简单的时候,这样的机制还是比较好用的。但是,随着业务的复杂度逐渐上升,组件嵌套的层数递增,会出现数据层层传递的困境 为了解决这个问题,[代码]Vue[代码] 推出了 [代码]Vuex[代码] 这样的状态管理工具,集中式存储、管理应用的所有组件的状态。并提出了“单向数据流”的理念: [图片] 小程序 小程序同样有类似的机制,[代码]property[代码]和事件。此外还提供了获取 子组件实例 的方法:[代码]selectComponent()[代码] 和 定义组件间关系的字段 [代码]relations[代码] 其中常用的就是获取子组件实例,比如: [代码]<parent-component> <child-component id="child"></child-component> </parent-component> [代码] 此时,在[代码]parent-component[代码]组件中可以直接获取[代码]child-component[代码]的实例: [代码]Component({ attached() { let $child = this.selectComponent('#child') // $child.doSomeThing() } }) [代码] 实战 背景 制作一个 对话框(modal) 组件 也许有的读者会感到困惑,官方不是有提供 [代码]wx.showModal[代码] 可以直接用吗,为什么要重复造轮子 其实,当你的产品想要结合 [代码]Modal[代码] 和 [代码]Button[代码] 的 [代码]open-type[代码] 能力时,你就会明白重复造轮子的必要性以及[代码]wx.showModal[代码]的局限性 属性定义 对话框的常见属性可以参考[代码]wx.showModal[代码] 除此以外,其中关键的一个属性就是 表示对话框当前的显示状态:[代码]visible[代码] 此时,有两种选择,第一种是将这个变量存在页面上,通过[代码]property[代码]传递给[代码]Modal[代码]组件;另外一种,就是作为[代码]Modal[代码]组件[代码]data[代码]中的一员 property传递 通过[代码]property[代码]传递的话,就相当于将 [代码]Modal[代码] 的控制权交到对应的页面,举例: [代码]<!-- home.wxml --> <modal visible="{{visible}}" /> [代码] [代码]// home.js Page({ data: { visible: false }, toggleModal() { this.setData({ visible: !this.data.visible }) } }) [代码] 此时对应的 [代码]Modal[代码]: [代码]// modal.js Component({ properties: { visible: { type: Boolean, value: false, observer(newVal, oldVal) { this.setData({ visible: newVal }) } } } }) [代码] 这里和[代码]Vue[代码]框架有个差异,[代码]Vue[代码]对于传进来的property会自动赋值,而小程序则需要自己手动赋值 问题与办法 当 [代码]visible[代码] 这个变量被 [代码]Modal[代码] 和 [代码]Page[代码] 同时使用时,会出现不显示的问题。 为了便于描述,我通过描述真实场景来讲解: 当页面需要显示对话框时,[代码]Page[代码] 传递 [代码]visible=true[代码] 给 [代码]Modal[代码] 经过一段时间之后,用户关闭了对话框,此时 [代码]Modal[代码] 将自身的 [代码]visible[代码] 设置为 [代码]false[代码] 当页面需要再次出现对话框时,[代码]Page[代码] 继续传递[代码]visible=true[代码] 给 [代码]Modal[代码],此时发现对话框不会显示 通过分析可以发现,由于 [代码]Page[代码] 两次传递相同的 [代码]visible=true[代码] 给 [代码]Modal[代码] ,因此第二次传递的时候,被 [代码]Modal[代码] 直接忽略掉了。 这个问题也很好解决,大致思路就是保证每次传递的值不同即可: 传递的值前面加上时间戳,组件再将时间戳移除(比较直观,但是不方便) 利用对象不相等的机制,数据传递只传对象,不传基础数据类型(比如[代码]{ visible: true } !== { visible: true }[代码]) 组件自身属性 这种是我推荐的方案。将 [代码]visible[代码] 属性交由组件 [代码]Modal[代码] 自行管理: [代码]// modal.js Component({ data: { visible: false }, methods: { show() { this.setData({ visible: true }) } } }) [代码] 由于父组件或者当前页面可以直接获取组件的实例,因此可以直接调用组件的[代码]setData[代码],如: [代码]let $modal = this.selectComponent('#modal') $modal.setData({ visible: true }) [代码] 但是不建议这样使用,而是组件暴露方法让外部调用: [代码]let $modal = this.selectComponent('#modal') $modal.show() [代码] 组件的事件 通常,对话框都会有按钮,一个或两个。 因此 [代码]Modal[代码] 需要与父组件通过 事件(event) 的方式传递信息:当前点击了取消还是确定按钮: [代码]<!-- home.wxml --> <modal id="modal" bind:btntap="handleModalTap" /> [代码] [代码]// home.js Page({ showModal() { let $modal = this.selectComponent('#modal') $modal.show() }, // 其他方法 handleModalTap(e) { let { type } = e.detail // type = cancel or confirm } }) [代码] 在 [代码]Modal[代码] 的构造函数则是这样的: [代码]// modal.js Component({ data: { visible: false } methods: { handleBtnTap(e) { let { type } = e.target.dataset this.triggerEvent('btntap', { type }) } } }) [代码] [代码]<!-- modal.wxml --> <view class="wrapper"> <!-- 省略其他结构 --> <view class="foot" bindtap="handleBtnTap"> <button data-type="cancel">取消</button> <button data-type="confirm">确定</button> </view> </view> [代码] 这样设计 [代码]Modal[代码] 组件,的确可以满足使用,但是不够好用 因为展示对话框时使用的是 [代码]showModal[代码] 而用户操作之后又是通过另外一个方法 [代码]handleModalTap[代码] 反馈的。当一段时间之后回看这样的代码,会发现这种写法存在思维的中断,不利于代码维护 所以,我建议结合 [代码]Promise[代码] 来封装 [代码]Modal[代码] 省略事件 由于展示对话框之后,用户必然要操作,因此可以在 [代码]showModal[代码] 的时候,通过 [代码]Promise[代码] 返回对应的操作信息即可 另外,需要引入发布订阅机制(以下使用 [代码]Node.js[代码] 的 [代码]Events[代码] 举例): [代码]// modal.js const EventEmitter = require('events'); const ee = new EventEmitter(); Component({ data: { visible: false }, methods: { show() { this.setData({ visible: true }) return new Promise((resolve, reject) => { ee.on('cancel', () => { reject() }) ee.on('confirm', () => { resolve() }) }) }, handleBtnTap(e) { let { type } = e.target.dataset ee.emit(type) this.triggerEvent('btntap', { type }) } } }) [代码] 此时,在 [代码]Page[代码] 即可这样展示对话框: [代码]// home.js Page({ onLoad() { let $modal = this.selectComponent('#modal') $moda.show().then(() => { // 当点击确认时 }).catch(() => { // 当点击取消时 }) } }) [代码] 总结 组件是很好用的机制,也是最常用到的能力 因此日常开发中,应该会遇到各种各样组件封装的问题,平时遇到应该多思考总结一下,对团队和自己都很有帮助!
2020-04-08 - 关于计时器,读秒,倒计时相关功能说明(wxTimer)
我推荐一个倒计时插件wxTimer,这个倒计时插件运用原生写法写了一个原型倒计时js,可拓展性十分的强大,但是网上资料甚少导致一些初学者看不懂文档,先放文档链接github: https://github.com/baqihg/wxTimer 文档里的内容还是要掰扯一下的,大家可能好奇,为什么我用了这一段倒计时不动了 var wxTimer = new timer({ beginTime:"00:00:10" }) wxTimer.start(this); wxTimer.stop(); wxTimer.stop();这个是停止的语法,在他的js文件原型里可以清楚的看到是包裹在外面的所以写在这里就是启动了一次又把他停止了,只需要去掉这个语法就可以读秒了。 还有就是写了一个button 绑定了bindtap事件去控制wxTimer.stop();可以发现并不生效,原因查看原型可知是wxTimer.stop();读不到停止的那个原型对象,如果你设定了倒计时为wxTimer,那你必须要要在page({})的外面事先声明这个量比如写var wxTimer 这样你就可以通过if(wxTimer ){wxTimer.stop();}找到这个计时器去停止这个倒计时了 这个倒计时是很好用的,页面隐藏后倒计时还是会进行,对读秒跳转和计时的功能有一定的帮助,可以添加在小程序的各个生命周期里,不过要事先做好销毁停止的处理
2020-06-16 - 【好文】如何管理页面的多弹窗实现
如何管理页面的多弹窗实现 最近在做一个小程序老项目的关于页面活动弹窗的修改,很多人一听到“老”字,汗毛竖起;当然我也不例外,我大概总结了一下,存在以下问题: [代码]js[代码] 文件判断弹窗组件的逻辑代码嵌套多层 if else 判断,难以抽离; [代码]wxss[代码] 文件关于处理弹窗组件的样式也是嵌套了多层 wx:if 判断,代码十分难看,字段判断嵌套过深,组件元素处理过于分散; 编写弹窗初期没有做好前期规划,多种弹窗样式耦合其中,文件代码冗余过多,实在难以快速定位到合适的样式进行修改; 历史剖析 重构老代码最怕就是缺少个别的逻辑处理,导致在某一个条件下无法触发窗口弹出,出现bug; 重构毕竟是一个吃力不讨好的活儿,毕竟功能在线上运行了这么久没出问题,万一自己踩到雷区,接盘侠妥妥的了,背锅也是妥妥的了,还会惹来一身麻烦,更别提绩效了,这也是很多开发不愿意触碰老代码的最主要原因; 由于业务关系,本文不会用公司项目的代码作为例子,我会重新写一个例子以供大家参考,这是抽丝拔茧后的解决方案,希望可以帮助大家。 调研 我大概分析了一下,弹窗大致分为以下几种样式: 通知弹窗 (纯文本) 广告弹窗(大图) 活动弹窗(即:自定义弹窗,样式不定,有可能是领优惠券,有可能是特定节日活动) 小记:通知类型弹窗和广告大图弹窗是固定样式,可以作为一个组件单独实现;活动弹窗具有时效性,根据自己需要自行扩展实现。 文字通知弹窗 + 广告大图弹窗实现 场景描述: 很多时候运营需要在同个页面顺序弹出多个窗口(可以参考拼夕夕,一打开首页疯狂弹窗,让人抓狂),于是,我这里用到了上一篇文章提到的 发布订阅模式 实现,有需要的同学可以先去简单阅读一下。 编写通用组件实现 编写静态文件 [代码]// modal.wxml <view class="modal" wx:if="{{ show }}"> <view class="modalContent"> <!-- 文本弹窗 --> <view class="textModal" wx:if="{{ type == 'text' }}">{{ value }}</view> <!-- 广告大图弹窗 --> <view class="picModal" wx:elif="{{ type == 'pic' }}"> <image class="adImg" mode="widthFix" src="{{ value }}"></image> </view> <view class="customModal"> <slot name="customModal"></slot> </view> <view class="flex flex-center" bindtap="hide"> <image class="close-icon" src="/asserts/icon_bt_close_white.png" /> </view> </view> </view> [代码] 处理组件逻辑代码 [代码]// modal.js const Event = require('../../designPatterns/observer'); Component({ options: { multipleSlots: true, }, properties: { }, data: { value: '', type: 'pic', show: false, }, attached() { // 建立监听 Event.listen('modal', (props) => { this.setData({ ...props, show: true, }) }) }, methods: { hide() { this.setData({ show: false }) // 关闭弹窗通知回调页面,用于处理下一个弹窗继续弹出 this.triggerEvent('hide') }, show() { this.setData({ show: true }) } } }) [代码] 页面调用弹窗组件实现 页面引用组件 [代码]// modalPage.json { "usingComponents": { "modal": "/components/modal/modal" } } [代码] [代码]// modalPage.wxml <view> <modal bind:hide="triggerModal"/> </view> [代码] 页面触发弹窗调用 [代码]// modalPage.js const Event = require('../../designPatterns/observer'); Page({ // 弹窗mock数据 modalData: [ { id: 1, level: 10, // 弹出的顺序,数字越大代表越先弹出 type: 'text', value: '我是文本弹窗1', }, { id: 2, level: 11, type: 'pic', value: '/asserts/OIP.jpeg' }, { id: 3, level: 9, type: 'text', value: '我是文本弹窗2', } ], onLoad() { this.modalData = this.modalData.sort((prev, next) => -(prev.level - next.level)) this.triggerModal() }, triggerModal() { const targetModalList = this.modalData.splice(0, 1) if (targetModalList && targetModalList.length > 0) { // 继续触发弹窗 Event.trigger('modal', targetModalList[0]) } } }) [代码] 小记: 本着一次编码,幸福后代的原则,要是简单地把弹窗的管理交由各自页面单独处理,其实也是不美观的; 在我看来,弹窗们应该是有一个管理员的角色,用来管理它们,由管理员去负责它们(排序,触发等动作),接下来我们在自定义弹窗组件编码中来看看应该如何实现 自定义活动弹窗 多余代码不再复制累赘了,代码可以参考 [代码]/components/modal/activityModal[代码] 的相关文件 弹窗管理类:ModalManage [代码]// ModalManage.js const Event = require('../designPatterns/observer'); class ModalManage { // 弹窗类型,用于触发特定弹窗 modalType = { text: 'modal', pic: 'modal', activity: 'activityModal' } constructor(modalList) { // 初始化的时候需要排序,决定弹出优先级 this.modalList = modalList.sort((prev, next) => -(prev.level - next.level)) } triggerModal() { const targetModalList = this.modalList.splice(0, 1) if (targetModalList.length > 0) { const currentModal = targetModalList[0] // 发送弹窗通知 Event.trigger(this.modalType[currentModal.type], currentModal) } } } module.exports = ModalManage [代码] 页面触发弹窗调用 [代码]const Event = require('../../designPatterns/observer'); const ModalManage = require('../../model/ModalManage'); Page({ modalData: [ { id: 1, level: 10, type: 'text', value: '我是文本弹窗1', }, { id: 2, level: 11, type: 'pic', value: '/asserts/OIP.jpeg' }, { id: 3, level: 9, type: 'text', value: '我是文本弹窗2', }, { id: 4, level: 10, type: 'activity', value: '我是活动弹窗啦啦啦啦' } ], onLoad() { this.modalManage = new ModalManage(this.modalData) this.modalManage.triggerModal() }, triggerModal() { this.modalManage.triggerModal() } }) [代码] 小记:相比于第一次的处理方式,这次更加的浅显易懂,简单明了。 示例展示 [图片] 项目地址 项目地址:https://github.com/csonchen/mina-app 我想记录一些关于小程序日常开发所遇到的问题,进而引起的一些思考,能否给大家提供多一些角度去思考问题,解决问题,能帮助大家就好。希望大家多多支持,多多star哈
2020-05-29 - 页面局部滑动的方法推荐 详解【scroll-view】【swiper】【overflow:scroll属性】区别
前言: 初期开发小程序的时候 我们可能遇到一个问题 一个scroll-view组件内的滑动与页面的滑动冲突 造成 scroll-view动画到边界后 页面才跟着滑动另外 有时可能遇到这样的需求 一个页面中有多处局部滑动 且 他们之间不能互相干扰 互相影响 而且大多数情况下 页面整体不能滑动 这时候我们通过文档 获取到的第一印象是使用scroll-view组件 或 使用swiper组件满足需求 而在实际的工程搭建上又会因为经验不足遇到许多坑 下面我们使用不同方法分别创建一个竖向的局部滑动区域 横向的参考竖向即可 多个滑动区域的使用三种方法创建对应组件 三种方法区别较大 需要根据实际场景选择 第一种方式 使用scroll-view方式创建一个滑动区域 我们创建一个长度为10的数组 在scroll-view内部 wxml: [图片] wxss: [图片] js: [图片] 注意 1.Page元素是该页面整体 类似于H5中的body 给其设置height:100%;变相屏蔽了页面的滑动事件 2.这里scroll-view组件除了设置scroll-y属性外 需要给list一个固定高度 才能生效 第二种方式 使用css属性 overflow-y:scroll 这里wxml和wxss稍作修改 其他的和上面相同 [图片] [图片] 其实使用css属性只是模拟了scroll-view组件的效果 但毕竟scroll-view组件提供了许多其他的属性和方法可供开发者操作 例如监控组件的滑动等 如果使用css的话 只有局部区域的滑动效果而已 第三种方式 使用swiper组件实现竖向滑动 wxml [图片] js [图片] wxss: [图片] 这里还是比较复杂的,其中swiper组件需要设置vertical属性为true 另外margin-next指 swiper-item与后面子元素的距离 介于我们要设置swiper内尽可能显示全部的子元素 margin-next应设置为swiper高度 减 swiper-item的高度 计算方法如下 使用 wx.createSelectorQuery() 方法获取swiper和swiper-item元素的属性 [图片] 预览图: [图片] 注意: 1.swiper的margin-next属性不支持 calc()动态计算高 因此只能使用在页面渲染完成后 获取滑动区域总体的高 - 元素内容的高 setdata 来渲染 2.swiper无论如何滑动 都会最终停在某个元素的顶端/左端 而上面两种方式可以scrollTop值可以停留在任何地方 3.swiper滑动到最后几个元素的时候 下方/右方还会留下空白位置 而上面两种方式scrollTop滑动到最后几个元素 滑动条不再滑动 下面/右方没有空白位置 4.swiper组件有 bindanimationfinish 这个方法监控 滑动动画的结束事件 而scroll-view只能通过 bindtouchend 拿到滑动手势的结束事件 swiper的这种特性可以满足特殊场景下的需求 比如无论如何滑动区域后 自动对焦 5.经过测试 swiper组件 在渲染子元素变多的情况下 性能会远小于 scroll-view 以5000个子元素为例 swiper 在安卓/IOS手机上的渲染速度已经严重影响用户体验 而scroll-view组件渲染可以在2秒内完成 并且实现滑动 因此在渲染数量在极限状态下 推荐使用scroll-view 6.如果想使用 scroll-view 模拟swiper的效果 可以使用 遮罩浮层盖住scroll-view主体 + 监听scroll-into-view事件 的方法达到效果 不过如果快速滑动 会造成高频使用 setdata 触发 scroll-into-view 事件造成页面撕裂 不同高度设备的适配: 但进一步想 现在设备五花八门 不同设备的宽高是不一样的 比如新出的安卓手机和iphone x/10/12屏幕都比较长,需要我们对滑动区域父元素进行高的计算 需要 height:calc()/width:calc() 和 createSelectorQuery 动态计算,一个页面顶部有一处固定广告 下面的区域是滑动区域 那么滑动区域的高为 height:calc(100% - 300rpx) 相关代码如下 另附一个可以多处区域滑动的页面 https://developers.weixin.qq.com/s/jgK6DNmR77hJ 作者:陶路 其他相关阅读: 1.swiper组件 https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html 2..scroll-view组件 https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view.html 3.动态获取页面元素方法 wx.createSelectorQuery https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html
2020-06-02 - 二维码base64转图片
var base64 = res.data.replace(/[\r\n]/g, "") //res.data是后端返给你的base64,有时候会存在换行符, // 小程序端解析不了,你需要自己做处理 var array = wx.base64ToArrayBuffer(base64) const req = wx.getFileSystemManager(); const FILE_BASE_NAME = 'mine_base64'; const filePath = wx.env.USER_DATA_PATH + '/' + FILE_BASE_NAME + '.png'; req.writeFile({ filePath, data: array, encoding: 'binary', success() { console.log(filePath) that.setData({ errormsg: '', code: filePath //图片地址 }) }, fail() { }, });
2020-05-21 - WeUI官方组件库:助力小程序高效设计与开发
提起 WeUI,相信大家都不陌生,WeUI 是一套同微信原生视觉体验一致的基础样式库,由微信官方设计团队为微信内网页和微信小程序量身设计,令用户的使用感知更加统一。 不过,对于 WeUI 样式库,开发者就有疑问了。 [图片] [图片] 我们来看看 WeUI 组件库到底有什么可用的 UI 组件呢?WeUI 样式库有的各个元素,WeUI 组件库是基于 WeUI 样式库做了组件化处理,开发者可以便捷的使用,无需考虑组件层面的逻辑问题。 [图片] 有了心动的组件之后,大家肯定想知道 WeUI 组件库是怎么使用的。 [图片] 要使用 WeUI,首先要把 WeUI 引入我们的小程序项目,引入 WeUI 的方式有以下两种,使用其中一种即可~ 方法一:通过 useExtendedLib 扩展库 的方式引入,这种方式引入的组件将不会计入代码包大小。(推荐👍) 方法二:可以通过 npm方式下载构建,npm 包名为 weui-miniprogram。 与方法一不同,npm 引入的方式需要多操作一步,在 app.wxss 中引用 weui.wxss。 // app.wxss @import '/miniprogram_npm/weui-miniprogram/weui-wxss/dist/style/weui.wxss'; [图片] 引入之后,我们就要开始来使用了,WeUI 组件库是基于小程序自定义组件构建的,所以使用是以自定义组件的形式来使用。 下面通过几个例子来感受下 WeUI 组件库的使用。 由于是自定义组件的形式,所以使用组件都需要在页面配置中引入,像这样: // page.json { "usingComponents": { "mp-half-screen-dialog": "weui-miniprogram/half-screen-dialog/half-screen-dialog", "mp-searchbar": "weui-miniprogram/searchbar/searchbar" } } 引入组件之后,就可以直接在 wxml 中使用了,当然,为了让开发者接入更加简便,我们也加入了做了一些常见的实用性功能。 半屏弹窗 小程序提供了 wx.showModal、wx.showToast 供开发者进行页面交互,在开发过程中,可能需要自定义按钮相关的内容,所以 WeUI 提供了半屏弹窗让开发者可以有更多的自定义空间。 我们来看下代码,使用很简单,直接使用 mp-half-screen-dialog,配置相关属性即可。 // page.wxml <mp-half-screen-dialog bindbuttontap="buttontap" show="{{show}}" mask-closable="{{false}}" title="测试标题B" sub-title="测试标题B的副标题" desc="辅助描述内容,可根据实际需要安排" tips="辅助提示内容,可根据实际需要安排" buttons="{{buttons}}"> </mp-half-screen-dialog> // page.js data配置 buttons: [ { type: 'default', className: '', text: '辅助操作', value: 0 }, { type: 'primary', className: '', text: '主操作', value: 1 } ] 来看下半屏弹窗的效果~ u1s1,这交互体验真的爱了😍 [图片] Form 表单校验 Form 表单这里,除了基础的的功能之外,WeUI 组件库还提供了表单校验的能力,通过 rules 规则的配置(支持长度、手机号码、电子邮件、url 链接地址等),轻松解决表单校验问题。 [图片] 左滑删除 相比 Web 端,手机端的操作按钮更多的是通过⬅️左滑等来实现,考虑到左滑删除的普遍性,WeUI 组件库也是支持的。 在 mp-slideview 组件中设置 buttons属性即可。 [图片] 搜索组件 同样是基本功能的搜索,WeUI 组件库也封装了搜索组件,开发者只需配置搜索结果即可拥有搜索功能~ [图片] 除了这些组件之外,WeUI 组件库还提供了很多实用的组件,包括基础的 icon、loading,表单的 uploader、cell 等等。 [图片] 伴随客户端、小程序对 DarkMode 的支持,WeUI 组件库也同步适配 DarkMode 的模式,让 WeUI 组件库的使用同学可以快速适配 DarkMode。 在根结点(或组件的外层结点)增加属性 data-weui-theme="dark" ,即可把 WeUI 组件切换到 DarkMode 的表现,如: <view data-weui-theme="dark"> ... </view> [图片] 最后,如果想体验 WeUI 组件库的效果,欢迎点击下方小程序示例体验👏及接入使用,使用过程中如有建议或者疑问,欢迎到微信开放社区与我们交流。 [图片]
2020-05-21 - (一) js高级程序设计 中转笔记
0. 变量声明,let , var , const 的区别,隐式创建全局变量 var var 有变量提升(先使用,后声明)可以跨块访问,不能跨函数访问, let let 声明的变量,作用域是块级作用域,不能重复声明变量 const (constant 常数 固定的 不变的)必须有初始化值(const a = 0;);值不可以修改(变量的指针不可以修改) 隐式创建全局变量 a = 0;这样全局都可以访问 a = 0; 创建全局变量 window.a = 0 1. typeof 返回哪些数据类型 [代码]boolean string number object function console.log(typeof true) //boolean console.log(typeof "a") //string console.log(typeof 100) //number console.log(typeof null) //object console.log(typeof undefined) //undefined console.log(typeof []) //object console.log(typeof function(){}}) //function console.log(typeof {}) //object [代码] 2.列举出部分强制类型转换和隐式类型转换 强制类型转换 [代码]parseInt() parseFloat() Number() [代码] 隐式类型准换 [代码]if() // 自动调用Boolean() == ! // 自动调用Boolean() 然后取非 [代码] Boolean() 转为boolean类型: 转为string类型 参数直接 + " " 也可以转为字符串 [代码]number boolean object 都有toString()方法 true 转为"true" false转为"false" null undefined 没有 toString()方法 在不知道参数有没有toString()方法时 用String()转为字符串 String({}) // "[object Object]" String(function(){a:0;function a(){console.log(a)}}) // "function(){a:0;function a(){console.log(a)}}" 用String()时,该参数有toString()就返回toString()的值,否则 null返回"null" ,undefined返回"undefined" String([222,333,4444,555,"222222222"]) // "222,333,4444,555,222222222" [代码] 转为number [代码]Number() parseInt() parseFloat() parseInt() parseFloat() 专门用于把字符串转为数值 parseInt("www") //NaN parseInt({}) //Nan parseInt("1.11") //1 parseFloat("1.11") //1.11 Number()可以将任何类型转为数值,规则如下: boolean值 true为1 false为0 null为0 对象/空对象 Number({}) -> NaN undefined为NaN 如果是字符串: 纯数字字符串,转为对应的整数或者浮点数 空字符串 "" 转为0 其他字符串 如 "hello world" 转为NaN 如果是对象 先调用对象的valueOf,再按前面的规则转换 [代码] == 转换规则: 如果其中一个是布尔值,则先将布尔值转为数值 false -> 0 true -> 1 如果一个是字符串,一个是数值,则将字符串转为数值 再 比较相等 如果一个是对象,先调用valueOf得到这个对象的基本类型值,再按前面的规则比较 另外: null 和 undefined 是相等的 比较null 和undefined前,不能将null和undefined转为其他值 ( 因为null和ndefined原型链没有valueOf()函数 ) 只要有一个是NaN,就返回false ,NaN不等于NaN 两个对象判断相等,只要看这两个对象是不是指向同一个对象,如果不是同一个对象,即使两个对象的属性与值完全一样,也不相等 3. split() 与join()的区别 split()将字符串分割为数组 join()将数组转为字符串 4.pop push unshift shift的区别 pop() 尾部移除 push() 尾部添加 shift() 头部移除 unshift() 头部添加 数组相关 判断数组 [代码]Object.ptototype.toString.call([]) // '[Object Array]' Array.isArray([]) //true [] instanceOf Array // true [代码] 数组增删改查 栈方法 后进先出 LIFO(last in first out) pop() ; push() 队列方法 先进先出 FIFO(first in first out) shift() unshift() 数组转换 由于每个对象都有toString() toLocaleString() valueOf() ,所以数组也有 [代码][1,2,3].toString() // "1,2,3" [1,2,3].valueOf() // [1,2,3] [代码] split() join() 重排列方法 [代码]reverse() // 改变原数组顺序,返回改变后的数组 sort() // 接受一个比较函数 返回赋值表示第一个参数在第二个参数之前 。。。 会改变原数组,返回值是改变后的数组 [代码] 操作方法 [代码]concat() // 基于当前数组创建一个副本,然后将接收到的参数放到这个副本的末尾,然后返回新的数组 slice(start , end) // 两个参数:返回开始-结束(不包括结束位置的)的数组 一个参数 : 从该参数到最后 不会对原参数造成影响 // 如果有负数,则用数组长度加上该数确定位置 splice() // splice 会对原来数组造成影响,返回剪切的值 // 接受三个参数或更多 起始位置 剪切数量 替换的值 // 删除: 两个参数 // 替换: 起始位置 替换数量 替换的值 // 插入: 起始位置 0 插入的值 let arr = [1,2,3,4,5,6] let arr2 = arr.splice(0,2 , '22' , '22') console.log(arr) // ['22' , '22' , 3,4,5,6] console.log(arr2) // [1,2] [代码] 位置方法 [代码]indexOf() // 接受两个参数 要查找的项和起点位置索引值 查找时相当于 === // 没查到返回-1 找到返回索引值 lastIndexOf() // 从后往前查 [代码] 迭代方法 [代码]every() // 对数组中每一项运行给定函数 如果每一项返回true,则返回true filter() // 对数组中每一项运行给定函数,返回每一项返回true组成的数组 forEach() //对数组中每一项运行给定函数,没有返回值 map() // 对数组中每一项运行给定数组,返回每一项结果组成的数组 some() // 对数组中每一项运行给定数组,只要有一项返回true,就返回true reduce((pre,curr,index,arr) => {}) // 迭代所有数组,返回固定的值。第一次迭代时,pre为arr[0] curr为arr[1],第二次时pre为第一次函数返回值,curr为arr[2] reduceRight() // 与reduce一样,迭代方向从后往前 [代码] 5.事件绑定和普通事件有什么区别 8.call , apply ,bind的区别 call apply bind都有改变this指向的作用 ,并且第一个参数都是目标this对象。 call apply用法一样,只是传参有区别 call第一个参数之后的参数是一个一个传,apply第一个参数是传一个数组 bind传参与call一样,但返回一个函数 [代码]let Animal = { name: '动物', say(dad , mom){ console.log("my name is " + this.name) console.log("my dad " + dad) console.log("my mom " + mom) } } let Dog = { name: 'Dog' } Animal.say.call(Dog , 'dadName' , 'momName') Animal.say.apply(Dog , ['dadName' , 'momName']) let d = Animal.say.bind(Dog , "dadName" , "momName") d(); [代码] 实际应用 Object.prototype.toString.call() 9.b继承a的方法 11.添加 删除 替换 插入到某个结点的方法 12.js的本地对象,内置对象和宿主对象 14 == 和 === 的区别 == 会自动转换类型,再进行比较 === 既比较类型,也比较值 15. js的同源策略 17.判断数据类型的几种方式 , js怎么判断数组 typeof 可以判断基本数据类型 判断函数 判断undefined 无法判断数组 null 对象 ,无法判断自定义的引用变量 instanceof 可以判断引用变量,判断函数,判断数组,判断自定义的引用变量 Object.prototype.toString.call() 可以判断基本数据类型和引用数据类型,但无法判断自定义数据类型 判断数组 [] instanceof Array Array.isArray([]) 对象 1. 理解对象 [代码]let person = new Object(); person.name = 'wu'; // 或者 let person2 = { name: 'wu' } [代码] 1.1 属性类型 数据属性 [代码]let person = { name: 'wujie' } Object.defineProperty(person ,'name' ,{ configurable: true , // 默认为true,表示能否删除修改属性的 特性 ,或者把属性改为访问器属性 enumerable: true , // 默认为true,表示能否通过for-in循环访问属性 writable: false, // 表示能否修改属性的值 value: 'wujie' // 包含属性的值,读取属性的时候,从这里读,改属性的时候,在这里改。 默认为undefined }) [代码] 访问器属性 [代码]// 访问器属性 // 访问器属性不包含具体值,它包含set和get函数(这两个函数不是必需的) let man = { _name: '' } Object.defineProperty(man , 'name' , { // 注意name 与 _name // 这里是重写name的get set , // 如果重写 _name 的话,获取_name的值时 相当于自己调自己,会栈溢出 // 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性, // 或能否把属性修改为数据属性,对于直接在对象上定义的属性,默认为true configurable: true, enumerable: true , // 表示能否通过for-in循环属性,对于直接在对象上定义的属性,默认为true get: function(){ return this._name; }, set: function(newValue){ this._name = newValue } }) [代码] 1.2 定义多个属性 [代码]// 定义多个属性 let woman = { _age: 0 } Object.defineProperties(woman , { name: { value: 'woman' }, age: { get: function(){ return this._age }, set: function(newValue){ console.log(newValue) this._age = newValue, this.year = newValue + 2020 } }, year: { value: 0 } }) woman.age = 20; [代码] 1.3读取属性特性 [代码] //读取属性特性 let dog = { name: 'dog' } let d = Object.getOwnPropertyDescriptor(dog , 'name') console.log(d.value) console.log(d.configurable) console.log(d.enumerable) console.log(d.writable) [代码] 双向绑定的实现 [代码]<input type="text" oninput="changeData(this.value)"> // 双向绑定的实现 let textHTML = document.getElementById("text") let data = { _name: '' } function changeData(e){ data.name = e; console.log(data.name) } Object.defineProperty(data , 'name' , { set: function(newValue){ this._name = newValue textHTML.innerHTML = name }, get: function(){ return this._name; } }) [代码] 2.创建对象 2.1 工厂模式 [代码]// 用一个函数构建包含所有参数的对象 ,用函数来封装以特定接口创建对象的细节 // 缺点是无法解决对象识别的问题 function createPerson(name , age ){ let o = new Object(); o.name = name; o.age = age; o.sayName = function(){ console.log(this.name) } return o; } let person6 = createPerson('wu' , 10) person6.sayName() [代码] 2.2 构造函数模式 [代码]// 2.2 构造函数模式 function Student(name , age ){ this.name = name; this.age = age; this.sayName = function(){ console.log(this.name) } } let stu = new Student('wu' , 10); // 要创建Student实例,必须使用new操作符 ,以这种方式调用构造函数会经历一下四个步骤 // 1. 创建一个新对象 // 2. 将构造函数的作用域赋给新对象(this指向新对象) // 3. 执行构造函数中的代码 // 4. 返回新对象 stu.sayName() console.log(stu.constructor) // stu实例constructor指向构造函数Student,但判断实例类型最好使用instanceOf // 1. 将构造函数当作普通函数 // 构造函数与普通函数的区别只在于调用方式不同。用new调用就是构造函数 Student('ww' , 90) // 在全局作用域中调用函数时,函数里的this指向window window.sayName() console.log(window.age) // 2. 构造函数的缺点 // 每个方法都要在每个实例上重新创一遍 例如: 每个student实例中的sayName()都不是同一个Funtion的实例 // 如果把方法单独写在外面,那么这个自定义的引用类型就毫无封装的意义了 [代码] 2.3 原型模式 [代码]// 1 .原型模式 // 只要创建了新函数,就会根据一定规则创建一个prototype,指向这个函数的原型对象 // 默认情况下,所有原型对象都会获得一个constructor属性,这个属性包含指向prototype所在函数的指针 // 代码读取某个对象的属性时,会现在实例中找,实例中没有才回到原型中找 // 可以通过实例访问保存在原型中的值,但不能通过实例重写原型中的值 function Animal(){} Animal.prototype.name = 'animal'; let dog = new Animal() console.log(Object.getPrototypeOf(dog)) dog.name = '333'; console.log(dog.name) // 修改后的值 delete dog.name; console.log(dog.name) // delete操作符 删除实例中的属性 这时name是原型中的值 dog.name = null; console.log(dog.name) // 即使null也不会修改到原型中的值 let dog2 = new Animal() console.log(dog2.name) // 原型的值 // 检测属性是在原型中还是实例中 属性在实例中返回true console.log(dog.hasOwnProperty('name')) // true console.log(dog.hasOwnProperty('age')) // false // 2. in操作符 只要原型或者实例中存在某个属性,就返回true dog2.age = 2; console.log('age' in dog2); // 所以可以结合hasOwnProperty和 in判断属性到底是存在原型中还是实例中 //3. 简单的原型写法 这样本质上是完全重写了Person的原型对象,因此prototype.constructor也只向的是Object function Person(){} Person.prototype = { name: 'wu', sayName: function(){ console.log(this.name) } } let per = new Person() console.log(Person.prototype.constructor) // Object // 但是用instanceOf测试 对Object和Person仍返回true console.log(per instanceof Person) // true console.log(per instanceof Object) // true // 如果constructor的指向真的很重要,可以将他设为特定的值 Person.prototype = { constructor: Person, name: 'wu', sayName: function(){ console.log(this.name) } } console.log(Person.prototype.constructor) // Person //4. 原型的动态性 // 可以先创建实例,再改变原型,这时仍可以获取原型的属性 function Person(){} let friend = new Person(); //先创建的实例,这时候无论原型还是实例都没有任何方法 Person.prototype.name = 'wujie'; Person.prototype.sayName = function(){ console.log(this.name) } friend.sayName() //仍然可以调用 // 但不能直接重写原型对象 , 因为实例friend2保存的只是一个指向原型的指针,重写原型时,相当于切断了实例friend2与原型的联系 // 如 let friend2 = new Person() Person.prototype = { age: 10, sayAge: function(){ console.log(this.age) } } friend2.sayAge() //error friend2.sayAge is not a function // 5. 原生对象的原型 // 6. 原型对象的问题 // 省略了为构造函数传递初始化参数这一环节,所有实例在默认情况下都取得相同的属性 // 对与引用类型来说,共享的属性就容易造成问题 function Person(){} Person.prototype.friends = ['jobs' , 'gates']; let person1 = new Person(); console.log(person1.friends) let person2 = new Person(); person1.friends.push('july'); // 由于friends存在于prototype而非person1中,所以对friends的修改通过person2也可以看出来 console.log(person1.friends) console.log(person2.friends) [代码] 2.4 组合使用原型模式和构造函数模式 [代码]// 2.4 组合使用原型模式和构造函数模式 // 实例的属性用构造函数模式定义,方法或需要共享的属性用原型模式定义 function Person (name , age , friends){ this.name = name; this.age = age; this.friends = friends; } Person.prototype.sayName = function(){ console.log(this.name) } let person1 = new Person('wu' , 20 , ['jobs' , 'gates']) let person2 = new Person('shen' , 21 , ['jack' , 'rose']) console.log(person1.friends) console.log(person2.friends) person1.friends.push('new') console.log(person1.friends) console.log(person2.friends) [代码] 2.5 动态原型模式 [代码]//2.5 动态原型模式 // 把所有功能都放在构造函数里面,通过在构造函数里初始化原型,同时保持构造函数和原型模式的优点 function Person(age ,name){ this.age = age; this.name = name; if(typeof this.sayName != 'function'){ // 初次调用构造函数时才会执行 多个属性只要检查一个即可 console.log(this) Person.prototype.sayName = function(){ console.log(this.name) } Person.prototype.sayAge = function(){ console.log(this.age) } } } let person1 = new Person('wu' , 12) let person2 = new Person('wang' , 12) [代码] 2.6 寄生函数构造模式 在上述方法都不适用的情况下,可以使用寄生构造函数模式。 创建一个函数,这个函数封装了创建对象的过程,然后返回一个对象。 [代码]function Person (name , age){ let o = new Object(); o.name = name; o.age = age; o.sayName = function(){ console.log(this.name) } return o; } let p = new Person('wu' , 10); console.log(p.age) p.sayName() [代码] 2.7 稳妥函数构造模式 与寄生构造函数类似,但有两点不同 新创建对象的实例方法不使用this 不适用new操作符调用构造函数 [代码]function Person(name){ let o = new Object(); o.sayName = function(){ console.log(name) // 不使用this } return o; } let p = Person('wu'); // 不使用 new p.sayName() [代码]
2020-05-25 - 微信上传图片被压缩终极解决方案
最近一直在对前期项目进行重构,遇到了之前一个悬而未解的问题,梳理下,寻找可能存在的解决方案 大家都知道微信上传图片被压缩了,但是这种情况是否能解决呢? 1 [图片] 2微信上传图片主要用到以下几个api [图片] 3 目前项目上传方案用到上述的①②两个接口,通过①选择图片,然后通过②获取图片的base64,其中在选择图片时,采用的尺寸模式为压缩。 由于该方案在减少传输和存储压力的同时,极大降低了图片的质量,导致在后续识别过程中,造成非常大的困扰。 同时,由于该方案在压缩模式选择这块,即使选择了原图,部分机型也会存在压缩的情况,并且没有一个明确的清晰的压缩策略,有时候这种压缩比非常大,同样会导致上传的身份证照片带的细节信息丢失,最后的图片甚至人眼不可识别 针对这个问题目前可供参考的解决方案是: 利用上述①③两个接口,在选择图片的时候,将图片上传到微信服务器,即通过微信的uploadImage上传到微信服务器,拿到服务器返回的文件serverId,然后通过素材管理,临时素材管理接口,根据serverId将图片下载到自己服务器,这种方案的优势在于,图片的压缩策略完全是由我们来掌控的,不管具体采用哪个压缩比,都是可以通过代码来控制。 下面简单分析下图片上传用到的几个api,传递参数以及输出相应 ①chooseImage拍照或从手机相册中选图接口 [图片] {"localIds":["wxLocalResource://6110441863775331"],"sourceType":"album","errMsg":"chooseImage:ok"} [图片] ②uploadImage上传图片接口 [图片] {"localId":"wxLocalResource://6110448596555452","serverId":"uNMAdM7ElbVX2m6bqfh77pMGD8t4u8TebDdcjOJpKidsWMKY3F0RHbQPFQp76ACB","errMsg":"uploadImage:ok"} [图片] 备注:上传图片有效期3天,可用微信多媒体接口下载图片到自己的服务器,此处获得的 serverId 即 media_id。 后端从微信服务器下拉图片 https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_temporary_materials.html 属于素材管理里面的获取临时素材接口 [图片] 关于access_token如何生成,具体可以参考下面链接的文档 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html 可在下面网址进行测试 https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&type=%E5%9F%BA%E7%A1%80%E6%94%AF%E6%8C%81&form=%E5%A4%9A%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%8E%A5%E5%8F%A3%20/media/upload [图片] 2 [图片] 具体参考文档 https://developers.weixin.qq.com/community/develop/doc/00088493fb47182c6e27b681b54c00 目前该方案已上生产,经得起实践的检验。
2020-05-22 - 干货
<view>{{~~(1.999)}} </view> 这样可以取整; 注意:是向下取整的 即 <view>{{~~(1.999)}} </view> 最终渲染为 <view>1</view> 感谢 寒雪 提醒 . 为了防止setData一次传输大量的代码导致页面卡顿或者报错,个人建议使用下面的做法: 滚动加载或者点击加载更多数据的时候,我们一般会采取分页的形式,后端一次会给我10条数据或者5条 给我们的数据一般都是数组 page({ data:{ mrData:[] } }) 我们把从后端拿到的数据这样做 【我是第一次获取的数据是一个数组】 当用户滚动加载的时候 从后台获取到第二次数据 【我是第一次获取的数据,我是第二次获取的数据】 具体写法 let that = this; this.setData({ [[代码]mrData[${that.data.mrData.length}][代码]]:后台给的数组 }) 不知道为什么 我写上 ``不显示了 记住是利用 ES6字符串模板 其转化后为 this.setData({ [‘mrData[0]’]:后台给的数组 }) 循环数据的话 <block wx:for=’{{mrData}}’ wx:key> <block wx:for=’{{item}}’ wx:key wx:for-item=‘it’ wx:for-index=‘ind’> {{it}} </block> </block> 这样就可以了 这样做的好处就是 每次修改只修改部分 这样就解决了 每次滚动加载的是 我们都要重新赋值大量的数据 我还是粘贴一下代码的好 如下: [图片] [图片] [图片] 渲染后的页面出现的效果 如下 [图片] 点击第一个 小浪花 [图片] 成功改变了 nice 一般的写法是这样的 不知道和你写的是否一样 [图片] [图片] 从后台获取数据后 然后把这个数据 push到原来的大数组中 然后再setData 每次setData的数据都会增大 最后超过限制导致页面卡顿影响性能 有可能还会报错 还有就是 第一次写文章 不知道这样写是否有人能看懂 ,希望大家能由此举一反三,看到类似的问题可以想到这样的方法 虽然有些繁琐 但是真的能优化性能,如果能帮到你 希望动动小手指 给我点个赞吧~ O(∩_∩)O哈哈~ 默默无闻的余小浪
2019-05-22 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] 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 - scroll-view 如何进行下拉刷新
通过配置文件实现下拉刷新是微信小程序做的比较好用的一个点,让我们不必再像 Android App 那样引入其他的库或是自定义组件来实现。 但是,当小程序的下拉刷新碰上 scroll-view 的时候,就会发现,在 scroll-view 区域进行滑动,即使页面已经到达顶部了,下拉刷新方法也不会触发,而 scroll-view 的区域往往会占据页面的大部分空间,这部分区域如果如法实现下拉,对小程序页面的体验还是有很大影响的。 于是研究了一下,发现了一个比较好的解决方法,在这里分享给大家,如果有什么不足之处,也请大家留言指正。 原理,scroll-view 在没有达到顶部的时候,不会触发 bindtouchmove 事件, 而一旦触发了这个事件, scroll-view 一定是达到了顶部或者是底部,所以,在 bindtouchmove 第一次触发的时候记录下此时的 clientY, 最后当 bindtouchend 触发的时候,比较一下此时的 clientY 和记录值得差,判断是否进行下拉刷新。 touchEnd(e) { let clientY = e.changedTouches[0].clientY; if (this.data.offsetTop != -1) { let offset = clientY - this.data.offsetTop; if (offset > 100) { wx.startPullDownRefresh(); } } this.data.offsetTop = -1; }, touchMove (e) { let clientY = e.changedTouches[0].clientY; if (this.data.offsetTop == -1) { this.data.offsetTop = clientY; } }
2019-12-06 - 制作带心跳动画按钮
App制作者希望有一些按钮,引导用户去操作,那么带动画会起到一定的引流作用吧! 本主编就在此分享一款。效果如下,如果是你需要的,请继续往下看代码。不是需要的朋友就不用看了。 [图片] WXML <view class=“bottomViewItem”> <button class=“bottomMiddleHeaderView” open-type=“share”> <view class=“bottomMiddleHeaderItem” animation="{{animationMiddleHeaderItem}}"> <!-- 心跳 --> <view class=“bottomMiddleHeaderItemSubView”> <image src="/image/wx.jpg" style=“width:50rpx; height:50rpx;” animation="{{animationMiddleHeaderItem}}"></image> </view> <!-- 分享文字 --> <view class=“bottomMiddleHeaderItemSubView”>分享好友</view> </view> </button> </view> css. .bottomMiddleHeaderView{ font-size:24rpx; position: fixed; top: 300rpx; right: 0px; } button::after { border: none; } JS代码 onReady: function () { var circleCount = 0; // 心跳的外框动画 this.animationMiddleHeaderItem = wx.createAnimation({ duration: 1000, // 以毫秒为单位 /** http://cubic-bezier.com/#0,0,.58,1 linear 动画一直较为均匀 ease 从匀速到加速在到匀速 ease-in 缓慢到匀速 ease-in-out 从缓慢到匀速再到缓慢 http://www.tuicool.com/articles/neqMVr step-start 动画一开始就跳到 100% 直到动画持续时间结束 一闪而过 step-end 保持 0% 的样式直到动画持续时间结束 一闪而过 */ timingFunction: ‘linear’, delay: 100, transformOrigin: ‘50% 50%’, success: function (res) { } }); setInterval(function () { if (circleCount % 2 == 0) { this.animationMiddleHeaderItem.scale(1.15).step(); } else { this.animationMiddleHeaderItem.scale(1.0).step(); } this.setData({ animationMiddleHeaderItem: this.animationMiddleHeaderItem.export() }); circleCount++; if (circleCount == 1000) { circleCount = 0; } }.bind(this), 1000); }, 拿走不谢。可以添加我的小程序查看效果(扫描下方二维码),如果小程序中哪些技术点大家感兴趣,可以直接私信我。 微信号:cleversoft [图片]
2019-11-05 - 页面跳转封装
export default (options, type = 1) => { return new Promise((reslove, reject) => { routes[type](Object.assign(getPath(options), { success: reslove, fail: reject, })); }); }; function getPath(options) { switch (Reflect.toString.call(options)) { case “[object Object]”: return { url: [代码]${options.url}?data=${encodeURIComponent(JSON.stringify(options.data))}[代码], }; case “[object Number]”: return { delta: options, }; case “[object String]”: return { url: options, }; } } const routes = { 1: wx.navigateTo, 2: wx.switchTab, 3: wx.navigateBack, 4: wx.reLaunch, 5: wx.redirectTo, };
2019-06-04 - 常见小程序优化方案总结
一、首次启动性能优化 1、首次打开一个小程序,用户一般会观察到如下图所示的三种状态 [图片] 这张图中的三种状态对应的都是什么呢?小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。 此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。 2、小程序加载的顺序 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 [图片] 通过这张图可以对比发现,小程序首次启动的 第一张图是资源准备(代码包下载);第二张图是业务代码的注入以及落地页首次渲染;第三张图是落地页数据请求时的loading态(部分小程序存在)。 3、优化方案 控制包大小:上传代码时要先进行压缩、静态图片资源除小的icon外其余放到cdn、无用代码清除; 分包加载:根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载; 分包预加载:在进入小程序某个页面时,由框架自动预下载可能需要的分包,提升进入后续分包页面时的启动速度。对于独立分包,也可以预下载主包。分包预下载 官方文档链接 独立分包技术:区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;独立分包 官方文档链接 二、渲染性能优化 1、数据渲染优化 双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。 [图片] 页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。 [图片] 在数据传输时,逻辑层会执行一次JSON.stringify来去除掉setData数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将setData所设置的数据字段与data合并,使开发者可以用this.data读取到变更后的数据。因此,为了提升数据更新的性能,可以参考如下方法: 1.不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; 2.数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据; 3.与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下; 4.勿在后台页面去setData; 5.建议创建一个检测data大小的方法,如果超过64K可以打印报警日志提醒开发者; 2、长列表优化方案 无限下拉加载后会大数据量展现导致的性能问题,一个常见的方法在诸多C端都有使用,一句话说就是"只渲染所需的元素"。虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。简而言之,虚拟列表指的就是「可视区域渲染」的列表。有三个概念需要了解一下: 滚动容器元素:一般情况下,滚动容器元素是 window 对象。然而,我们可以通过布局的方式,在某个页面中任意指定一个或者多个滚动容器元素。只要某个元素能在内部产生横向或者纵向的滚动,那这个元素就是滚动容器元素考虑每个列表项只是渲染一些纯文本。在本文中,只讨论元素的纵向滚动。 可滚动区域:滚动容器元素的内部内容区域。假设有 100 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 100 * 50。可滚动区域当前的具体高度值一般可以通过(滚动容器)元素的 scrollHeight 属性获取。用户可以通过滚动来改变列表在可视区域的显示部分。 可视区域:滚动容器元素的视觉可见区域。如果容器元素是 window 对象,可视区域就是浏览器的视口大小(即视觉视口);如果容器元素是某个 div 元素,其高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可视区域。 实现虚拟列表就是在处理用户滚动时,要改变列表在可视区域的渲染部分,其具体步骤如下: 计算当前可见区域起始数据的 startIndex 计算当前可见区域结束数据的 endIndex 计算当前可见区域的数据,并渲染到页面中 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上 计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上 [图片] 虚拟列表的实现原理可以参考这篇文章:浅说虚拟列表的实现原理 3、长列表局部渲染技巧 在一个列表中,有n条数据,采用上拉加载更多的方式。假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果,可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来)。 优化步骤: 1.将点赞的[代码]id[代码]传过去,知道点的是那一条数据, 将点赞的[代码]id[代码]传过去,知道点的是那一条数据 <view wx:if="{{!item.status}}" class=“btn” data-id="{{index}}" bindtap=“couponTap”>立即领取</view> 2.重新获取数据,查找相对应id的那条数据的下标([代码]index[代码]是不会改变的) 3.用setData进行局部刷新 this.setData({ list[index] : newList[index] }) 4、用户事件优化 视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。 1.去掉不必要的事件绑定(WXML中的bind和catch),从而减少通信的数据量和次数; 2.事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据。 三、生命周期优化 1、异步请求,页面渲染需要的数据最好在onLoad时异步请求数据,不要在onReady时请求;非页面渲染需要的数据,尽量放在onReady生命周期去调用; 2、定时器、事件监听、播放组件、音视频组件等,在页面转入后台(onHide)或者销毁(onUnload)时应该中止掉; 四、图片静态资源预加载 在日常小程序的开发中,有很多的大图片是放置于cdn上的,在需要进行展示的时候,如果没有预加载有可能出现图片展示的不及时,造成不好的体验,所以如下方式实现了图片预加载的功能,可以封装成组件的形式。 实现思路是将图片添加进页面中,设置不可见,然后加载图片,实现一个预加载的功能。 1、添加模版文件: img-loader.wxml <template name=“img-loader”> <image mode=“aspectFill” wx:for="{{ imgLoadList }}" wx:key="*this" src="{{ item }}" data-src="{{ item }}" bindload="_imgOnLoad" binderror="_imgOnLoadError" style=“width:0;height:0;opacity:0” /> </template> 2、添加js文件:img-loader.js /** 图片预加载组件 */ class ImgLoader { /** 初始化方法,在页面的 onLoad 方法中调用,传入 Page 对象及图片加载完成的默认回调 */ constructor(pageContext, defaultCallback) { this.page = pageContext this.defaultCallback = defaultCallback || function () { } this.callbacks = {} this.imgInfo = {} [代码]this.page.data.imgLoadList = [] //下载队列 this.page._imgOnLoad = this._imgOnLoad.bind(this) this.page._imgOnLoadError = this._imgOnLoadError.bind(this) [代码] } /** 加载图片 @param {String} src 图片地址 @param {Function} callback 加载完成后的回调(可选),第一个参数个错误信息,第二个为图片信息 */ load(src, callback) { if (!src) return; [代码]let list = this.page.data.imgLoadList, imgInfo = this.imgInfo[src] if (callback) this.callbacks[src] = callback //已经加载成功过的,直接回调 if (imgInfo) { this._runCallback(null, { src: src, width: imgInfo.width, height: imgInfo.height }) //新的未在下载队列中的 } else if (list.indexOf(src) == -1) { list.push(src) this.page.setData({ 'imgLoadList': list }) } [代码] } _imgOnLoad(ev) { let src = ev.currentTarget.dataset.src, width = ev.detail.width, height = ev.detail.height [代码]//记录已下载图片的尺寸信息 this.imgInfo[src] = { width, height } this._removeFromLoadList(src) this._runCallback(null, { src, width, height }) [代码] } _imgOnLoadError(ev) { let src = ev.currentTarget.dataset.src this._removeFromLoadList(src) this._runCallback(‘Loading failed’, { src }) } //将图片从下载队列中移除 _removeFromLoadList(src) { let list = this.page.data.imgLoadList list.splice(list.indexOf(src), 1) this.page.setData({ ‘imgLoadList’: list }) } //执行回调 _runCallback(err, data) { let callback = this.callbacks[data.src] || this.defaultCallback callback(err, data) delete this.callbacks[data.src] } } module.exports = ImgLoader 3、在需要使用预加载功能的xxx.wxml页面中加入模版文件和使用代码: <import src="…/…/templates/img-loader.wxml"/> <template is=“img-loader” data="{{ imgLoadList }}"></template> 4、在需要使用预加载功能页面的xxx.js文件中引入文件和使用代码: import ImgLoader from ‘…/…/templates/img-loader.js’; let images = [ ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/shoulie.png’, ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/shandian.png’, ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/fengbao.png’ ] //初始化图片预加载组件,并指定统一的加载完成回调 this.imgLoader = new ImgLoader(this, this.imageOnLoad.bind(this)); images.forEach(item => { this.imgLoader.load(item) }) 备注:如有错误请帮忙指出;如有侵权,请联系我们删除,谢谢!
2019-09-03 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 聊一聊小程序开发中的单位如何布局使用?
小程序支持的单位? 可以说小程序就是在微信体系网页的另一种表现方式。网页中的单位小程序基本都支持。但实际开发中,我常用到的是以下几种 ↓ rpx rpx做为小程序自家系统里的单位,特性是可以根据屏幕宽度进行自适应。rpx官方介绍 比如我写一个2:1比例的全屏轮播图,可以这样写: [代码]swiper { width:750rpx; height:375rpx; } [代码] 1rpx = 0.5px = 1物理像素。网页开发中,默认字体一般设置为14px,在小程序中我们就可以设置小程序的默认字体大小为28rpx。 px 在小程序开发中 rpx基本就代替了px,但在一些特殊的场合,px的表现要比rpx好。 兼容ipad时,由于ipad可以横屏和竖屏,并且屏幕宽度可以达到2K以上,如果你的小程序要考虑到兼容ipad,那么还是多考虑使用px吧。 覆盖微信原生组件样式。em????可以覆盖微信原生样式??? 是的,只有小程序老玩家才知道的秘密!小程序原生样式是可以覆盖美化的,以 <switch> 组件为例:switch代码片段 [图片] 导入代码片段到开发者工具中,并切换设备模式预览可以发现rpx表现不佳。使用px反而更好。 em与rem em与rem在H5的网页开发上可以大放异彩,但小程序中因为有rpx的存在,em与rem使用的就少了。基本只有在一些对字体宽度有特效的情况下才会使用。比如首行缩进。 vw、vh和百分比 vw:视窗宽度,1vw等于视窗宽度的1%。 vh:视窗高度,1vh等于视窗高度的1%。 %:父级容器的宽度百分百。 [图片] calc() 的使用 前面讲了单位,那么我们现在来聊聊怎么使用这些单位了。小程序是网页的一种,支持css,也支持calc()。 这里吃下书: calc() 函数用于动态计算长度值。 [代码] ● 需要注意的是,运算符前后都需要保留一个空格,例如:width: calc(100% - 10px); ● 任何长度值都可以使用calc()函数进行计算; ● calc()函数支持 "+", "-", "*", "/" 运算; ● calc()函数使用标准的数学运算优先级规则; [代码] 使用场景示例 垂直导航页,常用于外卖订餐或者商城的二级分类页。 上半部分是定死高度375rpx的轮播图区域,下半部分是可以随设备高度变化的可滚动的区域。容器高度可以这样写: [代码]{ height:calc(100vh - 375rpx) } [代码] [图片] 结尾 夜深了,晚安,不定期更新小程序使用技巧。新人写文章,大佬多指点! [图片]
2019-02-26 - 如何检测小程序出现了闪退?
目前存在用户反馈小程序闪退,我想监控一下闪退的行为路径而定位原因,请问如何检测到小程序闪退了呢?
2019-10-12 - 请问怎么在小程序里可以让用户设置小程序的字号,类似微信设置里的字号大小设置?
有个需求,需要用户可以在小程序里调整字体大小
2019-08-01 - 将小程序原生异步函数promisify后,在async/await中使用
目前,小程序中支持使用async/await有三种模式: 1、不勾选es6转es5,不勾选增强编译;该模式是纯es7的async/await,需要基础库高版本。 2、勾选es6转es5,勾选增强编译;一般是因为调用了第三方的es5插件,通过增强编译支持async/await。 3、勾选es6转es5,不勾选增强编译;手工引入runtime.js支持async/await。 据最近更新情况,原生的函数已经大部分同时原生支持同步化了,不需要本方案转化了,直接加上await即可;比如wx.chooseImage、wx.showModal。。。具体有哪些,可以自己试。 如果只是wx.request的同步化,可参考: https://developers.weixin.qq.com/community/develop/article/doc/0004cc839407a069f77a416c056813 app.js代码: function promisify(api) { return (opt, ...arg) => { return new Promise((resolve, reject) => { api(Object.assign({}, opt, { success: resolve, fail: reject }), ...arg) }) } } App({ globalData: {}, chooseImage: promisify(wx.chooseImage), request: promisify(wx.request), getUserInfo: promisify(wx.getUserInfo), onLaunch: function () { }, }) 某page的index.js代码: const app = getApp() testAsync: async function(){ let res = await app.chooseImage() console.log(res) res = await app.request({url:'url',method:'POST',data:{x:0,y:1}}) console.log(res) }, [图片]
2020-10-20 - 先获取服务端token再运行APP
现在token的校验写在接口上,导致token失效,会发送多次请求,产生并发,请问有什么好的解决方法
2019-03-11 - 前端加载优化及实践
大家都知道产品体验的重要性,而其中最重要的就是加载速度,一个产品如果打开都很慢,可能也就没有后面更多的事情了。这篇文章是我最近项目中的一些加载优化总结,欢迎大家一起讨论交流。 内容包括: 性能指标及数据采集 性能分析方法 性能优化方法 性能优化具体实践 第一部分:性能指标及数据采集 要优化性能首先需要有一套用来评估性能的指标,这套指标应该是是可度量、可线上精确采集分析的。现在来一起看看如何选择性能指标吧。 1. 性能指标 加载的过程是一个用户的感知变化的过程。所以我们的页面性能指标也是要以用户感知为中心的。下面是google定义了几个以用户感知为中心的性能指标。 1.1 以用户感知为中心的性能指标 首先确定页面视觉的变化传递给用户的感知变化关键点: 感知点 说明 发生了吗? 浏览是否成功。 有用了吗? 是否有足够的内容呈现给用户。 可用了吗? 用户是否可以和页面交互了。 好用吗? 用户和应用交互是否流畅自然。 我这里讲的是加载优化,所以第四点暂时不讨论。下面是感知点相关的性能指标。 First paint(FP) and first contentful paint(FCP) FP: Webview跳转到应用的首次渲染时间。 FCP:Webview首次渲染内容的时间:文本,图像(包括背景图像),非白色画布或SVG。这是用户第一次消费内容的时间。 Chrome支持用Paint Timing API获取这两个值: [代码] performance.getEntriesByType("paint") [代码] First meaningful paint(FMP) 首次绘制有效内容的时间,用来表明这个应用是否绘制了有效内容。比如天气应用可以看到天气了,商品列表可以看到商品了。 Time to Interactive(TTI) 应用可交互时间,这时应用渲染完成且可以响应用户输入的时间。这种情况下JS已经加载完成且主线程处于空闲状态。 Speed index 速度指标:代表填充页面内容的速度。要想降低速度指标分数,您需要让加载速度从视觉上显得更快,也就是渐进式展示。 上面指标对应的感知点如下: 感知点 说明 发生了吗? FP/FCP 有用了吗? FMP 可用了吗? TTI Speed index是个整体效果指标所以没有对应上面的任何一个,但也同时对应任何一个。 对于实际项目中我们选取指标要便于采集,下面是针对我的实际项目(APP内的单页面应用)选取的性能指标。 1.2 实际项目选取的性能指标 Webview加载时间 反应Webview性能。这样就可以更真实的知道我们应用的加载情况。 页面下载时间 反应浏览成功时间。 应用启动时间 反应应用启动完成时间,这个时候页面初始化完成,是JS首次执行完成的时间,应用所需异步请求都已经发出去了。 首次有效绘制内容时间 已经有足够的内容呈现给用户,是首屏所需重要接口返回且DOM渲染完成的时间,这个时间由程序员自行判断。 应用加载完成时间 应用完整的呈现给了用户,这个时候页面中所有资源都已经下载好,包括图片等资源。 这里我们的性能指标确定了,下面看看这些数据怎么采集吧。 2. 数据采集 performance.timing为我们提供页面加载每个过程的精确时间,如下图: [图片] 是不是很完美,这足够了?还不够,我们还需要加上原生APP为我们提供的点击我们应用的时间和我们自己确定的FMP才够完美。 下面是每个指标的获取方法: 公用代码部分 [代码]let performance = window.performance || window.msPerformance || window.webkitPerformance; if (performance && performance.timing) { let t = performance.timing; let navigationStart = t.navigationStart; //跳转开始时间 let enterTime = ""; //app提供的用户点击应用的时间,需要和app沟通传递方式 //... 性能指标部分 } [代码] Webview加载时间 [代码] let webviewLoaded = navigationStart - enterTime; [代码] 注意:enterTime应该是客户端ms时间戳,不是服务器时间。 页面下载时间 [代码] let pageDownLoadedTime = t.responseEnd - navigationStart; [代码] 应用启动时间 [代码]let appStartTime = t.domContentLoadedEventStart - navigationStart; [代码] 首次有效绘制内容时间 这里我们需要在有效绘制后调用 [代码]window._fmpTime = +(new Date())[代码]获取当前时间戳。 [代码]let fmpTime = window._fmpTime - navigationStart; [代码] 应用加载完成时间 [代码]let domCompleteTime = t.domComplete - navigationStart; [代码] 最后在document load以后使用上面代码就可以收集到性能数据了,然后就可以上报给后台了。 [代码]if (document.readyState == 'complete') { _report(); } else { window.addEventListener("load", _report, false); } [代码] 这样就封装了一个简单性能数据采集上报组件,这是非常通用的可以用在类似项目中使用只要按照标准提供enterTime和window._fmpTime就可以。 3. 数据分析 有了上面的原始数据,我们需要一些统计方法来观察性能效果和变化趋势,所以我们选取下面一些统计指标。 平均值 注意在平均值计算的时候要设置一个取值范围比如:0~10s以防脏数据污染。 平均值的趋势用折线图展示: [图片] 分布占比 可以清晰的看到用户访问时间的分布,这样你就可以知道有多少用户是秒开的了。 分布占比可以使用折线图、堆积图、饼状图展示: [图片] [图片] [图片] 第二部分:性能分析方法 上面有了性能指标和性能数据,现在我们来学习一下性能分析的一些方法,这样我们才能知道性能到底哪里不行、为什么不行。 1. 影响性能的外部因素 分析性能最重要的一点要确定外部因素。经常会有这种情况,有人反应页面打开速度很慢,而你打开速度很快,其实可能并不是页面性能不好,只是外部因素不同而已。 所以做好性能优化不能只考虑外部因素好的情况,也要让用户能在恶劣条件(如弱网络情况)下也有满足预期的表现。下面看看影响性能的外部因素主要有哪些。 1.1 网络 网络可以说是最影响页面性能最重要的外部因素了,网络的主要指标有: 带宽:表示通信线路传送数据的能力,即在单位时间内通过网络中某一点的最高数据率,单位有bps(b/s)、Kbps(kb/s)、Mbps(mb/s)等。常说的百兆带宽100M就是100Mbps,理论下载最大速度12.5MB/s。 时延:Delay,指数据从网络的一端传送到另一端所需的时间,反应的网络畅通程度。 往返时间RTT:Round-Trip Time,是指从发送端发送数据到接收端接受到确认的总时间。我们经常用的ping命令就是用这个指标表明我们和目标主机的网络顺畅程度。比如我们要对比几个翻墙代理哪里个好,我们就可以ping一下,看看这几个代理哪个RTT低来作出选择。 [图片] 这三个主要指标中后面两个类似,在Chrome中模拟网络主要用设置带宽和网络延迟(往返时间RTT出现最小延迟)来模拟网络。我们电脑一般用的是WI-FI(百兆),那么我们模拟网络,主要模拟常见3G(1兆)、4G(10兆)网络就好,这样我们就覆盖了三个级别的网络情况了。 可以在Chrome的NetWork面板直接选取Chrome模拟好的网络,这个项目network-emulation-conditions中有默认模拟网络的速度。 [图片] 如果默认不满足,你也可以自己配置网络参数,在设置面板的Throttling。 [图片] 上面设置的3G接近100KB/s,4G 0.5MB/s。你可以根据自己的需要来调整这个值,这两个值的差异应该能很好两种不同的网络情况了。设置模拟网络只要能覆盖不同的带宽情况就好,也不用那么真实因为真实情况很复杂。网络部分就介绍完了,接着看其他因素。 1.2 用户机器性能 经常会有这种情况,一个应用在别人手机上打开速度那么快、那么流畅,为啥到我这里就不行了呢?原因很简单人家手机好,自然有更好的配置、更多的资源让程序运行的更快。 Chrome现在非常强大你可以通过performance面板来模拟cpu性能。也可以让你看到应用在低性能机器上的表现。 [图片] 1.3 用户访问次:首次访问、2次访问、发版本访问 用户访问次数也是分析性能的重要外部因素,当用户第一次访问要请求所有资源,后面在访问因为有些资源缓存了访问速度也会不同。当我们开发者又发版本,会更新部分资源,这样访问速度又会跟着变。因为缓存的效果存在,所以这三种情况要分开分析。同时也要注意我们是否要支持用户离线访问。 通过在Chrome中的Network面板中选中Disable cache就可以强制不缓存了,来模拟首次访问。 [图片] 1.4 因素对选取 上面的外部因素虽然只有3种但相乘也有不少情况,为了简化我们性能分析,要选取代表性的因素去分析我们的性能。下面是指导因素对: 网络:WIFI 3G 4G 用户访问状态:首次 2次 这样有6种情况不算特别多,也能很好反应我们应用在不同情况下的性能。 2. devtools具体分析性能 通过devtools可以观察在不同外部因素下代码具体加载执行情况,这个工具是我们性能分析中最重要的工具,加载优化这里我们主要关注两个面板:Network、Performance。 先看Network面板的列表页: [图片] 这是网络请求的列表,右击表头可以增删属性列,根据自己需要作出调整。 下面我介绍网络列表中的几个重点属性: Protocol:网络协议,h2说明你的请求是http2协议的了。 Initiator:可以查到这个资源是哪里引用的。 Status:网络状态码。 Waterfall:资源加载瀑布流。 下面在看看Network面板中单个请求的详情页: [图片] 这里可以看到具体的请求情况,Timing面板是用来观察这次网络的请求时间占用的具体情况,对我们性能分析非常重要。具体每个时间段介绍可以点击Explanation。 虽然Network面板可以让我看到了网络请求的整体和单个请求的具体情况,但Network面板整体请求情况看着并不友好,而且也只有加载情况没有浏览器线程的执行情况。下面看看强大的Performance面板的吧。 [图片] 这里可以清晰看到浏览器如何加载资源如何解析html、解析css、执行js和渲染绘制的。 Performance简直太强大了,所以请你务必要掌握它的使用,这里篇幅有限,只能介绍了个大概,建议到google网站仔细学习一下。 3. Lighthouse整体分析性能 使用Lighthouse可以对应用做整体性能分析评分,并且会给我们专业的指导建议。我们可以安装Lighthouse插件或者安装Lighthouse npm包来使用它。 检测结果中可以看到很多性能指标的分值和建议。你也可以去测试下你的应用表现。 4. 线上用户统计分析性能 虽然使用devtools和Lighthouse可以知道页面的性能情况,但我们还要观察用户的真实访问情况,这才能真实反映我们应用的性能。线上数据采集分析,第一步部分已经介绍过了,这里就不在多说了。优化完看看自己对线上数据到底造成了什么影响。 上面介绍了性能分析的方法,可以很好帮你去分析性能,有了性能分析的基础,下面我们在来看看怎么做性能优化吧。 第三部分:性能优化方法 1. 微观:优化单次网络请求时间 在性能分析知道Network面板可以看到单次网络请求的详情 [图片] 从图可以看出请求包括:DNS时间、TCP时间、SSL时间(https)、TTFB时间(服务器处理时间)、ContentLoaded内容下载时间,所以有下面公式: [代码]requestTime = DNS + TCP + SSL+ TTFB +ContentLoaded [代码] 所以只要我们降低这里面任意一个值就可以降低单次网络请求的时间了。 2. 宏观:优化整体加载过程 加载过程的优化就是不断让第一部分的性能指标感知点提前的过程。通过关键路径优化、渐进式展示、内容效率优化手段,来优化资源调度。 2.1 加载过程 在介绍页面加载过程,先看看渲染绘制过程: [图片] Javascript:操作DOM和CSSOM。 样式计算:根据选择器应用规则并计算每个元素的最终样式。 布局:浏览器计算它要占据的空间大小及其在屏幕的位置。 绘制:绘制是填充像素的过程。 合成。由于页面的各部分可能被绘制到多层,合成是将他们按正确顺序绘制到屏幕上,正确渲染页面。 渲染其实是很复杂的过程这里只简单了解一下,想深入了解可以看看这篇文章。 了解了渲染绘制过程,在学习加载过程的时候就可以把它当作黑盒了,黑盒只包括渲染过程从样式计算开始,因为上面的Javascript主要是用来输入DOM、CSSOM。 浏览器加载过程: Webview加载 下载HTML 解析HTML:根据资源优先级加载资源并构建DOM树 遇到加载同步JS资源暂停DOM构建,等待CSSOM树构建 CSS返回构建CSSOM树 用已经构建的DOM、CSSOM树进行渲染绘制 JS返回执行继续构建DOM树,进行渲染绘制 当HTML中的JS执行完成,DOM树第一次完整构建完成触发:domContentLoaded 当所有异步接口返回后渲染制完成,并且外部加载完成触发:onload 注意点: CSSOM未构建好页面不会进行任何渲染 脚本在文档的何处插入,就在何处执行 脚本会阻塞DOM构建 脚本执行要等待CSSOM构建完成后执行 下面看看如何在加载过程提前感知点。 2.2 优化关键路径 把关键路径定义为:从页面请求到应用启动完成这个过程,也就是到JS执行完domContentLoaded触发的过程。 主要指标有: 关键资源: 影响应用启动完成的资源。 关键资源的数量:这个过程中加载的资源数据。 关键路径长度:关键资源请求的串行长度。 关键字节的数量:关键资源大小总和。 [图片] 上图关键资源有:html、css、3个js。关键资源数量:5个。关键字节的数量:5个资源的总大小。关键路径长度:2,html+剩余其他资源。 关键优化路径优化,就是要降低关键路径长度、关键字节的数量,在http1时代还要降低关键资源的数量,现在http2资源数不用关心。 2.3 优化内容效率 主要是关注的应用加载完成这个时间点,由首页加载完成所需的资源量决定。我们要尽量减少加载资源的大小,避免不必要加载的资源,比如做一些图片压缩懒加载尽快让应用加载完成。 主要指标有: 应用加载完成字节数:应用加载完成,所需的资源大小。 这个指标可以从Chrome上观察到,不过要剔除prefetch的资源。这个指标一般不太稳定,因为页面展示的内容不太相同,所以最好在相同内容相同情况下对比。 2.4 渐进式展示 从上面的加载过程中,可以知道渲染是多次的。那样我们可以先让用户看到一个Loading提示、先展示首屏内容。Loading主要优化的是FP/FCP这两个指标,先展示首屏主要是优化FMP。 3. 缓存:优化多次访问 缓存重点强调的是二次访问、发版访问、离线访问情况下的优化。 通过缓存有效减少二次访问、发版访问所要加载资源,甚至可以让应用支持离线访问,而且是对弱网络环境是最有效的手段,一定要善于使用缓存这是你性能优化的利器。 4. 优化手段 优化手段我归纳为5类:small(更小)、pre(更早)、delay(更晚)、concurrent(并发)、cache(缓存)。性能优化就是将这5种手段应用于上面的优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 5. 构建自己可动态改变的优化方法表和检查表 Checklist包括两部分,一个优化方法表,另外一个优化方法检查表。优化方法表是让我们对我们的性能优化方法有个评估和认识,优化方法检查表的好处是,可以清晰的知道你的项目用了哪些优化方法,还有哪些可以尝试做进一步优化,同时作为一个新项目的指导。 优化名:优化方法的名字。 优化介绍:对优化方法做简单的介绍。 优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 优化手段:small、pre、delay、concurrent、cache。 本地效果:选取合适的因素对,进行效果分析,确定预期作用大小。 线上效果:线上效果对比,确定这个优化方案的有效性及实际作用大小。 这样我们就能大概了解了这个效果的好处。我们新引入了一种优化方法都要按这张表的方法进行操作。 优化方法表: 名称 内容 优化名 JS压缩 优化介绍 压缩JS 优化点 关键路径优化 优化手段 small 本地效果 具体本地效果对比 线上效果 线上数据效果 上面是以JS压缩为例的优化方法表。 优化方法检查表: 分类 优化点 是否使用 不适用 问题说明 small JS压缩 √ pre preload/prefetch √ 不需要 通过这张表就能看出我们使用了哪些方法,还有哪些没使用,哪些方法不适用我们。可以很方便的应用于任何一个新项目。 第四部分:性能优化具体实践 现在就看看我在项目中的具体实践吧,项目中使用的技术栈是:Webpack3+Babel7+Vue2,下面我按照优化手段介绍: 1. small(更小) scope-hoisting scope-hoisting(作用域提升):Webpack分析出模块之间的依赖关系,把可以合并到一起模块合并到一起,但不造成冗余,因此只有被一个地方引用的代码可以合并到一起。这样做函数声明会变少,可以让代码更小、执行更快。 这个功能从Webpack3开始引入,依赖于ES2015模块的静态分析,所以要把Babel的preset要设置成[代码]"modules": false[代码]: [代码] ... [ "@babel/preset-env", { "modules": false ... [代码] Webpack3要引入ModuleConcatenationPlugin插件,Webpack4 product模式已经预置该插件: [代码]... new webpack.optimize.ModuleConcatenationPlugin(), ... [代码] [图片] 如上图,不压缩的JS中可以文件中看到CONCATENATED MODULE这就说明生效了。 tree-shaking 摇树:通常用于描述移除JavaScript上下文中的未引用代码,在webpack2中开始内置。依赖于ES2105模块的静态分析,所以我们使用babel同样要设置成 [代码]"modules": false[代码]。 [图片] 如上图,不压缩的JS中可以文件中看到unused harmony这就说明摇树成功了。 code-splitting(按需加载) 代码分片,将代码分离到不同的js中,进行并行加载和按需加载。 代码分片主要有两种: 按需加载:动态导入 vendor提取:业务代码和公共库分离 这里只介绍按需加载部分,动态导入Webpack提供了两个类似的技术。1. Webpack特定的动态导入require.ensure。2.ECMAScript提案[代码]import()[代码]。这里我只介绍我使用的[代码]import()[代码]这种方法。因为是推荐方法。 代码如下: Babel配置支持动态导入语法: [代码]... "@babel/plugin-syntax-dynamic-import", ... [代码] 代码中使用: [代码]... if(isDevtools()){ import(/* webpackChunkName: "devtools" */'./comm/devtools').then((devtools)=>{ let initDevtools = devtools.default; initDevtools(); }); } ... [代码] polyfill按需加载 我们代码是ES2015以上版本的要真正能在浏览器上能使用要通过babel进行编译转化,还要使用polyfill来支持新的对象方法,如:Promise、Array.from等。对于不同环境来说需要polyfill的对象方法是不一样的,所以到了Babel7支持了按需加载polyfill。 下面是我项目中的配置,看完以后我会介绍一下几个关键点: [代码]module.exports = function (api) { api.cache(true); const sourceType = "unambiguous"; const presets = [ [ "@babel/preset-env", { "modules": false, "useBuiltIns": "usage", // "debug": true, "targets": { "browsers": ["Android >= 4.0", "ios >= 8"] } } ] ]; const plugins= [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-strict-mode", "@babel/plugin-proposal-object-rest-spread", [ "@babel/plugin-transform-runtime", { "corejs": false, "helpers": true, "regenerator": false, "useESModules": false } ] ]; return { sourceType, presets, plugins } } [代码] @babel/preset-env preset是预置的语法转化插件的集合。原来有很多preset如:@babel/preset-es2015。直到出现了@babel/preset-env,它可以根据目标环境来动态的选择语法转化插件和polyfill,统一了preset众多的局面。 [代码]targets[代码]:是我们用来设置环境的,我的应用支持移动端所以设置了上面那样,这样就可以只加载这个环境需要的插件了。如果不设置[代码]targets[代码]通过@babel/preset-env引入的插件是 @babel/preset-es2015、@babel/preset-es2016和@babel/preset-es2017插件的集合。 [代码]"useBuiltIns": "usage"[代码]:将useBuiltIns设置为usage就会根据执行环境和代码按需加载polyfill。 @babel/plugin-transform-runtime 和polyfill不同,@babel/plugin-transform-runtime可以在不污染全局变量的情况下,使用新的对象和方法,并且可以移除内联的Babel语法转化时候的辅助函数。 我们这里只用它来移除辅助函数,不需要它来帮我处理其他对象方法,因为我们在开发应用不是做组件不怕全局污染。 sourceType:“unambiguous” 一个文件混用了ES2015模块导入导出和CJS模块导入导出。需要设置[代码]sourceType:"unambiguous"[代码],需要让babel自己猜测类型。如果你的代码都很合规不用加这个的。 压缩:js、css js、css压缩应该最基本的了。我在项目中使用的是[代码]UglifyJsPlugin[代码]和[代码]optimize-css-assets-webpack-plugin[代码],这里不做过多介绍。 压缩图片 通过对图片压缩来进行内容效率优化,可以极大的提前应用加载完成时间,我在项目中做了下面两件事。 广告图片,限制大小50K以内。原来基本会上传超过100K的广告图。 项目中图片使用的[代码]img-loader[代码]对图片进行压缩。 HTTP2支持,去掉css中base64图片 先看看HTTP1.1中的问题: 同一域名浏览器做了TCP连接数的限制,如:Chrome中只能有6个。 一个TCP连接只能同时处理一个请求响应。 在看看HTTP2的优势: 二进制分帧:HTTP2的性能增强的核心在于新的二进制分帧层。帧是最小传输单位,帧组成消息,数据以消息形式发送。 多路复用:所有请求在一个连接上完成,可以支持多数据流混合传输,在接收端拼接。 头部压缩:使用HPACK对头部压缩,网络中可以传递更少的数据。 服务端推送:服务端可以主动向客户端推送资源。 有了HTTP2我们在也不用担心资源数量,不用在考虑减少请求了。像:base64图片打到css、合并js、域名分片、精灵图都不要去做了。 这里我把原来base64压缩图片从css中去除了。 2. pre(更早) preload prefetch preload:将资源加载和执行分离,你可以根据你的需要指定要强制加载的资源,比如后面css要用到一个字体文件就可以在preload中指定加载,这样提高了页面展示效果。建议把首页展示必须的资源指定到preload中。 prefetch:用来告诉浏览器我将来会用到什么资源,这样浏览器会在空闲的时候加载。比如我在列表页将详情页js设置成prefetch,这样在进入详情页的时候速度就会快很多,因为我提前加载好了。 这里我用的是来使用[代码]preload-webpack-plugin[代码]preload和prefetch的。 代码: [代码]... const PreloadWebpackPlugin = require('preload-webpack-plugin'); ... new PreloadWebpackPlugin({ rel: 'prefetch', include: ['devtools','detail','VideoPlayer'] }), ... [代码] dns-prefetch preconnect dns-prefetch:在页面中请求该域名下资源前提前进行dns解析。preconnect:比dns-prefetch更近一步连TCP和SSL都为我们处理好了。 使用注意点:1. 考虑到兼容性问题,我们对一个域名两个都设置 2. 对于应用中不一定会使用的域名我们设置dns-prefetch就好以防占用资源。 代码如下: [代码]... <link rel="preconnect" href="//game.gtimg.cn"> ... <link rel="dns-prefetch" href="//game.gtimg.cn"> ... [代码] 3. delay(更晚) lazyload 对图片进行懒加载,我使用的是[代码]vue-lazyload[代码]。 代码如下: [代码]... import VueLazyload from 'vue-lazyload' ... Vue.use(VueLazyload, { preLoad: 1.3, error: '...', loading: '...', attempt: 1 }); ... <div class='v-fullpage' v-lazy:background-image="item.roomPic" :key="item.roomPic"></div> ... [代码] 这里的:key特别注意,如果你的列表数据是动态变化的一定要设置,否则图片是最开始一次的。 code-splitting(按需加载) code-splitting(按需加载)前面已经介绍过这里只是强调下它的delay作用,不使用的部分先不加载。 4. concurrent(并发) HTTP2 HTTP2前面已经应用在了css体积减少,这里主要强调它的多路复用。需要大家看看自己的项目是否升级到HTTP2,是否所有资源都是HTTP2的,如果不是的,需要推进升级。 code-splitting(vendor提取) vendor提取是把业务代码和公共库分离并发加载,这样有两个好处: 下次发版本这部分不用在加载(缓存的作用)。 JS并发加载:让先到并在前面的部分先编译执行,让加载和执行并发。 Webpack配置: [代码] ... entry:{ "bundle":["./src/index.js"], "vendor":["vue","vue-router","vuex","url","fastclick","axios","qs","vue-lazyload"] }, ... new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest' }), ... [代码] 5. cache(缓存) HTTP缓存 HTTP缓存对我们来说是非常有用的。 下面介绍下HTTP缓存的重点: Last-Modified/ETag:用来让服务器判断文件是否过期。 Cache-Control:用来控制缓存行为。 max-age: 当请求头设置max-age=deta-time,如果上次请求和这次请求时间小于deta-time服务端直接返回304。当响应头设置max-age=deta-time,客户端在小于deta-time使用客户端缓存。 强制缓存:这主要把不经常变化的文件设置强制缓存,这样就不需要在发起HTTP请求了。通过设置响应头Cache-Control的max-age设置。 如果像缓存很久设置一个很大的值,如果不想缓存设置成:Cache-Control:no-cahce。 协商缓存:如果没有走强制缓存就要走协商缓存,服务器根据Last-Modified/ETag来判断文件是否变动,如果没变动就直接返回304。 这里我们做的就是让运维调整资源的强制缓存时间,前端在结合文件hash命名就可以进行资源更新了。 ServiceWorker ServiceWorker是Web应用和浏览器之间的代理服务器,可以用来拦截网络来进行资源缓存、离线体验,还可以进行推送通知和后台同步。功能非常强大,我们这里使用的是资源缓存功能,看看和HTTP缓存比有什么优势: 功能多:支持离线访问、资源缓存、推送通知、后台同步。 控制力更强:缓存操作+络拦截功能都由开发者控制,可以做出很多你想做的事情比如动态缓存。 仅HTTPS下可用,更安全。 看看我在项目中的使用: js使用HTTP缓存和ServiceWorker双重缓存在cacheid变化后依然可以缓存。 不得对service-worker.js缓存,因为我们要用这个更新应用。在Chrome中看到请求的cache-control被默认设置了no-cache。 我们项目中使是Google的Workbox,Webpack中插件是 workbox-webpack-plugin。 [代码]... const WorkboxPlugin = require('workbox-webpack-plugin'); ... new WorkboxPlugin.GenerateSW({ cacheId: 'sw-wzzs-v1', // 缓存id skipWaiting: true, clientsClaim: true, swDest: './html/service-worker.js', include: [/\.js(.*)$/,/\.css$/], importsDirectory:'./swmainfest', importWorkboxFrom: 'local', ignoreUrlParametersMatching: [/./] }), ... [代码] localStorage localStorage项目中主要做接口数据缓存。通常localStorage是没有缓存时间的我们将其封装成了有时间的缓存,并且在应用启动的时候对过期的缓存清理。 code-splitting(vendor提取) 这里在提vendor提取主要是说明它发版本时候的缓存价值,前面介绍过了。 6. 整体优化效果评价 经过上面的优化,看看效果提升吧。 主要增长点来源: 关键路径资源:698.6K降低到538.6K降低22.9% 内容效率提升:广告图由原来的基本100K以上降低到现在50K以下,页面内图片全部走强制缓存。 缓存加快多次访问速度:js+css强制缓存加ServiceWorker。 线上数据效果: 页面下载时间: 平均值下降:25.74%左右 应用启动完成时间: 平均值下降:33.45%左右 秒开占比提高:23.42%左右 应用加载完成时间: 平均值下降:48.02%左右 第六部分:总结 以上就是我在加载优化方面的一些总结,希望对您有所帮助,个人理解有限,欢迎一起讨论交流。
2019-03-11 - wx.getlocation接口部分手机在位置权限都打开的情况下访问失败
小程序在调用位置权限接口时,获取失败;位置权限是打开的,可以在微信分享位置,小程序的位置权限也授权了,但是就是调用location接口时报错 [图片]
2019-08-05 - @fontface添加远程iconfont文件报错
@font-face { font-family: 'iconfont'; src: url('https://xxx/iconfont.eot'); src: url('https://xxx/iconfont.eot?#iefix') format('embedded-opentype'), url('https://xxx/iconfont.woff') format('woff'), url('https://xxx/iconfont.ttf') format('truetype'), url('https://xxx/iconfont.svg#iconfont') format('svg'); } 之前是用经过base64压缩的ttf文件,现在想尝试用网络地址去加载iconfont。但是开发工具出现Failed to load font错误,不过可以显示正常,而且真机下也没有报错。 请问这个方法可行? 我在开发工具使用wx.loadFontFace接口似乎不会执行该方法(真机正常)。 开发工具、基础库版本、真机微信版本都是最新的。 电脑操作系统是mac10.13 真机是ios12.1
2018-11-21 - 图片地址怎么转成base64传给后台
上传图片后返回:http://tmp/wx795d05170322e3f5.o6zAJs3EC9i_B_iZEITV7QeU1q3w.dGSRuPCe1tNj05ec8c85ee88d974fce316faf79d7990.png; 请问怎么转成base64呢?各路大咖出来救救急啊
2018-06-29 - 小程序用setData()方法时,key中包含变量怎么处理?
我要变更一个数据的值,但用setData({key:value})方法时,key不能正确使用,因为key中包含变量: 如如这样一个数据test: Page({ data:{ test[ {'name':'zhang'}, {'name':'wang'}, {'name':'zhao'}, ] } }) 当我想更改test[i].name的值时,因为我这个i是通过计算得到的,所以i是一个变量 用setData({'test[i].name':'liu'})时,运行提示我tese[]中[]里的下标只能用数字,如果我用字串拼接把i值加进去,又不能通语法,其它的一些方式我也试过,没有成功, 谁有解决这个问题 的办法?
2017-01-14 - 腾讯课堂小程序详情页开发总结
状态管理 一开始为了借鉴和复用课堂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 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - 如何做左上角返回拦截弹框?
定制左上角返回按钮一般有两个目的: 引导按钮:页面作为小程序启动后第一个页面时,左上角没有操作按钮,用户只能点击在右边胶囊更多回到首页(入口太深)。定制顶部可以引导用户很方便去其他页面。 拦截弹框:比如当用户点击返回时,弹出弹框,挽留用户。 效果: 点击查看没定制顶部录屏,发现没定制时,用户只能点击右边回到首页,考虑入口太深,故在左边做了个很奇怪的返回icon。点击可自己体验效果 点击查看有定制顶部录屏,虽然也是启动后第一个页面,但左上角有按钮返回首页,并当用户抽到红包后,返回会弹框挽留用户。点击可自己体验效果 开发顶部组件知识: 主要知识是组件开发、 wx.getMenuButtonBoundingClientRect()的使用。 代码下载说明: 由于在文章里贴代码导致文章比较乱,所以示例放在代码块里,大家可在开发工具输入链接直接获取,或直接在链接下载文件。 顶部组件代码块: https://developers.weixin.qq.com/s/A3Ki7Bmi7V9Z。 或点击下载示例代码 备注: 代码里有对应注释,我也不确定大家能不能看懂?虽然是很小的组件,代码量也不多,但如果有超过5位同学留言有必要观看视频来熟悉自定义顶部组件的开发过程,我也可录个视频。 推荐阅读 亲测有效隐藏scroll-view滚动条方法 实现自定义等间隔的tab
2019-07-04 - 自定义标题栏
使用效果 [图片][图片][图片][图片] 使用方法 属性介绍 属性名 类型 默认值 是否必须 说明 menuSrc String ‘’ 否 按钮图片地址 bgImgSrc String ‘’ 否 背景图片地址 bgImgMode String aspectFill 否 背景图片的显示模式 title String ‘’ 否 标题 titleTextColor String ‘’ 否 字体和按钮以及loading图标的颜色,按钮和loading暂时只有黑白2色 backgroundColor String ‘’ 否 整个标题栏的背景颜色 loading Boolean false 否 是否是加载状态 backProxy Boolean false 否 是否重写了返回键 标题栏中属性的默认数据会自动获取json配置以及系统的默认数据,如果不需要动态更改样式,可以在json中设置,组件中同样起作用 事件介绍 属性名 detail NaviBack 返回的逻辑方法 MenuTap 按钮的点击事件 [代码]"usingComponents": { "toolBar": "/component/toolbar" }, [代码] [代码]<toolBar menuSrc='/image/menu_white.png' bindMenuTap='onMenuTap' bgImgSrc='/image/navi-bg.jpg' /> [代码] 高度说明: 为了方便适配,这里给出自定义标题栏的计算公式: const MenuRect = wx.getMenuButtonBoundingClientRect() const statusBarHeight = wx.getSystemInfoSync().statusBarHeight; const height = (MenuRect.top - statusBarHeight) * 2 + MenuRect.height +MenuRect.top Github地址:https://github.com/Aracy/wx-mini-navigationbar
2019-05-21 - 小程序自定义tabBar(类似咸鱼)
在App上做类似咸鱼的Tabbar时,只能用自定义的方法,考虑小程序中如果想自定义像咸鱼这样的Tabbar,该如何实现呢?网上搜索的大多资料的tabbar都会在页面切换的时候重新渲染,下面的方法页面跳转时不会闪。 [图片] 效果图 下载地址:https://github.com/dt8888/tabbar 具体实现方法: 1.分装一个tabbar的组件属性列表实现项目的Tabbar的个数,文字,颜色,图片大小最好用官网推荐的81px*81px的icon。 JS关键代码为: [代码]properties[代码][代码]:[代码] [代码]{[代码][代码] [代码][代码]tabbar[代码][代码]:[代码] [代码]{[代码][代码] [代码][代码]type[代码][代码]:[代码] [代码]Object[代码][代码],[代码][代码] [代码][代码]value[代码][代码]:[代码] [代码]{[代码][代码] [代码][代码]"backgroundColor"[代码][代码]:[代码] [代码]"#ffffff"[代码][代码],[代码][代码] [代码][代码]"color"[代码][代码]:[代码] [代码]"#979795"[代码][代码],[代码][代码] [代码][代码]"selectedColor"[代码][代码]:[代码] [代码]"#1c1c1b"[代码][代码],[代码][代码] [代码][代码]"list"[代码][代码]:[代码] [代码][[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"pagePath"[代码][代码]:[代码] [代码]"pages/index/index"[代码][代码],[代码][代码] [代码][代码]"iconPath"[代码][代码]:[代码] [代码]"icon/icon_home.png"[代码][代码],[代码][代码] [代码][代码]"selectedIconPath"[代码][代码]:[代码] [代码]"icon/icon_home_HL.png"[代码][代码],[代码][代码] [代码][代码]"text"[代码][代码]:[代码] [代码]"首页"[代码][代码] [代码][代码]}[代码][代码],[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"pagePath"[代码][代码]:[代码] [代码]"pages/middle/middle"[代码][代码],[代码][代码] [代码][代码]"iconPath"[代码][代码]:[代码] [代码]"icon/icon_release.png"[代码][代码],[代码][代码] [代码][代码]"isSpecial"[代码][代码]:[代码] [代码]true[代码][代码],[代码][代码] [代码][代码]"text"[代码][代码]:[代码] [代码]"发布"[代码][代码] [代码][代码]}[代码][代码],[代码][代码] [代码][代码]{[代码][代码] [代码][代码]"pagePath"[代码][代码]:[代码] [代码]"pages/mine/mine"[代码][代码],[代码][代码] [代码][代码]"iconPath"[代码][代码]:[代码] [代码]"icon/icon_mine.png"[代码][代码],[代码][代码] [代码][代码]"selectedIconPath"[代码][代码]:[代码] [代码]"icon/icon_mine_HL.png"[代码][代码],[代码][代码] [代码][代码]"text"[代码][代码]:[代码] [代码]"我的"[代码][代码] [代码][代码]}[代码][代码] [代码][代码]][代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码]}[代码][代码],[代码] 2.在App.js中的onLaunch方法中 用wx.hideTabBar();隐藏系统自带的tabbar,点击时作为按钮选中的判断方法为: [代码]editTabbar[代码][代码]:[代码] [代码]function [代码][代码]([代码][代码])[代码] [代码]{[代码][代码] [代码][代码]let tabbar [代码][代码]=[代码] [代码]this.globalData.tabBar;[代码][代码] [代码][代码]let currentPages [代码][代码]=[代码] [代码]getCurrentPages[代码][代码]([代码][代码])[代码][代码];[代码][代码] [代码][代码]let _this [代码][代码]=[代码] [代码]currentPages[currentPages.length [代码][代码]-[代码] [代码]1[代码][代码]];[代码][代码] [代码][代码]let pagePath [代码][代码]=[代码] [代码]_this.route;[代码][代码] [代码][代码]if[代码][代码]([代码][代码]pagePath.indexOf[代码][代码]([代码][代码]'[代码][代码]/[代码][代码]'[代码][代码])[代码] [代码]![代码][代码]=[代码] [代码]0[代码][代码])[代码][代码]{[代码][代码] [代码][代码]pagePath [代码][代码]=[代码] [代码]'[代码][代码]/[代码][代码]' [代码][代码]+[代码] [代码]pagePath;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]let i [代码][代码]in[代码] [代码]tabbar.[代码][代码]list[代码][代码])[代码] [代码]{[代码][代码] [代码][代码]tabbar.[代码][代码]list[代码][代码][i].selected [代码][代码]=[代码] [代码]false[代码][代码];[代码][代码] [代码][代码]([代码][代码]tabbar.[代码][代码]list[代码][代码][i].pagePath [代码][代码]=[代码][代码]=[代码] [代码]pagePath[代码][代码])[代码] [代码]&[代码][代码]&[代码] [代码]([代码][代码]tabbar.[代码][代码]list[代码][代码][i].selected [代码][代码]=[代码] [代码]true[代码][代码])[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]_this.setData[代码][代码]([代码][代码]{[代码][代码] [代码][代码]tabbar[代码][代码]:[代码] [代码]tabbar[代码][代码] [代码][代码]}[代码][代码])[代码][代码];[代码][代码] [代码][代码]}[代码][代码],[代码] 如何引用该项目实现自己的自定义Tabbar: 1.找到项目中的tabbarComponent目录,放到自己的工程中,然后将tabbarComponent->icon图标替换成你自己的tabbar图片,文字颜色根据需求做适当的更改。 2.app.json中配置tabBar,因为点击发布时做的页面跳转,不配置在tabBar的list中。 3.在app.js中的globalData中加入自定义tabbar的参数,再加入一个方法给tabBar.list配置中的页面使用。 4.在页面的JS中的data中加入tabbar:{},并在onload方法中调用app.editTabbar(); 5.页面的.json文件中加入代码 [代码]"usingComponents"[代码][代码]:[代码] [代码]{[代码][代码]"tabbar"[代码][代码]:[代码] [代码]"../../tabbarComponent/tabbar"[代码][代码]}[代码] 6.在页面的.wxml文件中加入<tabbar tabbar="{{tabbar}}"></tabbar> 作者: honey缘木鱼
2019-01-10