- 利用css圆锥渐变实现环形进度条
[图片] 锥形渐变的正式语法如下: conic-gradient( [ from <angle> ]? [ at <position> ]?, <angular-color-stop-list> ) 可以看出锥形渐变由3部分组成: 起始角度 中心位置 角渐变断点 其中起始角度和中心位置都是可以省略的 这里只用了角渐变断点 background-image: conic-gradient(green 80%,#fff 80% 100%); 主要实现就两步 1.利用conic-gradient画一个圆 [图片] 2.利用任意元素做个内圆遮挡 [图片] 最外层的背景色原来是白色的,这里为了便于识别改成了灰色,现在将灰色的背景还原为白色 [图片] 环形进度条完成,就这么简单 通过动态的设置green的值就可以改变进度条的值 补充:还可以实现渐变进度条效果 [图片] 代码片段: 利用css圆锥渐变实现环形进度条
2020-06-22 - Wxml2Canvas -- 快速生成小程序分享图通用方案
Wxml2Canvas库,可以将指定的wxml节点直接转换成canvas元素,并且保存成分享图,极大地提升了绘制分享图的效率。目前被应用于微信游戏圈、王者荣耀、刺激战场助手等小程序中。 github地址:https://github.com/wg-front/wxml2canvas 一、背景 随着小程序应用的日渐成熟,多处场景需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 对于简单的分享图,比如固定大小的背景图加几行简短文字构成的分享小图,我们可以利用官方提供的canvas接口将元素直接绘制, 虽然繁琐了些,但能满足基本要求。 对于复杂的分享图,比如用户在微信游戏圈发表完话题后,需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制。又如王者荣耀助手小程序,需要将十人对局的详细战绩绘制成分享图,包含英雄数据、装备、技能、对局结果等信息,要绘制100多张图片和大量的文字信息,如果依旧使用官方的接口一步一步绘制,对开发者来说简直就是一场噩梦。我们急需一种通用、高效的方式完成上述的工作。 在这样的背景下,wxml2cavnas诞生了,作为一种分享图绘制的通用方案,它不仅能快速的绘制简单的固定小图,还能直接将wxml元素真实地转换成canvas元素,并且适配各种机型。无论是复杂的图文混排的富文本内容,还是展现形式多样的战绩结果页,都可以利用wxml2cavnas完美地快速绘制并生成所期望的分享图片。 二、Wxml2Canvas介绍及示例 1. 介绍 Wxml2Cavnas库,是一个生成小程序分享图的通用方案,提供了两种绘制方式: 封装基础图形的绘制接口,包括矩形、圆形、线条、图片、圆角图片、纯文本等,使用时只需要声明元素类型并提供关键数据即可,不需要再关注canvas的具体绘制过程; wxml直接转换成canvas元素,使用时传入待绘制的wxml节点的class类名,并且声明绘制此节点的类型(图片、文字等),会自动读取此节点的computedStyle,利用这些数据完成元素的绘制。 2. 生成图示例 下面是两张极端复杂的分享图。 2.1 游戏圈话题 [图片] 点击查看完整长图 2.2.2 王者荣耀战绩 [图片] 点击查看完整大图 三、小程序的特性及局限 小程序提供了如下特性,可供我们便捷使用: measureText接口能直接测量出文本的宽度; SelectorQuery可以查询到节点对应的computedStyle。 利用第一条,我们在绘制超长文本时便于文本的省略或者换行,从而避免文字溢出。 利用第二条,我们可以根据class类名,直接拿到节点的样式,然后将style转换成canvas可识别的内容。 但是和html的canvas相比,小程序的canvas局限性很多。主要体现在如下几点: 不支持base64图片; 图片必须下载到本地后才能绘制到画布上; 图片域名需要在管理平台加入downFile安全域名; canvas属于原生组件,在移动端会置于最顶层; 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容以及图片节点的链接。 针对以上问题,我们需要将base64图片转换jpg或png格式的图片,实现图片的统一下载逻辑,并且离屏绘制内容。针对第五条,好在SelectorQuery可以获取到节点的dataset属性,所以我们需要在待绘制的节点上显示地声明其类型(imgae、text等),并且显示地传入文本内容或图片链接,后文会有示例。 四、Wxml2Canvas使用方式 1. 初始化 首先在wxml中创建canvas节点,指定宽高: [代码] <canvas canvas-id="share" style="height: {{ height * zoom }}px; width: {{ width * zoom }}px;"> </canvas> [代码] 引入代码库,创建DrawImage实例,并传入如下参数: [代码] let DrawImage = require('./wxml2canvas/index.js'); let zoom = this.device.windowWidth / 375; let width = 375; let height = width * 3; let drawImage = new DrawImage({ element: 'share', // canvas节点的id, obj: this, // 在组件中使用时,需要传入当前组件的this width: width, // 宽高 height: height, background: '#161C3A', // 默认背景色 gradientBackground: { // 默认的渐变背景色,与background互斥 color: ['#17326b', '#340821'], line: [0, 0, 0, height] }, progress (percent) { // 绘制进度 }, finish (url) { // 画完后返回url }, error (res) { console.log(res); // 画失败的原因 } }); [代码] 所有的数字参数均以iphone6为基准,其中参数width和height决定了canvas画布的大小,规定值是在iphone6机型下的固定数值; zoom参数的作用是控制画布的缩放比例,如果要求画布自适应,则应传入 windowWidth / 375,windowWidth为手机屏幕的宽度。 2. 传入数据,生成图片 执行绘制操作: [代码] drawImage.draw(data, this); [代码] 执行绘制时需要传入数据data,数据的格式分为两种,下面展开介绍。 2.1 基础图形 第一种为基础的图形、图文绘制,直接使用官方提供接口,下面代码是一个基本的格式: [代码] let data = { list: [{ type: 'image', url: 'https://xxx', class: 'background_image', // delay: true, x: 0, y: 0, style: { width: width, height: width } }, { type: 'text', text: '文字', class: 'title', x: 0, y: 0, style: { fontSize: 14, lineHeight: 20, color: '#353535', fontFamily: 'PingFangSC-Regular' } }] } [代码] 如上,type声明了要元素的类型,有image、text、rect、line、circle、redius_image(圆角图)等,能满足绝大多数情况。 class类名指定了使用的样式,需要在style中写出,符合css样式规范。 delay参数用来异步绘制元素,会把此元素放在第二个循环中绘制。 x,y用来指定元素的起始坐标。 将css样式与元素分离的目的是便于管理与复用。 此种方式每个元素都相互独立,互不影响,能够满足自由度要求高的情况,可控性高。 2.2 wxml转换 第二种方式为指定wxml元素,自动获取,下面是示例: [代码] let data = { list: [{ type: 'wxml', class: '.panel .draw_canvas', limit: '.panel' x: 0, y: 0 }] } [代码] 如上,type声明为wxml时,会查找所有类名为draw_canvas的节点,并且加入到绘制队列中。 class传入的第一个类名限定了查询的范围,可以不传,第二个用来指定查找的节点,可以定义为任意不影响样式展现的通用类名。 limit属性用来限定相对位置,例如,一个文本的位置(left, top) = (50, 80), class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80)绘制。 由于小程序节点元素查询接口的局限,无法直接获取节点的文本内容和图片标签的src属性,也无法直接区分是文本还是图片,但是可以获取到dataset,所以我们需要在节点上显示地声明data-type来指明类型,再声明data-text传入文字或data-url传入图片链接。下面是个示例: [代码] <view class="panel"> <view class="panel__img draw_canvas" data-type="image" data-url="https://xxx"></view> <view class="panel__text draw_canvas" data-type="text" data-text="文字">文字</view> </view> [代码] 如上,会查询到两个节点符合条件,第一个为image图片,第二个为text文本,利用SelectorQuery查询它们的computedStyle,分别得到left、top、width、height等数据后,转换成canvas支持的格式,完成绘制。 除此之外,下面的示例功能更加丰富: [代码] <view class="panel"> <view class="panel__text draw_canvas" data-type="background-image" data-radius="1" data-shadow="" data-border="2px solid #000"></view> <view class="panel__text draw_canvas" data-type="text" data-background="#ffffff" data-padding="2 3 0 0" data-delay="1" data-left="10" data-top="10" data-maxlength="4" data-text="这是个文字">这是个文字</view> </view> [代码] 如上,第一个data-type为background-image,表示读取此节点的背景图片,因为可以通过computedStyle直接获取图片链接,所以不需要显示传入url。声明data-radius属性,表示要将此图绘成乘圆形图片。data-border属性表示要绘制图片的边框,虽然也可以通过computedStyle直接获取,但是为了避免非预期的结果,还是要声明传入,border格式应符合css标准。此外,图片的box-shadow等样式都会根据声明绘制出来。 第二个文本节点,声明了data-background,则会根据节点的位置属性给文字增加背景。 data-padding属性用来修正背景的位置和宽高。data-delay属性用来延迟绘制,可以根据值的大小,来控制元素的层级,data-left和data-top用来修正位置,支持负值。data-maxlength用来限制文本的最大长度,超长时会截取并追加’…’。 此外,data-type还有inline-text,inline-image等行内元素的绘制,其实现较为复杂,会在后文介绍。 五、Wxml2Canvas实现原理 1. 绘制流程 整个绘制流程如下: [图片] 因为小程序的限制,只能在画布上绘制本地图片,所以统一先对图片提前下载,然后再绘制,为了避免图片重复下载,内部维护一个图片列表,会对相同的图片链接去重,减少等待时间。 2. 基本图形的实现 基础图形的绘制比较简单,内部实现只是对基础能力的封装,使用者不用再关注canvas的绘制过程,只需要提供关键数据即可,下面是一个图片绘制的实现示例: [代码] function drawImage (item, style) { if(item.delay) { this.asyncList.push({item, style}); }else { if(item.y < 0) { item.y = this.height + item.y * zoom - style.height * zoom; }else { item.y = item.y * zoom; } if(item.x < 0) { item.x = this.width + item.x * zoom - style.width * zoom; }else { item.x = item.x * zoom; } ctx.drawImage(item.url, item.x, item.y, style.width * zoom, style.height * zoom); ctx.draw(true); } } [代码] 如上,x,y值坐标支持传入负值,表示从画布的底部和右侧计算位置。 3. Wxml转Canvas元素的实现 3.1 computedStyle的获取 首先需要获取wxml的样式,代码示例如下: [代码] query.selectAll(`${item.class}`).fields({ dataset: true, size: true, rect: true, computedStyle: ['width', 'height', ...] }, (res) => { self.drawWxml(res); }) [代码] 3.2 块级元素的绘制 对于声明为image、text的元素,默认为块级元素,它们的绘制都是独立进行的,不需要考虑其他的元素的影响,以wxml节点为圆形的image为例,下面是部分代码: [代码] if(sub.dataset.type === 'image') { let r = sub.width / 2; let x = sub.left + item.x * zoom; let y = sub.top + item.y * zoom; let leftFix = +sub.dataset.left || 0; let topFix = +sub.dataset.top || 0; let borderWidth = sub.borderWidth || 0; let borderColor = sub.borderColor; // 如果是圆形图片 if(sub.dataset.radius) { // 绘制圆形的border if(borderWidth) { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setStrokeStyle(borderColor) ctx.setLineWidth(borderWidth) ctx.stroke() ctx.closePath() } // 绘制圆形图片的阴影 if(sub.boxShadow !== 'none') { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setFillStyle(borderColor); setBoxShadow(sub.boxShadow); ctx.fill() ctx.closePath() } // 最后绘制圆形图片 ctx.save(); ctx.beginPath(); ctx.arc((x + r), (y + r) - limitTop, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(url, x + leftFix * zoom, y + topFix * zoom, sub.width, sub.height); ctx.closePath(); ctx.restore(); }else { // 常规图片 } } [代码] 如上,块级元素的绘制和基础图形的绘制差异不大,理解起来也很容易,不再多述。 3.3 令人头疼的行内元素的绘制 当wxml的data-type声明为inline-image或者inline-text时,我们认为是行内元素。行内元素的绘制是一个难点,因为元素之前存在关联,所以不得不考虑各种临界情况。下面展开细述。 3.3.1 纯文本换行 对于长度超过一行的行内元素,需要计算出合适的换行位置,下图所示的是两种临界情况: [图片] [图片] 如上图所示,第一种情况为最后一行只有一个文字,第二种情况最后一行的文字长度和宽度相同。虽然长度不同,但都可通过下面代码绘制: [代码] let lineNum = Math.ceil(measureWidth(text) / maxWidth); // 文字行数 let sinleLineLength = Math.floor(text.length / lineNume); // 向下取整,保证多于实际每行字数 let currentIndex = 0; // 记录文字的索引位置 for(let i = 0; i < lineNum; i++) { let offset = 0; // singleLineLength并不是精确的每行文字数,要校正 let endIndex = currentIndex + sinleLineLength + offset; let single = text.substring(currentIndex, endIndex); // 截取本行文字 let singleWidth = measureWidth(single); // 超长时,左移一位,直至正好 while(singleWidth > maxWidth) { offset--; endIndex = currentIndex + sinleLineLength + offset; single = text.substring(currentIndex, endIndex); singleWidth = measureWidth(single); } currentIndex = endIndex; ctx.fillText(single, item.x, item.y + i * style.lineHeight); } // 绘制剩余的 if(currentIndex < text.length) { let last = text.substring(currentIndex, text.length); ctx.fillText(last, item.x, item.y + lineNum * style.lineHeight); } [代码] 为了避免计算太多次,首先算出大致的行数,求出每行的文字数,然后移位索引下标,求出实际的每行的字数,再下移一行继续绘制,直到结束。 3.3.2 非换行的图文混排 [图片] 上图是一个包含表情图片和加粗文字的混排内容,当使用Wxml2Canvas查询元素时,会将第一行的内容分为五部分: 文本内容:这是段文字; 表情图片:发呆表情(非系统表情,image节点展现); 表情图片:发呆表情; 文本内容:这也; 加粗文本内容:是一段文字,这也是文字。 对于这种情况,执行查询computedStyle后,会返回相同的top值。我们把top值相同的元素聚合在一起,认为它们是同一行内容,事实也是如此。因为表情大小的差异以及其他影响,默认规定top值在±2的范围内都是同一行内容。然后将top值的聚合结果按照left的大小从左往右排列,再一一绘制,即可完美还原此种情况。 3.3.3 换行的图文混排 当混排内容出现了换行情况时,如下图所示: [图片] 此时的加粗内容占据了两行,当我们依旧根据top值归类时,却发现加粗文字的left值取的是第二行的left值。这就导致加粗文字和第一部分的文字的top值和left值相同,如果直接绘制,两部分会发生重叠。 为了避免这种尴尬的情况,我们可以利用加粗文字的height值与第一部分文字的height值比较,显然前者是后者的两倍,可以得知加粗部分出现了换行情况,直接将其放在同组top列表的最后位置。换行的部分根据lineHeight下移绘制,同时做记录。 最后一部分的文本内容也出现了换行情况,同样无法得到真正的起始left值,并且其top值与上一部分换行后的top值相同。此时应该将他的left值追加加粗换行部分的宽度,正好得到真正的left值,最后再绘制。 大多数的行内元素的展现形式都能以上述的逻辑完美还原。 六、总结 基于基础图形封装和wxml转换这两种绘制方式,可以满足绝大多数的场景,能够极大地减少工作量,而不需要再关注内部实现。在实际使用中,二者并非孤立存在,而更多的是一起使用。 [图片] 如上图所示,对于列表内容我们利用wxml读取绘制,对于下部的白色区域,不是wxml节点内容,我们可以使用基础图形绘制方式实现。二者的结合更加灵活高效。 目前Wxml2Canvas已经在公司内部开源,不久会放到github上,同时也在不断完善中,旨在实现更多的样式展现与提升稳定性和绘制速度。 如果有更好的建议与想法,请联系我。
2019-02-28 - 小程序中实现页面截图
最近接到一个需求,需要在小程序中实现页面截图,我一开始的考虑是使用官方提供的扩展组件wxml-to-canvas,但是实际体验下来效果很糟糕,首先它并不能截取实际的页面,而是必须传入[代码]wxml[代码]和[代码]wxss[代码];然后他能支持的效果也很少,并不能满足需求中稍微复杂的效果。最终我决定用web-view加载的网页中使用html2canvas来实现功能。 实际代码 网页部分我用了[代码]vue[代码],首先需要安装[代码]html2canvas[代码] [代码]npm install html2canvas [代码] 页面中引入 [代码]import html2canvas from 'html2canvas'; [代码] 需要截图的dom节点上添加ref属性 [代码]<div ref="page"> [代码] 截图代码 [代码]... document.body.scrollTop = 0; // 将页面滚动至顶部后再开始截图,才能保证截图的完整 html2canvas(this.$refs.page, { allowTaint: false, useCORS: true, width: document.body.scrollWidth, height: document.body.scrollHeight // 实际体验中发现最好设置宽高为页面的宽高才能获得完整的截图 }).then(canvas => { this.savedPic = canvas.toDataURL('images/png') // 用于在页面中展示的截图完成的网址 ... // 以下代码为模拟a标签的点击直接下载截图 // 但是这部分代码在移动端网页和小程序中并不会生效 let a = document.createElement('a'), blob = this.dataURLToBlob(canvas.toDataURL('images/png')); a.setAttribute('href', URL.createObjectURL(blob)); a.setAttribute('download', 'pic.png'); document.body.appendChild(a); a.click(); URL.revokeObjectURL(blob); document.body.removeChild(a); }); ... [代码] 兼容性 网页毕竟不是原生小程序,还是会存在一些兼容性问题,比如网页中不能使用小程序的wx.saveImageToPhotosAlbum直接保存生成好的截图。移动端和微信中也不支持模拟a标签的点击来下载图片,最终只能通过展示生成的截图并提示用户长按图片来实现保存图片的功能,用户体验会差点,但是考虑到截图效果比[代码]wxml-to-canvas[代码]好太多了,还是可以接受的。 最后说一下[代码]html2canvas[代码]的支持度,目前实际用下来发现不支持的样式为阴影和伪元素,其他基本上都支持。网页中的图片必须为本地图片或者支持跨域的网络图片。用到图片的地方建议直接使用[代码]img[代码]标签,而不是背景图片,[代码]img[代码]标签展示的图片清晰度远远高于背景图片。
2021-07-13 - [开盖即食]小程序图表插件 ECharts 实战
[图片] H5时代用来做图表的插件有很多比如:[代码]ECharts[代码]、[代码]Bizcharts[代码]、[代码]JSCharts[代码]等,而这次的小程序本人选用了 ECharts 作为图表组件。 1、选择原因主要有3点: 官方某度在持续维护这个插件 官方推出了直接适配小程序的版本,且有demo,开盖即食,不用迁移 简单实用,覆盖面广且可通过配置控制包的大小,小程序毕竟大小有限制~ eCharts来自BAT中的B前端团队,对应的小程序版本为:echarts-for-weixin 官网地址 https://echarts.apache.org/ github地址 https://github.com/ecomfe/echarts-for-weixin 小程序demo地址 https://github.com/ecomfe/echarts-examples 2、用法 (1)官方教程 [代码]index.json[代码] 配置如下: [代码]{ "usingComponents": { "ec-canvas": "../../ec-canvas/ec-canvas" } } [代码] 这一配置的作用是,允许我们在 [代码]pages/bar/index.wxml[代码] 中使用 [代码]<ec-canvas>[代码] 组件。注意路径的相对位置要写对,如果目录结构和本例相同,就应该像上面这样配置。 [代码]index.wxml[代码] 中创建了一个 [代码]<ec-canvas>[代码] 组件: [代码]<view class="container"> <ec-canvas id="mychart-dom-bar" canvas-id="mychart-bar" ec="{{ ec }}"></ec-canvas> </view> [代码] 其中 [代码]ec[代码] 是一个我们在 [代码]index.js[代码] 中定义的对象,它使得图表能够在页面加载后被初始化并设置。 [代码]index.js[代码] 配置: [代码]function initChart(canvas, width, height, dpr) { const chart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr // 像素 }); canvas.setChart(chart); var option = { ... }; chart.setOption(option); return chart; } Page({ data: { ec: { onInit: initChart } } }); [代码] 这对于所有 ECharts 图表都是通用的,用户只需要修改上面 [代码]option[代码] 的内容,即可改变图表。[代码]option[代码] 的使用方法参见 ECharts 配置项文档。 官方demo里的一些用法指导: 如何延迟加载图表? 参见 [代码]pages/lazyLoad[代码] 的例子,可以在获取数据后再初始化数据。 如何在一个页面中加载多个图表? 参见 [代码]pages/multiCharts[代码] 的例子。 如何使用 Tooltip? 目前,本项目已支持 ECharts Tooltip,但是由于 ECharts 相关功能尚未发版,因此需要使用当前本项目中 [代码]ec-canvas/echarts.js[代码],这个文件包含了可以在微信中使用 Tooltip 的相关代码。目前在 ECharts 官网下载的 [代码]echarts.js[代码] 还不能直接替换使用,等 ECharts 正式发版后即可。 具体使用方法和 ECharts 相同,例子参见 [代码]pages/line/index.js[代码]。 如何保存为图片? 参见 [代码]pages/saveCanvas[代码] 的例子。 (2)本人实战操作 [图片] [代码]import * as echarts from '../ec-canvas/echarts'; const app = getApp(); let chart; function initChart(canvas, width, height, dpr) { chart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr // new }); canvas.setChart(chart); chart.setOption(option); return chart; } var option = { title: { text: '智酷君 echarts 切换效果测试', left: 'center' }, tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' }, legend: { orient: 'vertical', left: 10, data: ['AAA', 'BBB', 'CCC', 'DDD', 'EEE'] }, series: [ { name: '访问来源', type: 'pie', radius: ['50%', '70%'], avoidLabelOverlap: false, label: { show: false, position: 'center' }, emphasis: { label: { show: true, fontSize: '30', fontWeight: 'bold' } }, labelLine: { show: false }, data: [ {value: 335, name: 'AAA'}, {value: 310, name: 'BBB'}, {value: 234, name: 'CCC'}, {value: 135, name: 'DDD'}, {value: 1548, name: 'EEE'} ] } ] }; Page({ data: { ec: { onInit: initChart } }, onLoad: function () {}, //单曲线 line() { let option2 = { title: { text: '同一canvas更新成折线图', left: 'center' }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line' }] }; chart.setOption(option2) }, //切换柱状图 bar(){ let option3 = { title: { text: '直接更新数据,减少性能消耗', left: 'center' }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [120, 200, 150, 80, 70, 110, 130], type: 'bar', showBackground: true, backgroundStyle: { color: 'rgba(220, 220, 220, 0.8)' } }] }; chart.setOption(option3) } }) [代码] 建议大家尽量使用同一个canvas对象来切换不同的图表效果,而不是初始加载多个不同的,我们可以将 [代码]chart[代码] 对象设置为全局,然后通过 [代码]chart.setOption()[代码] 的方法来更新配置数据,可以减少性能消耗避免闪退等 (3)代码片段 代码段:https://developers.weixin.qq.com/s/OOTwnsms7Cin 建议将IDE工具升级到 1.02.18以上,避免一些BUG [图片] 3、Tips (1)包大小可以配置 在线定制地址: https://echarts.apache.org/zh/builder.html [图片] [图片] 通过选择和配置想要的功能,可以大大减少原本JS包的尺寸。 (2)Canvas 2d 版本要求 最新版的 ECharts 微信小程序支持微信 Canvas 2d,当用户的基础库版本 >= 2.9.0 且没有设置 [代码]force-use-old-canvas="true"[代码] 的情况下,使用新的 Canvas 2d(默认)。 使用新的 Canvas 2d 可以提升渲染性能,解决非同层渲染问题,强烈建议开启 如果仍需使用旧版 Canvas,使用方法如下: [代码]<ec-canvas id="xxx" canvas-id="xxx" ec="{{ ec }}" force-use-old-canvas="true"></ec-canvas> [代码] (3)数据点过多造成闪退和卡死 本人简单测试了下,iphone7p手机在[代码]1500个[代码]左右数据点的时候,出现了小程序闪退,iphoneX 测试下来大概在[代码]2500个[代码]左右,猜测可能由于微信本身给小程序的内存有限,所以建议大家控制数据点的个数 (4)单页面图表canvas加载过多卡死 建议单页面图表加载不要超过[代码]5个canvas[代码],尽可能共用一个图表Canvas对象,通过动态更新数据的方式来展示内容(还有帅气的特效),如果一定要加载多个canvas的话,建议控制数量,提供复用性~ 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-06-29 - 登录接口又双叕变了,三行代码挑战全网最少修改工作量
小程序登录、用户信息样关接口又双叕变了。 https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801 几家悲伤几家愁。。。 微信的一小步,人猿的一大步。。。 没办法,改吧。。。 翻出以前小程序这部分的代码,惊喜地发现,只需要三行代码,就能平滑过渡; 感谢我以前看似丑陋却很省事的登录代码逻辑!!! 登录逻辑如下: 1、判断库里有用户的信息没有,没有,则wx.navigateTo一个专门的授权页面:auth 2、授权成功后获得userInfo,保存到库里; auth页代码修改如下: auth.wxml: 修改一行代码 <button style='margin:15px;font-size:16px' type='primary' size="mini" bindtap='getUserProfile'>授权微信头像和昵称</button> auth.js: 修改两行代码 //原wx.getUserInfo接口 getUserInfo: function (e) { let userInfo = e.detail.userInfo if (userInfo) this.onSaveUserInfo(userInfo) }, //新增wx.getUserProfile接口 getUserProfile: function (e) { wx.getUserProfile({ desc: '业务需要', success: res => this.onSaveUserInfo(res.userInfo) }) }, //保存userInfo到DB onSaveUserInfo:function(userInfo){ console.log(app.globalData.userInfo = userInfo) db.collection('user') .where({ _id: this.openid }) .count() .then(res => { if (res.total > 0) { //doc.update db.collection('user').doc(this.openid).update({ data: userInfo }).then(res => console.log(res)) } else { //doc.add db.collection('user').doc(this.openid).add({ data: userInfo }).then(res => console.log(res)) } }) wx.navigateBack() }, 以下是判断用户信息是否存在的代码: xxxx.js: onSubmit:async function () { if (await app.hasUserInfo()) { } else return //其他代码 }, app.js: hasUserInfo: async function () { if (this.globalData.userInfo && this.globalData.userInfo.nickName && this.globalData.userInfo.avatarUrl) return true let res = await wx.cloud.database().collection('user').doc(this.openid).get().catch(err => console.log(err)) if (res && res.data && res.data.nickName && res.data.avatarUrl) { this.globalData.userInfo = res.data return true } else { wx.navigateTo({ url: '/base/auth/auth' }) return false } }, 关于用户信息自动更新: 我们一直以来的方法如下: 1、留给用户手动授权的入口,用户更换头像后,发现自己的头像不显示,则需要手动授权刷新userInfo; 2、一般会在这个页面:我的--个人信息--授权微信头像和昵称,用户点击后,wx.navigateTo到授权页。 笔者团队认为:用户信息自动更新其实是个伪需求。理由如下: 假设某用户修改了头像: 1、用户自己打开小程序,发现头像和昵称怎么没有改过来,那么手动更新一下。用户体验没毛病,没必要非要自动更新; 2、用户如果后来不再进入小程序,别人看到的都是一张碎的头像,那么此时,自动更新也毫无作用,因为该用户都不打开小程序。 3、微信团队肯定考虑过自动更新这种要求,但他们宁愿千夫所指,也依然坚持推出新的登录接口,那就肯定是已经经过了中国最牛逼团队的全面考衡了。 补充: 登录和授权其实是两码事,可以毫无关系这么说,以上的内容主要都是关于授权微信用户信息的,下面补充一下登录的内容: 登录其实就是获取用户的openid,我们一直采用云函数来获取openid。方案如下: 在每个页面:page.js: onLoad: async function (options) { this.openid = await app.getOpenid() }, 在app.js: getOpenid: async function () { if (this.openid) return this.openid let res = await this.globalData.cloud.callFunction({ name: 'login' }) console.log(res) return this.openid = res.result.FROM_OPENID||res.result.OPENID },
2021-04-07 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15 - 05.适配 wx.getUserProfile 的一点简单想法
先看官方的最新通知 周知:getUserInfo 开发版和体验版 已对齐 getUserInfo 匿名表现,正式版将于 4月13日 正式对齐 getUserInfo 匿名表现。 请开发者使用 getUserProfile 获取用户信息。 小程序登录、用户信息相关接口调整说明:https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801 原先的getUserInfo能力 原先的 getUserInfo 的能力,具体看这里:https://developers.weixin.qq.com/community/develop/doc/000c2424654c40bd9c960e71e5b009?highline=getUserInfo [图片] [图片] 我对 getUserInfo 的理解 我们一般开发者,会用 wx.getUserInfo 来实现 openId 和 用户信息的获取,是非常的便捷的。 并且还能通过 wx.getSetting 获取 "scope.userInfo" 判断用户用户是否已经授权。 wx.getSetting + wx.getUserInfo 能近似完美解决用户登陆授权场景 华丽的分割线 2021-02-04 官方要对小程序登录、用户信息相关接口调整说,具体链接:https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801?blockType=1 这样我们开发者会面临的用户登陆授权的调整,会比较痛,但也必须要去改,因为没办法。 怎么改呢 先不要着急去适配 getUserProfile,先要弄清楚自己的小程序用 getUserInfo 获取那些能力?实现哪些能力? 我的理解,如果是用 wx.getSetting + wx.getUserInfo 来实现获取用户的openId(unioinId) 和用户头像昵称信息的话,可以这样去调整? wx.login 可以拿到用户的 openId,流程如下: [图片] 如果绑定了开发平台,还可以通过 wx.login 静默获取用户的 uninonId [图片] 这个过程是静默的,不需要用户参与。 然后对于需要用户头像信息的时候,在使用 wx.getUserProfile 能力即可。 小结 1.wx.login + wx.getUserProfile 能实现 wx.getUserInfo 的能力,也能满足我们的业务场景。 2.看到官方的调整,先不要着急去调整,先弄清楚要我们的业务场景使用了那些API获取那些能力 3.关于wx.getSetting 返回 "scope.userInfo" 为 undefined。我记得社区有人提问了,等官方回复就好,其实我们可以不依赖这个,原因留给你 4.用户头像更新不及时的问题,我们看看微信聊天记录,当你好友更新头像,当你不点击时,有时候展示的还是老头像,当你点击时,会显示新头像(我们何不借鉴一下,提供给用户更新的能力即可)
2021-04-09 - 扫普通二维码跳转小程序页面. 传多个动态参数?
我传 devuser-opengas.ecej.com/user/app/2/1/house?action=gogogo&code=3 都不对 跳转的不对 已发布状态 [图片]
2020-12-11 - 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 - 小程序注销问题汇总
1、注销成功后,流程不可逆? 帐号一旦成功注销,流程不可逆。 2、小程序注销过程中,是否可以撤销注销,恢复使用? 冻结期间可登录小程序后台,点击“取消注销”,可恢复帐号正常使用。 3、组织类型如何修改打款信息? 在发起注销申请后,需自行填写帐号主体正确的对公账户信息。若您不小心填写错,建议等待验证失败或者超时未验证后再次重新填写。 4、组织类型支付验证打款后,多久验证成功? 若打款账户信息和金额正确,1个工作日内会验证成功。 5、小额打款验证成功后,退款到账时间多久? 验证成功后,打款金额退回具体到账时间视银行而定,一般为3-10个工作日内,原路退回。 6、管理员手机端的注销帐号确认,不小心删除,是否支持补发? 不支持。目前管理员确认注销有效期为7天,且每天都会下发提醒,建议您留意下一次的消息提醒。 注:若已确认,将不会再下发消息提醒。 7、注销流程中是否会通知小程序项目成员? 在进入冻结期、取消注销、注销成功时会下发模板消息通知小程序项目成员。 8、小程序注销成功后已关联该小程序的的公众号是否会收到通知? 在冻结期、注销成功时会下发模板消息通知关联该小程序的公众号管理员。 9、小程序冻结期间,用户可以正常访问吗? 已发布小游戏冻结期间用户可正常访问小游戏,每日首次访问,弹窗提示用户该小游戏即将下架;其余类型小程序用户无法访问。 10、已发布的小游戏,在注销期间,虚拟支付如何结算? 虚拟支付延期30天结算。 11、已发布的小游戏,在注销期间,用户可以进行支付吗? 不可以。 12、小程序注销条件是什么? ①小程序必须是已注册成功的帐号。 ②已开通广告主服务的小程序广告投放账户余额须为零。 ③须自主暂停线上小程序版本服务(除已发布小游戏帐号外)。 温馨提示: 海外小程序不支持注销。 小程序帐号内已发布插件线上版本的不支持注销。 13、小程序注销冻结期内,帐号还可以使用吗? 普通小程序、未发布的小游戏: 冻结期7天,帐号所有功能不可用,资源仍为占用 已发布的小游戏: ①冻结期为30天,资源仍为占用 ②已发布的小游戏冻结期内部分功能可用,但需关闭充值功能 ③虚拟支付延期30天结算 温馨提示:冻结期间可登录小程序后台,点击“取消注销”,可恢复帐号正常使用。 14、注销成功后可以释放哪些资源? ①管理员确认注销后,立即释放以下资源: 绑定邮箱、主体名称、管理员信息(姓名、身份证号、手机号码、微信号)、项目成员信息、关联关系。 ②成功注销后,原来的昵称有2*24小时(即2天)的保护期,在此期间,符合命名唯一规则情况下,同一主体下的其他帐号可以使用该名称,主体不一致的,则需要在保护期满后才能申请使用该名称。 温馨提示:冻结期内,资源仍处于占用状态
2020-03-18 - 小程序开发必备,这 5 款超实用开源插件!
Parser - 富文本解析 GitHub:https://github.com/jin-yufeng/Parser [图片] calendar - 预约日历 GitHub:https://github.com/jasondu/wxa-plugin-calendar [图片] cropper - 图片裁剪 GitHub:https://github.com/wx-plugin/image-cropper [图片] wxSearch - 搜索框 Github:https://github.com/icindy/wxSearch [图片] WxValidate - 表单验证 Github:https://github.com/wux-weapp/wx-extend/blob/master/docs/components/validate.md [图片] 如有收获,记得点赞、收藏 如有补充或者疑问,欢迎进行留言讨论
2020-08-14 - 分享一个固定头和列的 table 组件的简单实现
本案案例基于 WePY 实现,大家可根据自身需要进行更改扩展。 代码地址>> 演示 [图片] 演示视频地址>> 实现原理 [图片] 橙色和紫色区域组成了横向滚动的 [代码]scroll-view[代码]。 红色虚线区域是纵向滚动的 [代码]scroll-view[代码]。但由于绿色区域设置了 [代码]pointer-events: none;[代码],即实际只能触摸橙色区域。通过在橙色区域绑定的 [代码]scroll[代码] 事件(纵向),实时设置绿色虚线区域的 [代码]scrollTop[代码]。 紫色区域是固定头部,绿色区域是固定列。左上角的绿色区域是横向与纵向共同固定的区域。 实现要点 绑定了 [代码]scroll[代码] 事件的 [代码]scroll-view[代码] 要指定 [代码]throttle: false[代码],否则回调函数有可能取不到最终位置的 [代码]scrollTop[代码] 值。官方文档目前未提及此属性,参考资料>>。 固定列需要设置 [代码]pointer-events: none;[代码],实现点击穿透。使得 [代码]tbody[代码] 能触发 [代码]scroll[代码] 事件,而不是为固定列也绑定 [代码]scroll[代码] 事件。 找出每列的最大单元格作为该列的宽度,当然你也可以显示设置。 peace out!👋 小程序 Bug 2019.09.03 更新 当将该组件至于 Popup 弹框,且该弹框通过 [代码]visibility: hidden/visible[代码] 切换,那么在 iOS 中,会使固定列([代码].table__fixed-columns[代码])的 [代码]pointer-events: none[代码] 失效。
2019-09-03 - 实战分享: 小程序云开发玩转订阅消息(二)
[图片]这是实战分享: 小程序云开发玩转订阅消息的第二部分 第一部分链接 《实战分享: 小程序云开发玩转订阅消息(一)》 将订阅消息存入云开发数据库接下来我们创建一个云函数 [代码]subscribe[代码] ,这个云函数的作用是将用户的订阅信息存入云开发数据库的集合 [代码]messages[代码] 中,等待将来需要通知用户时进行调用。 在微信开发者工具的云开发面板中创建数据库集合 [代码]messages[代码] [图片]微信开发者工具新增数据库集合 创建一个 [代码]subscribe[代码] 云函数,在云函数中我们将小程序端发送过来的课程订阅信息,存储在云开发数据库集合中,开发完成后,在微信开发者工具中右键上传并部署云函数。 cloudfunctions/subscribe/index.js [代码]const cloud = require('wx-server-sdk'); cloud.init(); const db = cloud.database(); exports.main = async (event, context) => { try { const {OPENID} = cloud.getWXContext(); // 在云开发数据库中存储用户订阅的课程 const result = await db.collection('messages').add({ data: { touser: OPENID, // 订阅者的openid page: 'index', // 订阅消息卡片点击后会打开小程序的哪个页面 data: event.data, // 订阅消息的数据 templateId: event.templateId, // 订阅消息模板ID done: false, // 消息发送状态设置为 false }, }); return result; } catch (err) { console.log(err); return err; } }; [代码]利用定时触发器来定期发送订阅消息接下来我们需要实现一个定时执行的云函数[代码]send[代码],来检查数据库中是否有需要发送给用户的订阅消息。如果有需要发送的订阅消息,会通过云调用 [代码]cloud.openapi.subscribeMessage.send[代码] 将订阅消息发送给用户。 创建一个名叫 [代码]send[代码] 的云函数,首先要配置云函数,在 [代码]config.json[代码] 的 [代码]permissions[代码] 中新增 [代码]subscribeMessage.send[代码]的云调用权限,然后新增一个 [代码]sendMessagerTimer[代码] 的定时触发器,定时触发器的语法和 [代码]linux[代码] 的 [代码]crontab[代码] 类似,比如,我们配置的 [代码]"0 * * * * * *"[代码] 代表每分钟执行一次云函数。 cloudfunctions/send/config.json [代码]{ "permissions": { "openapi": ["subscribeMessage.send"] }, "triggers": [ { "name": "sendMessagerTimer", "type": "timer", "config": "0 * * * * * *" } ] } [代码]接下来是实现发送订阅消息的云函数,这个云函数会从云开发数据库集合[代码]messages[代码]中查询等待发送的消息列表,检查数据库中是否有需要发送给用户的订阅消息,发送条件可以根据自己的业务实现,比如开课提醒可以根据课程开课日期来检查是否需要发送订阅消息,在我们下面的代码示例里做了简化,筛选条件只检查了状态为未发送。 查询到待发送的消息列表之后,我们会循环消息列表,依次发送每条订阅消息,发送成功后将数据库中消息的状态改为已发送。 cloudfunctions/send/index.js [代码]const cloud = require('wx-server-sdk'); exports.main = async (event, context) => { cloud.init(); const db = cloud.database(); try { // 从云开发数据库中查询等待发送的消息列表 const messages = await db .collection('messages') // 查询条件这里做了简化,只查找了状态为未发送的消息 // 在真正的生产环境,可以根据开课日期等条件筛选应该发送哪些消息 .where({ done: false, }) .get(); // 循环消息列表 const sendPromises = messages.data.map(async message => { try { // 发送订阅消息 await cloud.openapi.subscribeMessage.send({ touser: message.touser, page: message.page, data: message.data, templateId: message.templateId, }); // 发送成功后将消息的状态改为已发送 return db .collection('messages') .doc(message._id) .update({ data: { done: true, }, }); } catch (e) { return e; } }); return Promise.all(sendPromises); } catch (err) { console.log(err); return err; } }; [代码]最终效果 [图片]开课提醒订阅消息截图 源代码https://github.com/binggg/tcb-subscribe-demo[3] 参考资料 [1]注册小程序帐号: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-register/ [2]开通云开发服务: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-service/ [3]https://github.com/binggg/tcb-subscribe-demo: https://github.com/binggg/tcb-subscribe-demo
2019-10-23 - 小程序中使用css var变量,使js可以动态设置css样式属性
使用sass,stylus可以很方便的使用变量来做样式设计,其实css也同样可以定义变量,在小程序中由于原生不支持动态css语法,so,可以使用css变量来使用开发工作变简单。 基本用法 基础用法 [代码]<!--web开发中顶层变量的key名是:root,小程序使用page--> page { --main-bg-color: brown; } .one { color: white; background-color: var(--main-bg-color); margin: 10px; } .two { color: white; background-color: black; margin: 10px; } .three { color: white; background-color: var(--main-bg-color); } [代码] 提升用法 [代码]<div class="one"> <div class="two"> <div class="three"> </div> <div class="four"> </div> <div> </div> [代码] [代码].two { --test: 10px; } .three { --test: 2em; } [代码] 在这个例子中,[代码]var(--test)[代码]的结果是: class=“two” 对应的节点: 10px class=“three” 对应的节点: element: 2em class=“four” 对应的节点: 10px (继承自父级.two) class=“one” 对应的节点: 无效值, 即此属性值为未被自定义css变量覆盖的默认值 上述是一些基本概念,大致说明css变量的使用方法,注意在web开发中,我们使用[代码]:root[代码]来设置顶层变量,更多详细说明参考MDN的 文档 妙用css变量 开发中经常遇到的问题是,css的数据是写死的,不能够和js变量直通,即有些数据使用动态变化的,但css用不了。对了,可以使用css变量试试呀 wxml js [代码]// 在js中设置css变量 let myStyle = ` --bg-color:red; --border-radius:50%; --wid:200px; --hgt:200px; ` let chageStyle = ` --bg-color:red; --border-radius:50%; --wid:300px; --hgt:300px; ` Page({ data: { viewData: { style: myStyle } }, onLoad(){ setTimeout(() => { this.setData({'viewData.style': chageStyle}) }, 2000); } }) [代码] wxml [代码]<!--将css变量(js中设置的那些)赋值给style--> <view class="container"> <view class="my-view" style="{{viewData.style}}"> <image src="/images/abc.png" mode="widthFix"/> </view> </view> [代码] wxss [代码]/* 使用var */ .my-view{ width: var(--wid); height: var(--hgt); border-radius: var(--border-radius); padding: 10px; box-sizing: border-box; background-color: var(--bg-color); transition: all 0.3s ease-in; } .my-view image{ width: 100%; height: 100%; border-radius: var(--border-radius); } [代码] 通过css变量就可以动态设置css的属性值 代码片段 https://developers.weixin.qq.com/s/aWfUGCmG7Efe github 小程序演示 [图片]
2020-03-05 - swiper 如何禁止用户手动滑动,只自动播放
swiper 仅用于幻灯片自动播放,但不允许用户手动滑动。有参数设定吗? 我在搜了一下网上用的: <swiper-item catchtouchmove="stopTouchMove"></swiper-item > 这个方法正常用起来感觉是可以的,但我发现一个小bug: 假设 A、B两页 用js代码滑动改变swiper的“current”,从A页滑动到B页的一瞬间(B页面出来十分之一前),手指反方向滑动回去,能将页面换会A页
2019-02-19 - 筛选分类,阻止底层页面穿透滚动
采用movable-area,movable-view,catchtouchmove实现筛选时,阻止底层页面穿透滚动,根据之前的项目需求,做了相应的简化: movable-area,movable-view实现弹窗内筛选项的滚动 catchtouchmove阻止页面滚动 https://developers.weixin.qq.com/s/xUPdx7mX75b8
2019-09-04 - 页面骨架图加载
什么是骨架图加载? 看图 [图片] 具体实现 1. 引入vant-weapp有赞小程序UI框架提供的van-skeleton组件 2. 代码 [代码]// 请求接口时设置indexGetIng为true,就会显示这个block,就达到了在请求数据的时候显示列表骨架图 <block wx:if="{{indexGetIng||indexList.length}}"> <view class="list"> <list-item type="index" list="{{indexGetIng?5:indexList}}"></list-item> </view> <load-more hasMore="{{indexHasMore}}"></load-more> </block> // 接口请求完毕了,设置indexGetIng为false,indexList.length=0,就显示这个block,提示无内容 <block wx:else> <null-page> <view class="null-tip">暂无相关内容哦~</view> </null-page> </block> [代码] 上面代码运行如下: 1)、加载中显示骨架图: [图片] 2)、加载完,如果请求到数据了: [图片] 3)、加载完,如果没有请求到数据: [图片] 3. 接下来说说list-item子组件中是如何显示骨架图的: 从上面的代码看出,在请求数据的时候给子组件的list-item传了个5,这个5就是在子组件需要渲染的骨架图列表数量 [代码] // 通过判断父组件传过来的list是不是数组,来判断是否要显示列表数据 <block wx:if="{{util.isArray(list)}}"> <view>列表数据展示</view> </block> // 如果父组件传过来的不是数组,而是5,即父组件在请求列表数据,这里就循环显示5个骨架图列表,具体属性参照van-skeleton文档 <block wx:else> <!--这里的list 为渲染的骨架图数量--> <view class="item" wx:for="{{list}}"> <van-skeleton title avatar avatar-size="85px" avatar-shape="square" row="3" row-width="{{['100%','100%','80%']}}" row-height="{{['100px','16px','16px']}}" loading="{{true}}" > </van-skeleton> </view> </block> [代码] 通过查阅van-skeleton文档,发现并没有 row-height属性,这是我自己改的van-skeleton组件的源码,(因为这个组件只能设置骨架灰图的宽,不能设置高,默认高度为16px),具体改动如下: [代码]// skeleton.js props: { rowWidth: { type: null, value: '100%', observer(val) { this.setData({ isArray: val instanceof Array }); } }, /*TODO 新增*/ rowHeight: { type: null, value: '16px', observer(val) { this.setData({ isArrayHeight: val instanceof Array }); } }, }, // skeleton.wxml // 在这个view中的style属性中增加 height:' + (isArrayHeight ? rowHeight[index] : rowHeight) <view wx:for="row" wx:key="index" wx:for-index="index" class="{{ utils.bem('skeleton__row') }}" style="{{ 'width:' + (isArray ? rowWidth[index] : rowWidth) + ';height:' + (isArrayHeight ? rowHeight[index] : rowHeight) }}" /> [代码] 这样就可以设置骨架灰图的高度了,效果如下 [图片] 第一次写技术分享文章,这是小白我在工作中遇到的问题的解决方案,抛砖引玉,麻烦大佬们多多提建议!
2020-01-07 - IOS手机真机测试,子组件中元素fixed定位设置z-index问题
在IOS手机真机测试中发现,在引用的子组件中,其某个元素fixed定位设置z-index的值大于父组件中的fixed定位元素的z-index值,但是子组件却在父组件元素层级下面
2019-05-30 - 小程序实现看一看视频滑动切换
最终效果 由于很多人不知道看一看还有个视频功能,所以这里先让大家看下我们最终要完成的效果。 它的入口在 发现 -> 看一看 -> 精选 -> 随便找个视频点进去即可。 [图片] 初步想法 由效果可以看出,其实就是需要监听视频的滚动,当超出可视区范围多少px,就切换到下一个视频。 要实现这个功能,大多数人的想法都是:监听scroll 事件后,在调用目标元素的getBoundingClientRect()方法,得到它对应于视图的坐标,再判断是否在可视区域之内,然后切换视频。 缺点 但是这样做的缺点是:调用目标元素的getBoundingClientRect 是会触发重排的,尤其是元素一多起来,调用所有元素的getBoundingClientRect得到信息在进行判断所以很容易造成性能问题。而且这种切换计算的逻辑会写的非常复杂,可以自行脑补一下。 所以我们要换个思维,不要通过监听scoll事件去计算目标元素距离顶部或者底部距离。而应该是直接监听当前目标元素是否还在可视区域内。当离开可视区域的时候,切换到下一个。 整理完大致思路之后,终于要开搞了。 [图片] 实现 前面已经分析了要通过监听当前目标元素是否还在可视区域内来做切换的动作,那么有什么API是可以用来做这件事的呢? 答案是: IntersectionObserver API 这个API是用来观察目标元素与指定元素交集的变化。当交集 < 0 的时候,说明不在指定元素区域内。当目标元素进入或者退出指定元素的时候,会执行相应的回调函数。所以我们可以通过这个API注册一个回调函数用于切换视频。 在小程序里,同样提供了这个API,是IntersectionObserver。 有了这个API,就可以开始干活了。由于我们这个区域是一个滚动区域,所以我用了scoll-view。 index.wxml 文件 [代码]<scroll-view> <view wx:for="{{ videos }}" wx:for-index="idx" wx:for-item="videoItem"> <!-- <view class="{{ currentPlayVideoIndex === idx ? 'active test' : 'test'}}" data-index="{{ idx }}" id="{{ videoItem.video_id }}"> {{ idx }}dddd</view> --> <span class="{{ currentPlayVideoIndex === idx ? 'active' : ''}}">{{ idx }}ddddddd </span> <video id="{{ videoItem.video_id }}" data-index="{{ idx }}" preload src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" class='video-item' muted controls> </video> </view> </scroll-view> [代码] index.wxss [代码].video-item { height: 450px; } .test { width: 100%; height: 450px; border: 1px solid red; padding: 30px; } .active { color: pink; } [代码] [代码]Page({ /** * 页面的初始数据 */ data: { videos: [{ video_id: 'mpVideo0', url: 'http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400', }, { video_id: 'mpVideo1', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo2', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo3', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo4', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo5', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo6', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo7', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }], currentPlayVideoIndex: 0, isActive: true }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { // onLoad 的时候立刻调用handleVideoScroll // 对视频进行监听 this.handleVideoScroll(); // cgi请求,用于获取videos 的数据,由于是demo演示,我直接写死了videos... }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, controlVideos: function (res) { console.log('调用controvideos', res); }, handleVideoScroll: function () { const currentId = this.data.videos[this.data.currentPlayVideoIndex].video_id; // 关键代码 // relativeToViewport 这里指定对比的就是viewport,viewport的意思就是document中的可视区域 this.observerObj = wx.createIntersectionObserver().relativeToViewport(); console.log('listen ' + currentId); // 监听目标视频跟viewport相交区域的变化 this.observerObj.observe(`#${currentId}`, this.controlVideos); } }) [代码] copy上面的代码进入小程序,你就会看到这样一个界面。 [图片] 其中外面的一圈表示的是viewPort,里面一层就是我们现在正在监听的视频,我用右上角的粉色字体来标记了,它的回调函数是controlVideos。当目标视频进入或者退出viewport的时候,controlVideos就会执行。 onLoad的时候执行了handleVideoScroll,这时候开始对目标视频进行监听,此时目标元素在viewport内,所以会调用controVideos,打印出相关信息。 [图片] 其他的字段先不说,其中的intersectionRatio表示了他们相交的比例。其中1表示完全在viewport内,0表示不在viewport内。 如果我持续去滚动第一个视频,直到它看不到了,就会看到控制台打印出 [图片] 这时候的intersectionRatio = 0,代表已经不在viewport内了,所以我们就可以将currentPlayIndex 切换到下一个了。 切换代码如下: [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放 // 并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 切换到下一个视频 this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 注意切换完成之后,还需要在调用handleVideoScroll 来对下一个视频进行绑定 this.handleVideoScroll(); }); } }, [代码] 到这一步,应该就可以看到这样的向下切换的效果了。 [图片] 但是,我们现在只是做下向下滚动的切换。那么向上的呢?要做向上滑动的切换,首先要知道视频是在向下还是向上滑动。这里有个字段可以帮助我们识别:boundingClientRect 。 它表示的是目标元素相对与viewport的节点信息。当视频向上滚动的时候,它距离viewport的top值为负,向下滚动的时候,为正值。 [图片] 有了这个字段,我们就可以通过判断向上还是向下的滚动,来切换视频了。 [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放,并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 当top < 0的时候,说明是在向上滑动,这时候currentPlayVideoIndex 需要加1 if (res.boundingClientRect.top < 0) { if (currentPlayVideoIndex < this.data.videos.length - 1) { this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 同时解绑第一个视频,保证同一个时间只监听一个视频 this.observerObj.disconnect(); this.handleVideoScroll(); }); } } else { // 当top > 0的时候,说明是在向下滑动,这时候currentPlayVideoIndex 需要减1 if (currentPlayVideoIndex - 1 < 0) { return; } this.setData({ 'currentPlayVideoIndex': this.data.currentPlayVideoIndex - 1 }, () => { this.observerObj.disconnect(); this.handleVideoScroll(); }) } }, [代码] 但是我们的产品在体验的过程中,会提出并不是完全看不见了才去切换,可能想要还剩个150px就切换了,所以我这里要对viewport调整一下 [代码] this.observerObj = wx.createIntersectionObserver().relativeToViewport({ top: -300, bottom: -300 }); [代码] 完成之后,你就可以缓缓的滑动你的视频,实现视频切换的效果了。可以看到当视频差不多被遮住不到一半,就开始切换了。 [图片] 总结 整个过程其实就是好好利用了IntersectionObserver这个API而已。当然现在只是一个非常简单的实现,性能问题,以及快读滑动的情况都无法应对,我们下一篇在接着~。
2019-08-15 - 小程序canvas绘制base64的二维码图片,真机上不显示
后端接口返回二维码的base64格式图片,canvas绘制图片:ctx.drawImage(qrcodePath, xx, xx, xx, xx),开发者工具显示正常,但是真机上无法显示canvas绘制的二维码图片。手机是iphone 6sp
2018-12-05 - 如何实现一个自定义数据版省市区二级、三级联动
社区可能有其他的方案了,但是再分享下吧,给有需要的童鞋。 效果图: [图片] 额,这个视频转GIF因为社区上传不了大图,所以剪了一部分,具体的效果还是直接工具打开代码片段预览吧~ 第一步:你的页面JSON引入该组件: [代码]{ "usingComponents": { "city-picker": "/components/cityPicker/index" } } [代码] 第二步:你的页面WXML引入该组件 [代码]<city-picker visible="{{visible}}" column="2" bind:close="handleClick" bind:confirm="handleConfirm" /> [代码] 第三步:你的页面JS调用 [代码]// 显示/隐藏picker选择器 handleClick() { this.setData( visible: !this.data.visible }) }, // 用户选择城市后 点击确定的返回值 handleConfirm(e) { const { detail: { provinceName = '', provinceId = '', cityName, cityId='', areaName = '', areaId = '' } = {} } = e this.setData({ cityId, cityName, areaId, areaName, provinceId, provinceName }) } [代码] 组件属性 属性 默认值 描述 visible false 是否显示picker选择器 column 3 显示几列,可选值:1,2,3 values [0, 0, 0] 必填,默认回填的省市区下标,可选择具体省市区后查看AppData的regionValue字段 close function 点击关闭picker弹窗 confirm function 点击选择器的确定返回值 confirm: 属性 默认值 描述 provinceName 北京市 省份名称 provinceId 110000 省份ID cityName 市辖区 城市名称 cityId 110100 城市ID areaName 东城区 区域名称 areaId 110000 区域Id 至于怎么获取你想默认城市的下标,可以滑动操作下选中省市区后,点击确定后查看appData里的regionValue的值。 以上就是一个自定义数据版本的省市区二级、三级联动啦,老规矩,结尾放代码片段。 https://developers.weixin.qq.com/s/F9k9cTmT7LAz
2022-07-20 - 小程序里使用es7的async await语法
我们做小程序开发时,有时候想让自己代码变得整洁,异步操作时避免回调地狱.我们会使用es6的promise. es7的async,await . promise在小程序和云开发的云函数里都可以使用. async和await只能在云开发的云函数里使用.我们在小程序的代码里直接使用,就会报如下错误. [图片] 这个报错就是告诉我们不能在小程序里直接使用es7的async和await语法.但是这么好的语法我们用起来确实显得代码整洁,逼格高. 那接下来我就教大家如何在小程序代码里使用es7的async和await语法. 一,下载facebook出的runtime.js类库 [图片] 其实这个问题,一些大厂已经给出了解决方案.如上图,我们只需要把facebook出的这个runtime.js类库下载下来,然后放到我们的小程序项目里. 下载链接:https://github.com/facebook/regenerator/blob/master/packages/regenerator-runtime/runtime.js github有时候下载比较慢,我也提前把这个类库下载好放我网盘里了. [图片] 下载链接:https://pan.baidu.com/s/19n5wmjIKK3PAPbcXBzWmQA 提取码:xxll 如果链接失效,可以在底部 留言,或者私信石头哥获取. 二,下载后,把runtime.js放到我们项目里 我这里把runtime.js放到我的utils目录下,如果你没有utils目录,可以新建. [图片] 三,代码里引入runtime.js类库 这里建议大家用 require语法引入. [图片] 这里需要注意的是.上图我们引入runtime.js时的变量名regeneratorRuntime必须和我这里一模一样.要不然就会引入不成功. 引入完后,在编译代码,可以看到控制台不再报我们一开始的错误 [图片] 四,简单使用async和await 首先要知道我们async和await是结合使用的. [图片] 上图是我简单写的一个定时器来模拟异步等待.只要我们这里成功的引入runtime.js类库,后面想使用async和await就方便很多了. 今天就讲到这里.想学习更多小程序相关的知识,请持续关注.下期见
2019-12-10 - rich-text 解析富文本 图片过大 如何自定义大小?
从后台查出来的富文本数据,使用 rich-text 进行展示时,其中的图片过大,超出屏幕。 [图片] [图片] [图片] 真的是一点效果都没有,我都要急疯了,大佬们救救我把,
2019-10-09 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: 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 - 如何实现一个6位数的密码输入框
背景: 因为公司业务调整需要做用户支付这一块 开发者需要在小程序上实现一个简单的6位数密码输入框 [图片] 首先想下如何实现该效果: 1.使用input覆盖在框上面,设置letter-spacing达到数字之间间距的效果,实现时发现在input组件上使用letter-spacing无效果 2.循环六个view模拟的框,光标使用动画模拟,一个隐藏的input,点击view框时触发input的Focus属性弹起键盘,同时模拟的光标展示出来,输入值后,input的value长度发生变化,设置光标位置以及模拟的密码小黑圆点 好了,废话不多数,咱们直接上手。 wxml [代码]<view class='container'> <!-- 模拟输入框 --> <view class='pay-box {{focusType ? "focus-border" : ""}}' bindtap="handleFocus" style='width: 604rpx;height: 98rpx'> <block wx:for="{{boxList}}" wx:key="{{index}}"> <view class='password-box {{index === 0 ? "b-l-n":""}}'> <view wx:if="{{(dataLength === item - 1)&& focusType}}" class="cursor"></view> <view wx:if="{{dataLength >= item}}" class="input-black-dot"></view> </view> </block> </view> <!-- 隐藏input框 --> <input value="{{input_value}}" focus="{{isFocus}}" maxlength="6" type="number" class='hidden-input' bindinput="handleSetData" bindfocus="handleUseFocus" bindblur="handleUseFocus" /> </view> [代码] wxss [代码]/* 第一个格子输入框 */ .container .b-l-n { border-left: none; } .pay-box { margin: 0 auto; display: flex; flex-direction: row; border-left: 1px solid #cfd4d3; } /* 支付密码框聚焦的时候 */ .focus-border { border-color: #0c8; } /* 单个格式样式(聚焦的时候) */ .password-box { flex: 1; border: 1px solid #0c8; margin-right: 10rpx; display: flex; align-items: center; justify-content: center; } /* 模拟光标 */ .cursor { width: 2rpx; height: 36rpx; background-color: #0c8; animation: focus 1.2s infinite; } /* 光标动画 */ @keyframes focus { from { opacity: 1; } to { opacity: 0; } } /* 模拟输入的password的黑点 */ .input-black-dot { width: 20rpx; height: 20rpx; background-color: #000; border-radius: 50%; } /* 输入框 */ .hidden-input { margin-top: 200rpx; position: relative; } [代码] JS [代码]Component({ data: { //输入框聚焦状态 isFocus: false, //输入框聚焦样式 是否自动获取焦点 focusType: true, valueData: '', //输入的值 dataLength: '', boxList: [1, 2, 3, 4, 5, 6] }, // 组件属性 properties: { }, // 组件方法 methods: { // 获得焦点时 handleUseFocus() { this.setData({ focusType: true }) }, // 失去焦点时 handleUseBlur() { this.setData({ focusType: false }) }, // 点击6个框聚焦 handleFocus() { this.setData({ isFocus: true }) }, // 获取输入框的值 handleSetData(e) { // 更新数据 this.setData({ dataLength: e.detail.value.length, valueData: e.detail.value }) // 当输入框的值等于6时(发起支付等...) if (e.detail.value.length === 6) { // 通知用户输入数字达到6位数可以发送接口校验密码是否正确 this.triggerEvent('initData', e.detail.value) } } } }) [代码] 实现方式很简单,有点小问题,还有一些后续准备做的优化点,等完善后上线后再来修改一波。 最后附上代码片段: https://developers.weixin.qq.com/s/8CtRqJmT7W8k
2020-07-06 - 微信小程序三种授权登录的方式
经过一段时间对微信小程序的研发后 总结出以下三种授权登录的方式,我给他们命名为‘一次性授权’‘永久授权’‘不授权’ 1.一次性授权 常规写法,需要获取用户公开信息(头像,昵称等)时,判断调取授权登录接口,但是此方法如果不经处理的话 用户如果拒绝授权或者删除该微信小程序后 需要重新调取并获取用户公开信息(头像,昵称等),此方法用户体验较差,不建议使用; 2.永久授权 在不必要使用用户公开信息(头像,昵称等)时,不调取授权登录接口,只有在必要的时候再去判断调取授权登录接口并把获取到的用户公开信息存入数据库,这样在每次登录时直接先运行指定函数从数据库索取需要的用户公开信息(头像,昵称等)即可,此方法在删除小程序后不用再次去授权登录(因为在用户第一次授权登录时已经把用户的公开信息存入数据库了以后直接向数据库索取即可),建议使用; 3.不授权 不需要授权登录获取用户公开信息(头像,昵称等),使用wx.login获取用户code并传入后台,后台可以通过用户的code值向微信要一个值(具体需要问后台,我只是个小前端,后台的东西不是很懂,只是知道一些逻辑而且也已经成功实现)然后通过这个用code换取的值就可以识别到指定用户,如果需要的话,前端要显示的头像、昵称等这些信息可以使用自定义可编辑的功能,当然,也可以通过<open-data type=“userAvatarUrl”></open-data><open-data type=“userNickName”></open-data>小程序提供的这个组件显示用户的头像及昵称(不过这个组件只有显示功能),用户如果想直接使用自己的头像昵称,也可以自行授权(比如添加个引导按钮什么之类的),建议使用; [图片][图片] 文中使用的微信自带接口、组件及函数: <open-data type=“userAvatarUrl”></open-data> <open-data type=“userNickName”></open-data> wx.login({ success(res){ console.log(res.code) } }) 微信授权登录 以上三种方式可以灵活运用,也可以把需要的结合到一起,并不冲突; 当然,大佬很多,我也只是个小前端而已,第一次发表技术方面的帖子,希望互相学习,互相指导,如有说的不对的地方还望大佬们及时指出!!! 谢谢
2019-04-18 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - 让小程序页面和自定义组件支持 computed 和 watch 数据监听器
习惯于 VUE 或其他一些框架的同学们可能会经常使用它们的 [代码]computed[代码] 和 [代码]watch[代码] 。 小程序框架本身并没有提供这个功能,但我们基于现有的特性,做了一个 npm 模块来提供 [代码]computed[代码] 和 [代码]watch[代码] 功能。 先来个 GitHub 链接:https://github.com/wechat-miniprogram/computed 如何使用? 安装 npm 模块 [代码]npm install --save miniprogram-computed [代码] 示例代码 [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, }) [代码] [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, sum: 2, }, watch: { 'a, b': function(a, b) { this.setData({ sum: a + b }) }, }, }) [代码] 怎么在页面中使用? 其实上面的示例不仅在自定义组件中可以使用,在页面中也是可以的——因为小程序的页面也可用 [代码]Component[代码] 构造器来创建! 如果你已经有一个这样的页面: [代码]Page({ data: { a: 1, b: 1, }, onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }) [代码] 可以先把它改成: [代码]Component({ data: { a: 1, b: 1, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 然后就可以用了: [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 应该使用 [代码]computed[代码] 还是 [代码]watch[代码] ? 看起来 [代码]computed[代码] 和 [代码]watch[代码] 具有类似的功能,应该使用哪个呢? 一个简单的原则: [代码]computed[代码] 只有 [代码]data[代码] 可以访问,不能访问组件的 [代码]methods[代码] (但可以访问组件外的通用函数)。如果满足这个需要,使用 [代码]computed[代码] ,否则使用 [代码]watch[代码] 。 想知道原理? [代码]computed[代码] 和 [代码]watch[代码] 主要基于两个自定义组件特性: 数据监听器 和 自定义组件扩展 。其中,数据监听器 [代码]observers[代码] 可以用来监听数据被 [代码]setData[代码] 操作。 对于 [代码]computed[代码] ,每次执行 [代码]computed[代码] 函数时,记录下有哪些 data 中的字段被依赖。如果下一次 [代码]setData[代码] 后这些字段被改变了,就重新执行这个 [代码]computed[代码] 函数。 对于 [代码]watch[代码] ,它和 [代码]observers[代码] 的区别不大。区别在于,如果一个 data 中的字段被设置但未被改变,普通的 [代码]observers[代码] 会触发,但 [代码]watch[代码] 不会。 如果遇到问题或者有好的建议,可以在 GitHub 提 issue 。
2019-07-24 - 如何更优雅的使用IconFont你应该知道
微信小程序iconfont(也适用于网页和其他平台的小程序) 在微信小程序等使用iconfont进阶版,17年的时候就发过一次在微信小程序里使用iconfont的文章 去看看 时隔这么久也掌握了一些这方面的心得,稍微整理了一下。 用微信开发工具好还是用成熟的IDE好? 刚开始的时候我也是用开发工具写代码,因为刚接触的时候并没有什么特殊的需求,规规矩矩的按照开发文档来,到后面觉得效率太低了,于是改用了sublime text webstorm来做小程序开发,开发工具仅仅用来创建项目和预览。久而久之就已经完全弃用开发工具写代码了,并不是说开发工具不行而是说结合起来能给更高效的开发项目。 用webstorm来开发小程序 为什么选择ws而不用vscode,原因就是我用习惯了不想在去更换了。 讲重点: 项目目录结构 安装Less来编写css 利用ws编辑器来自动编译Less生成wxss 快速将iconfont引入项目 简单的一个例子 项目目录结构 [代码]iconfont -app 小程序项目目录 --pages 页面目录 ---index ----index.wxml ----index.less ----index.ts ----index.json ----index.js (自动编译) ----index.wxss (自动编译) --styles 样式目录 ---global.less 全局公共样式 ---global.wxss (自动斌阿姨) ---iconfont.less ---iconfont.wxss (自动编译) -app.ts -app.js (自动编译) -app.less -app.wxss (自动编译) app.json project.config.json 项目配置文件 sitemap.json 搜索引擎相关配置 -node_modules [代码] 安装Less 首先确保自己电脑是否安装了node.js,关于怎么安装node.js请自行谷歌或者百度。 全局或者单项目安装less [代码]npm install -g less [代码] [代码]webstorm[代码]配置[代码]less[代码]自动编译 打开ws设置找到[代码]File Watchers[代码] [图片] 添加一个less类型的监视器 [图片] 修改默认配置,按下图设置 [图片] 可以根据自己的习惯来设置,没有规定一定是这样,这套设置是当我less文件有改动我保存时会自动编译输出到wxss文件,这样的好处就是不会有改动就编译,而是保存的时候需要编译了才编译。 新建[代码]iconfont[代码]项目 打开阿里巴巴的图标库点我 新建一个项目,然后添加或者上传一些icon图标,生成Font Class的css链接,然后在styles目录下的[代码]iconfont.less[代码]文件里引入刚刚创建的链接,[代码]iconfont.wxss[代码]就会自动生成对应的代码,这样就不用每次icon有改变就得去打开创建的链接然后复制粘贴到iconfont.wxss了。 [图片] [图片] [图片] [图片] 举个栗子 [代码]// index.less文件中引入global.less跟iconfont.wxss // 至于为什么global引入的是less而iconfont引入的却是wxss // 因为less是css的预处理语言,所以最终还是会被打包编译成css // 所以我们global要引入的并不是要打包编译后的css代码 // 而iconfont因为我们在less里面引入了外链后被打包编译了需要的css @import (reference) "../../styles/global"; @import (css) "../../styles/iconfont.wxss"; [代码] [代码]// 在view里面直接使用icon <view class="icon iconaixin"></view> // 循序遍历所有的icon <view class="container"> <view wx:for="{{ iconList }}" wx:key="item" data-index="{{ index }}" class="item {{ index === iconIndex ? 'active' : '' }}" bind:tap="click"> <view class="icon icon{{ item.font_class }}"></view> <view class="name">{{ item.name }}</view> </view> </view> [代码] 效果图 [图片] 大致使用步骤就是,通过ws创建文件的监视器,自动编译打包,大大的提高了开发效率。 我less中使用rpx的时候用的是unit(10, rpx),当然你也可以直接用10rpx,但是IDE可能会识别不了,编译后或者格式化后会出现10 rpx这种情况,这样开发工具肯定就会报错了,解决办法就是在创建一个监视器,保存打包的时候,把10 rpx替换成10rpx即可,js部分是用ts编写,如果不懂可以直接看编译后的js,差别不会很大!如果也想ts修改代码,请执行[代码]npm install[代码]把相关依赖包下载就行了。 [图片] over 如果有疑问或者更好建议欢迎找我交流! 微信小程序代码片段 github
2019-07-12 - 借助小程序云开发实现小程序支付功能(含源码)
我们在做小程序支付相关的开发时,总会遇到这些难题。小程序调用微信支付时,必须要有自己的服务器,有自己的备案域名,有自己的后台开发。这就导致我们做小程序支付时的成本很大。本节就来教大家如何使用小程序云开发实现小程序支付功能的开发。不用搭建自己的服务器,不用有自己的备案域名。只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1,云开发的部署和使用 2,支付相关的云函数开发 3,商品列表 4,订单列表 5,微信支付与支付成功回调 支付成功给用户发送推送消息的功能会在后面讲解。 下面就来教大家如何借助云开发使用小程序支付功能。 支付所需要用到的配置信息 1,小程序appid 2,云开发环境id 3,微信商户号 4,商户密匙 一,准备工作 1,已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中,【设置】 →【开发设置】 下可以获取微信小程序 AppID 和 Secret。 [图片] 2,微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中,【账户中心】→【商户信息】 下可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 下可以设置商户密钥。 [图片] 这里特殊说明下,个人小程序是没有办法使用微信支付的。所以如果想使用微信支付功能,必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3,微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4,开通小程序云开发功能:https://edu.csdn.net/course/play/9604/204526 二,商品列表的实现 效果图如下,由于本节重点是支付的实现,所以这里只简单贴出关键代码。 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view> </view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息,然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看我的历史博客,有写过的) 也有视频讲解: https://edu.csdn.net/course/detail/9604 [图片] 三,支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解。 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1,配置config下的index.js, 这一步所需要做的就是把小程序appid,云开发环境ID,商户id,商户密匙。填进去。 [图片] 2,配置入口云函数 [图片] 详细代码如下,代码里注释很清除了,这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init() const app = require('tcb-admin-node'); const pay = require('./lib/pay'); const { mpAppId, KEY } = require('./config/index'); const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res = require('./lib/res'); const ip = require('ip'); /** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */ exports.main = async function(event, context) { const { type, data, userInfo } = event; const wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database(); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order'); // 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo = `${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; //'127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spbill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }) .get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了。 [图片] 再来看下,支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表,支付状态,支付成功后的回调。 今天就先讲到这里,后面会继续给大家讲解支付的其他功能。比如支付成功后的消息推送,也是可以借助云开发实现的。 由于源码里涉及到一些私密信息,这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言。单独找我要源码也行(微信2501902696) 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - CSS 火焰?不在话下
正文从下面开始。 今天的小技巧是使用纯 CSS 生成火焰,逼真一点的火焰。 嗯,长什么样子?在 CodePen 上输入关键字 [代码]CSS Fire[代码],能找到这样的: [图片] 或者这样的: [图片] 我们希望,仅仅使用 CSS ,效果能再更进一步吗?能不能是这样子: [图片] 如何实现 嗯,我们需要使用 [代码]filter[代码] + [代码]mix-blend-mode[代码] 的组合来完成。 很多 CSS 华而不实的效果都是 [代码]filter[代码] + [代码]mix-blend-mode[代码],很有意思,但是业务中根本用不上,当然多了解了解总没坏处。 如上图,整个蜡烛的骨架, 除去火焰的部分很简单,掠过不讲。主要来看看火焰这一块如何生成,并且如何赋予动画效果。 Step 1: filter blur && filter contrast 模糊滤镜叠加对比度滤镜产生的融合效果。 单独将两个滤镜拿出来,它们的作用分别是: [代码]filter: blur()[代码]: 给图像设置高斯模糊效果。 [代码]filter: contrast()[代码]: 调整图像的对比度。 但是,当他们“合体”的时候,产生了奇妙的融合现象。 先来看一个简单的例子: [图片] 仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。 利用上述 [代码]filter blur & filter contrast[代码],我们要先生成一个类似火焰形状的三角形。(略去过程) 这里类似火焰形状的三角形的具体实现过程,在这篇文章有详细的讲解:你所不知道的 CSS 滤镜技巧与细节 [图片] 父元素添加 [代码]filter: blur(5px) contrast(20)[代码],会变成这样: [图片] Step 2: 火焰粒子动画 看着已经有点样子了,接下来是火焰动画,我们先去掉父元素的 [代码]filter: blur(5px) contrast(20)[代码] ,然后继续 。 这里也是利用了 [代码]filter[代码] 的融合效果,我们在上述火焰中,利用 SASS 随机均匀分布大量大小不一的圆形棕色 div ,隐匿在火焰三角内部,大概是这样: [图片] 接下来,我们再利用 SASS,给中间每个小圆赋予一个从下往上逐渐消失的动画,并且均匀赋予不同的 [代码]animation-delay[代码],看起来会是这样: [图片] OK,最重要的一步,我们再把父元素的 [代码]filter: blur(5px) contrast(20)[代码] 打开,神奇的火焰效果就出来了: [图片] Step 3: mix-blend-mode 润色 当然,上述效果已经很不错了。经过各种尝试,调整参数,最后我发现加上 [代码]mix-blend-mode: screen[代码] 混合模式,效果更好,得到头图上面的最终效果如下: [图片] 完整源码在我的 CodePen 上:CodePen Demo – CSS Fire 另外一些效果 当然,掌握了这种方法后,这种生成火焰的技巧也可以迁移到其他效果去。下图是我鼓捣到另外一个小 Demo,当 hover 到元素的时候,产生火焰效果: [图片] CodePen Demo – Hover Fire 嗯,这些其实都是对滤镜及混合模式的一些搭配运用。按照惯例,肯定有人会留言喷了,整这些花里胡哨的有什么用,性能又不好,业务中敢上不把你的腿给打骨折。 [图片] 于我而言,虚心接受各种批评质疑及各种不同的观点,当然我是觉得搞技术一方面是实用,另一方面是兴趣使然,自娱自乐。希望喷子绕道~ 回到正题,了解了这种黏糊糊湿答答的技巧后,还可以折腾出其他很多有意思的效果,当然可能需要更多的去尝试,如下面使用一个标签实现的滴水效果: [图片] CodePen Demo – 单标签实现滴水效果 值得注意的细节点 动画虽然美好,但是具体使用的过程中,仍然有一些需要注意的地方: CSS 滤镜可以给同个元素同时定义多个,例如 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] ,但是滤镜的先后顺序不同产生的效果也是不一样的; 也就是说,使用 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] 和 [代码]filter: brightness(1.5) contrast(150%) blur(5px)[代码] 处理同一张图片,得到的效果是不一样的,原因在于滤镜的色值处理算法对图片处理的先后顺序。 滤镜动画需要大量的计算,不断的重绘页面,属于非常消耗性能的动画,使用时要注意使用场景。记得开启硬件加速及合理使用分层技术; [代码]blur()[代码] 混合 [代码]contrast()[代码] 滤镜效果,设置不同的颜色会产生不同的效果,这个颜色叠加的具体算法暂时没有找到很具体的规则细则,使用时比较好的方法是多尝试不同颜色,观察取最好的效果; 细心的读者会发现上述效果都是基于黑色底色进行的,动手尝试将底色改为白色,效果会大打折扣。 最后 本文只是简单的介绍了整个思路过程,许多 CSS 代码细节,调试过程没有展现出来。主要几个 CSS 属性默认大家已经掌握了大概,阅读后可以自行去了解补充更多细节: [代码]filter[代码] [代码]mix-blend-mode[代码] 更多精彩 CSS 技术文章汇总在我的 Github – iCSS ,持续更新,欢迎点个 star 订阅收藏。 好了,本文到此结束,希望对你有帮助 😃 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。 最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS: [图片]
2019-04-26 - Web直播,你需要先知道这些
转自IMWeb社区,原文链接 Web直播,你需要先知道这些 直播知识小科普 一个典型的直播流程:录制->编码->网络传输(推流->服务器处理->CDN分发)->解码->播放 IPB:一种常用的视频压缩方案,用I帧表示关键帧,B帧表示前向差别帧,P帧表示双向差别帧 GOP (Group of Pictures):GOP 越长(I帧之间的间隔越大),B 帧所占比例越高,编码的率失真性能越高。虽然B帧压缩率高,但解码时CPU压力会更大。 音视频直播质量好坏的主要指标:内容延时、卡顿(流畅度)、首帧时长 音视频直播需要克服的主要问题:网络环境、多人连麦、主辅路、浏览器兼容性、CDN支持等 MSE(Media Source Extensions):W3C 标准API,解决 HTML5 的流问题(HTML5 原生仅支持播放 mp4/webm 非流格式,不支持 FLV),允许JavaScript动态构建 [代码]<video>[代码] 和 [代码]<audio>[代码] 的媒体流。可以用MediaSource.isTypeSupported() 判断是否支持某种MINE类型。在ios Safari中不支持。 [图片] 文件格式/封装格式/容器格式:一种承载视频的格式,比如flv、avi、mpg、vob、mov、mp4等。而视频是用什么方式进行编解码的,则与Codec相关。举个栗子,MP4格式根据编解码的不同,又分为nMP4、fMP4。nMP4是由嵌套的Boxes 组成,fMP4格式则是由一系列的片段组成,因此只有后者不需要加载整个文件进行播放。 Codec:多媒体数字信号编码解码器,能够对音视频进行压缩(CO)与解压缩( DEC ) 。CODEC技术能有效减少数字存储占用的空间,在计算机系统中,使用硬件完成CODEC可以节省CPU的资源,提高系统的运行效率。 常用视频编码:MPEG、H264、RealVideo、WMV、QuickTime。。。 常用音频编码:PCM、WAV、OGG、APE、AAC、MP3、Vorbis、Opus。。。 现有方案比较 RTMP协议 基于TCP adobe垄断,国内支持度高 浏览器端依赖Flash进行播放 2~5秒的延迟 RTP协议 Real-time Transport Protocol,IETF于1996提出的一个标准 基于UDP 实时性强 用于视频监控、视频会议、IP电话 CDN厂商、浏览器不支持 HLS 协议 Http Live Streaming,苹果提出的基于HTTP的流媒体传输协议 HTML5直接支持(video),适合APP直播,PC断只有Safari、Edge支持 必须是H264+AAC编码 因为传输的是切割后的音视频片段,导致内容延时较大 [图片] flv.js Bilibli开源,解析flv数据,通过MSE封装成fMP4喂给video标签 编码为H264+AAC 使用HTTP的流式IO(fetch或stream)或WebSocket协议流式的传输媒体内容 2~5秒的延迟,首帧比RTMP更快 WebRTC协议 [图片] 1、Google力推,已成为W3C标准 2、现代浏览器支持趋势,X5也支持(微信、QQ) [图片] 3、基于UDP,低延迟,弱网抗性强,比flv.js更有优势 方案 CPU占用 帧率 码率 延时 首帧 flv.js 0.4 30 700kbit/s 1.5s 2s WebRTC 1.9 30 700kbit/s 0.7s 1.5s 4、支持Web上行能力 5、编码为H264+OPUS 6、提供NAT穿透技术(ICE) **实际情况下,当用户数量很大时,对推流设备的性能要求很高,复杂的权限管理也难以实现,采用P2P的架构基本不可行。对于个别用户提供上行流、海量用户只进行拉流的场景,腾讯课堂实现了一种P2S的解决方案。**进一步学习可阅读jaychen的系列文章《WebRTC直播技术》。 [图片] 小程序+直播 技术方案 基于RTMP,官方说底层使用HTTP/2的一种内部传输机制,但又说是基于UDP的,这就搞不懂了。。。 live-pusher 和 live-player 没有限制第三方云服务 可直接使用腾讯云视频直播能力,只需配置好推流url、播放url即可 推流url: [图片] 播放url: [图片] 下面是我根据官网教程搭建的一个音视频小程序,搭建过程简单,同一个局域网下直播体验也很流畅(读者也可直接搜索腾讯视频云小程序进行体验): [图片] 前端核心代码还是相当简洁的: live-pusher组件:设置好url推流地址(仅支持 flv, rtmp 格式)等参数即可,使用bindstatechange获取播放状态变化 [图片] live-player组件:设置后src音视频地址(仅支持 flv, rtmp 格式)等参数即可,使用bindstatechange获取播放状态变化 [图片] 能否和WebRTC同时使用? 对于腾讯课堂的应用场景,老师上课推流采用的是RTMP协议,考虑到WebRTC目前只能用于PC端拉流,那么在移动端能否让用户可以直接通过小程序来观看直播课呢?我觉得在技术层面可行的,接入小程序直播对于扩大平台影响力、社交圈分享、提高收费转化都会产生很大的帮助。难点在于复杂的权限控制、多路音视频流、多人连麦等问题,比如权限控制只能单独放到房间控制逻辑中完成,而音视频流本身缺乏这种校验;主辅路的切换还需要添加单独的信令控制,同时在小程序中加入相应的判断逻辑。 补充:最近看到已经有小程序的webrtc方案了,基于live-player、live-pusher组件,加入腾讯云强大的音视频后台服务,官方提供了一套封装度更高的自定义组件方案 —— <webrtc-room> ,甚至可以和Chrome打通。详情可以参考WebRTC 互通、webrtc-room [图片] 参考文章 HTTP 协议入门 使用flv.js做直播 面向未来的直播技术-WebRTC【视频、PPT】 小程序音视频能力技术负责人解读“小程序直播” 小程序开发简易教程 小程序音视频解读
2019-03-26 - 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 - 小程序多端框架全面测评
最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11日)已发布稳定版为标准进行讨论。 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: [代码]uni-app[代码] > [代码]Taro[代码] > [代码]chameleon[代码] > [代码]WePY[代码]、[代码]mpvue[代码] [图片] 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码] 则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: [代码]chameleon[代码] > [代码]Taro[代码]、[代码]uni-app[代码] > [代码]mpvue[代码]、[代码]WePY[代码] [图片] 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: [代码]WePY[代码] > [代码]uni-app[代码] 、[代码]taro[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] [图片] 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: [代码]Taro[代码] > [代码]mpvue[代码] 、 [代码]uni-app[代码] > [代码]WePY[代码] > [代码]chameleon[代码] 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: [代码]uni-app[代码] > [代码]Taro[代码]、[代码]WePY[代码]、[代码]mpvue[代码] > [代码]chameleon[代码] [图片] 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: [代码]Taro[代码] > [代码]WePY[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] > [代码]uni-app[代码] 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码] 不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-03-19 - 小程序保持长连接小经验
大家都知道,小程序的websocket在切入后台5秒左右,会断开链接,或者长时间无数据收发,也会切断链接。 然而心跳数据并不能完全保证链接的正常, 我在这里说一下我的经验,我初中毕业(真的),代码方面写的不好不要见笑。 首先,我为了知道当前的网络是否断了,使用了一个标志位比如 : var NetworkIsOK = false; 当首次链接打开时比如socket的 onOpen 事件!在这个事件回调中,将NetworkIsOK = true; 同理,如果网络出现错误,在错误的回调中将NetworkIsOK = false; 建立一个发送 发送区数据 暂存站 var SendBuffer =[]; 将socket的send 函数包装一下,比如取名SendToServer(data) 在SendToServer中,首先检查网络的状态,如果状态是正常的,则直接调用socket.send()发送数据, 如果不是正常的,则重新链接服务器,并在onOpen事件中检查 SendBuffer.length是否大于 0 ,如果有存入的缓冲数据,则依次发送掉, 以下是部分实际代码,请忽略我垃圾的编程水平! [代码]var[代码] [代码]app = getApp();[代码][代码]/**[代码][代码] [代码][代码]* 与服务器进行通信的所有操作在此进行[代码][代码] [代码][代码]*/[代码][代码]var[代码] [代码]Server = {};[代码][代码]Server.socket = [代码][代码]null[代码][代码]; [代码][代码]//socket连接句柄[代码][代码]Server.isOK = [代码][代码]false[代码][代码]; [代码][代码]//服务器连接状态处理数组[代码][代码]Server.event = []; [代码][代码]//事件注册处理数组[代码][代码]Server.url = [代码][代码]'wss://********'[代码][代码]; //服务器地址[代码] [代码]Server.SendBuffer=[]; [代码][代码]//数据包发送缓冲[代码][代码]/**[代码][代码] [代码][代码]* 初始化操作[代码][代码] [代码][代码]*/[代码][代码]Server.Init=[代码][代码]function[代码][代码](apps){[代码][代码] [代码][代码]console.log([代码][代码]'hello server'[代码][代码]);[代码][代码] [代码][代码]app = apps;[代码][代码] [代码][代码]console.log(app);[代码][代码] [代码][代码]var[代码] [代码]Timer;[代码][代码] [代码][代码]/**执行服务器连接逻辑 */[代码][代码] [代码][代码]Server.socket = wx.connectSocket({[代码][代码] [代码][代码]url:Server.url[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//132.232.87.229[代码][代码] [代码][代码]Server.socket.onOpen([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server open'[代码][代码]);[代码][代码] [代码][代码]Server.isOK=[代码][代码]true[代码][代码]; [代码][代码]//可以通信了[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'已连接服务器 '[代码][代码],[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Timer = setInterval([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]Server.startHeart();[代码][代码] [代码][代码]}, 1000 * 50);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onError([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server error'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接错误'[代码][代码],[代码][代码] [代码][代码]icon:[代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onClose([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server close'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接关闭'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onMessage([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//Server.socketPress.onMessage(res);[代码][代码] [代码][代码]var[代码] [代码]message = JSON.parse(res.data);[代码][代码] [代码][代码]if[代码][代码](!message) [代码][代码]return[代码][代码]; [代码][代码]//空数据[代码][代码] [代码][代码]var[代码] [代码]Operator = message.Operator;[代码][代码] [代码][代码]var[代码] [代码]event=Server.event[Operator];[代码][代码] [代码][代码]if[代码][代码](event){[代码][代码] [代码][代码]event(message); [代码][代码]//实际执行[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]} /**服务器重连**/[代码] [代码]Server.reLink=[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]Timer;[代码][代码] [代码][代码]/**执行服务器连接逻辑 */[代码][代码] [代码][代码]Server.socket = wx.connectSocket({[代码][代码] [代码][代码]url:Server.url[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//132.232.87.229[代码][代码] [代码][代码]Server.socket.onOpen([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server open'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]true[代码][代码]; [代码][代码]//可以通信了[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'已连接服务器 '[代码][代码],[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.Login();[代码][代码]//重新注册登入[代码][代码] [代码][代码]Timer = setInterval([代码][代码]function[代码][代码](){[代码][代码] [代码][代码]Server.startHeart();[代码][代码] [代码][代码]},1000*50);[代码][代码] [代码][代码]//检查缓冲区是否有未发送数据[代码][代码] [代码][代码]while[代码] [代码](Server.SendBuffer.length>0){[代码][代码] [代码][代码]var[代码] [代码]data = Server.SendBuffer.pop();[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: data[代码][代码] [代码][代码]});[代码][代码] [代码][代码]console.log([代码][代码]'将缓存中的信息发送'[代码][代码],data)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onError([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server error'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接错误'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onClose([代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'Server close'[代码][代码]);[代码][代码] [代码][代码]Server.isOK = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]//显示成功[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'服务器连接关闭'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码] [代码][代码]});[代码][代码] [代码][代码]//Server.reLink();[代码][代码] [代码][代码]clearInterval(Timer);[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.socket.onMessage([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//Server.socketPress.onMessage(res);[代码][代码] [代码][代码]var[代码] [代码]message = JSON.parse(res.data);[代码][代码] [代码][代码]if[代码] [代码](!message) [代码][代码]return[代码][代码]; [代码][代码]//空数据[代码][代码] [代码][代码]var[代码] [代码]Operator = message.Operator;[代码][代码] [代码][代码]var[代码] [代码]event = Server.event[Operator];[代码][代码] [代码][代码]if[代码] [代码](event) {[代码][代码] [代码][代码]event(message); [代码][代码]//实际执行[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 启动心跳[代码][代码] [代码][代码]*/[代码][代码]Server.startHeart=[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]if[代码] [代码](Server.isOK == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]Server.reLink(); [代码][代码]//重连[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'Heart'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]};[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]// console.log(JsonData);[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: JsonData[代码][代码] [代码][代码]});[代码][代码] [代码][代码]Server.addEvent([代码][代码]'HeartOK'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]// console.log('心跳OK',message);[代码][代码] [代码][代码]})[代码][代码]}[代码][代码]/**用户登陆 */[代码][代码]Server.Login=[代码][代码]function[代码][代码](ques){[代码][代码] [代码][代码]var[代码] [代码]userOpenId = wx.getStorageSync([代码][代码]'openId'[代码][代码]);[代码][代码] [代码][代码]console.log(app);[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'userLogin'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]userCode: app.userCode, [代码][代码]//用户code[代码][代码] [代码][代码]userInfo: app.userInfo, [代码][代码]//用户信息[代码][代码] [代码][代码]};[代码][代码] [代码][代码]if[代码] [代码](userOpenId) { [代码][代码]//如果缓存中的有效,就用缓存中的openId发过去给服务器[代码][代码] [代码][代码]sendData.userOpenId = userOpenId;[代码][代码] [代码][代码]console.log([代码][代码]'调用缓存openID'[代码][代码]);[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]console.log([代码][代码]"登陆数据:"[代码][代码], JsonData);[代码][代码] [代码][代码]// console.log(JsonData);[代码][代码] [代码][代码]//将用户的code发往服务器[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**如果是黑名单用户,禁止使用 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'BanLogin'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]if[代码] [代码](ques) {[代码][代码] [代码][代码]ques();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 注册消息执行处理函数[代码][代码] [代码][代码]*/[代码][代码]Server.addEvent=[代码][代码]function[代码][代码](eventName,eventHandle){[代码][代码] [代码][代码]Server.event[eventName] = eventHandle;[代码][代码]}[代码][代码]/**注册登入成功处理事件 */[代码][代码]Server.addEvent([代码][代码]'LoginOK'[代码][代码],[代码][代码]function[代码][代码](message){[代码][代码] [代码][代码]console.log([代码][代码]'LOGIN OK'[代码][代码]);[代码][代码] [代码][代码]console.log(message);[代码][代码] [代码][代码]wx.setStorageSync([代码][代码]'openId'[代码][代码],message.openId);[代码][代码]});[代码][代码]/**注册登入失败处理事件 */[代码][代码]Server.addEvent([代码][代码]'LoginError'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]content: [代码][代码]'登入失败了!部分功能可能无法使用,可能是网络原因,也可能是没有获得授权'[代码][代码],[代码][代码] [代码][代码]showCancel: [代码][代码]false[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]//console.log('用户点确定');[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码]});[代码][代码]/**[代码][代码] [代码][代码]* 创建帖子[代码][代码] [代码][代码]*/[代码][代码]Server.CreateInvitation=[代码][代码]function[代码][代码](table,image,text,isRichText){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'CreateInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]isRichText:isRichText,[代码][代码]//是否为富文本[代码][代码] [代码][代码]Table:table,[代码][代码]//标题[代码][代码] [代码][代码]Image:image,[代码][代码]//图片地址[代码][代码] [代码][代码]Text:text,[代码][代码]//文本内容[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 从服务器获取贴子[代码][代码] [代码][代码]*/[代码][代码]Server.GetInvitation=[代码][代码]function[代码][代码](id,mode,time,limt,skip,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'GetInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]mode:mode, [代码][代码]//操作模式[代码][代码] [代码][代码]time:time, [代码][代码]//时间查询时使用的时间[代码][代码] [代码][代码]limt:limt, [代码][代码]//分页查询时需要获取的贴子数量[代码][代码] [代码][代码]skip:skip, [代码][代码]//需要跳过的帖子数量[代码][代码] [代码][代码]};[代码][代码] [代码][代码]//var JsonData = JSON.stringify(sendData);[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'GetInvitationOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'GetInvitationOK'[代码][代码]);[代码][代码] [代码][代码]var[代码] [代码]data = message.data; [代码][代码] [代码][代码]var[代码] [代码]serverTime=message.serverTime;[代码][代码] [代码][代码]if[代码][代码](success) success(data,serverTime); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]}[代码][代码]/**[代码][代码] [代码][代码]* 创建帖子评论[代码][代码] [代码][代码]*/[代码][代码]Server.CreateInvitationComment=[代码][代码]function[代码][代码](_id,text,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'CreateInvitationComment'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:_id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]text:text,[代码][代码]//评论内容[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'CreateInvitationCommentOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'CreateInvitationCommentOK'[代码][代码]);[代码][代码] [代码][代码]var[代码] [代码]data = message.data;[代码][代码] [代码][代码]if[代码] [代码](success) success(data); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]};[代码][代码]/**[代码][代码] [代码][代码]* 给帖子点赞[代码][代码] [代码][代码]*/[代码][代码]Server.LoveInvitation=[代码][代码]function[代码][代码](_id,success){[代码][代码] [代码][代码]var[代码] [代码]sendData = {[代码][代码] [代码][代码]Operator: [代码][代码]'LoveInvitation'[代码][代码], [代码][代码]//操作方式[代码][代码] [代码][代码]id:_id, [代码][代码]//使用帖子ID的查询方式[代码][代码] [代码][代码]};[代码][代码] [代码][代码]Server.SendData(sendData);[代码][代码] [代码][代码]/**注册登入成功处理事件 */[代码][代码] [代码][代码]Server.addEvent([代码][代码]'LoveInvitationOK'[代码][代码], [代码][代码]function[代码] [代码](message) {[代码][代码] [代码][代码]console.log([代码][代码]'LoveInvitationOK'[代码][代码]);[代码][代码] [代码][代码]if[代码] [代码](success) success(); [代码][代码]//回调执行[代码][代码] [代码][代码]});[代码][代码]}[代码][代码]/**[代码][代码] [代码][代码]* 向服务器发送数据,数据类型为任意数据,[代码][代码] [代码][代码]* 如果服务器断线,则自动重连服务器,数据被暂存,重连成功后将被发送[代码][代码] [代码][代码]*/[代码][代码]Server.SendData=[代码][代码]function[代码][代码](data){[代码][代码] [代码][代码]if[代码] [代码](Server.isOK == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]Server.reLink(); [代码][代码]//重连[代码][代码] [代码][代码]//将未发送数据存入Buffer[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(data);[代码][代码] [代码][代码]Server.SendBuffer.push(JsonData);[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码]else[代码][代码]{ [代码][代码]//否则直接给服务器发送数据[代码][代码] [代码][代码]var[代码] [代码]JsonData = JSON.stringify(data);[代码][代码] [代码][代码]Server.socket.send({[代码][代码] [代码][代码]data: JsonData[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码]}[代码][代码]//暴露接口[代码][代码]module.exports.Server = Server;[代码]
2019-03-18 - 云开发实战分享|诗和远方:旅行小账本云开发
原创:豪豪 前言 最近沉迷小程序开发,发现了一款功能、界面、体验俱佳的小程序“旅行小账本”。着手做了个简约版——“旅行小账本”。效果比较满意,毕竟前后台一人单干。 [图片] IDE 微信开发者工具 VSCode 小程序开发必然少不了微信开发者工具,再加上其对云开发的全面支持,再好不过的开发利器。但熟悉微信开发者工具的朋友们应该知道,它不支持Emmet缩写语法,并且wxml的属性值默认用单引号表示(强迫症表示很难受)。 而VSCode很好的补足了微信开发者工具的不足之处,并且支持多元化插件开发,轻量好用。 所以这里推荐采用微信开发者工具+VSCode配合开发。微信开发者工具负责调试、模拟小程序运行情况,VSCode负责代码编辑工作。二者各司其职,会使开发更加的高效、便捷。 总体架构 该项目基于小程序云开发,使用的模板是云开发快速启动模板。 由于是个全栈项目,前端使用小程序所支持的wxml + wxss + js开发模式,命名采用BEM命名规范。后台则是借助云数据库+云储存进行数据管理。 项目总体结构 [代码]|-travelbook 项目名 |-cloudfunctions 云函数模块 |-deleteItems 级联删除--云函数 |-getTime 获取时间--云函数 |-miniprogram 项目模块 |-components 自定义组件 |-accountCover 账本封面组件 |-spendDetail 支出细节组件 |-pages 页面 |-accountBooks 总账本页 |-accountCalendar 账本日历页 |-accountDetail 支出细节页 |-accountList 支出明细页 |-accountPage 选定账本页 |-editAccount 账本编辑页 |-index 首页 |-vant-weapp 有赞vant框架组件库 |-··· 系列组件... app.js 全局js app.json 全局json配置 app.wxss 全局wxss [代码] 逆向工程 在做该小程序之前,有必要进行项目的逆向工程,进一步解构每一个页面,从而深入了解这款小程序的交互细节。那么现在我假设自己为腾讯旅游的产品设计师,在绘制完界面原型后,撰写了相应的交互文档。当然解构过程中可能有些细节处理并没有那么仔细到位… 以下是我绘制的界面原型 [图片] [图片] [图片] 接下来对每个页面的细节进行解构,并完成简单的wxml结构 [图片] [代码]<!--switchList使用定位布局--> <view bindtap="switchList" class="list"></view> <!--newAccount使用flex布局--> <view class="newAccount" bindtap="createNewAccount"> <view class="desc">旅行中的每一笔开支都有独特的意义!</view> <image src="{{}}"></image> <view class="title">创建一个新账本</view> </view> [代码] [图片] [代码]<!--整体用flex + 百分比布局--> <input type="text" class="accuntName" placeholder="旅行账本名称" bindinput="getInput" /> <van-panel title="选择封面" class="panel"> <van-row class="imageBox"> <!--使用wx:for遍历数据库账本图片信息--> <van-col span="8" class="imgCol" bindtap="selectThis"> <image class="select" src="{{}}"></image> </van-col> <van-col span="8"> <view class="addBox" bindtap="useMore">更多封面</view> </van-col> </van-row> </van-panel> <button type="primary" bindtap="save">保存</button> <button type="warn" bindtap="delete">删除</button> [代码] [图片] [代码]<view class="accountDesc" bindtap="viewDetail"> <!--使用wx:for遍历数据库账本信息--> <view class="accountName"> <view>{{}}</view> <view class="accountTime">{{}}</view> </view> <!--绝对定位--> <image class="updateImg" catchtap="editAccount" src="{{}}"></image> </view> [代码] [图片] [代码]<!--switchList使用定位布局--> <view bindtap="switchList" class="list"></view> <view class="account__list-year">{{}}</view> <view class="account__list-new account__list-public" bindtap="createNewAccount"> <!--日期小圆点--> <view class="account__list-point"></view> <view class="account__list-time">{{}}</view> <image src="{{}}"></image> <view class="account__list-title">创建一个新账本</view> </view> <!--使用wx:for遍历数据库账本信息--> <view class="account__list-item account__list-public" bindtap="viewDetail"> <!--日期小圆点--> <view class="account__list-point"></view> <image src="{{}}" mode="aspectFill"></image> <view class="account__list-name">{{}}</view> <view class="account__list-time">{{}}</view> <image class="account__list-update" catchtap="editAccount" src="{{}}"></image> </view> [代码] [图片] [代码]<view class="account__spend"> <image bindtap="getCalendar" class="account__spend-calendar" src="{{}}"></image> <view class="account__spend-text"> <view class="account__spend-total">总花费(元)</view> <view class="account__spend-num">{{}}</view> </view> <image bindtap="accountAnalyze" class="account__spend-detail" src="{{}}"></image> </view> <view class="account__show-time">今天</view> <view class="account__show-detail"> <view class="account__show-income account__show-public"> <view class="account__show-title">收入(元)</view> <text class="account__show-in">+{{}}</text> </view> <view class="account__show-spend account__show-public"> <view class="account__show-title">支出(元)</view> <text class="account__show-out">-{{}}</text> </view> </view> <!--使用wx:for遍历数据库账本信息--> <view class="account__show-items-spend"> <view> <image src="{{}}"></image> </view> <text>{{}}</text> <text class="account__show-items-money">{{}}</text> </view> [代码] [图片] [代码]<!--日历使用极点日历的插件--> <!--json中做配置--> "usingComponents": { "calendar": "plugin://calendar/calendar" } <!--js改变样式--> days_style.push({ month: 'current', day: new Date().getDate(), color: 'white', background: '#e0a58e' }) <!--wxml中引用--> <calendar weeks-type="cn" cell-size="50" next="{{true}}" prev="{{true}}" show-more-days="{{true}}" calendar-style="demo6-calendar" header-style="calendar-header"board-style="calendar-board" active-type="rounded" lunar="true" header-style="header"calendar-style="calendar"days-color="{{days_style}}"> </calendar> [代码] [图片] [代码]<!--顶栏日期及收支结构--> <view class="account__title"> <text class="account__title-time">{{}}</text> <text class="account__title-spend">支出{{}}元 收入{{}}元</text> </view> <!--收支细节结构 使用flex弹性布局--> <view class="account__detail"> <image src="{{}}"></image> <view class="account__detail-name">{{}}</view> <view class="account__detail-money">{{}}</view> </view> [代码] [图片] [代码]<!--使用vant框架的van-tabs组件--> <!--并封装自定义组件复用收支页,自定义组件后面会详细说明--> <van-tabs active="{{ active }}" bind:change="onChange"> <van-tab title="支出"> <spendDetail detail="{{detail}}" accountKey="{{accountKey}}"></spendDetail> </van-tab> <van-tab title="收入"> <spendDetail detail="{{income}}" accountKey="{{accountKey}}"></spendDetail> </van-tab> </van-tabs> [代码] 云开发 在做完逆向工程的解构,页面基础结构基本搭建完成。但页面依旧是静态的,需要数据来填充。所以第二步就是数据库的设计。而小程序的云控制台恰好提供了数据的操作功能,为数据驱动提供基石。 [图片] 云数据库设计 云数据库是一种NoSQL数据库。每一张表是一个集合。值得注意的是在设计数据库时,[代码]_id[代码] 和[代码]_openid[代码]这两个字段需要带上。[代码]_id[代码]是表的主键,而[代码]_openid[代码]是用户标识,每个用户都有不同的[代码]_openid[代码],可区分不同用户。 以下是项目中的数据表设计 [代码]cover_photos 账本封面表 用于存储创建账本时需要的封面信息 - _id - _openid - cover_index 封面索引 - cover_url 封面url - isSelected 封面是否选中 [代码] [代码]accounts 账本表 用于存储用户创建的账本 - _id - _openid - accountKey 账本唯一标识 - coverUrl 账本封面 - i 账本索引 - inputValue 账本名字 - now 账本创建时间 - spend 账本总花费 [代码] [代码]account_detail 支出类型表 用于存储消费类型 - _id - _openid - detail 类型细节 - pic_index 消费类型索引 - pic_url 未点击时的图片 - pic_url_act 点击后的图片 - type 消费类型 [代码] [代码]account_income 收入类型表 用于存储收入类型 - _id - _openid - pic_index 收入类型索引 - pic_url 未点击时的图片 - pic_url_act 点击后的图片 - type 收入类型 [代码] [代码]spend_items 消费明细表 - _id - _openid - accountKey 账本唯一标识 - address 消费地点 - desc 消费描述 - fullDate 消费时间 - money 消费金额 - pic_type 消费类型 - pic_url 消费类型图片 [代码] 云储存管理 这是个非常实用的板块。类似于<a href=“https://pan.baidu.com/”>百度云盘</a>,它提供了文件存储、上传与下载功能。 [图片] 除此之外,它还会将你所上传的资源自动进行压缩操作,并生成一个地址供你引用。该项目中的一些图片资源就是存在于此,然后在云数据库的字段中引用这些资源地址即可,十分方便,不必在本地存储,占用小程序内存。 [图片] 云函数设计 云函数简单来说就是在云后端(Node.js)运行的代码,本地看不到这些代码的执行过程,全封闭式只暴露接口供本地调用执行,本地只需等待云端代码执行完毕后返回结果。这也是面向接口编程的思想体现。 项目中的云函数设计 [图片] [代码]// getTime 获取当前时间并格式化为 yyyy-mm-dd // 云函数入口文件 const cloud = require('wx-server-sdk') // 初始化云函数 cloud.init() // 云函数入口函数 exports.main = async (event, context) => { var date = new Date() var seperator1 = "-" var year = date.getFullYear() var month = date.getMonth() + 1 var strDate = date.getDate() if (month >= 1 && month <= 9) { month = "0" + month } if (strDate >= 0 && strDate <= 9) { strDate = "0" + strDate } // 格式化当前时间 var currentdate = year + seperator1 + month + seperator1 + strDate return currentdate } [代码] [代码]// deleteItems 批量删除,云数据库的批量删除只允许在云函数中执行 // 云函数入口文件 const cloud = require('wx-server-sdk') // 初始化云函数 cloud.init() // 连接云数据库 const db = cloud.database() const _ = db.command // 云函数入口函数 exports.main = async (event, context) => { try { return await db.collection('spend_items') .where({ accountKey: event.accountKey }) .remove() } catch (e) { console.error(e) } } [代码] MVVM 界面有了,数据有了。万事俱备,只欠东风!所以下一步就是MVVM的设计。小程序本质就是基于MVVM所设计的,在MVVM的世界里,数据是灵魂,一切都由数据来驱动。 账本页显示 [图片] 账本页有两种显示的风格,左上角的按钮可以来回切换风格,下拉可刷新页面,显示accounts数据表中存储的账本信息。显示时有个小细节,需要根据创建的时间先后来显示,越晚创建的越先显示。 [代码]// 页面数据设计, 在wxml中使用{{}}符号引用数据,数据就动态显示到了页面上 data: { isList: false, // 转换页面风格的标识 true为竖向风格 false为横向风格 accounts: [], // 存储查询的账本数据 now: null, // 存储当日时间 year: null // 存储年份 } // 转换显示风格 switchList() { // 设置页面风格样式 let isList = !this.data.isList this.setData({ isList }) wx.setStorage({ key: "isList", data: isList }) } // 获取页面风格转换标识 var isList = wx.getStorageSync('isList') // 查询账本 db.collection('accounts') .get({ success: res => { this.setData({ accounts: res.data.reverse(), // 反转数组,优先显示创建早的账本 isList }) wx.hideLoading() } }) // 调用云函数接口 获取当前日期 wx.cloud.callFunction({ // 云函数接口名就是创建的云函数名字,这里是'getTime' name: 'getTime', success: (res) => { let year = res.result.split('-')[0] this.setData({ now: res.result, year }) }, fail: console.error }) [代码] 账本页增删改 [图片] 账本页通过调用相应的云数据库API,可进行一系列的增删改操作。值得一提的是,修改时需要表单回显,删除时需要级联删除。因为一个账本中有许多收支情况,spend_items表就是进行收支记录,所以删除账本时需要级联删除对应的spend_items表中的收支信息。 一些重要的逻辑 封面单选逻辑[代码]data: { images: [], // 封面数组 selectImg: null, // 选择其它封面 isSelected: {}, // 选中的图片 inputValue: '', // 账本名字 now: null, // 当前时间 account: {} // 传入账本信息 } // 单选逻辑 通过构造{'0': isSelected}来实现 selectThis(e) { let index = e.currentTarget.dataset.index let coverUrl = e.currentTarget.dataset.coverurl let is = this.data.isSelected[index] let obj = { coverUrl } // obj[index] 属性动态改变 obj[index] = !is obj.i = index this.setData({ isSelected: obj }) } [代码] 表单回显逻辑[代码]// 页面加载时先通过对应的accountKey, 得到回显信息 let { i, id, value, url, accountKey } = options photos.get({ success: res => { this.setData({ images: res.data, account: { id, value, url, i, accountKey }, isSelected: obj }) wx.hideLoading() } }) // 修改 save() { let { id } = this.data.account let { i, coverUrl, value } = this.data.isSelected // 若没修改 则为之前的value let inputValue = this.data.inputValue || value [代码] db.collection(‘accounts’) .doc(id) .update({ data: { inputValue, coverUrl, i } }) } ``` 级联删除逻辑[代码]db.collection('accounts') .doc(this.data.account.id) .remove() .then(() => { wx.hideLoading() wx.showToast({ title: '删除成功' }) setTimeout(() => { wx.reLaunch({ url: '../accountBooks/accountBooks' }) }, 400) }) // 调用deleteItems云函数, 传入对应accountKey主键, 通过云函数批量删除 wx.cloud.callFunction({ name: 'deleteItems', data: { accountKey } }) [代码] 账本页收支 [图片] 因为收入与支出页面基本类似,所以使用自定义组件封装,可以复用。 [代码]// 封装spendDetail组件 // 注册组件 properties: { detail: { type: Object }, accountKey: { type: Number }, isSpend: { type: Boolean } } // 引用组件 <van-tab title="支出"> <spendDetail detail="{{detail}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail> </van-tab> <van-tab title="收入"> <spendDetail detail="{{income}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail> </van-tab> [代码] 收入与支出类型icon选择使用两个view来存放,通过选择不同类型,跳转不同的icon [代码]// js data: { address: '', money: 0, desc: '', selectPicIndex: 0, selectIndex: 0 } // 选择消费类别 selectSpend(e) { let { index } = e.currentTarget.dataset let { selectPicIndex } = this.data selectPicIndex = index this.setData({ selectPicIndex }) }, // 选择消费类别中的细节 selectSpendDetail(e) { let { index } = e.currentTarget.dataset let { selectIndex } = this.data selectIndex = index this.setData({ selectIndex }) } // wxml // 消费类型 <view class="expense"> <block wx:for="{{detail}}" wx:key="index"> <view class="expense__type" bindtap="selectSpend" data-index="{{index}}"> <block wx:if="{{selectPicIndex == item.pic_index}}"> <view class="expense__type-icon" style="background-color: #e64343"> <image src="{{item.pic_url_act}}"></image> </view> </block> <block wx:else> <view class="expense__type-icon"> <image src="{{item.pic_url}}"></image> </view> </block> <view class="expense__type-name">{{item.type}}</view> </view> </block> </view> // 消费子类型 <view class="detail"> <block wx:for="{{detail[selectPicIndex].detail}}" wx:key="index"> <view class="detail__type" bindtap="selectSpendDetail" data-index="{{index}}"> <image class="detail__type-icon" src="{{item.detail_url}}"></image> <block wx:if="{{selectIndex == item.detail_index}}"> <view class="detail__type-name" style="color: #f86319; border-bottom: 1rpx solid #f86319;"> {{item.detail_type}} </view> </block> <block wx:else> <view class="detail__type-name" style="border-bottom: 1rpx solid #e4e2e2;"> {{item.detail_type}} </view> </block> </view> </block> </view> [代码] 账本页明细 [图片] 因为收支明细中需要显示每一天的消费信息,所以需要将数据表中的数据通过时间来分类,分成若干个数组,页面从而使用wx:for来遍历这些数组。在显示之前,首先需要判断有无收支信息。 [代码]// 通过时间分类算法 {} => [ [{时间1}], [{时间2}], [{时间3}] ] arr.forEach(item => { if (!_this.isExist(item.fullDate, dateArr)) { dateArr.push([item]) } else { dateArr.forEach(res => { if (res[0].fullDate == item.fullDate) { res.push(item) } }) } }) // 使用map 方法构造 [{}, {}, {}, ...] 类型数组 dateArr = dateArr.map((item) => { let spend = 0 let income = 0 item.forEach(res => { if (res.money > 0) { spend += res.money } else { income += (-res.money) } }) return { item, spend, income } }) // 判断自身是否存在数组中 isExist(item, arr) { for (let i = 0; i < arr.length; i++) { if (item == arr[i][0].fullDate) return true } return false } [代码] 以上是小程序中比较复杂的逻辑实现。 运用云开发,开发一份专属自己的旅行小账本吧~
2019-03-15 - 如何监听小程序中的手势事件(缩放、双击、长按、滑动、拖拽)
mina-touch [图片] [代码]mina-touch[代码],一个方便、轻量的 小程序 手势事件监听库 事件库部分逻辑参考[代码]alloyFinger[代码],在此做出声明和感谢 change log: 2019.03.10 优化监听和绘制逻辑,动画不卡顿 2019.03.12 修复第二次之后缩放闪烁的 bug,pinch 添加 singleZoom 参数 2020.12.13 更名 mina-touch 2020.12.27 上传 npm 库;优化使用方式;优化 README 支持的事件 支持 pinch 缩放 支持 rotate 旋转 支持 pressMove 拖拽 支持 doubleTap 双击 支持 swipe 滑动 支持 longTap 长按 支持 tap 按 支持 singleTap 单击 扫码体验 [图片] demo 展示 demo1:监听 pressMove 拖拽 手势 查看 demo 代码 [图片] [图片] demo2: 监听 pinch 缩放 和 rotate 旋转 手势 (已优化动画卡顿 bug) 查看 demo 代码 [图片] [图片] demo3: 测试监听双击事件 查看 demo 代码 [图片] [图片] demo4: 测试监听长按事件 查看 demo 代码 [图片] [图片] demo 代码 demo 代码地址 mina-tools-client/mina-touch 使用方法 大致可以分为 4 步: npm 安装 mina-touch,开发工具构建 npm 引入 mina-touch onload 实例化 mina-touch wxml 绑定实例 命令行 [代码]npm install mina-touch[代码] 安装完成后,开发工具构建 npm *.js [代码]import MinaTouch from 'mina-touch'; // 1. 引入mina-touch Page({ onLoad: function (options) { // 2. onload实例化mina-touch //会创建this.touch1指向实例对象 new MinaTouch(this, 'touch1', { // 监听事件的回调:multipointStart,doubleTap,longTap,pinch,pressMove,swipe等等 // 具体使用和参数请查看github-README(底部有github地址 }); }, }); [代码] NOTE: 多类型事件监听触发 setData 时,建议把数据合并,在 touchMove 中一起进行 setData ,以减少短时内多次 setData 引起的动画延迟和卡顿(参考 demo2) *.wxml 在 view 上绑定事件并对应: [代码]<view catchtouchstart="touch1.start" catchtouchmove="touch1.move" catchtouchend="touch1.end" catchtouchcancel="touch1.cancel" > </view> <!-- touchstart -> 实例对象名.start touchmove -> 实例对象名.move touchend -> 实例对象名.end touchcancel -> 实例对象名.cancel --> [代码] NOTE: 如果不影响业务,建议使用 catch 捕获事件,否则易造成监听动画卡顿(参考 demo2) 以上简单几步即可使用 mina-touch 手势库 😊😊😊 具体使用和参数请查看Github https://github.com/Yrobot/mina-touch 如果喜欢mina-touch的话,记得在github点个start哦!🌟🌟🌟
2021-06-24 - 在公用Util文件中给小程序页面变量赋值
util文件为小程序公用的一个方法文件,我们可以将程序内经常用的方法写在这个文件中,通过其他文件引用,可以达到一个方法全局使用的方法,减少代码量,一定程度上减少维护时间.这是util中是没有办法用this.setData()方法去设置变量值的.因为他不属于任何一个页面.那我们如果想要在util文件的方法中给页面赋值怎么实现呢? 这里用到了微信公用方法:getCurrentPages() 之前做过一个案例希望可以加载页面的时候自动加载底部导航栏实现自定义热更新的小程序底部导航栏,如果分开写的话每个导航页面都需要写一个方法,就考虑想用Util写一个公共方法 代码如下: //获取底部导航方法 function settabbar() { wx.request({ url: getApp().globalData.url + ‘syssetting/gettabbar’,//获取底部导航图标,文字信息 method: ‘POST’, header: {// 设置请求的 header ’content-type’: ‘application/x-www-form-urlencoded’ }, data: { appid: getApp().globalData.appid }, success: function (res) { wx.hideLoading() var pages = getCurrentPages(); //这里使用了getCurrentPages()方法,该方法是用来获取浏览记录的 console.log(pages) //打印结果如下图 var prevPage = pages[pages.length - 1] prevPage.setData({ tabbar: res.data, page:’/’+ prevPage.route }) }, fail: function (res) { wx.showToast({ title: ‘请求失败’, }) }, }) } [图片] 我们可以看到结果中的route表示了当前打开的页面 如果我们再首页点进商品详情页再打印出路由记录时会显示如下结果: [图片] 我们会发现变量pages为2个元素的数组了,展开第二个元素可以看到现在打开的商品详情页,第一个元素的路径为首页路径 这样我们可以得到的在Util页面中标示当前页面为pages[pages.length - 1] var prevPage = pages[pages.length - 1] prevPage.setData({ tabbar: res.data, page:’/’+ prevPage.route }) 我们用这个方法就可以在util页面中给引用的页面赋值了!!! PS:util文件的编写方法 和暴露接口的方法请参照小程序文档
2021-04-15