- Donut 安全网关现已推出!业务加速,安全守护!
[图片] [图片] [图片] [图片]
2023-06-12 - 小程序请求数据双向混合加密和防篡改+防重放攻击的实现
前言 大家好,借着中秋放假明日又要上班的这个晚上,平常又没空,趁这个时间点就决定来一篇。 [图片] 我们都知道微信小程序的服务端API上,官方可谓是做足了心思,对用户的数据进行加密,虽然对我们开发者来说似乎是一种麻烦,但是从长远角度来看,是十分有必要的,用户隐私高于一切。 那么在小程序的开发过程中与后端对接接口时是否有想过这样的问题呢? HTTPS真的百分百安全吗? 数据被成功抓包后安全吗? 数据被篡改后还有效吗? 请求被重放后安全吗? 世界上没有绝对安全的系统,但我们可以让它被破解的成本变高。 本篇文章专业性并不高,如果存在错误请大家为我指出来,以免误导别人,谢谢! 以下我们将小程序端称为C端,服务端称为S端,服务端代码是Node.js,仅供参考,但原理都一样,后端可以是其它语言。 思考 围绕着以上的问题,探究一下问题的答案? 本部分只对问题做思考,具体实现请参考下面的实现部分。 HTTPS真的百分百安全吗?只能说是相对安全,当然,在微信小程序的沙箱环境里,HTTPS通信会更加安全,否则官方可能会要求我们对请求加密了对吧。但百密一疏,C端是否存在漏洞,假设C端安全了难道S端就安全嘛?细思恐极,退一万步讲,百分百安全是不存在的,由于篇幅问题相关漏洞大家可以搜索探究。 假设数据被中间人成功抓包,如果数据是明文传输,那么将导致数据泄露,因此对数据进行加密是必要的,但应该如何加密呢?如何做到密钥安全?C端和S端如何进行数据的加解密?RSA和AES加密应该使用哪种加密明文?如何充分发挥RSA和AES两个加密算法的特长? 假设数据被篡改,如果因为请求数据被篡改而导致严重后果,那么很大程度上其实是代码设计有问题,正常设计中安全应被摆在重要的位置,至少不应该出现购买某样商品时是通过C端发送商品价格给S端调起支付那样(只需篡改商品价格即可支付极少的钱购买商品),如果代码设计并不存在严重问题,那么数据被篡改也是不可忽视的,我们需要进行数据签名,让不同的数据拥有唯一的MD5哈希值,如果数据被篡改,通过哈希值即可判断数据是否被篡改,当然也可能会有人问,既然数据被篡改,那攻击者会不会重新生成一个哈希值代替?是的。但是我们有接下来会说到的key,key会作为签名的数据变量之一,由于攻击者并不知道key值因此无法重新签名,key值建议不是固定值而是周期性更换的随机值,例如随着用户的登录态而产生并随之抹除。 假设请求被重放,大部分时候由于数据被加密和防篡改处理过,攻击者并无法直接获得数据或篡改,但如果通过劫持到的登录认证请求的原始数据并重新发起该请求,则攻击者将可能获得重要的认证数据,使得系统将攻击者作为正常用户处理,后续的请求攻击者仍能伪装成正常的用户进行后续攻击。 期望效果 在开始实现之前先看一下完成后的效果,以下是截取了Network中其中一个GET请求发送的数据: [图片] 实际上发送的数据是如下图所示: [图片] 然后是该请求返回的数据: [图片] 实际上返回的数据是如下图所示: [图片] 整个流程: [图片] 实现 由于整个流程在C端的实现上顺序反过来的,因此下面的步骤也将是反向而行。 工具库下载 工欲善其事,必先利其器!这三个库是经过修改压缩的,支持在小程序上使用并且体积可观(总共69.1KB,如果不涉及密码哈希处理只需要前两个库,体积只有65.3KB),接下来的实现操作将会使用到,建议大家可以根据实际情况对功能进行二次封装: CryptoJS.js:点击下载 RSA.js:点击下载 SHA256.js(可选):点击下载 在线的各类加解密工具(可选,可以收藏起来,平时测试挺有用):点击访问 请求数据防重放 要防范请求重放攻击,首先需要了解Unix时间戳 timestamp概念,和时间戳不一样的是它的单位是秒,事实上这个需求也只需要秒级即可。除此之外还将用到另一个值:nonce,它是一个随机产生并只能被使用一次的值,长度自定,请求越频繁长度需要越长(降低同一时间产生相同nonce的几率),C端发送请求时需要将timestamp和生成的nonce加入发送的参数中。那么,如何将两者结合呢?我这为防重放的目标下了一个简单的定义。 同样的请求只能发生一次,且请求必须在规定时间内发出。 何为同样的请求?不是指两次发送的参数一致就是一样的,而是连timestamp和nonce也一样才算是同样的请求。 那么S端如何确认其是同样的请求呢? S端每次接收到一个请求,都会将该请求的nonce存入缓存并保持60秒(这个阈值不一定是60秒,可以根据实际需要定义),时间过后该值将被移除,建议S端采用Redis存储nonce,这样可省去检测和移除nonce的代码。如果S端发现当前请求的nonce存在于已存储的nonce之中,则此请求发生重复,那么timestamp有何用? 如果只使用nonce我们只能保证该请求60秒内不会重复,但60秒后依然任人宰割,这不是要的结果。所以timestamp将用来限制时间,S端时间戳减去C端发送请求的时间戳,得到的差值为N秒,如果N秒大于60秒则此请求过期,那么则可以保证,60秒内因为nonce相同而被判为请求重放,60秒后因为时间差超过而被判为请求已过期,因此确保了请求不会被重放。。 以下展示三种情况: [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:5rKbMs2Fm3 C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:false 【重放校验通过】 [代码] [代码]C端时间戳:1568487722 //2019/9/15 03:05:22 C端NONCE:IzFEs52bAC C端发送请求 -> S端接收请求 S端时间戳:1568487922 //2019/9/15 03:02:00 CS端时间差:1568487922 - 1568487722 = 200 C端NONCE是否存在于缓存:false 【重放校验不通过,请求已过期,因为时间差超过60秒】 [代码] [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:IxwPHQU0nA C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:true 【重放校验不通过,此请求为重放请求,因为nonce已经存在,此请求已经完成,不可重复】 [代码] timestamp和nonce将作为参数参与下面部分的签名。 请求数据防篡改 C端数据签名 首先通过对参数按照参数名进行字典排序(调过一些第三方API的朋友应该明白),假设当前需要传输的参数如下: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3" } [代码] 进行字典排序,参数名顺序应为: [代码]const keys = Object.keys(data); //获得参数名数组 keys.sort(); //字典排序 console.log(key); //["a", "b", "c", "nonce", "timestamp"]; [代码] 参数字典排序后应和参数一起拼接为字符串,至于使用什么拼接符就要与S端商量了,如果参数值是一个数组或一个对象(如c为[1,2,3])那么可以将数据值转为JSON字符串再拼接。以上参数拼接后字符串如下: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 [代码] 下一步是计算拼接字符串的MD5哈希值了嘛? 不是。因为这样拼接的字符串很容易被攻击者伪造签名并篡改数据,这样就失去了签名的意义了。也就是说缺了一个key值。key值又从何而来?上面思考部分有提到建议从登录后发放,并且这个key与该用户的登录态绑定,登录态有效期间,将使用这个key进行请求的签名与验签,至于如何鉴别用户,我们会在最终发送给S端的参数加入一个sessionId作为登录态唯一标识,这里不加是因为这部分数据是需要参与后续的加密的,而sessionId不参与加密。 但是可能又会有一个问题,登录前没有key怎么实现的登录请求?事实上,登录请求并不怕篡改,因为攻击者自己也不知道账号密码,所以无需提供key用于登录请求的签名。 提到登录密码这个需要注意一点,密码不能明文传输,请计算哈希值后传输,S端比对账户密码哈希值即可确认是否正确,同样S端非特殊情况也不能明文存储密码,建议SHA-1或更高级的SHA-256计算后的值,MD5值可能被使用彩虹表(一种为各种常见密码建立的MD5映射表)破解。SHA256计算的库已在上面工具库下载提供。 拼接上我们登录时随机生成的key: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB [代码] 接下来计算32位MD5哈希值 为什么上面说MD5会被破解而这里却用MD5计算?因为此处计算MD5的目的并不是为了隐藏明文数据,而只是用于数据校验 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const signStr = "a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB"; const sign = CryptoJS.MD5(signStr).toString(); //a42af0962de99e698d27030c5c9d3b0e [代码] 这么一来我们的数据签名阶段就完成了,然后需要把签名加入参数之中,将和参数一起传输,需要注意的是传输参数无需在意参数名排序。以下是当前的参数处理结果: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } [代码] 其实从上面这两部分内容来看,会发现防重放和防篡改是相辅相成的,就像两兄弟一样,少了谁都干不好这件事。 S端验证签名 既然有签名那必定也有验签,验签流程其实就是重复C端的前面流程并比对CS两端得出的签名值是否一致。S端取得请求数据后(假设数据未加密,暂时不讨论解密),将除了sign之外的参数名进行字典排序,sign用一个临时变量存下,然后排好序的参数和C端一样拼接得到字符串。 接下来根据上面部分提到的未加密参数sessionId获得用户登录态并获取到该状态的临时key拼接到字符串末尾,接下来进行MD5计算即可得到S端方获得的签名,此时与请求中携带的sign比较是否一致则可确定签名是否有效,如果不一致返回签名错误。具体流程请参考以下: [代码]C端发送请求 -> S端接收请求 //请求参数为data const _sign = data.sign; //排序并拼接除sign外的参数 let signStr = a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 const sessionId = ...; //用户的sessionId const sessionData = getUserSessionData(sessionId);//根据sessionId查询用户登录态sessionData并取出key,并拼接到字符串尾部 const key = sessionData.key signStr += key; //MD5计算并比对,代码仅供参考 const sign = crypto.md5(signStr); if(sign !== _sign) { //签名错误! } [代码] 请求数据加解密 有了上面部分的参数处理铺垫后,接下来就该开始本文章最核心的加解密,在这之前我们先了解AES和RSA两种加密算法。了解概念后我们再来思考一下两者的一些特性。 AES加解密需要密钥,除某些模式外还需要提供初始化向量,可使用密钥解出明文,是对称加密算法。 RSA加解密需要一对密钥,分别为公钥和私钥,公钥加密,私钥解密,是非对称加密算法。 两者在加密长文本性能上AES占优势。 根据这些让我们发现,他们可以形成互补关系,RSA加解密安全性高但长文本处理性能不及AES。AES加解密长文本性能优于RSA但需要明文密钥和向量加解密,密钥的安全性成问题。 那么在C端生成随机的AES密钥和向量,使用密钥和向量使用aes-128-cbc加密模式(也可以根据实际需要采用其它的模式)加密真正需要传输的参数(参数则是经过防重放+防篡改处理的参数)得到encryptedData,然后将该密钥使用RSA公钥加密得到encryptedKey,下面我顺便把AES加密的向量也一起加密了得到encryptedIV,这样就完美的互补了对方的缺点,既能够较快的完成数据加密又能保证密钥安全性,两全其美。 整个个加解密流程如下图所示: [图片] 下面的四个小章节将逐一描述流程实现: C端加密数据 C端加密后的数据应如下(并非固定格式,根据自己需要定制): [代码]{ "sessionId": "xxxxxxxx", "encryptedData": "xxxxxx", "encryptedKey": "xxxxxx", "encryptedIV": "xxxxxxx" } [代码] 但RSA公钥又是怎么发放到C端的呢?答案是在登录认证的时候服务器下发的,登录成功时服务器会创建RSA密钥对并把公钥发放给C端,私钥存在服务器上该用户的登录态数据中。流程如下所示: [代码]C端发起登录请求 -> S端接收登录请求 S端登录认证是否通过 true S端生成RSA密钥对 - publicKey , privateKey S端查询相关用户信息 S端生成登录态信息 -> 向登录态信息存入privateKey私钥,登录态信息类似如下 "xxxxxx": { id: "xxxxx", authData: { key: "xxxxxx", privateKey: "xxxxxx" } } S端返回登录态唯一标识sessionId和publicKey公钥以及相关用户信息 -> C端 C端存储登录态信息于本地,后续请求将使用服务器提供的公钥进行加密 [代码] 其中privateKey就是该用户当前登录态所使用的解密私钥,C端通过公钥加密后的AES密钥数据只能用该私钥解密。 如果希望登录阶段的请求也加密,那么可以手动生成一个RSA密钥对,然后客户端放置一个固定的公钥,服务器也使用一个固定的私钥进行登录阶段的加解密。 具体实现如下: 此处引入了CryptoJS.js和RSA.js [代码]const CryptoJS = require('./CryptoJS'); const RSA = require('./RSA.js'); //假设当前已登录成功并获得S端下发的RSA公钥且已存入本地存储 //createRandomStr为生成随机大小写英文和数字的字符串 const aesKey = createRandomStr(16); //生成AES128位密钥 16字节=128位 const aesIV = createRandomStr(16); //生成初始化向量IV const raw = JSON.stringfly({ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" }); const encryptedData = CryptoJS.AES.encrypt(data: raw, key: aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //使用CBC模式和Pkcs7填充加密 const authData = wx.getStorageSync("authData"); //读取本地存的RSA公钥 RSA.setPublicKey(authData.publicKey); //设置RSA公钥 const encryptedKey = RSA.encrypt(aesKey); //RSA加密AES加密密钥 const encryptedIV = RSA.encrypt(aesIV); //RSA加密AES加密初始化向量,是否加密向量可由自己决定 //最后的处理结果 const result = { sessionId: authData.sessionId, encryptedData, encryptedKey, encryptedIV }; [代码] S端解密数据 [代码]const crypto = require('crypto'); const cryptojs = require('crypto-js'); const { sessionId, encryptedData, encryptedKey, encryptedIV } = requestData; const authData = getUserSessionData(sessionId); //获取用户登录态数据 const privateKey = authData.privateKey; const aesKey = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedKey); //使用私钥解密得到AES密钥 const aesIV = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedIV); //使用私钥解密得到AES iv向量 const key = cryptojs.enc.Base64.parse(aesKey); const iv = cryptojs.enc.Utf8.parse(aesIV); const decryptedData = cryptojs.AES.decrypt(encryptedData, key, { iv, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //采用与加密统一的模式和填充进行解密 const { "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } = decryptedData; //至此解密得到C端传来的数据 [代码] S端加密数据 当S端处理完C端的请求后应加密响应数据,那么加密响应数据应该使用什么密钥呢?既然C端已经将加密的密钥发送过来了,那么干脆将C端使用的AES密钥拿来加密响应数据就可以了。加密的数据传回C端后,C端只需使用该请求所使用的AES加密密钥进行解密即可。响应的加密数据如下: [代码]const cryptojs = require('crypto-js'); const responseData = JSON.stringify(...); //此为S端需要返回给C端的数据 const aesKey = ...; //此为之前C端用来加密数据的AES密钥 const aesIV = createRandomStr(16); //生成初始化向量IV const encryptedData = cryptojs.AES.encrypt(data, aesKey, { iv: aesIV, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //加密响应数据 const encryptedResponse = { "encryptedData": encryptedData, "iv": aesIV }; //得到加密后的响应数据并返回给C端 [代码] C端解密数据 C端接收到S端的响应数据后应对加密的数据进行解密,此次解密就是单纯的AES解密了,使用发起请求时用于加密数据的AES密钥配合响应数据的iv向量对encryptedData进行解密,得到解密后的数据即为S端真正的响应数据。实现过程如下: 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const key = ...; //之前用于加密请求参数的AES加密密钥 const { encryptedData, iv } = responseData; const aesIV = CryptoJS.enc.Utf8.parse(iv); const aesKey = CryptoJS.enc.Utf8.parse(key); const decryptedData = CryptoJS.AES.decrypt(encryptedData, aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //AES解密响应数据 const { ... } = decryptedData; //得到解密后的响应数据 [代码] 结语 第一次写这么长的文章,可能存在大量纰漏,如果大佬发现问题欢迎指出(`・ω・´)我会马上修改。 也许小程序的运行环境会比我想象中的更安全 也许HTTPS也会比我想象中的更安全 也许Web服务器引擎也比我想象中的更安全 但,安全不总是先行一步的吗?
2019-10-24 - 一种优雅的小程序授权方案分享
一种优雅的小程序授权方案分享 ~ [图片] ~ ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ 其实在很久之前的一次产品调研中,我体验过这个交互,但是当时体验的太多了,没有做好记录,回头再找已经找不到了,昨天突然又翻到了这个小程序,十分惊喜 还是原先的设计 这个授权的交互设计,是我见过的所有授权中最棒的一个, 把用户信息授权和手机授权做到了一起,同时展示了授权的进度 目前我还有一个疑惑就是 如果只做了用户授权,而没有手机授权,下次进来会是什么交互呢,这个值得继续体验下
2021-12-01 - 【笔记】横向滑动列表的渲染
前言 今天在学习列表渲染的时候,尝试实现了支持横向滑动的列表,但是遇到了很多问题,做一个小小的总结。 组件scroll-view scroll-view是一种视图容器,指定可滚动视图区域。通过设置属性scroll-y=true并给给scroll-view一个固定高度height,可以实现竖向滚动;通过设置属性scroll-x=true可以实现横向滚动,其他的属性可以参考官方文档。 列表渲染 列表渲染是一种很基础的渲染方法,在组件上使用 wx:for 绑定一个数组,即可使用数组中各项的数据重复渲染该组件。在组件中,使用 wx:for-item 可以指定数组当前元素的变量名,默认为item;使用 wx:for-index 可以指定数组当前下标的变量名,默认为index;使用 wx:key 来指定列表中项目的唯一的标识符,可以提高渲染效率(没有特殊需求的话可以直接用index指定)。 例如,在.js中声明一个含有六个元素的数组list,可以用以下代码循环地渲染list中的所有元素 [代码]<!-- index.wxml 列表渲染 --> <view wx:for="{{list}}" wx:key="{{index}}" class="view-parent"> <view class="view-item">{{item.txt}}</view> </view> [代码] 显示效果如下: [图片] 横向滑动列表 将组件scroll-view和列表渲染结合,就可以实现横向滑动列表了,样例代码如下: [代码]<!-- index.wxml 横向滑动列表 --> <text style="margin-left: 40%;">横向滑动列表</text> <scroll-view scroll-x="true" class="scroll-x-list"> <view wx:for="{{list}}" wx:key="{{index}}" class="view-parent"> <view class="view-item">{{item.txt}}</view> </view> </scroll-view> /* index.wxss */ .scroll-x-list{ height:150px; } .view-item{ width:100px; height:100px; background:#1bf891; margin:10px; } [代码] 显示效果如下: [图片] 可以看到结果和预期差很多,不仅没有实现横向滑动,还没有显示出所有的元素。原因是代码虽然在组件中设置了需要的属性,但是在样式上没有做对应的调整,我们必须在wxss中设置布局才可以达到预期效果。最容易想到的就是我们常用的flex布局,关于flex布局的内容比较多,这里就不展开了,推荐看官方文档学习。在进行下一步修改前,先声明几个必须要知道的小细节: 组件scroll-view是不支持flex布局的,要想在scroll-view中使用flex布局,必须嵌套一个其他的支持flex布局的容器,如view。 scroll-view 中的需要滑动的元素不可以用 float 浮动。 scroll-view 中在需要装载滑动元素的父容器中开启flex布局是没有作用的,应该使用dislay:inline-block来进行元素的横向编排。 第一种方法,由于要实现的是横向滑动列表,那么容器中的元素一定是不允许换行的,刚刚提到,scroll-view是不支持flex布局的,所以开启flex布局并设置flex-wrap=nowrap是无效的行为。我们选择在类scroll-x-list中设置white-space: nowrap来处理元素中的空白,让容器内的换行无效。同时,还应设置装载滑动元素的父容器——view-parent的dislay为inline-block,代码如下: [代码]/* index.wxss */ .scroll-x-list{ height:150px; white-space: nowrap; } .view-parent{ display:inline-block; } [代码] 显示效果如下,已经可以横向滚动列表了。 [图片] 第二种方法,可以在scroll-view中嵌套一个view,在这个view中开启flex布局并设置flex-wrap=nowrap来阻止换行,代码如下: [代码]<!-- index.wxml 另一种横向滑动列表 --> <text style="margin-left: 40%;">横向滑动列表</text> <scroll-view scroll-x="true" class="scroll-x-list"> <view class='flex-view'> <view wx:for="{{list}}" wx:key="{{index}}" class="view-parent"> <view class="view-item">{{item.txt}}</view> </view> </view> </scroll-view> /* index.wxss */ .scroll-x-list{ height:150px; } .flex-view{ display:flex; flex-wrap: nowrap; } .view-parent{ display:inline-block; } [代码] 显示效果与刚刚相同: [图片] 一些改进 在第一种方法中,我们没有使用到flex布局,就很容易遇到一些对齐的问题,假设我们设置数组中第三个元素为空,就会出现下面的情况: [图片] 原因很简单,inline-block的属性中在某个元素没有内容的情况下,它的基线对齐方式是基于这个元素的底边的,解决方式是设置一个垂直的对齐方式: [代码]/* index.wxss */ .view-parent{ display:inline-block; vertical-align: top; } [代码] 显示效果如下: [图片] 同时,你会发现第二种开启flex布局方法的横向列表不会有这种对齐的问题,我们还可以在装载滑动元素的容器中开启flex布局来让内容更加美观: [代码].view-item{ width:100px; height:100px; background:#1bf891; margin-right: 20px; align-items:center; display:flex; justify-content:center; } [代码] 显示效果如下: [图片] 总结 scroll-view是一个十分常见实用的组件,但是使用时也有一些需要注意的问题,比如不支持直接使用flex布局。总体来看,比起设置inline-block的布局,更推荐在scroll-view中嵌套一层view再开启flex布局的方法,可以更灵活的摆放控制滑动元素。
2021-11-15 - 基于云开发的答题活动小程序v1.0,开开开源啦
基于云开发的微信答题活动小程序v1.0,开开开源啦!!!这个答题小程序,技术栈是基于云开发的微信原生小程序。 搭建教程系列文章11月是全国“119”消防宣传月,不少企事业单位会举办消防安全知识竞赛,因此我搭建了最新版的消防安全知识答题活动小程序。 我不但快速地完成了设计与开发工作并部署上线交付使用,还写了手把手教你搭建答题活动小程序系列文章,用以帮助初学者快速入门云开发。感兴趣的可以去我主页翻看系列文章,这里就不一一列举出来了。 答题小程序源码地址附上源码地址:uem/消防安全知识竞答活动小程序 下一篇,将补充一篇部署帮助文档,用以帮助初学者基于云开发快速部署上线一个答题活动小程序。 答题活动小程序界面[图片] 答题活动小程序版本当前版本v1.0 首页、答题页、结果页实现页面间跳转功能实现转发分享答题成绩功能实现用云开发实现查询题库功能实现动态题目数据绑定答题交互逻辑切换下一题提交答卷保存到云数据库集合系统自动判分答题结果页从云数据库查询答题成绩 后续我会在这个基础上继续开发,答题类微信小程序v2.0、v3.0,功能将会更加多以及更加完善。 计划版本v2.0 答题记录列表页登录页活动规则页题库随机抽题查询历史成绩微信授权登录获取微信头像和昵称排行榜等... 计划版本v3.0 题库练习支持多种题型选项乱序答题倒计时错题解析支持生成海报分享支持答题后抽奖后台数据监控后台管理等...
2021-11-17 - 分享几个最近访问100+的有趣小程序
1、下一个假期 这个小程序能够帮助你查看下一个假期的时间,十分方便 [图片] [图片] 2、潮汐睡眠 如果平时睡不着的话,也可以听一听,放松一下,有助于睡眠 [图片]、 [图片] 3、人生进度 看完这个小程序,不由为时间做好了规划~ [图片] [图片]
2021-11-19 - 如何实现一个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 - 如何实现圣诞节星星飘落效果
圣诞节快到啦~🎄🎄🎄🎄咱们也试着做做小程序版本的星星✨飘落效果吧~ 先来个效果图: [图片] 484听起来很叼,看起来也就那样。 来咱们上手开始撸 页面内容wxml,先简单切个图吧。 [代码]<view class="container"> <image src="https://qiniu-image.qtshe.com/merry_001.jpg" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_02.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_03.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_04.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_05.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_06.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_07.jpg" alt="" mode="widthFix"/> </view> <canvas canvas-id="myCanvas" /> [代码] 页面样式wxss,因为切片用的不太熟练,图片之间有个2rpx的空隙。 [代码].container { height: 100%; box-sizing: border-box; min-height: 100vh; } image { width: 100%; display: block; margin-top: -2rpx; //不会切图造的孽 } canvas { width: 100%; min-height:100vh; position: fixed; top: 0; z-index: 888; } [代码] 重点JS: [代码]//获取应用实例 const app = getApp() // 存储所有的星星 const stars = [] // 下落的加速度 const G = 0.01 const stime = 60 // 速度上限,避免速度过快 const SPEED_LIMIT_X = 1 const SPEED_LIMIT_Y = 1 const W = wx.getSystemInfoSync().windowWidth const H = wx.getSystemInfoSync().windowHeight var starImage = '' //星星素材 wx.getImageInfo({ src: 'https://qiniu-image.qtshe.com/WechatIMG470.png', success: (res)=> { starImage = res.path } }) Page({ onLoad() { this.setAudioPlay() }, onShow() { this.createStar() }, createStar() { let starCount = 350 //星星总的数量 let starNum = 0 //当前生成星星数 let deltaTime = 0 let ctx = wx.createCanvasContext('myCanvas') let requestAnimationFrame = (() => { return (callback) => { setTimeout(callback, 1000 / stime) } })() starLoop() function starLoop() { requestAnimationFrame(starLoop) ctx.clearRect(0, 0, W, H) deltaTime = 20 //每次增加的星星数量 starNum += deltaTime if (starNum > starCount) { stars.push( new Star(Math.random() * W, 0, Math.random() * 5 + 5) ); starNum %= starCount } stars.map((s, i) => { //重复绘制 s.update() s.draw() if (s.y >= H) { //大于屏幕高度的就从数组里去掉 stars.splice(i, 1) } }) ctx.draw() } function Star(x, y, radius) { this.x = x this.y = y this.sx = 0 this.sy = 0 this.deg = 0 this.radius = radius this.ax = Math.random() < 0.5 ? 0.005 : -0.005 } Star.prototype.update = function() { const deltaDeg = Math.random() * 0.6 + 0.2 this.sx += this.ax if (this.sx >= SPEED_LIMIT_X || this.sx <= -SPEED_LIMIT_X) { this.ax *= -1 } if (this.sy < SPEED_LIMIT_Y) { this.sy += G } this.deg += deltaDeg this.x += this.sx this.y += this.sy } Star.prototype.draw = function() { const radius = this.radius ctx.save() ctx.translate(this.x, this.y) ctx.rotate(this.deg * Math.PI / 180) ctx.drawImage(starImage, -radius, -radius * 1.8, radius * 2, radius * 2) ctx.restore() } }, setAudioPlay() { let adctx = wx.createInnerAudioContext() adctx.autoplay = true adctx.loop = true adctx.src = 'https://dn-qtshe.qbox.me/Jingle%20Bells.mp3' adctx.onPlay(() => { console.log('开始播放') adctx.play() }) } }) [代码] 以上只是简单实现了一个星星飘落的效果,预览的时候需要开启不校验合法域名哦~ 目前还有更优的h5版本,使用Three.js实现的,在小程序内使用Three.js对于我来说有点打脑壳,先把效果分享出来吧。 h5版本,手机看效果最佳 h5源码可直接右键查看:https://qiniu-web.qtshe.com/merryChrimas.html
2023-12-07 - 炫酷的wxss动画效果
因为没啥事,研究了下小程序的粒子动画,最后放弃了,实在是头大。去搞了一些花里胡哨的效果,没啥实际用处,就分享玩玩,看能不能提供一些其他灵感啥的。 [图片] 代码片段如下:https://developers.weixin.qq.com/s/VQwYjYm47dgH 使用wxss绘制烟花动画 [图片] https://developers.weixin.qq.com/s/xcJdoMmW7lh3 蜡烛逼真燃烧效果: [图片] https://developers.weixin.qq.com/s/Iom47XmO7rh5 螺旋旋转效果 [图片] https://developers.weixin.qq.com/s/1BnRTXmZ7Rhj 炫酷wxss粒子动画 [图片] https://developers.weixin.qq.com/s/cRpjQXmb7khN 水文章
2020-08-03 - 使用腾讯位置服务API如何实现打卡功能?
一、什么是腾讯位置服务 腾讯位置服务提供了定位,地图,地点抖索,导航等各种各样的服务。并且提供了各行各业相关行业解决方案,腾讯位置服务在多平台为开发者提供了丰富的地图展现形式,帮助从属于不同领域的开发人员轻松完成构建地图并在其基础上打造专属内容的工作。同时配合海量数据、个性化定制、可视化等能力满足各个行业场景下对地图的需求。 二、 如何使用腾讯位置服务API a. 注册成为开发者 b. 申请密钥,在如图位置创建新密钥 [图片]c. 进行配置 [图片]推荐使用授权IP的方式进行配置,将你个人的IP写进输入框就OK啦! 3 官方Api使用示例(JavaScript) 3.1 通过搜索接口找到个人公司位置: [图片] key换成你的 [图片]查询到公司位置为(31.329716,121.508386) 3.2 初始化地图,中心设为公司位置 [代码]<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Hello world!</title> <style type="text/css"> #container{ /*地图(容器)显示大小*/ width:1200px; height:400px; } </style> <!--引入Javascript API GL,参数说明参见下文--> <script src="https://map.qq.com/api/gljs?v=1.exp&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77"></script> <script> //地图初始化函数,本例取名为init,开发者可根据实际情况定义 function initMap() { //定义地图中心点坐标 var center = new TMap.LatLng(31.329716, 121.508386); //定义map变量,调用 TMap.Map() 构造函数创建地图 var map = new TMap.Map(document.getElementById('container'), { center: center,//设置地图中心点坐标 viewMode:'2D',//设置显示模式 2D 3D可以自己修改 zoom: 17.2, //设置地图缩放级别 pitch: 43.5, //设置俯仰角 rotation: 45 //设置地图旋转角度 }); } </script> </head> <!-- 页面载入后,调用init函数 --> <body onload="initMap()"> <!-- 定义地图显示容器 --> <div id="container"></div> </body> </html> [代码] 效果展示: [图片]标注的位置就是设置的中心啦。 3.3 为公司位置位置打上标记 效果示例: [图片] 公司的位置加上了style中写的图片,代码如下: [代码]<script src="https://map.qq.com/api/gljs?v=1.exp&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77"></script> <script> //地图初始化函数,本例取名为init,开发者可根据实际情况定义 function initMap() { //创建map对象,初始化地图 var center = new TMap.LatLng(31.329716, 121.508386); var map = new TMap.Map(document.getElementById('container'), { center: center,//设置地图中心点坐标 zoom: 17.2, //设置地图缩放级别 pitch: 43.5, //设置俯仰角 rotation: 45 //设置地图旋转角度 }); //创建并初始化MultiMarker var markerLayer = new TMap.MultiMarker({ map: map, //指定地图容器 //样式定义 styles: { //创建一个styleId为"myStyle"的样式(styles的子属性名即为styleId) "myStyle": new TMap.MarkerStyle({ "width": 25, // 点标记样式宽度(像素) "height": 35, // 点标记样式高度(像素) "src": '微信图片_20210107230437.jpg', //图片路径 //焦点在图片中的像素位置,一般大头针类似形式的图片以针尖位置做为焦点,圆形点以圆心位置为焦点 "anchor": { x: 16, y: 32 } }) }, //点标记数据数组 geometries: [{ "id": "1", //点标记唯一标识,后续如果有删除、修改位置等操作,都需要此id "styleId": 'myStyle', //指定样式id "position": new TMap.LatLng(31.329716, 121.508386), //点标记坐标位置 "properties": {//自定义属性 "title": "我的公司" } } ] }); } </script> [代码] 3.4 让标记的点动起来 主要通过使用moveAlong方法,定义移动的轨迹path,在moveAlong中传入移动的路线和坐标点,坐标点是geometries中的标记。 [代码] <script> //地图初始化函数,本例取名为init,开发者可根据实际情况定义 function initMap() { //创建map对象,初始化地图 var center = new TMap.LatLng(31.329716, 121.508386); var map = new TMap.Map(document.getElementById('container'), { center: center,//设置地图中心点坐标 zoom: 17.2, //设置地图缩放级别 pitch: 43.5, //设置俯仰角 rotation: 45 //设置地图旋转角度 }); //创建并初始化MultiMarker var marker = new TMap.MultiMarker({ map: map, //指定地图容器 //样式定义 styles: { //创建一个styleId为"myStyle"的样式(styles的子属性名即为styleId) "myStyle": new TMap.MarkerStyle({ "width": 25, // 点标记样式宽度(像素) "height": 35, // 点标记样式高度(像素) "src": '微信图片_20210107230437.jpg', //图片路径 //焦点在图片中的像素位置,一般大头针类似形式的图片以针尖位置做为焦点,圆形点以圆心位置为焦点 "anchor": { x: 16, y: 32 } }) }, //点标记数据数组 geometries: [{ "id": "person", //点标记唯一标识,后续如果有删除、修改位置等操作,都需要此id "styleId": 'myStyle', //指定样式id "position": new TMap.LatLng(31.329716, 121.508386), //点标记坐标位置 "properties": {//自定义属性 "title": "我的公司" }} ] }); //定义的移动路线 var path = [ new TMap.LatLng(31.329716, 121.508386), new TMap.LatLng(31.330720, 121.508388), new TMap.LatLng(31.349725, 121.508389), new TMap.LatLng(31.359730, 121.508389), new TMap.LatLng(31.369718, 121.518400) ]; marker.moveAlong({ //让坐标动起来 "person": { path, //速度为100 speed: 100 } }, { autoRotation:true //车头始终向前(沿路线自动旋转) } ) } </script> [代码] 4. 使用腾讯位置服务API打卡功能实现 4.1 实现思路 [图片] 4.1.1 位置搜索 接口: https://apis.map.qq.com/ws/place/v1/search 搜索上海 上海五角场地铁站的位置 示例:https://apis.map.qq.com/ws/place/v1/search?keyword=五角场地铁站&boundary=region(上海,0) &key=HLSBZ– 搜索结果截取: { “id”: “10804093066539767491”, “title”: “五角场[地铁站]”, “address”: “地铁10号线”, “tel”: “”, “category”: “基础设施:交通设施:地铁站”, “type”: 2, “location”: { “lat”: 31.298109, “lng”: 121.514651 }, “ad_info”: { “adcode”: 310110, “province”: “上海市”, “city”: “上海市”, “district”: “杨浦区” } } 现在知道五角场地铁站的经纬度了,就是Location那两个值。 注意key是创建好的这一串ID [图片]通过搜索出来的是一个List,上述只拿出了List的第一位,通常第一位也是嘴和搜索关键字相符合的。通过搜索取出我们固定的经纬度的地址。 4.1.2 定位(Android定位) 单次定位,获取当前设备位置 [代码]mLocationManager.requestSingleFreshLocation(null, mLocationListener, Looper.getMainLooper()); [代码] 其他IOS等设备可以通过腾讯位置服务官方文档查看。 获取到位置后将该位置传到后台接口。 4.1.3 后台接口 求距离: [代码]// 写死的公司位置,实际项目中应该是数据库配置 private static final double COMPANY_LAT = 31.298109; private static final double COMPANY_LNG = 121.514651; @GetMapping("distance") public Map<String,Double> queryDistance(Double lat,Double lng){ HashMap<String, Double> result = new HashMap<>(4); double distance = distance(COMPANY_LAT, lat, COMPANY_LNG, lng); result.put("distance",distance); return result; } /** * 求两个经纬度之间的距离 */ public static double distance(double lat1, double lat2, double lng1, double lng2) { final int r = 6371; double latDistance = Math.toRadians(lat2 - lat1); double lonDistance = Math.toRadians(lng2 - lng1); double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); double distance = r * c * 1000; distance = Math.pow(distance, 2); return Math.sqrt(distance); } [代码] 5 腾讯位置服务整体介绍 [图片] 可以看到腾讯位置服务已经提供了一套针对各个终端的开发接口,快去学习吧! 原文作者:兴趣使然的程序猿
2021-07-22 - CryptoJS 加解密使用示例
[代码]// SHA1 加密[代码] [代码]var[代码] [代码]value = [代码][代码]"123456"[代码][代码];[代码] [代码]var[代码] [代码]wordArray = CryptoJS.SHA1(value);[代码] [代码]var[代码] [代码]str = wordArray.toString(CryptoJS.enc.Hex);[代码] [代码]// HmacSHA1加密[代码] [代码]var[代码] [代码]message = [代码][代码]"message"[代码][代码];[代码] [代码]var[代码] [代码]key = [代码][代码]"key"[代码][代码];[代码] [代码]var[代码] [代码]wordArray = CryptoJS.HmacSHA1(message, key);[代码] [代码]var[代码] [代码]str = wordArray.toString(CryptoJS.enc.Hex);[代码] [代码]// md5 加密[代码] [代码]var[代码] [代码]md5 = CryptoJS.MD5([代码][代码]"md5"[代码][代码]).toString();[代码] [代码]// AES 加解密 开始[代码] [代码]/**[代码] [代码] [代码][代码]* //AES 解密方法[代码] [代码] [代码][代码]* word 字符串[代码] [代码] [代码][代码]*/[代码] [代码]const AES_JIA = [代码][代码]function[代码] [代码](word, key, iv) {[代码] [代码] [代码][代码]let encryptedHexStr = CryptoJS.enc.Hex.parse(word);[代码] [代码] [代码][代码]let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);[代码] [代码] [代码][代码]let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });[代码] [代码] [代码][代码]let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);[代码] [代码] [代码][代码]return[代码] [代码]decryptedStr.toString();[代码] [代码]}[代码] [代码]/**[代码] [代码] [代码][代码]* //AES 加密方法[代码] [代码] [代码][代码]* word 字符串[代码] [代码] [代码][代码]*/[代码] [代码]const AES_JIE = [代码][代码]function[代码] [代码](word, key, iv) {[代码] [代码] [代码][代码]let srcs = CryptoJS.enc.Utf8.parse(word);[代码] [代码] [代码][代码]let encrypted = CryptoJS.AES.encrypt(srcs, key, {[代码] [代码] [代码][代码]iv: iv,[代码] [代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码] [代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]return[代码] [代码]encrypted.ciphertext.toString().toUpperCase();[代码] [代码]}[代码] [代码]const word = [代码][代码]"字符串格式"[代码][代码]; [代码][代码]// 字符串格式[代码] [代码]const key = CryptoJS.enc.Utf8.parse([代码][代码]"1234567890123456"[代码][代码]); [代码][代码]//十六位十六进制数作为密钥 ,十六位,十六位,不要 误以为 1234567890123456 == 123 是行得通的 字符长度16不等于 3,除非 key = 123[代码] [代码]const iv = CryptoJS.enc.Utf8.parse([代码][代码]''[代码][代码]); [代码][代码]//十六位十六进制数作为密钥偏移量[代码] [代码]var[代码] [代码]ctext = AES_JIA(word, key, iv);[代码] [代码]console.log([代码][代码]"ctext=>"[代码][代码], ctext); [代码][代码]// AES 加密[代码] [代码]var[代码] [代码]ptext = AES_JIE(ctext, key, iv);[代码] [代码]console.log([代码][代码]"ptext=>"[代码][代码], ptext); [代码][代码]// AES 解密[代码] [代码]// AES 加解密 结束[代码] [代码]//DES 加密[代码][代码]function[代码] DES_JIA[代码](message, key, iv) {[代码][代码] [代码][代码]var[代码] [代码]keyHex = CryptoJS.enc.Utf8.parse(key);[代码][代码] [代码][代码]var[代码] [代码]encrypted = CryptoJS.DES.encrypt(message, keyHex, {[代码][代码] [代码][代码]iv: iv,[代码][代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码][代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码][代码] [代码][代码]});[代码][代码] [代码][代码]return[代码] [代码]encrypted.toString();[代码][代码]}[代码] [代码]//DES 解密[代码][代码]function[代码] [代码]DES_JIE(ciphertext, key, iv) {[代码][代码] [代码][代码]var[代码] [代码]keyHex = CryptoJS.enc.Utf8.parse(key);[代码][代码] [代码][代码]// direct decrypt ciphertext[代码][代码] [代码][代码]var[代码] [代码]decrypted = CryptoJS.DES.decrypt({[代码][代码] [代码][代码]ciphertext: CryptoJS.enc.Base64.parse(ciphertext)[代码][代码] [代码][代码]}, keyHex, {[代码][代码] [代码][代码]iv: iv,[代码][代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码][代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码][代码] [代码][代码]});[代码][代码] [代码][代码]return[代码] [代码]decrypted.toString(CryptoJS.enc.Utf8);[代码][代码]}[代码] [代码]var[代码] [代码]des_text = DES_JIA(word, key, iv);[代码][代码]console.log([代码][代码]"des_text=>"[代码][代码], des_text); [代码][代码]// des 加密[代码] [代码]var[代码] [代码]ntext = DES_JIE(des_text, key, iv);[代码][代码]console.log([代码][代码]"ntext=>"[代码][代码], ntext); [代码][代码]// des 解密[代码] 调试(SHA1 加密)图片示例: [图片] 参考资料: https://cryptojs.gitbook.io/docs/ https://www.bootcdn.cn/crypto-js/
2019-05-29 - PHP7怎么加解密AES
微信小程序开发过程中,开启了消息推送,并且消息是完全模式,需要自己加解密,服务器语言使用PHP,但是问题来了,官方给的代码中的消息加解密方法在PHP7中并不能使用,mcrypt_module_open 等相关方法不支持了,请问在PHP7中如何加解密消息内容。下面是微信官方给的PHP5的加密方法,在php7中出现问题,求大佬解答 [图片]
2018-07-18 - 小程序内怎么调试web-view?
开发工具上在web-view页面内点击鼠标右键有个调试的选项 需要在真机上调试需要自行引入vconsole:https://github.com/Tencent/vConsole/blob/dev/README_CN.md
2019-10-09 - 【后端】checkSession加密无限失败
https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/login/auth.checkSessionKey.html 到底怎么加密? 像jssdk会有非常详细的拼字符串,等等非常详细。 你们这文档写的不清不楚。只告诉加密方式hmacsha256就没了。。。用hmacsha256加密什么?只加密session就可以了吗? 但是我不管怎么加密出来,都无限返回 invalid signature hint: [82oAkA03451090] 具体到底是怎么加密的??尝试了所有有可能有问题的点。所有结果都是invalid signature.... 我加密之后得到的字符串和你们文档提供的示例长度是一样的: 这是我的结果: [代码]b8b6065192421b94937f0c0b3b8fe690ecb8721fc5e192eba54af5860e60bbf4[代码]你们文档里的: [代码]fefce01bfba4670c85b228e6ca2b493c90971e7c442f54fc448662eb7cd72509[代码] 我的加密代码: [代码]private[代码] [代码]string[代码] [代码]GetHmacSha256CodeString([代码][代码]string[代码] [代码]raw, [代码][代码]string[代码] [代码]secret)[代码][代码]{[代码][代码] [代码][代码]secret = [代码][代码]string[代码][代码].IsNullOrEmpty(secret) ? [代码][代码]""[代码] [代码]: secret;[代码][代码] [代码][代码]byte[代码][代码][] keyByte = Encoding.UTF8.GetBytes(secret);[代码][代码] [代码][代码]byte[代码][代码][] messageBytes = Encoding.UTF8.GetBytes(raw);[代码][代码] [代码][代码]using[代码] [代码]([代码][代码]var[代码] [代码]hmacsha256 = [代码][代码]new[代码] [代码]HMACSHA256(keyByte))[代码][代码] [代码][代码]{[代码][代码] [代码][代码]byte[代码][代码][] outBytes = hmacsha256.ComputeHash(messageBytes);[代码][代码] [代码][代码]string[代码] [代码]result = BitConverter.ToString(outBytes).Replace([代码][代码]"-"[代码][代码], [代码][代码]""[代码][代码]).ToLower();[代码][代码] [代码][代码]return[代码] [代码]result;[代码] [代码] [代码][代码]}[代码][代码]}[代码] 麻烦指出问题所在。文档写得太不清楚了实在没招了。谢谢。
2019-08-26 - signature 失败?
在导入jssdk的时候 63002,invalid signature 我域名也设置了 使用官方的签名算法和后台返回给我的也一致 但为什么还是报63002,invalid signature [图片] 签名是一致的 为什么还会报invalid signature
2020-05-11 - 小程序主体迁移过程中,如果原主体已经注销后还能迁移吗?
企业主体注销的需要走公证流程 http://kf.qq.com/faq/180831mmyAJZ180831nE7jqE.html迁移申请函公证指引
2019-09-16 - 为什么 wx.onLocationChange 无法在调试工具中无法调用?
微信官方希望我们多用“wx.onLocationChange”以提高用户体验,于是我们正在从 "wx.getLocation" 向 “wx.onLocationChange”迁移,但是发现这个接口在调试工具中没有反应,这给开发带来了一定的不便,请官方尽快适配。 视频地址:https://static.zzzw666.top/video/2021-05-18-11-02-03.mp4 视频用代码片段:https://developers.weixin.qq.com/s/XX1Gxoma7dqf [图片] 代码片段:https://developers.weixin.qq.com/s/XX1Gxoma7dqf
2021-06-15 - PC微信,有聊天记录,但是搜索显示“无结果”
[图片] 所有的聊天记录都是这样,不管是群聊还是单聊,所有聊天记录搜索都是“无结果”,实际上是有聊天记录的。
2021-07-01 - 400元迁移微信公众号留言功能,迁移全过程
背景:2021年5月27日晚8点30分开始策划迁移公众号,迁移的目的为了是为了【留言】功能,因为从2018年开始注册的公众号都没有留言功能。 [图片] 为了不浪费朋友时间,我直接把他的管理员转移给我微信小号了,方便我来回各种扫码。进入主题,开始迁移公众号,先看官方流程图: [图片] 迁移流程总耗时5天(可以缩短到3天) 开始发起迁移: [图片] 管理员(我的微信小号)扫码验证后出现迁移协议,就是下面这个图 [图片] 输入目标账号原始ID(本公众号的原始ID)发送验证后,在目标账号管理员微信端(我的微信大号)允许即可 [图片] 同意后下一步下载迁移公函 [图片] 下载下来我们需要填写迁移双方基本信息,迁移公函需要双方盖章 [图片] 继续准备资料办理公证书:原公众号营业执照、原公众号后台截图、原法人身份证正反面、目标公众号营业执照、目标公众号后台截图、目标法人身份证正反面,公众号后台截图。 [图片] 准备好后开始办理公函公证书,可以在网上办理某宝1-2百左右。线下的话地图搜索:公证处,提前打电话问问能不能做公众号迁移公函公证书,问好了再去,大约3-5百左右。(下面是我的公证书) [图片] 然后我们上传这些资料去提交即可,最后支付300元。(如果资料传错了,有两次免费修改的机会,两次都错了那继续交300) [图片] 然后我们等着就好了,一般情况2、3天(工作日)就搞定了,期间会打电话确认,然后双方公众号管理员同意迁移,约1个工作日迁移成功。 [图片] 至此公众号迁移功能已完成,整理一下我准备的资料。 [图片] 最后做个timeline 2021年5月27日:和朋友沟通拿号 2021年5月28日:把朋友公众号管理员签到我的微信 2021年5月29日:开始准备迁移所有资料 2021年5月30日:TB线上办理公函公证书100,上传后支付300迁移费 2021年5月31日:迁移电话确认,双方管理员同意,迁移成功
2021-06-01 - 如何让for循环结束后,再执行下一条语句?
因为小程序是异步,在我for循环还没有执行完之前,就已经开始执行下一条语句了。但是我后面语句是需要调用for循环完成后的完整数据的,for循环好像又没有回调函数。所以,请问有没有办法让让for循环结束后,再执行下一条语句呢?
2021-06-29 - #小程序安全 小程序安全风控接口内测接入、对接过程
一、须知 二、申请 三、开发 四、疑问 五、官方反馈 一、须知 什么东西?->官方原话:为提高微信开放平台生态安全性,针对小程序各应用场景中可能存在的恶意注册、营销作弊等黑产风险和安全问题,平台将提供安全风控接口,协助开发者保障小程序正常安全运营。 目前处于内测申请,做营销活动时的有力把控参考,过滤专业撸羊毛组织人员; 前置条件:查询的用户必须在近30min内有进行小程序访; 原文:https://developers.weixin.qq.com/community/develop/doc/000e0ab1978668187deae479851001?blockType=1&page=2#comment-list API:https://developers.weixin.qq.com/community/develop/doc/000642b6d18500a87bea332515b409 二、申请 1.以小程序为单位申请(拿xxx测试) 2.邮件格式 2.1.标题->开通安全风控接口——XX小程序 2.2.收件人->MiniProgram@tencent.com 2.3.内容->必须包含:小程序名称、小程序AppID、小程序服务内容简介、近一周日活跃用户量、预估接口日调用量 如: 往期秒杀活动存在撸羊毛现象,同一个手机号连续下几单的情况, 前期系统限制了同ip、同用户只能下一单,后面加了同手机号功能只能下一单, 但从数据来看存在组织行为刷单,比如下单路径,下单操作时间,从正常来看不会操作那么快。 特申请此功能来鉴别是否是风险等级高的用户,然后做相应处理。 【小程序信息】 小程序名称:xxx官方旗舰店 小程序AppID:wxxx7902195fdd42b6 小程序服务内容简介:xxx品牌专为爱惜自己,注重个人及家庭护理、健康,追求高品质生活的女性而设,同时传递着对万千女性的关爱,追求卫生与健康。 近一周日活跃用户量:1xxxxxx 预估接口日调用量:1xxxxxxx ------------------------------------------------------11天后------------------------------------------------------ [图片] 三、开发 文档再此:https://developers.weixin.qq.com/community/develop/doc/000642b6d18500a87bea332515b409 没啥好说的,开通权限后对照文档调用即可; 1.因为需要访问小程序30分钟内,所以之前没有获取最近又没有访问小程序的是没有办法的; 2.建议系统记录安全等级,方便后面使用,读自己的库肯定比http给力;(定期更新服务) 3.目前不支持同一个接口返回两个场景的风险等级。 四、疑问 Q1:风险等级变更周期?比如一个人从0到1 或者从1到0,这个周期是多长,系统根据官方给的时间来定期更新,做到实时。 A1:官方给到回复是实时结果(可能上午是0,下午变成1)。 Q2:风险高会降成风险低或者0吗? A2:存在。 Q3:被投诉过风险等级高的,也有机会降低或者0吗,还是永久高风险? A3:取决于具体情况的哈,比如有的帐号处置是永久封禁,有的是功能限制,风险等级变化会依据帐号的变化而调整。 Q4:咨询一个情况,看看怎么防御,如果小程序开一个抽奖或者秒杀活动,识别用户是恶意注册或者营销作弊为3,提示他不能参与活动(提示语能否写微信官方识别为恶意注册用户或刷单用户,不能参与本活动),如果用户恶意投诉小程序做虚假活动这个怎么破? 另外1 2 3 4 内部是怎么定义名称与建议方案 比如 1 低风险 建议怎样 2、3中风险 建议怎样 4 高风险 建议怎样 A4:这个建议基于业务本身来做设计,黑产低回报场景,可以仅过滤高风险等级,减少误伤,高回报场景,过滤全量风险用户,增大覆盖。 另外设置这样的提示语可能不太合适,因为并非微信侧限制用户无法参与到某些业务活动;可以给被拦截的用户,提供一个申诉渠道的, 用户申诉后可以将情况发给我们,我们可以一起看下具体的拦截原因。 [图片] Q5:收费吗? A5:目前不收费
2020-11-19 - PHP微信支付类V3接口
不知不觉微信支付也更新了,接口版本也升级到了V3, 跟着微信的升级,将个人使用微信支付类也进行了升级, V3微信支付文档:https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml (相关方法已经过测试,不保证完全没问题,仅供参考,如测试有问题,可联系作者修改----查看原文) 使用方法还和之前的一样(V2微信支付),直接传递参数就可使用: 新版新增了[代码]composer[代码]安装,便于集成框架使用(Github地址): [代码]composer require fengkui/pay [代码] 首先把配置文件填写完整(细心不要填错,否则会导致签名错误): [代码]# 微信支付配置 $wechatConfig = [ 'xcxid' => '', // 小程序 appid 'appid' => '', // 微信支付 appid 'mchid' => '', // 微信支付 mch_id 商户收款账号 'key' => '', // 微信支付 apiV3key(尽量包含大小写字母,否则验签不通过) 'appsecret' => '', // 公众帐号 secert (公众号支付获取 code 和 openid 使用) 'notify_url' => '', // 接收支付状态的连接 改成自己的回调地址 'redirect_url' => '', // 公众号支付,调起支付页面 'serial_no' => '', // 证书序列号 'cert_client' => './cert/apiclient_cert.pem', // 证书(退款,红包时使用) 'cert_key' => './cert/apiclient_key.pem', // 商户私钥(Api安全中下载) 'public_key' => './cert/public_key.pem', // 平台公钥(调动证书列表,自动生成,注意目录读写权限) ]; [代码] 支付类封装相关方法: method 描述 js JSAPI下单 app APP支付 h5 H5支付 scan Navicat支付 xcx 小程序支付 query 查询订单 close 关闭订单 refund 申请退款 notify 支付结果通知 使用方法(这里已小程序支付为示例): [代码]<?php require_once('./vendor/autoload.php'); $config = []; // 支付配置 $order = [ 'order_sn' => time(), // 订单编号 'total_amount' => 1, // 订单金额(分) 'body' => '测试商品', // 商品名称 'openid' => '', // 用户openid // 'type' => 'Wap', ]; $wechat = new fengkui\Pay\Wechat($config); $re = $wechat->xcx($order); die(json_encode($re)); // JSON化直接返回小程序客户端 [代码] 如下代码是封装好的完整支付类文件(Wechat.php), 可以根据自己需求随意修改,详细的使用方法后期会有文档: [代码]<?php /** * @Author: [FENG] <1161634940@qq.com> * @Date: 2019-09-06 09:50:30 * @Last Modified by: [FENG] <1161634940@qq.com> * @Last Modified time: 2021-07-12 18:24:18 */ namespace fengkui\Pay; use Exception; use RuntimeException; use fengkui\Supports\Http; /** * Wechat 微信支付 * 新版(V3)接口(更新中) */ class Wechat { const AUTH_TAG_LENGTH_BYTE = 16; // 新版相关接口 // GET 获取平台证书列表 private static $certificatesUrl = 'https://api.mch.weixin.qq.com/v3/certificates'; // 统一下订单管理 private static $transactionsUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/'; // 申请退款 private static $refundUrl = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds'; // 静默授权,获取code private static $authorizeUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize'; // 通过code获取access_token以及openid private static $accessTokenUrl = 'https://api.weixin.qq.com/sns/oauth2/access_token'; // 支付完整配置 private static $config = array( 'xcxid' => '', // 小程序 appid 'appid' => '', // 微信支付 appid 'mchid' => '', // 微信支付 mch_id 商户收款账号 'key' => '', // 微信支付 apiV3key(尽量包含大小写字母,否则验签不通过) 'appsecret' => '', // 公众帐号 secert (公众号支付获取 code 和 openid 使用) 'notify_url' => '', // 接收支付状态的连接 改成自己的回调地址 'redirect_url' => '', // 公众号支付,调起支付页面 'serial_no' => '', // 证书序列号 'cert_client' => './cert/apiclient_cert.pem', // 证书(退款,红包时使用) 'cert_key' => './cert/apiclient_key.pem', // 商户私钥(Api安全中下载) 'public_key' => './cert/public_key.pem', // 平台公钥(调动证书列表,自动生成,注意目录读写权限) ); /** * [__construct 构造函数] * @param [type] $config [传递微信支付相关配置] */ public function __construct($config=NULL, $referer=NULL){ $config && self::$config = array_merge(self::$config, $config); } /** * [unifiedOrder 统一下单] * @param [type] $order [订单信息(必须包含支付所需要的参数)] * @param boolean $type [区分是否是小程序,是则传 true] * @return [type] [description] * $order = array( * 'body' => '', // 产品描述 * 'order_sn' => '', // 订单编号 * 'total_amount' => '', // 订单金额(分) * ); */ public static function unifiedOrder($order, $type=false) { $config = array_filter(self::$config); // 获取配置项 $params = array( 'appid' => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的应用ID 'mchid' => $config['mchid'], // 直连商户的商户号 'description' => $order['body'], // 商品描述 'out_trade_no' => (string)$order['order_sn'], // 商户系统内部订单号 'notify_url' => $config['notify_url'], // 通知URL必须为直接可访问的URL 'amount' => ['total' => (int)$order['total_amount'], 'currency' => 'CNY'], // 订单金额信息 ); !empty($order['attach']) && $params['attach'] = $order['attach']; // 附加数据 if (!empty($order['time_expire'])) { // 订单失效时间 preg_match('/[年\/-]/', $order['time_expire']) && $order['time_expire'] = strtotime($order['time_expire']); $time = $order['time_expire'] > time() ? $order['time_expire'] : $order['time_expire'] + time(); $params['time_expire'] = date(DATE_ATOM, $time); } if (!in_array($order['type'], ['native'])) { !empty($order['openid']) && $params['payer'] = ['openid' => $order['openid']]; $params['scene_info'] = ['payer_client_ip' => self::get_ip()]; } if (in_array($order['type'], ['iOS', 'Android', 'Wap'])) { $params['scene_info']['h5_info'] = ['type' => $order['type']]; $url = self::$transactionsUrl . 'h5'; // 拼接请求地址 } else { $url = self::$transactionsUrl . strtolower($order['type']); // 拼接请求地址 } $header = self::createAuthorization($url, $params, 'POST'); $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); $result = json_decode($response, true); if (isset($result['code']) && isset($result['message'])) { throw new \Exception("[" . $result['code'] . "] " . $result['message']); } return $result; } /** * [query 查询订单] * @param [type] $orderSn [订单编号] * @param boolean $type [微信支付订单编号,是否是微信支付订单号] * @return [type] [description] */ public static function query($orderSn, $type = false) { $config = self::$config; $url = self::$transactionsUrl . ($type ? 'id/' : 'out-trade-no/') . $orderSn . '?mchid=' . $config['mchid']; $params = ''; $header = self::createAuthorization($url, $params, 'GET'); $response = Http::get($url, $params, $header); $result = json_decode($response, true); return $result; } /** * [close 关闭订单] * @param [type] $orderSn [微信支付订单编号] * @return [type] [description] */ public static function close($orderSn) { $config = self::$config; $url = self::$transactionsUrl . 'out-trade-no/' . $orderSn . '/close'; $params['mchid'] = $config['mchid']; $header = self::createAuthorization($url, $params, 'POST'); $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); $result = json_decode($response, true); return true; } /** * [js 获取jssdk需要用到的数据] * @param [type] $order [订单信息数组] * @return [type] [description] */ public static function js($order=[]){ $config = self::$config; if (!is_array($order) || count($order) < 3) die("订单数组信息缺失!"); if (count($order) == 4 && !empty($order['openid'])) { $data = self::xcx($order, false, false); // 获取支付相关信息(获取非小程序信息) return $data; } $code = !empty($order['code']) ? $order['code'] : ($_GET['code'] ?? ''); $redirectUri = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . rtrim($_SERVER['REQUEST_URI'], '/') . '/'; // 重定向地址 $params = ['appid' => $config['appid']]; // 如果没有get参数没有code;则重定向去获取code; if (empty($code)) { $params['redirect_uri'] = $redirectUri; // 返回的url $params['response_type'] = 'code'; $params['scope'] = 'snsapi_base'; $params['state'] = $order['order_sn']; // 获取订单号 $url = self::$authorizeUrl . '?'. http_build_query($params) .'#wechat_redirect'; } else { $params['secret'] = $config['appsecret']; $params['code'] = $code; $params['grant_type'] = 'authorization_code'; $response = Http::get(self::$accessTokenUrl, $params); // 进行GET请求 $result = json_decode($response, true); $order['openid'] = $result['openid']; // 获取到的openid $data = self::xcx($order, false, false); // 获取支付相关信息(获取非小程序信息) if (!empty($order['code'])) { return $data; } $url = $config['redirect_url'] ?? $redirectUri; $url .= '?data=' . json_encode($data, JSON_UNESCAPED_UNICODE); } header('Location: '. $url); die; } /** * [app 获取APP支付需要用到的数据] * @param [type] $order [订单信息数组] * @return [type] [description] */ public static function app($order=[], $log=false) { if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){ die("订单数组信息缺失!"); } $order['type'] = 'app'; // 获取订单类型,用户拼接请求地址 $result = self::unifiedOrder($order, true); if (!empty($result['prepay_id'])) { $data = array ( 'appId' => self::$config['appid'], // 微信开放平台审核通过的移动应用appid 'timeStamp' => (string)time(), 'nonceStr' => self::get_rand_str(32, 0, 1), // 随机32位字符串 'prepayid' => $result['prepay_id'], ); $data['paySign'] = self::makeSign($data); $data['partnerid'] = $config['mchid']; $data['package'] = 'Sign=WXPay'; return $data; // 数据小程序客户端 } else { return $log ? $result : false; } } /** * [h5 微信H5支付] * @param [type] $order [订单信息数组] * @return [type] [description] */ public static function h5($order=[], $log=false) { if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || empty($order['type']) || !in_array(strtolower($order['type']), ['ios', 'android', 'wap'])){ die("订单数组信息缺失!"); } $result = self::unifiedOrder($order); if (!empty($result['h5_url'])) { return $result['h5_url']; // 返回链接让用户点击跳转 } else { return $log ? $result : false; } } /** * [xcx 获取jssdk需要用到的数据] * @param [type] $order [订单信息数组] * @param boolean $log [description] * @param boolean $type [区分是否是小程序,默认 true] * @return [type] [description] */ public static function xcx($order=[], $log=false, $type=true) { if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || empty($order['openid'])){ die("订单数组信息缺失!"); } $order['type'] = 'jsapi'; // 获取订单类型,用户拼接请求地址 $config = self::$config; $result = self::unifiedOrder($order, $type); if (!empty($result['prepay_id'])) { $data = array ( 'appId' => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的应用ID 'timeStamp' => (string)time(), 'nonceStr' => self::get_rand_str(32, 0, 1), // 随机32位字符串 'package' => 'prepay_id='.$result['prepay_id'], ); $data['paySign'] = self::makeSign($data); $data['signType'] = 'RSA'; return $data; // 数据小程序客户端 } else { return $log ? $result : false; } } /** * [scan 微信扫码支付] * @param [type] $order [订单信息数组] * @return [type] [description] */ public static function scan($order=[], $log=false) { if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){ die("订单数组信息缺失!"); } $order['type'] = 'native'; // Native支付 $result = self::unifiedOrder($order); if (!empty($result['code_url'])) { return urldecode($result['code_url']); // 返回链接让用户点击跳转 } else { return $log ? $result : false; } } /** * [notify 回调验证] * @return [array] [返回数组格式的notify数据] */ public static function notify($server = [], $response = []) { $config = self::$config; $server = $server ?? $_SERVER; $response = $response ?: file_get_contents('php://input', 'r'); if (empty($response) || empty($server['HTTP_WECHATPAY_SIGNATURE'])) { return false; } $body = [ 'timestamp' => $server['HTTP_WECHATPAY_TIMESTAMP'], 'nonce' => $server['HTTP_WECHATPAY_NONCE'], 'data' => $response, ]; // 验证应答签名 $verifySign = self::verifySign($body, trim($server['HTTP_WECHATPAY_SIGNATURE']), trim($server['HTTP_WECHATPAY_SERIAL'])); if (!$verifySign) { die("签名验证失败!"); } $result = json_decode($response, true); if (empty($result) || $result['event_type'] != 'TRANSACTION.SUCCESS' || $result['summary'] != '支付成功') { return false; } // 加密信息 $associatedData = $result['resource']['associated_data']; $nonceStr = $result['resource']['nonce']; $ciphertext = $result['resource']['ciphertext']; $data = $result['resource']['ciphertext'] = self::decryptToString($associatedData, $nonceStr, $ciphertext); return json_decode($data, true); } /** * [refund 微信支付退款] * @param [type] $order [订单信息] * @param [type] $type [是否是小程序] */ public static function refund($order) { $config = self::$config; if(empty($order['refund_sn']) || empty($order['refund_amount']) || (empty($order['order_sn']) && empty($order['transaction_id']))){ die("订单数组信息缺失!"); } $params = array( 'out_refund_no' => (string)$order['refund_sn'], // 商户退款单号 'funds_account' => 'AVAILABLE', // 退款资金来源 'amount' => [ 'refund' => $order['refund_amount'], 'currency' => 'CNY', ] ); if (!empty($order['transaction_id'])) { $params['transaction_id'] = $order['transaction_id']; $orderDetail = self::query($order['transaction_id'], true); } else { $params['out_trade_no'] = $order['order_sn']; $orderDetail = self::query($order['order_sn']); } $params['amount']['total'] = $orderDetail['amount']['total']; !empty($order['reason']) && $params['reason'] = $order['reason']; $url = self::$refundUrl; $header = self::createAuthorization($url, $params, 'POST'); $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); $result = json_decode($response, true); return $result; } /** * [queryRefund 查询退款] * @param [type] $refundSn [退款单号] * @return [type] [description] */ public static function queryRefund($refundSn, $type = false) { $url = self::$refundUrl . '/' . $refundSn; $params = ''; $header = self::createAuthorization($url, $params, 'GET'); $response = Http::get($url, $params, $header); $result = json_decode($response, true); return $result; } /** * [success 通知支付状态] */ public static function success() { $str = ['code'=>'SUCCESS', 'message'=>'成功']; die(json_encode($str, JSON_UNESCAPED_UNICODE)); } /** * [createAuthorization 获取接口授权header头信息] * @param [type] $url [请求地址] * @param array $data [请求参数] * @param string $method [请求方式] * @return [type] [description] */ //生成v3 Authorization protected static function createAuthorization($url, $data=[], $method='POST'){ $config = self::$config; //商户号 $mchid = $config['mchid']; // 证书序列号 if (empty($config['serial_no'])) { $certFile = @file_get_contents($config['cert_client']); $certArr = openssl_x509_parse($publicStr); $serial_no = $certArr['serialNumberHex']; } else { $serial_no = $config['serial_no']; } // 解析url地址 $url_parts = parse_url($url); //生成签名 $body = [ 'method' => $method, 'url' => ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")), 'time' => time(), // 当前时间戳 'nonce' => self::get_rand_str(32, 0, 1), // 随机32位字符串 'data' => (strtolower($method) == 'post' ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data), // POST请求时 需要 转JSON字符串 ]; $sign = self::makeSign($body); //Authorization 类型 $schema = 'WECHATPAY2-SHA256-RSA2048'; //生成token $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $mchid, $body['nonce'], $body['time'], $serial_no, $sign); $header = [ 'Content-Type:application/json', 'Accept:application/json', 'User-Agent:*/*', 'Authorization: '. $schema . ' ' . $token ]; return $header; } /** * [makeSign 生成签名] * @param [type] $data [加密数据] * @return [type] [description] */ public static function makeSign($data) { $config = self::$config; if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { throw new \RuntimeException("当前PHP环境不支持SHA256withRSA"); } // 拼接生成签名所需的字符串 $message = ''; foreach ($data as $value) { $message .= $value . "\n"; } // 商户私钥 $private_key = self::getPrivateKey($config['cert_key']); // 生成签名 openssl_sign($message, $sign, $private_key, 'sha256WithRSAEncryption'); $sign = base64_encode($sign); return $sign; } /** * [verifySign 验证签名] * @param [type] $data [description] * @param [type] $sign [description] * @param [type] $serial [description] * @return [type] [description] */ public static function verifySign($data, $sign, $serial) { $config = self::$config; if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { throw new \RuntimeException("当前PHP环境不支持SHA256withRSA"); } $sign = \base64_decode($sign); // 拼接生成签名所需的字符串 $message = ''; foreach ($data as $value) { $message .= $value . "\n"; } // 获取证书相关信息 self::certificates($serial); // 平台公钥 $public_key = self::getPublicKey($config['public_key']); //平台公钥 // 验证签名 $recode = \openssl_verify($message, $sign, $public_key, 'sha256WithRSAEncryption'); return $recode == 1 ? true : false; } //获取私钥 public static function getPrivateKey($filepath) { return openssl_pkey_get_private(file_get_contents($filepath)); } //获取公钥 public static function getPublicKey($filepath) { return openssl_pkey_get_public(file_get_contents($filepath)); } /** * [certificates 获取证书] * @return [type] [description] */ public static function certificates($serial) { $config = self::$config; $publicStr = @file_get_contents($config['public_key']); if ($publicStr) { // 判断证书是否存在 $openssl = openssl_x509_parse($publicStr); if ($openssl['serialNumberHex'] == $serial) { // 是否是所需证书 // return self::getPublicKey($config['public_key']); //平台公钥 return ''; } } $url = self::$certificatesUrl; $params = ''; $header = self::createAuthorization($url, $params, 'GET'); $response = Http::get($url, $params, $header); $result = json_decode($response, true); if (empty($result['data'])) { throw new RuntimeException("[" . $result['code'] . "] " . $result['message']); } foreach ($result['data'] as $key => $certificate) { if ($certificate['serial_no'] == $serial) { $publicKey = self::decryptToString( $certificate['encrypt_certificate']['associated_data'], $certificate['encrypt_certificate']['nonce'], $certificate['encrypt_certificate']['ciphertext'] ); file_put_contents($config['public_key'], $publicKey); break; // 终止循环 } // self::$publicKey[$certificate['serial_no']] = $publicKey; } // return self::getPublicKey($config['public_key']); //平台公钥 } /** * [decryptToString 证书和回调报文解密] * @param [type] $associatedData [附加数据包(可能为空)] * @param [type] $nonceStr [加密使用的随机串初始化向量] * @param [type] $ciphertext [Base64编码后的密文] * @return [type] [description] */ public static function decryptToString($associatedData, $nonceStr, $ciphertext) { $config = self::$config; $ciphertext = base64_decode($ciphertext); if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) { return false; } // ext-sodium (default installed on >= PHP 7.2) if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) { return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $config['key']); } // ext-libsodium (need install libsodium-php 1.x via pecl) if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) { return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $config['key']); } // openssl (PHP >= 7.1 support AEAD) if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) { $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE); $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE); return \openssl_decrypt($ctext, 'aes-256-gcm', $config['key'], \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData); } throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php'); } /** fengkui.net * [get_rand_str 获取随机字符串] * @param integer $randLength [长度] * @param integer $addtime [是否加入当前时间戳] * @param integer $includenumber [是否包含数字] * @return [type] [description] */ public static function get_rand_str($randLength=6, $addtime=0, $includenumber=1) { if ($includenumber) $chars='abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789'; $chars='abcdefghijklmnopqrstuvwxyz'; $len = strlen($chars); $randStr = ''; for ($i=0; $i<$randLength; $i++){ $randStr .= $chars[rand(0, $len-1)]; } $tokenvalue = $randStr; $addtime && $tokenvalue = $randStr . time(); return $tokenvalue; } /** fengkui.net * [get_ip 定义一个函数get_ip() 客户端IP] * @return [type] [description] */ public static function get_ip() { if (getenv("HTTP_CLIENT_IP")) $ip = getenv("HTTP_CLIENT_IP"); else if(getenv("HTTP_X_FORWARDED_FOR")) $ip = getenv("HTTP_X_FORWARDED_FOR"); else if(getenv("REMOTE_ADDR")) $ip = getenv("REMOTE_ADDR"); else $ip = "Unknow"; if(preg_match('/^((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1 -9]?\d))))$/', $ip)) return $ip; else return ''; } } [代码] 本文参考文档: 1、微信支付 小程序 (v3)- PHP 完整后端代码 2、PHP 微信小程序 微信支付 v3 3、微信支付V3版本小程序支付 php签名,验签,数据解密代码分享(完整方法主参考) 4、微信支付 API v3 回调通知 签名验证 PHPdemo有嘛?
2022-02-21 - [开盖即食]九宫格抽奖component组件分享
[图片] 这次继续分享第二个抽奖组件,参考了网上多个版本,本人根据实际工作中进行了一些优化,并将其做成component组件方便大家食用~ [图片] 1、现在上吃的,呸,上代码 页面引用部分: [代码]<!-- 数据是根据外部配置的,同时也修改组件自定义callback返回内容 --> <LuckComponent lucks-data="{{lucksData}}" bind:callBack="luckCb"></LuckComponent> <view class="roll">当前抽奖结果index:{{luck_num}}</view> [代码] [代码]Page({ data: {}, onLoad() { //在这里配置显示数据,未来还能添加图片等等 let lucksData = [{ //这里修改后,可以通过后台请求配置 "key": "baofu", "name": "暴富", "indexli": 1 }...]; this.setData({ lucksData }) }, /** * 结果回调函数 * @param {*} e */ luckCb(e){ console.log(e); if(e.detail){ this.setData({ luck_num:e.detail }) } } }) [代码] Component组件部分 [代码]<view class="luck_box"> <view class="luck"> <view class='li {{amplification_index===item.indexli?"indexli":""}}' wx:key="item" wx:for="{{lucksData}}"> <!-- 开始 --> <view bindtap="startrolling" class="startrolling" wx:if="{{item.indexli === -1}}"> <view class="st1">抽奖</view> </view> <block wx:if="{{item.indexli !== -1}}"> <view class="setup_title"> <view class="txt">{{item.name}}</view> <view class="index">当前index:{{item.indexli}}</view> <view wx:if="{{item.parentsClass}}" class="^parentsClass">{{item.parentsClass}}</view> </view> <view class="indexli_view"></view> </block> </view> </view> </view> [代码] [代码]Component({ /** * 组件的属性列表 */ properties: { lucksData: { type: Array, value: [] }, }, /** * 组件的初始数据 */ data: { amplification_index: 0, //轮盘的当前滚动位置 roll_flag: true, //是否允许滚动 max_number: 8, //轮盘的全部数量 speed: 300, //速度,速度值越大,则越慢 初始化为300 myInterval: "", //定时器 max_speed: 40, //滚盘的最大速度 minturns: 8, //最小的圈数为2 runs_now: 0, //当前已跑步数 luck_num: 0, // 中奖位置!!!!!!!!!!!!!!!!!!!!!!!!! end_amp: 0, //上一次滚动的位置 start_flag: true, lucksData: [], //这里是渲染数据 }, /** * 组件的方法列表 */ methods: { //开始滚动 startrolling: function () { let _this = this; //roll点 let random = parseInt(Math.random() * 8 + 1); if (this.data.start_flag == true) { _this.setData({ luck_num: random, start_flag: false }) //初始化步数 _this.data.runs_now = 0; //当前可以点击的状态下 if (_this.data.roll_flag) { _this.data.roll_flag = false; //启动滚盘, _this.rolling(); } }; //回调行数,把结果传出去 this.triggerEvent('callBack', random); }, //滚动轮盘的动画效果 rolling: function (amplification_index) { let _this = this; this.data.myInterval = setTimeout(function () { _this.rolling(); }, this.data.speed); this.data.runs_now++; //已经跑步数加一 this.data.amplification_index++; //当前的加一 //获取总步数,接口延迟问题,所以最后还是设置成1s以上 let count_num = this.data.minturns * this.data.max_number + this.data.luck_num - this.data.end_amp; //上升期间 if (this.data.runs_now <= (count_num / 3) * 2) { this.data.speed -= 30; //加速 if (this.data.speed <= this.data.max_speed) { this.data.speed = this.data.max_speed; //最高速度为40; } } //抽奖结束 else if (this.data.runs_now >= count_num) { clearInterval(this.data.myInterval); this.data.roll_flag = true; this.setData({ end_amp: _this.data.amplification_index, start_flag: true }) if (_this.data.is_selected == 0) { wx.showModal({ title: '很遗憾', content: _this.data.prize_name, showCancel: false, success(res) { } }) } else if (_this.data.is_selected == 1) { wx.showModal({ title: '恭喜您', content: _this.data.prize_name, showCancel: false, success(res) { } }) } } //下降期间 else if (count_num - this.data.runs_now <= 10) { this.data.speed += 20; } //缓冲区间 else { this.data.speed += 10; if (this.data.speed >= 100) { this.data.speed = 100; //最低速度为100; } } if (this.data.amplification_index > this.data.max_number) { //判定!是否大于最大数 this.data.amplification_index = 1; } this.setData(this.data); }, } }) [代码] 2、食用指南 可以通过 [代码]<slot>[代码] 、 [代码]^class[代码] 和 [代码]~class[代码] 等方法外部配置组件的样式,使其能在多个地方复用 如果还想配置如起始点,速度等,可以统一通过option传参的方式,二次开发下这个组件。 可以通过修改组件让callback返回更多参数 [图片] 3、具体代码片段 地址: https://developers.weixin.qq.com/s/a5NiCwms7gpI 建议将IDE工具升级到 1.03.24以上,避免一些BUG [图片] 如有疑问请留言~ 觉得有用,请点个赞哦,让我继续分享更有动力~
2021-04-13 - 抽奖类小程序,具体抽奖逻辑如何实现
本文背景本人运营一个抽奖类小程序已步入正轨,期间虽然也出过大大的问题,好在吃一堑长一智,现在一切都比较稳定,特别是在抽奖环节。 本文内容本文依托我运营的小程序,来分享下在具体抽奖环节的逻辑是如何实现的 首先要说下目前小程序的实现机制,目前抽奖小程序主要有三步 (1)开~奖、所谓开奖就是将当前奖项根据时间,从未开奖,标记为可开奖状态 (2)抽~奖、所谓抽奖就是,在可开奖的奖项里面,根据当前奖项参与的用户,以及奖品设置,把具体的奖项给对应的某个参与用户 (3)推~送、所谓推送就是在开奖完成后,推送订阅消息给所有参与抽奖的用户 对应这三步,该小程序有三个核心的云函数 (1)run,触发器,每个整点的1分开始执行,具体逻辑是根据当前时间和开奖时间进行比较,如果当前时间大于开奖时间,那么标记状态位为可开奖 (2)draw,触发器,每个整点的5分开始执行,具体抽奖的逻辑,也是本文具体分享的环节 (3)sendmore,触发器,每个整点的10分开始执行,进行推送订阅消息 f 本文的重点是在上面的第二步 在具体实现抽奖的逻辑,本文分享两个,所谓抽奖无非就是根据奖项设置的奖品个数随机从参与用户那里选取两个用户,这里注意一个关键词,是随机 随机就代表公平,这是该小程序的核心 方法1、云函数的sample 这个是云开发里面提供的随机检索的函数,小程序云开发支持 方法2、第三方库的suffle http://underscorejs.org/ [图片] [图片] 本文总结本文通过分享抽奖类小程序核心逻辑场景,然后给出具体抽奖环节的解决方案以及具体代码
2021-01-10 - 如何获取小程序分享的链接及参数?
别人分享一个链接给你(别人的小程序),怎么才能获取到点击的path及参数
2021-06-24 - createIntersectionObserver 滑动过快有时候有时候无法监听到元素
<template> <view class="container"> <view class="page-body"> <view class="page-section message"> <view>当前滚动方向 -> {{ dir === 'top' ? '向上' : dir === 'bottom' ? '向下' : dir === 'nav' ? '顶' : '底' }}</view> <view>当前激活的item -> {{ notic }}</view> </view> <scroll-view :display-multiple-items="subItem1.length" scroll-x="true" :scroll-into-view="notic" class="sc" scroll-with-animation> <view v-for="item in subItem1" :id="`item-${item}`" :key="item" :current-item-id="`item-${item}`" @click="useClick(`item-${item}`)" class="sc-item" :class="{ active: `item-${item}` === notic }"> {{item}}</view> </scroll-view> <view class="page-section"> <scroll-view enhanced @scroll="scroller" class="scroll-view" scroll-y @scrolltolower="add" @scrolltoupper="reset" :scroll-into-view="navId"> <view :data-next-id="`item-${item+1 > subItem.length ? item : item+1}`" v-for="item in subItem" :class="item === 3 && 'small'" class="ball" :id="`item-${item}`" :key="item" :data-id="item"> {{item}}</view> </scroll-view> </view> </view> </view> </template> <script> export default { data: () => ({ appear: false, subItem: [], subItem1: [], dir: 'nav', notic: 'item-1', navId: 'item-1', cachTop: 0, onClick: false }), created() { /// 设置当前数量 const idx = 10 for (let i = 0; i < idx; i++) { this.subItem.push(i + 1) this.subItem1.push(i + 1) } }, onLoad() { this.observe() }, onUnload() {}, methods: { desc() { if (this._observer) this._observer.disconnect() }, observe() { this._observer = wx.createIntersectionObserver(this, { observeAll: true }) this._observer .relativeTo('.scroll-view') .observe('.ball', (res) => { if (!this.onClick) { let _id = this.notic if (this.dir === 'top') { /// 关注谁出现 if (res.intersectionRatio > 0) { _id = res.id } } if (this.dir === 'bottom') { /// 关注谁消失,然后下一个进入 if (res.intersectionRatio === 0) { _id = res.dataset.nextId } } this.setNavId(_id) } else { this.navId = this.notic } }) }, setNavId(id) { console.log(id) this.notic = id }, scroller(e) { this.$nextTick(() => { const top = e.detail.scrollTop if (top > this.cacheTop) { this.dir = 'bottom' } else { this.dir = 'top' } this.cacheTop = top /// 结束的时候把全局的干掉 this.onClick = false }) }, reset() { this.cacheTop = 0 this.dir = 'nav' }, useClick(item) { this.onClick = true this._observer && this._observer.disconnect() this._observer = null this.$nextTick(() => { this.notic = item this.observe() }) }, add(e) { // if(this.isLoad) return // this.subItem = [] // this.$nextTick(() => { // this.subItem.push(...[1,2,3, 4,5,6]) // this.isLoad = true // this.desc() // this.$nextTick(() => { // this.observe() // }) // }) } } } </script> <style> .scroll-view { height: 1000rpx; background: #fff; border: 1px solid #ccc; box-sizing: border-box; } .scroll-area { height: 1300rpx; display: flex; flex-direction: column; align-items: center; transition: .5s; } .notice { margin-top: 150rpx; } .ball { width: 400rpx; height: 400rpx; background: #1AAD19; font-size: 100rpx; border-radius: 50%; color: red; text-align: center; line-height: 400rpx; } /* .ball.small { width: 400rpx; height: 100rpx; line-height: 100rpx; } */ .message { width: 100%; } .message text { font-size: 40rpx; font-family: -apple-system-font, Helvetica Neue, Helvetica, sans-serif; } .sc { display: flex; height: 200rpx; overflow-y: scroll; } .sc-item { padding: 10px; border: 1px solid #ccc; margin: 10px; display: inline-block; } .sc-item.active { color: #fff; border: #1AAD19; background: #1AAD19; } </style> 代码是用uni-app写的很简单。下面的是效果,快速拖动的时候,有可能有时候会漏掉一些监听。。这代码提交怎么成这样了。。。见了鬼 [图片]
2021-04-19 - setdata追加数据
使用下拉加载更多时,每次加载数据都需要先跟之前的数据合并,再this.setData({})进去,当加载次数比较大时,一次性setData的数据很大,setData能不能提供更丰富的api操作数组功能。比如追加、删除、插入等。
2018-08-02 - vant + recycle-view 常见长列表实战demo
recycle-view 常列表实战 昨天拿到一个需求 [图片] 分解需求: 自定义标题栏 搜索框禁用,做点击跳转到搜索详情页 分类,左右滑动 点击菜单展示所有分类 tabs吸顶 常列表 需求是老项目新增需求,前期类似的需求都是手写原生,后期用了vant Ui。维护好难-。-,标题栏统一处理忽略,接下来就是处理 vant ui 和 常列表,先对着原型撸个demo,代码片段在最后。 [图片] vant ui 之前打包已经2M多了,在用一个ui框架的话后期维护真的。。所以继续 vant ui 使用的组件 index.json [代码] // dropdown-menu 待定 { "usingComponents": { "recycle-view": "miniprogram-recycle-view/recycle-view", "recycle-item": "miniprogram-recycle-view/recycle-item", "van-tab": "@vant/weapp/tab/index", "van-tabs": "@vant/weapp/tabs/index", "van-dropdown-menu": "@vant/weapp/dropdown-menu/index", "van-dropdown-item": "@vant/weapp/dropdown-item/index", "van-icon": "@vant/weapp/icon/index", "van-popup": "@vant/weapp/popup/index", "van-search": "@vant/weapp/search/index" } } [代码] 代码片段:https://developers.weixin.qq.com/s/1ZmiCGm87qdb 删除了node_modules,太大了超过一M不能上传代码片段,会报错请忽略,介意的话 npm 重新安装走一遍。 总结 有点儿短~ 想水一篇文章的,发现没得写。。。水文
2019-12-17 - 一次订阅消息发送失败排查记录
1 一次订阅消息发送失败排查记录 ~ 事情是这样的,我的抽奖活动小程序在开奖后,会推送订阅消息给用户,但是在开奖后,却没有收到。 今天我排查了下是因为推送消息模板里面有个值,我在测试的时候,使用了两个不同的文本,一个长,一个短,短的文本推送成功了,长的文本没有推送成功, 所以说,大概率是模板消息里面这个内容的长度有关 ~ 具体我目前还没有排查到是不是因为因为文字长度还是敏感字符 这个我一会查下官方文档,第一次遇到这个问题,比较棘手 value: "心相印抽纸茶语丝享系列3层120抽面巾纸*27包纸巾"[图片] 1 value: "心相印抽纸"[图片] [图片] [图片] [图片] 一次订阅消息发送失败排查记录1 参考文章 1)订阅消息官方文档 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html 2)订阅消息模板 各个字段长度的限制在哪看?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000004c364c460d85a9a012ec59c00 3)
2021-01-06 - 正式版本wx.getUserProfile无法弹起授权框?
发现很大的问题,体验版完全是可以使用wx.getUserProfile点击弹出授权框,但是发布后,到正式点击没有任何反应,导致没办法进行登录,严重影响用户登录流程,希望官方大大检查下,是不是微信api兼容问题? [图片] 出现的微信版本有7.0.15和7.0.17一个安卓机一个ios,难搞了。
2021-04-21 - 仅限公众号粉丝抽奖的技术实现原理
仅限公众号粉丝抽奖的技术实现原理 ~ 前几天体验了下抽奖助手的付费高级功能,其中有一项就是公众号粉丝抽奖,我对这个功能的技术实现一直没有理清,因为抽奖助手是无法拿到当前参与抽奖用户是否是活动方公众号粉丝的权限,因为属于不同的主体 那么抽奖助手是怎么做到的呢? 带着这个问题我两个晚上没有合眼?就在刚刚,我在看相关文章的时候,想明白了这里面的道道,操作路径和技术原理,现在分析给大家听 ~ ②设置仅公众号粉丝参与 设置添加授权的公众号,再设置回复关键词。此功能不能与组队、表单条件同时设置。 ~ 上面这句话很关键 1 [图片] 1 [图片] 1 [图片] 1 参考文章 1)微信抽奖助手操作流程(全攻略) https://mp.weixin.qq.com/s/X8oUT7ztDnfUxN8IW6GXqA 2)如何设置必须关注公众号,才能参与红包抽奖? https://mp.weixin.qq.com/s/cAiiWZSTKDCGudLkc_C1iA
2021-01-05 - 自定义客服 消息卡片
小程序如何合理合规引导用户关注公众号或者添加客服微信点击客服,出现"你可能发送的图片"或者"你可能发送的小程序" 在之前的文章中我写过,如果通过小程序合理合规引导用户关注公众号,具体见下文 https://developers.weixin.qq.com/community/develop/article/doc/000208cb3fc438638ae9fd6d451013 其中提到一种方式就是客服消息, 自定义卡片消息交互设计示例 具体示例图如下所示: 以下截图来自小程序抽奖助手 [图片] [图片] [图片] [图片] 自定义卡片消息代码片段 https://developers.weixin.qq.com/miniprogram/dev/component/button.html 该技术是自定义客服卡片,具体代码可以参考 欢迎使用代码片段,可在控制台查看代码片段的说明和文档 联系客服 https://developers.weixin.qq.com/s/NHTz9tml76c3 [图片]
2020-03-14 - web页面可以跳转小程序中的具体页面方法吗?
问题就是用H5页面接入了七鱼客服,推送商品卡片的时候用点击会跳转我们在商品卡片的web网页,所以我们在web网页来判断他的运行环境,如果是小程序就从web网页返回到具体的页面,不是小程序环境就跳到我们写的外部链接
2021-03-09 - 如何把一个数组的某一项的某个元素 增加到另一个数组的某一项的中并成为该项的一个新元素?(小白求助)
例如: 一个array1:[{'b':0,'d':1,'e':2'},{'b':3,'d':4,'e':5}], 一个array2:[{'x':1,'y':3,'z':5},{'x':2,'y':0,'z':7},{'x':3,'y':4,'z':4},] 要怎么才能把array1中的某一项中的某个元素的值(比如array1中的第0项:{'b':0,'d':1,'e':2,'z':''}中的b元素的值)新增加到array2中的某一项中成为array2这一项的一个新元素(比如把刚刚b元素的值加到array2的第0项{'x':1,'y':3,'z':5}中 并让里面多一个元素d →{'x':1,'y':3,'z':5,'d':0}呢?
2021-03-09 - 03.getUserInfo和getUserProfile 对比
最近动态 wx.getUserProFile() 在2.16.0成功回调有iv、encryptedData,具体看这里https://developers.weixin.qq.com/community/develop/doc/000c04d0490118d8a6ebf675a56c00 调整背景 很多开发者在打开小程序时就通过组件方式唤起 getUserInfo 弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。详情可以点击官方调整链接(https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801) 调整前后API功能的对比[图片] [图片] 能力检测 两个前提条件: 1.开发者工具版本不低于 1.05.21030222.基础库版本不低于 2.10.4[图片] 代码片段: https://developers.weixin.qq.com/s/odMs3wmX7Ko3 测试过程 step1: 在开发工具设置清除全部缓存step2: 点击 getUserInfo 按钮,会弹出用户授权,允许后会得到这些信息,见截图[图片] step3: 在终端输入下面代码,也可以获取上面截图数据(今天还不到截止时间,还能获取完整的用户头像和昵称)wx.getUserInfo({ complete: (res) => { console.log(res) } }) step4: 点击 getUserProfile 按钮,会弹出用户授权,允许后会得到这些信息,见截图(只有用户昵称和头像信息)[图片] step5: 通用在终端输入下面代码,获取不到任何信息,符合`若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,且开发者每次通过该接口获取用户个人信息均需用户确认`wx.getUserProfile({ complete: (res) => { console.log(res) } }) step6: 可以重复点击 getUserInfo 按钮和 getUserProfile 按钮进行测试。功能对比讲解 1.4月13日前未发布的,wx.getUserInfo 能力 wx.getUserInfo(Object object) 会返回 encryptedData、signature、rawData,通过将返回的数据传递给服务器,服务端能解析出用户的身份标识,即 unionId(unionId 获取机制:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html) 【对我们业务来说】 从 wx.getUserInfo 就是要两样东西:unionId和用户信息(头像和昵称)。 但从 2021年2月23日起,可以通过 wx.login 接口获取的登录凭证可直接换取 unionID,可以替代一部分wx.getUserInfo 的功能了。 2.新增 getUserProfile 能力 wx.getUserProfile 能获取到头像和昵称,可以替代 wx.getUserInfo 的另外一部分功能。 3.小结 从这里是不是可以得出,wx.login + wx.getUserProfile 基础可以替代之前的 4月13日前未发布的,wx.getUserInfo 能力。其实不然,如果真是这样的,官方是不是没必要这样搞,咱们接着看。 4.wx.getUserInfo 和 wx.getUserProfile 区别 1.功能上是 wx.getUserInfo 不在返回用户授权的头像昵称,只返回匿名信息,但 wx.getUserProfile 会返回用户授权的头像昵称。2.wx.getUserInfo 授权成功后,当下次调用时,可以直接获取授权成功返回数据,不需要每次都需要用户确认,但 wx.getUserProfile 每次都需要用户确认允许后才能拿到用户信息3.对于业务来说,可以通过 wx.getUserProfile 获取用户信息和昵称后,要存在自己服务器,不能像之前那样每次都通过 wx.getUserInfo 方式获取,否则体验会比较差疑问 1.4月13日后发布的新版本小程序,如果用户未更新到新版本,此时调用 wx.getUserInfo 会不会返回用户授权的头像昵称(如果不确定,业务可能需要兼容处理)2.4月13日后发布的新版本小程序,用户更新到新版本,调用 wx.getUserInfo 返回匿名的头像昵称支持服务器解密吗? 常见问题汇总 1.wx.canIUse 判断getUserProfile结果是false,可以通过直接判断 wx.getUserProfile 即可,类似问题可以查看官方知识库(https://developers.weixin.qq.com/community/develop/doc/000cac40cf0eb8d3e429647c351c09?_at=1614912876047)
2021-04-02 - wx.getUserProfile调用不成功,换了开发者工具的版本调用依然不成功?
[图片]
2021-03-09 - 登录接口又双叕变了,三行代码挑战全网最少修改工作量
小程序登录、用户信息样关接口又双叕变了。 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 - 关于答题排行榜的设计叨逼叨
排行榜作为答题小程序一个高频场景 本文背景排行榜作为答题小程序不可获取的一部分,特别是对于答题活动小程序而言,但是怎么设计排行榜是一件非常伤脑筋的事情,在我之前的党建答题助手小程序中,提供了 当日排行和总排行两个维度,但是有时候答题活动会是一个长期的,这时候,周排行、月排行也是非常有必要展示的。 总之一切为了留住用户 本文内容这几天一直在体验一个小程序叫打卡鸭,具体设计请移步 https://m.zcool.com.cn/work/ZMzc1OTUzMDQ=.html https://img.zcool.cn/community/019ba45d2d6350a80120b5ab365079.png@1280w_1l_2o_100sh.png [图片] 该小程序的排行就做的非常好 [图片] 榜单分为: 总榜/月榜/周榜/日榜4个维度, 其中月榜/周榜/日榜均可查看历史榜单 [图片] 本文总结这个排行榜设计,可以作为党建答题小程序以及后续的答题活动助手小程序的一个迭代,非常期待。
2020-11-02 - 成语答题赚小程序的运营那些事
接前文 1)成语答题小程序云开发记录系列一? https://developers.weixin.qq.com/community/develop/article/doc/000a64f2dbcff012591b0361356813 最近从答题小程序切入成语答题这个细分领域,发现一些有意思的事情,今天跟大家谈谈 本文背景说起答题小程序,可能更多的是面向行业,面向某部分人群,但是作为成语答题,这种天然带娱乐性质的,老少皆宜,不区分人群,任何人都可能会感兴趣来两把, 所以当你你把一个成语答题小程序分享给其他微信好友的时候,不会给人打来烦恼,反而有可能因为你的分享,而喜欢上这个答题活动 今天主要谈谈在开发和运营过程中的一些实际运营操作 本文内容我大概从以下几个方面来讲 1)关于兑换 2)关于盈利 关于兑换,就要提到金币、红包、商品 这块我参考了很多小程序,也跟很多成语答题的运营同学有过沟通,主要的兑换粗略是这样 金币主要是用来答题的,用户进来默认会给一些金币,比如默认是5000金币; 金币获取的场景 1)用户授权,会默认5000金币; 2)用户邀请好友,邀请一个好友,会增加一定金币;(此处是否合规有待考虑) 3)观看激励式视频广告可获取一定金币(此处是否合规有待考虑) [图片] 金币消费的场景 1)答题消费,用户每作答一次,减少100金币,不管有没有正确 金币不能兑换红包,红包是另外单独的维度,在答题过程中,每次答题挑战失败,都会有一定金额的红包,这个红包金额的发放策略非常讲究任性,可以参考拼多多的砍价模式 我刚开始玩的时候,以为我做对的越多,红包给的也越多,后面跟几个运营同学聊天才发现,完全不是这么一回事,我贴个图 [图片] 通过上图我们不难看出 这个红包奖励金额跟用户当前余额有关系,余额越少奖励红包越大,当用户接近兑换商品的临界值时,基本是一分,二分的递增 具体的红包获取界面截图如下所示 [图片] 关于兑换 当红包累计到一定金额,可以通过兑换商品来消费掉这些红包 关于广告 对于成语答题赚这种小程序,开通流量主,通过广告变现是唯一一种盈利方式,具体几个广告位我来介绍下 首页可以安排底部banner和一个插屏广告,具体截图如下所示 [图片] 广告位置2-答题页广告位,此处建议安排视频广告,因为在这个答题的页面,往往用户持续的时间比较长,这个地方非常适合视频广告这种长时间展示的。 [图片] 3)广告位置3,商品兑换页面,此处安排一般的banner广告和插屏广告 [图片] 4)广告位置4,免费得金币模块,此处安排普通视频广告和激励式视频广告 [图片] 广告安排最近总结了下几点广告设置的小经验 1、用户停留时间长的页面,安排视频广告 2、用户刚需页面,可安排插屏广告,由于插屏广告是可以带来操作障碍的,非刚需页面尽量不要安排这种广告类型。 广告收益 近期运营广告收益截图如下 [图片] [图片] 1 本文总结本文通过自身最近几天的体验,总结了成语答题赚小程序的一些运营策略,希望通过这种分享,认识更多的朋友 经过一番针对广告类型的踩坑操作(此处省略一万字...😢),最终得到正解(此处感谢小伙伴的雪中送炭🙏) 本文备注该成语答题小程序由其他几个小程序输血,包括抽奖小程序、激活码小程序以及矩阵内的其他几个小程序,都有对应的入口引导到成语答题小程序。
2020-10-21 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个star咯 https://github.com/lucaszhu2zgf/mp-progress 环形进度条由灰色底圈+渐变不确定圆弧+双色纽扣组成,首先先把页面结构写好: .canvas{ position: absolute; top: 0; left: 0; width: 400rpx; height: 400rpx; } 因为进度条需要盖在文字上面,所以采用了绝对定位。接下来先把灰色底圈给画上: const context = wx.createContext(); // 打底灰色曲线 context.beginPath(); context.arc(this.convert_length(200), this.convert_length(200), r, 0, 2*Math.PI); context.setLineWidth(12); context.setStrokeStyle('#f0f0f0'); context.stroke(); wx.drawCanvas({ canvasId: 'progress', actions: context.getActions() }); 效果如下: [图片] 接下来就要画绿色的进度条,渐变暂时先不考虑 // 圆弧角度 const deg = ((remain/total).toFixed(2))*2*Math.PI; // 画渐变曲线 context.beginPath(); // 由于外层大小是400,所以圆弧圆心坐标是200,200 context.arc(this.convert_length(200), this.convert_length(200), r, 0, deg); context.setLineWidth(12); context.setStrokeStyle('#56B37F'); context.stroke(); // 辅助函数,用于转换小程序中的rpx convert_length(length) { return Math.round(wx.getSystemInfoSync().windowWidth * length / 750); } [图片] 似乎完成了一大部分,先自测看看不是满圆的情况是啥样子,比如现在剩余车位是120个 [图片] 因为圆弧函数arc默认的起点在3点钟方向,而设计想要的圆弧的起点从12点钟方向开始,现在这样是没法达到预期效果。是不是可以使用css让canvas自己旋转-90deg就好了呢?于是我在上面的canvas样式中新增以下规则: .canvas{ transform: rotate(-90deg); } 但是在真机上并不起作用,于是我把新增的样式放到包裹canvas的外层元素上,发现外层元素已经旋转,可是圆弧还是从3点钟方向开始的,唯一能解释这个现象的是官方说:小程序中的canvas使用的是原生组件,所以这样设置css并不能达到我们想要的效果 [图片] 所以必须要在canvas画图的时候把坐标原点移动到弧形圆心,并且在画布内旋转-90deg [图片] // 更换原点 context.translate(this.convert_length(200), this.convert_length(200)); // arc原点默认为3点钟方向,需要调整到12点 context.rotate(-90 * Math.PI / 180); // 需要注意的是,原点变换之后圆弧arc原点也变成了0,0 真机预览效果达成预期 [图片] 接下来添加环形渐变效果,但是canvas原本提供的渐变类型只有两种: 1、LinearGradient线性渐变 [图片] 2、CircularGradient圆形渐变 [图片] 两种渐变中离设计效果最近的是线性渐变,至于为什么能够形成似乎是随圆形弧度增加而颜色变深的效果也只是控制坐标开始和结束的坐标位置罢了 const grd = context.createLinearGradient(0, 0, 100, 90); grd.addColorStop(0, '#56B37F'); grd.addColorStop(1, '#c0e674'); // 画渐变曲线 context.beginPath(); context.arc(0, 0, r, 0, deg); context.setLineWidth(12); context.setStrokeStyle(grd); context.stroke(); 来看一下真机预览效果: [图片] 非常棒,最后就剩下跟随进度条的纽扣效果了 [图片] 根据三角函数,已知三角形夹角根据公式radian = 2*Math.PI/360*deg,再利用cos和sin函数可以x、y,从而计算出纽扣在各部分半圆的坐标 const mathDeg = ((remain/total).toFixed(2))*360; // 计算弧度 let radian = ''; // 圆圈半径 const r = +this.convert_length(170); // 三角函数cos=y/r,sin=x/r,分别得到小点的x、y坐标 let x = 0; let y = 0; if (mathDeg <= 90) { // 求弧度 radian = 2*Math.PI/360*mathDeg; x = Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 90 && mathDeg <= 180) { // 求弧度 radian = 2*Math.PI/360*(180 - mathDeg); x = -Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 180 && mathDeg <= 270) { // 求弧度 radian = 2*Math.PI/360*(mathDeg - 180); x = -Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } else{ // 求弧度 radian = 2*Math.PI/360*(360 - mathDeg); x = Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } [图片] 有了纽扣的圆形坐标,最后一步就是按照设计绘制样式了 // 画纽扣 context.beginPath(); context.arc(x, y, this.convert_length(24), 0, 2 * Math.PI); context.setFillStyle('#ffffff'); context.setShadow(0, 0, this.convert_length(10), 'rgba(86,179,127,0.5)'); context.fill(); // 画绿点 context.beginPath(); context.arc(x, y, this.convert_length(12), 0, 2 * Math.PI); context.setFillStyle('#56B37F'); context.fill(); 来看一下最终效果 [图片] 最后我重新review了整个代码逻辑,并且已经将代码开源到https://github.com/lucaszhu2zgf/mp-progress,欢迎大家使用
2020-05-27 - 记录--根据经纬度计算直线距离
/** 经纬度计算两点之间的距离,不是很准确 */ [代码]function distance(lat1, lng1, lat2, lng2) { lat1 = lat1 || 0; lng1 = lng1 || 0; lat2 = lat2 || 0; lng2 = lng2 || 0; var rad1 = lat1 * Math.PI / 180.0; var rad2 = lat2 * Math.PI / 180.0; var a = rad1 - rad2; var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; var r = 6378.137; var distance = r * 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(rad1) * Math.cos(rad2) * Math.pow(Math.sin(b / 2), 2))); return distance; } [代码]
2019-07-24 - 小程序宝箱抽奖组件
在上一篇中《小程序canvas开发水果老虎机》我们做了一个水果机的抽奖组件,为水果抽奖机新增一种新的抽奖模式,复用水果机抽奖组件,实现上基本没什么难度,跳开水果机算法就基本实现了,效果如下 [图片] 支持有序抽奖 支持无序抽奖 支持自定义每个奖品 支持自定义奖品个数(>=2) 支持中奖后的回调方法 安装方法 GITHUB上有详细说明。 配置 wxml [代码]<view wx:if="{{fruitConfig}}" class="fruit-container {{fruitConfig.containerClass||''}}" style="{{fruitConfig.containerStyle||''}}"> <ui-list list="{{fruitConfig}}" /> <canvas type='2d' disable-scroll="true" id="fruit-canvas" class='fruit-canvas'></canvas> </view> [代码] js [代码]const Pager = require('../../components/aotoo/core/index') const mkFruits = require('../../components/modules/fruit') Pager({ data: { fruitConfig: mkFruits({ id: 'fruitTable', ... ... }, // callback,动画结束后响应 function (param) { console.log(param); // 中奖数据 console.log(param.value); // 中奖值 }) } }) [代码] id 配置实例的Id containerClass 容器样式 containerStyle 容器内联样式 max 设置max值后,根据平方算法,自动计算果盘数量,max定义一行几个水果,默认九宫格,max为3 count 与max的区别在于果盘数量由用户指定,且果盘不再为老虎机的游戏模式,max数量仍为定义一行几个水果 设置[代码]count[代码]为有效数据后,将以常规抽奖盘代替老虎机抽奖盘 confuse 使果盘数据无序化,只有当count为有效数据时,confuse才能生效 fruitsData Object,指定对应位置的格子内容,支持文字/图片 回调方法 mkFruits(config, callback)水果盘动画结束后响应方法 如何设置 设置宝箱抽奖盘 常规抽奖盘不再以水果老虎机的游戏形式展现 [代码]mkFruits({ id: 'fruit', count: 3, // count默认为0,当count设定>0时,组件不再输出水果盘 max: 3, // count>0时,max表示每行显示个数 fruitsData: { 1: {title: '石头', value: '001'}, // value默认为下标叙述,也可以手动指定 2: {title: '剪刀', value: '002'}, 3: {title: '布', value: '003'} }, }) [代码] 设置果盘内容(图片) 通过[代码]fruitsData[代码]设置指定位置显示内容,支持文本/图片 [代码]mkFruits({ id: 'fruit', max: 4, fruitsData: { 1: {title: '一等奖', value: '001'}, 5: {img: {src: '/images/xxx.jpg'}, value: '003'}, }, }) [代码] 如何获取水果盘的实例 运行水果盘需要调用实例方法,首先需要通过getElementsById获取水果盘实例 [代码]const Pager = require('../../components/aotoo/core/index') const mkFruits = require('../../components/modules/fruit') Pager({ data: { fruitConfig: mkNavball({ id: 'fruit' }), }, onReady(){ // 获取水果盘实例 const instance = this.fruit // 或者 // const instance = this.getElementsById('fruit') } }) [代码] run 运行水果盘 [代码]onReady() { // 获取水果盘实例 const instance = this.getElementsById('fruit') instance.run() } [代码] 源码戳这里 下列小程序DEMO包含[代码]多形态日历,下拉菜单、筛选列表、索引列表、markdown(包含表格)、评分组件、水果老虎机、折叠面板、双栏分类导航(左右)、刮刮卡、日历[代码]等组件 [图片]
2020-06-24 - 小程序悬浮按钮movable-area实现及解决按钮图层遮罩问题
先上效果图 [图片] 实现方案 1)通过使用movable-area、movable-view实现滑动 2)限制按钮在屏幕可用范围内 3)解决按钮图层遮罩问题 作为多个页面公用按钮,新建组件components更合适,先上代码。 wxml代码 + ? wxss代码 .movableAreaTask{ position: fixed; right: 0; } .addTaskBtn{ width: 50px; height: 50px; border-radius: 50%; background-image: var(--gradualBlue); color: white; text-align: center; line-height: 50px; font-size: 32px; } .helperBtn{ width: 40px; height: 40px; border-radius: 50%; background-color: rgb(230, 230, 230); color: #0081FF; text-align: center; line-height: 40px; font-size: 32px; } movable-view { pointer-events: auto; } movable-area { pointer-events: none; } JS代码 const app = getApp() Component({ options: { addGlobalClass: true, }, /** * 组件的属性列表 */ properties: { }, /** * 组件的初始数据 */ data: { screenWidth: app.globalData.screenWidth, screenHeight: app.globalData.screenHeight }, /** * 组件的方法列表 */ methods: { skipTo: function(e){ let type = e.currentTarget.dataset.type wx.navigateTo({ url: '/pages/{0}/{1}'.format(type, type), }) } } }) 1)通过使用movable-area、movable-view实现滑动 多个悬浮按钮可在movable-area下添加多个movable-view标签,没有必要建多个movable-area,而引起遮罩问题,后面会讲! 2)限制按钮在屏幕可用范围内 movable-area必须指定区域大小,即悬浮按钮的可移动区域。 screenHeight、screenWidth可根据wx.getSystemInfo获取,自己可根据页面已有元素计算按钮可移动区域大小。 3)解决按钮图层遮罩问题 现在应该可以正常点击了,但是会遮罩底层操作,其实上面css中已经给出了解决 movable-view { pointer-events: auto; } movable-area { pointer-events: none; } 其中,遮罩问题是由movable-view设置的移动区域引起的,设置为 pointer-events: auto; 表示可穿透当前层,使图层下的点击生效。
2023-11-19 - 编程大赛--用小程序云开发做《拍卖》项目
[图片] 引子:微盟第三届编程比赛今年的日期定于12月19号,我们组了一个叫“NAZA”的小队,然后参赛项目定了《拍卖》,开发周期一周半,测试时间半周。团队成员全部为前端组的,所以产品的原型、数据库、接口、小程序、测试全部自己完成(ps:UI实在没人会所以找了场外私人支持),技术选型为了图快速和方便就选择了“云开发”(开赛前2天才学的文档😂)。这篇文章就以叙事的风格纪录一下我们完整的开发流程,内容主要涉及产品介绍、数据库设计、接口设计、接口开发、小程序页面开发,以及实时数据推送和定时触发器。 一、产品设计 这一期编程比赛的主题是:小技术、大营销,主要聚焦于商业创新、赋能商家,所以在综合考虑了类似公益项目、众筹项目、活动项目、游戏项目等之后选择了“拍卖”这个主题,主要是为了赋予商家针对其“爆款商品”或者“活动商品”去吸引用户,让其停留在该小程序中,并在各种激励、引导下让客户去购买商家真正“热销”“高利润”的商品。所以主要的设计模块如下: 1、后台管理,包括:拍卖商品列表、拍卖商品编辑页、商品列表、商品编辑页 2、拍卖中心,包括:首页、拍卖详情页、拍卖纪录页 3、商品展示,包括:商品列表页、商品详情页、订单页 4、个人中心,包括:个人中心页、收支明细页、竞拍商品&订单商品展示 5、其他页面,包括:拍卖币获取引导页、大转盘页、刮刮卡页、其他说明页等 二、开发准备 1、创建云开发模版,新建项目选择一个空目录,填入 AppID(使用云开发能力必须填写 AppID),勾选创建 “云开发 QuickStart 项目”,点击创建即可得到一个展示云开发基础能力的示例小程序。 2、小程序准备,这里使用了私人的一个小程序,然后开通云开发,在小程序开发者工具中点击“云开发”按钮,打开云控制台、根据提示开通云开发、创建云环境。 [图片] 3、微信公众平台,小程序后台中加入小伙伴的微信账号到“项目成员”中。 三、产品原型 从网上搜索了一下好的原型开发工具,然后选择了“墨刀”,坑的是我只有2天的原型设计时间,因为我用的是试用版(本屌丝比较qiong~),试用期过了之后不能再导出html文件了😂。原型导出来之后放到了同事的云服务器上:《拍卖》项目原型。大家感兴趣的可以看看。 四、UI设计 团队成员只会照着UI开发页面,自己作图短时间内学不会啊啊啊啊啊!!!!所以找了我的表妹一个超萌的妹子帮忙设计,篇幅以及本文章的重点内容不在此,所以只展示我们的首页让大家看一下吧~ [图片] 五、数据库设计 哈哈哈哈,为了大家看的更加方便,我直接罗列了一个表格放下面了 [图片] 在云开发操作界面的“数据库”中,先增加好这些“集合” [图片] 六、接口设计 嗯。。。。罗列如下 [图片] [图片] 七、开发 开发主要分为两个部分:云函数(接口开发)、小程序页面开发,这里罗列几个技术点吧供大家参考~ 1、我们开发的时候云开发的“事务”还不支持😢,在12月24号的时候看官方人员回复已经支持。事务官方文档 [图片] 在12月13号的那一周他们还在灰度,所以我们找了替代方案,代码找一个简单的罗列如下吧~ [图片] 2、数据实时推送 针对拍卖的这种玩法,对数据实时更新要求很高,做好的解决方案其实是用websocket来做,我们用云开发,所以只能使用云开发提供的数据实时推送。该功能使用场景为:1)、拍卖详情页,多人打开这个页面时,A用户出价后,其他的B用户、C用户、D用户......等看到的页面上的当前拍卖加个实时更新成最新数据、拍卖纪录列表更新最新的数据;2)、拍卖商品的最后5分钟内如果有人出价,那么该拍卖的最终结束时间顺延5分钟,所有其他用户都更新这个拍卖的结束时间。 小程序端的拍卖详情页的代码实现如下: [图片] 此处有两个坑,1):第一个是从监听开始的那一刻,之后每增加一条都会推送一条消息,第一次一条数据,第二次有两条数据且最新数据是在最后,即:当数据库跟你监听的状态对比下来所有不一样的数据都会推送到用户端;2):第二个坑是,需要设置该数据表(集合)的权限为“所有用户可读,仅创建者可写”,不然获取不到数据的实时推送 [图片] 3、定时触发器,可以查看官方文档 我们的业务使用场景为保证金的返还。每天的中午12点,会把前一天结束的需要先缴纳保证金的拍卖统计出来,然后把保证金返还给每个参与了这些拍卖且未最后竞价未成功的用户。这里主要是给大家看一下这个云函数的定时器的设置 [图片] 4、云函数的本地调试,官方文档 一开始看完文档自己本地调试去勾选“开启本地调试”的时候,每次都说未安装node modules [图片] 这里需要先本地安装npm包 [图片] 5、拍卖详情页的一个效果展示,可以更直观的看到数据的实时更新,本视频在本篇文章的最后~ 八、测试 当时项目比较紧张,抽了半下午加一晚上的时间测试,解决了80%的bug,比赛当天还在边展示边该代码,哈哈哈哈😂 九、参赛ppt & 演讲 这个过程主要靠我们的演讲打动评委,就不详细记载了~ 十、后记 两周的时间,我们在不影响项目排期的情况下,每天加班来做,大约有8000行代码吧,从一开始没有选题的茫然和不自信、到开发过程中的边学边做、到项目完成后满满的成就感,挺辛苦的,但是也学到了很多东西。最后,我们获得了二等奖,我们公司是一个卧虎藏龙的地方。哈哈哈哈,欢迎各路大神大牛加入我们公司、加入我们团队~ [视频]
2020-01-10 - 小程序pc端全屏(小程序页面横竖屏)的代码实现
1、在app.json文件中,与“window”同级别的地方增加配置 "resizable": true; 2、在app.json文件中,“window”模块中增加"pageOrientation":"landscape"。 这样配置后,就可以让小程序的页面呈现横屏状态,然后用户只需要点击右上角的全屏按钮就可以全屏了,赶紧去试试吧。 3、如果有的页面不想横屏显示的话,只需要在这个页面下的json文件中加上配置"pageOrientation":"portrait"即可。 这样配置后,只有页面json文件中配置了portrait的才会竖屏显示,其他的就都默认横屏显示了。 4、发现的问题:如果全局window设为了landscape,而某个页面,比如叫A页面中的json文件中单独设置了portrait(竖屏显示),假如你恰好在A页面加了激励式视频广告,那么你就会发现本来事竖屏显示的A页面,在点击观看激励式视频广告后返回来的时候就被强制显示为横屏了。 以上是我在项目中时间pc端全屏和小程序横竖屏显示配置时的总结和发现的问题,希望能给有需要的人带来帮助。
2023-04-25 - 做好内容安全检测,和风险说「再见」!(上)
前言 内容安全检测,是每一个小程序主都面临的“头疼”问题,轻则短暂性不可访问,重则永久封号,甚至关小黑屋。本文将为您详细说明,如何在小程序中对一段文本进行合法内容检测,以判断是否含有违法违规内容。 本文重点为你讲述: 内容安全检测常见应用场景及解决办法 学会使用小程序·云开发的云函数+结合request-promise第三方库实现内容请求校验 掌握如何在小程序端请求云函数(有别于传统的wx.request的方式(类似AJax)) 在云开发的云函数端,利用第三方https请求库(request,request-promise),获取Access_token,以及向微信官方提供的内容检测接口发请求进行校验 云函数端与小程序端错误码的处理 01.背景 无论是小程序还是自行开发的一些类似社交,带有用户自行产生内容的软件应用,例如:即时通讯,社群,论坛,音视频直播等,对于接入内容安全的检测是非常有必要的。 对于小程序而言,这一点在审核上是非常严格的,净化言行,做一个知法守法的人很重要… [图片] 接入内容安全检测,规避输入一些违法违规低俗等内容,避免辛辛苦苦开发出来的应用。 被恶意上传反动言论或上传一些违规内容(文字/图片/视频等),导致小程序或应用被下架,或遭永久禁封,或个人及公司被公安机关打电话,约喝茶等,这样的话,就得不偿失了的。 02.应用场景 检测小程序用户个人文字资料是否违规 针对特点词汇(如过于商业以及营销之类的词)可以进行过滤或禁止输入 在内容发布之前自动检测用户发表的信息(包括评论、留言等)是否违规 03.解决办法 围绕如何处理内容安全检测问题,一般有3种方法: 方案1**:引入第三方接口对内容进行校验(例如:百度AI内容审核平台,网易云盾等)** 方案2: 公司后台小伙伴自行开发文本,图片,音视频等内容审核接口 方案3: 小程序服务端提供的API进行校验 每一种方法各有优劣势,具体如下图。 解决方案 优势 劣势 1 引入第三方接口对内容进行校验 前端同学只需按照官方提供的第三方接口文档,进行校验即可,无需后台介入,功能强大,覆盖范围广 接口调用的频次有限制,收费 2 公司后台小伙伴自行开发文本,图片,音视频等内容审核接口 后台小伙伴自己造轮子,根据自己的业务需求以及用户属性,自定义内容审核机制 开发周期长,成本大,难以覆盖全面 3 调用小程序服务端提供的内容安全API进行校验 简单,高效 想不出来,因为相比前两种方案,对于不依赖后端接口的开发者来说,简直是雪中送炭 在微信小程序生态下,官方提供了2种路径帮助用户解决内容检测问题,即 使用服务器开发模式,通过HTTPS调用 使用小程序·云开发,通过云函数或云调用来实现。 服务器开发模式,相信大家都相对比较熟悉,在此就不再赘述。接下来为大家重点介绍,如何通过小程序·云开发的云函数实现内容安全检测。 04.通过云开发的云函数+request-promise第三方库实现内容请求校验 Step 1: 在小程序端先布局:完成静态页面。(pages文件夹下的文件都是属于小程序前端代码,每个文件夹目录代表的就是一个模块,一个页面) 小程序前端wxml代码示例 [代码]<view class="container"> <textarea class="content" placeholder="写点文字..." bindinput="onInput" auto-focus bindfocus="onFocus" bindblur="onBlur"> </textarea> </view> <view class="footer"> <button class="send-btn" size="default" bind:tap="send">发布</button> </view> [代码] 小程序前端wxss代码示例 [代码]/* pages/msgSecCheck/msgSecCheck.wxss */ .container { padding: 20rpx; } .content { width: 100%; height: 360rpx; box-sizing: border-box; font-size: 32rpx; border: 1px solid #ccc; } .footer { width: 100%; height: 80rpx; line-height: 80rpx; position: fixed; bottom: 0; box-sizing: border-box; background: #34bfa3; } .send-btn { width: 100% !important; color: #fff; font-size: 32rpx; } button { width: 100%; background: #34bfa3; border-radius: 0rpx; } button::after { border-radius: 0rpx !important; } [代码] 经过wxml与wxss的编写后,UI最终长成这样 [图片] Step 2: 完成小程序端业务逻辑的处理 小程序端逻辑JS代码示例 [代码]// pages/msgSecCheck/msgSecCheck.js Page({ /** * 页面的初始数据 */ data: { textareaVal: '' // 页面中需要显示的数据,初始化定义在data下面 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, // 监听表单时,数据有变化时 onInput(event) { let textVal = event.detail.value; this.setData({ textareaVal: textVal }) }, // 聚焦焦点时 onFocus() { console.log('聚焦焦点时'); }, // 失去焦点时 onBlur(event) { console.log("失去焦点时"); // 前端可进行手动的弱校验,也可以在失去焦点时发送请求进行文本的校验,但是每次失去焦点就请求一次,这样是消耗云资源的,在发布时候与失去焦点做校验两者都可以 }, // 发布 send() { console.log("触发发布按钮") wx.cloud.callFunction({ // 请求msgSecCheck1云函数 name: 'msgSecCheck1', data: { content: this.data.textareaVal // 需要向云函数msgSecCheck1传入的值 } }).then(res => { // 成功时的响应返回结果 console.log(res); }).catch(err => { // 失败时,返回的结果 console.error(err); }) } }) [代码] **Step 3 :服务端逻辑处理。**在小程序云函数端创建云函数msgSecCheck1,这个名字你可以自定义,与小程序前端请求的名字保持一致就可以了。 [图片] 选中云函数,右键并打开命令行终端安装request,request-promise,因为request-promise依赖于request,两个都要安装,最后一键上传部署就可以了 [代码]npm install request npm install request-promise [代码] 如果遇到在小程序端请求云函数时,遇到类似下面的错误,找不到什么xxx模块之类的 先看错误码,然后在官方文档中找到该错误码代表的含义 [图片] 一看错误,没有找到模块,在云函数的目录下的package.json中查看是否有安装错误中提示的包的,要是没有的话,就安装一下就可以了的,同时记得每次更改后都要上传部署一下,也可以选择云函数中文件的增量上传 接下来是将是本文的重点内容! **Step 4 :通过云函数+**request-promise实现内容安全检测 对于小程序开发,其实与web端开发也是类似,给元素绑定事件,然后获取元素,只是小程序端没有DOM,BOM的那一套东西,它是数据驱动视图的,吸收了Angular,Vue,React的各个框架的优点,形成了自己的一套规范。 如果有这方面开发经验的小伙伴来说,平缓过度到小程序开发当中来,你会发现总会有惊人的相似,用的语言都是JavaScript,但是与web开发还是多少有很多差异的,这里就不拓展了。 废话不多说,直接上代码 : 小程序前端逻辑代码: [代码]// 点击发送按钮,对输入的文本内容进行校验 send() { wx.cloud.callFunction({ name: 'msgSecCheck1', // 云函数的名称 data: { // 需要向云函数传递过去的数据 content: this.data.textareaVal // 具体要检测的内容 } }).then(res => { // 成功时,做什么事情 console.log(res); // 检测到文本成功时,做一些业务 }).catch(err => { // 失败时,做什么事情 // 失败时,也就是违规做一些用户提示,或者禁止下一步操作等之类的业务逻辑操作 console.error(err); }) } [代码] 上面的代码还可以在优化一下,就是将请求云函数的代码封装成一个函数。 如下所示,不封装也是没事的,只是我习惯性封装一下,如果其他地方也用到该云函数,那么直接调用,避免写重复的代码。 下面是将请求云函数的部分核心代码: [代码]// 发布 send() { // 请求msgSecCheck1云函数,对文本内容进行校验 this._requestCloudMsgCheck(); }, _requestCloudMsgCheck() { let textareaVal = this.data.textareaVal; wx.cloud.callFunction({ name: 'msgSecCheck1', data: { content: textareaVal // 这里可以使用官方文档测试用例,特3456书yuuo莞6543李zxcz蒜7782法fgnv级 } }).then(res => { console.log(res); // 检测到文本成功时,做一些业务 }).catch(err => { // 失败时,也就是违规做一些用户提示,或者禁止下一步操作等之类的业务逻辑操作 console.error(err); }) } [代码] 至于是在失去焦点事件时发送请求还是在点击发送按钮时发送请求,两种方式都可以。 您也可以自定义文本校验,而我个人觉得在小程序端,失去焦点时,可以自定义做一些常规敏感词的弱校验,而在点击发送按钮时,做强校验 。 如果是放在失去焦点时就立马请求,这样请求次数会增多,而放在点击发送按钮时进行校验,一定程度上可以减少小程序端频繁请求。 接下来就是处理云函数端,使用request-promise请求请求微信内容安全接口的示例代码。 [代码]/* * Description: 利用第三方库request-promise请求微信内容安全接口 * * 相关文档链接: * 微信文本内容安全接口文档https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.msgSecCheck.html * access_token获取调用凭证文档 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html * * request-promise使用文档: https://github.com/request/request-promise * */ const APPID = "wx21baa58c6180c2eb"; // 注意是你自己小程序的appid const APPSECRET = ""; // 你自己小程序的appsecret // 安全校验接口 const msgCheckURL = `https://api.weixin.qq.com/wxa/msg_sec_check?access_token=`; // 向下面的这个地止发送请求,携带appid和appsecret参数,获取token认证 const tokenURL = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}` // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 引入request-promise const rp = require('request-promise'); // 云函数入口函数 exports.main = async(event, context) => { try { let tokenResponse = await rp(tokenURL); // 获取token值,因为返回的结果是字符串,需要用JSON.parse转化为json对象 let getAccessToken = JSON.parse(tokenResponse).access_token; // 请求微信内容安全接口,post请求,返回最终的校验结果 let checkResponse = await rp({ method: 'POST', url: `${msgCheckURL}${getAccessToken}`, body: { content: event.content // 这里的event.content是小程序端传过来的值,content是要向内容接口校验的内容 }, json: true }) return checkResponse; } catch (err) { console.error(err); } } [代码] 当你在小程序端输入文本,发送请求时,查看控制台下的结果时,功能是没有问题的。 [代码]特3456书yuuo莞6543李zxcz蒜7782法fgnv级 完2347全dfji试3726测asad感3847知qwez到 [代码] 您可以根据官方文档中提供的测试用例,进行测试,看具体的返回结果的。 [图片] [图片] (控制台错误码) [图片] (合规内容) 云函数请求成功,看看错误信息的反馈,对于熟悉该错误码的人清楚该文本违规了,但是反馈不是很明显,即使当下自己很清楚,然而,在过几个月在回来看代码,你或许都不知道是啥意思。 Step 5 :错误码的正确处理方式 [图片] 对于处理错误码,返回具体的合适信息,对于调试代码,排查问题,也是非常重要 。 这些错误码具体的含义,在官方文档里都有对应的解释,不用去记,去查文档就行。 在面试中,有很多面试官喜欢问http相关状态码的问题,状态码有很多,也真的记不住,但是常见的错误http状态码还是要知道的,我觉得,具体知道怎么处理,怎么查文档就可以了。 真正考验背后目的是,对于根据后端返回的状态码,判断接口哪里出了问题,定位是前端问题还是后端问题,这是一个非常常见的问题。 如果你说你不知道,没有处理过,对于候选人,那肯定是没有信服力的,无论是成功状态还是失败状态,都是应该有对应的用户提示。 05.完整文本安全校验示例代码 [代码]/* * * 相关文档链接: * 微信文本内容安全接口文档https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.msgSecCheck.html * access_token获取调用凭证文档 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html * * request-promise使用文档: https://github.com/request/request-promise * */ const APPID = "wx21baa58c6180c2eb"; const APPSECRET = ""; const msgCheckURL = `https://api.weixin.qq.com/wxa/msg_sec_check?access_token=`; // 向下面的这个地止发送请求,携带appid和appsecret参数,获取token认证 const tokenURL = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}` // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 引入request-promise const rp = require('request-promise'); // 云函数入口函数 exports.main = async(event, context) => { try { let tokenResponse = await rp(tokenURL); // 获取token值,因为返回的结果是字符串,需要用JSON.parse转化为json对象 let getAccessToken = JSON.parse(tokenResponse).access_token; // 请求微信内容安全接口,post请求,返回最终的校验结果 let checkResponse = await rp({ method: 'POST', url: `${msgCheckURL}${getAccessToken}`, body: { content: event.content // 这里的event.content是小程序端传过来的值,content是要向内容接口校验的内容 }, json: true }) // 有必要根据错误码,确定内容是否违规 if (checkResponse.errcode == 87014) { return { code: 500, msg: "内容含有违法违规内容", data: checkResponse } } else { return { code: 200, msg: "内容OK", data: checkResponse } } } catch (err) { if (err.errcode == 87014) { return { code: 500, msg: '内容含有违法违规内容', data: err } } else { return { code: 502, msg: '调用msgCheckURL接口异常', data: err } } } } [代码] 在云函数端,经过添加错误码的判断之后,在来看看小程序端发送的请求,返回的结果。 [图片] (这与没有添加错误码判断,是不一样的,有具体的错误信息内容) 至此,我们在小程序端可以根据这个返回的错误码或成功码,进行一些业务逻辑处理的,比如给一些用户提示,在数据插入数据库之前就做一些判断操作,只有内容合规时,才插入数据库,进入下一步的业务逻辑处理。 [代码]_requestCloudMsgCheck() { let textareaVal = this.data.textareaVal; wx.cloud.callFunction({ name: 'msgSecCheck1', data: { content: textareaVal } }).then(res => { console.log(res); const errcode = res.result.data.errcode; // 检测到文本错误时,做一些业务 if (87014 === errcode) { wx.showToast({ // 当内容违规时,做一些用户提示 title: '您输入的文本内容含有敏感内容,请重新输入', }) }else { // 成功时做其他业务操作 } }).catch(err => { // 失败时,也就是违规做一些用户提示,或者禁止下一步操作等之类的业务逻辑操作 console.error(err); }) } [代码] [图片](当输入的内容有违规时,给一些用户提示或者阻止下一步操作等的) 注意在云函数(后)端处理错误码与小程序端都是要进行处理的,两者不要混淆了的,小程序端最终的一些业务逻辑判断,是根据后端接口返回的状态,最终决定要做什么操作的。 至此,通过request-promise库就完成了文本内容校验的问题。 这个request,request-promise库非常实用,功能也非常强大,类似这种库,常见什么got,axios等之类的,都是支持promise风格的 处理方式大同小异,大家可以去npm或github上阅读相关使用文档的。 06.结语 在小程序中有多种解决方案,推荐使用小程序端请求云开发云函数的方式,无论是不使用云函数方式,自己有后端服务,获取access_token都应该是从后端返回给前端的。 而小程序的秘钥 AppSecret是不应该放在小程序端的,那样不安全的,无论是服务器开发模式还是小程序·云开发模式,都绕不过后台请求微信提供的内容安全接口,然后在返回给小程序端 。 其实在小程序·云开发中,还提供了一种更简便的方法,那就是云调用,它是小程序·云开发提供的在云函数中调用微信开放接口的能力,只需简单的进行配置一下就可以了。 限于篇幅所致,放在下一节介绍。 reference:方案1参考链接: 微信内容安全: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.msgSecCheck.html 云调用 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/capabilities.html#%E4%BA%91%E5%87%BD%E6%95%B0 百度文本审核 https://ai.baidu.com/tech/textcensoring 网易云盾 https://dun.163.com/product/text-detection
2020-09-14 - popButtons
本月份的组件分享,事件效果由 wxs事件完成,注释在代码片段里有。 按钮可拖动,子菜单上没有点击事件,是根据touchend位置来判断点击的谁。有bug欢迎反馈 代码片段:https://developers.weixin.qq.com/s/9BNYnhmG7tcl 社区这会儿效果图gif上传不了。。不是我的错 图标iconfont下的,没版权问题吧。。
2019-10-21 - 5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)
由于小程序2020年1月10日以后改模板消息为订阅消息,所以我写了一篇新的文章来更新这个知识点 《小程序订阅消息推送(含源码)java实现小程序推送,springboot实现微信消息推送》 我们在做小程序开发时,消息推送是不可避免的。今天就来教大家如何实现小程序消息推送的后台和前台开发。源码会在文章末尾贴出来。 其实我之前有写过一篇:《springboot实现微信消息推送,java实现小程序推送,含小程序端实现代码》 但是有同学反应这篇文章里的代码太繁琐,接入也比较麻烦。今天就来给大家写个精简版的,基本上只需要几行代码,就能实现小程序模版消息推送功能。 老规矩先看效果图 [图片] 这是我们最终推送给用户的模版消息。这是用户手机微信上显示的推送消息截图。 本节知识点 1,java开发推送后台 2,springboot实现推送功能 3,小程序获取用户openid 4,小程序获取fromid用来推送 先来看后台推送功能的实现 只有下面一个简单的PushController类,就可以实现小程序消息的推送 [图片] 再来看下PushController类,你没看错,实现小程序消息推送,就需要下面这几行代码就可以实现了。 [图片] 由于本推送代码是用springboot来实现的,下面就来简单的讲下。我我们需要注意的几点内容。 1,需要在pom.xml引入一个三方类库(推送的三方类库) [图片] pom.xml的完整代码如下 [代码]<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.qcl</groupId> <artifactId>wxapppush</artifactId> <version>0.0.1-SNAPSHOT</version> <name>wxapppush</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--微信小程序模版推送--> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>3.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> [代码] 其实到这里我们java后台的推送功能,就已经实现了。我们只需要运行springboot项目,就可以实现推送了。 下面贴出完整的PushController.java类。里面注释很详细了。 [代码]package com.qcl.wxapppush; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.bean.WxMaTemplateData; import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage; import cn.binarywang.wx.miniapp.config.WxMaInMemoryConfig; import me.chanjar.weixin.common.error.WxErrorException; /** * Created by qcl on 2019-05-20 * 微信:2501902696 * desc: 微信小程序模版推送实现 */ @RestController public class PushController { @GetMapping("/push") public String push(@RequestParam String openid, @RequestParam String formid) { //1,配置小程序信息 WxMaInMemoryConfig wxConfig = new WxMaInMemoryConfig(); wxConfig.setAppid("XXX");//小程序appid wxConfig.setSecret("xxx");//小程序AppSecret WxMaService wxMaService = new WxMaServiceImpl(); wxMaService.setWxMaConfig(wxConfig); //2,设置模版信息(keyword1:类型,keyword2:内容) List<WxMaTemplateData> templateDataList = new ArrayList<>(2); WxMaTemplateData data1 = new WxMaTemplateData("keyword1", "获取老师微信"); WxMaTemplateData data2 = new WxMaTemplateData("keyword2", "2501902696"); templateDataList.add(data1); templateDataList.add(data2); //3,设置推送消息 WxMaTemplateMessage templateMessage = WxMaTemplateMessage.builder() .toUser(openid)//要推送的用户openid .formId(formid)//收集到的formid .templateId("eDZCu__qIz64Xx19dAoKg0Taf5AAoDmhUHprF6CAd4A")//推送的模版id(在小程序后台设置) .data(templateDataList)//模版信息 .page("pages/index/index")//要跳转到小程序那个页面 .build(); //4,发起推送 try { wxMaService.getMsgService().sendTemplateMsg(templateMessage); } catch (WxErrorException e) { System.out.println("推送失败:" + e.getMessage()); return e.getMessage(); } return "推送成功"; } } [代码] 看代码我们可以知道,我们需要做一些配置,需要下面信息 1,小程序appid 2,小程序AppSecret(密匙) 3,小程序推送模版id 4,用户的openid 5,用户的formid(一个formid只能用一次) 下面就是小程序部分,来教大家如何获取上面所需的5个信息。 1,appid和AppSecret的获取(登录小程序管理后台) [图片] 2,推送模版id [图片] 3,用户openid的获取,可以看下面的这篇文章,也可以看源码,这里不做具体讲解 小程序开发如何获取用户openid 4,获取formid [图片] 看官方文档,可以知道我们的formid有效期是7天,并且一个form_id只能使用一次,所以我们小程序端所需要做的就是尽可能的多拿些formid,然后传个后台,让后台存到数据库中,这样7天有效期内,想怎么用就怎么用了。 所以接下来要讲的就是小程序开发怎么尽可能多的拿到formid了 [图片] 看下官方提供的,只有在表单提交时把report-submit设为true时才能拿到formid,比如这样 [代码] <form report-submit='true' > <button form-type='submit'>获取formid</button> </form> [代码] 所以我们就要在这里下功夫了,既然只能在form组件获取,我们能不能把我们小程序里用到最多的地方用form来伪装呢。 下面简单写个获取formid和openid的完整示例,方便大家学习 效果图 [图片] 我们要做的就是点击获取formid按钮,可以获取到用户的formid和openid,正常我们开发时,是需要把openid和formid传给后台的,这里简单起见,我们直接用获取到的formid和openid实现推送功能 下面来看小程序端的实现代码 1,index.wxml [图片] 2,index.js [图片] 到这里我们小程序端的代码也实现了,接下来测试下推送。 [代码]formid: 6ee9ce80c1ed4a2f887fccddf87686eb openid o3DoL0Uusu1URBJK0NJ4jD1LrRe0 [代码] [图片] 可以看到我们用了上面获取到的openid和formid做了一次推送,显示推送成功 [图片] [图片] 到这里我们小程序消息推送的后台和小程序端都讲完了。 这里有两点需要大家注意 1,推送的openid和formid必须对应。 2,一个formid只能用一次,多次使用会报一下错误。 [代码]{"errcode":41029,"errmsg":"form id used count reach limit hint: [ssun8a09984113]"} [代码] 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言,我会把源码下载链接贴在留言区。 单独找我要源码也行(微信2501902696) 视频讲解:https://edu.csdn.net/course/detail/23750 源码链接:https://github.com/qiushi123/wxapppush
2020-01-08 - 一个类似探探的高性能卡片滑动组件
cardSwipe - 小程序卡片滑动组件 介绍 此组件是使用原生微信小程序代码开发的一款高性能的卡片滑动组件,无外部依赖,导入即可使用。其主要功能效果类似探探的卡片滑动,支持循环,新增,删除,以及替换卡片。 [图片] 用法 获取: [代码]git clone https://github.com/1esse/cardSwipe.git [代码] 相关文件介绍: /components /card /cardSwipe /pages /index 其中,components文件夹下的card组件是cardSwipe组件的抽象节点,放置卡片内容,需要调用者自己实现。而cardSwipe组件为卡片功能的具体实现。pages下的index为调用组件的页面,可供参考。 功能介绍 亮点: 支持热循环(循环与不循环动态切换),动态新增,动态删除以及动态替换卡片 卡片的wxml节点数不受卡片列表的大小影响,只等于卡片展示数,如果每次只展示三张卡片,那么卡片所代表的节点数只有三个 支持调节各种属性(滑动速度,旋转角度,卡片展示数…等等) 节点数少,可配置属性多,自由化程度高,容易嵌入各种页面且性能高 实现方式: 循环/不循环: 属性circling(true/false)控制 新增: 向卡片的循环数组中添加(不推荐新增,具体原因后面分析。硬要新增的话,如果卡片列表不大,并且需要新增多张卡片,可以直接将数据push到卡片列表中然后setData整个数组。如果是每次只增加一张卡片,推荐使用下面这种方式,以数据路径的形式赋值) [代码]this.setData({ [`cards[${cards.length}]`]: { title: `新增卡片${cards.length + 1}`, // ... } }) [代码] 删除: [代码]// removeIndex: Number,需要删除的卡片的索引,将删除的卡片的值设置为null // removed_cards: Array,收集已删除的卡片的索引,每次删除卡片都需要更新 this.setData({ [`cards[${removeIndex}]`]: null, removed_cards }) [代码] 替换: [代码]// replaceIndex:Number,需要替换的卡片的索引 // removed_cards: Array,收集已删除的卡片的索引,如果replaceIndex的卡片是已删除的卡片的话,需要将该卡片索引移出removed_cards this.setData({ [`cards[${replaceIndex}]`]: { title: `替换卡片${replaceIndex}`, // ... } // removed_cards }) [代码] 注意:删除和替换操作都需要同步removed_cards why?为什么使用removed_cards而不直接删除数组中的元素 由于小程序的机制,逻辑层和视图层的通信需要使用setData来完成。这对大数组以及对象特别不友好。如果setData的数据太多,会导致通信时间过长。又碰巧数组删除元素的操作无法通过数据路径的形式给出,这会导致每次删除都需要重新setData整张卡片列表,如果列表数据过大,将会引发性能问题。 删除的时候,如果删除的卡片索引在当前索引之前,那么当前索引所代表的卡片将会是原来的下一张,导致混乱。 保留删除元素,为卡片列表的替换以及删除提供更方便,快捷,稳定的操作。 优化 由于组件支持动态的删除以及替换,这使得我们可以以很小的卡片列表来展示超多(or 无限)的卡片 场景1:实现一个超多的卡片展示(比如1000张) 实现思路:以分页的形式每次从后台获取数据,先获取第一页和第二页的数据。在逻辑层(js)创建一个卡片列表,把第一页数据赋值给它,用于跟视图层(wxml)通信。开启循环,用户每滑动一次,将划过的卡片替换成第二页相同索引的卡片,第一页卡片全部划过,第二页的内容也已经同步替换完毕,再次请求后台,获取第三页的内容,以此类推。到最后一页的时候,当前索引为0时,关闭循环,删除最后一页替换不到的上一页剩余的卡片 场景2:实现一个无限循环的卡片 实现思路:类似场景1。不关闭循环。 为什么不建议新增卡片 新增卡片会增加卡片列表的长度,由于每次滑动都要重新计算卡片列表中所有卡片的状态,卡片列表越大,预示着每次滑动卡片需要计算状态的卡片越多,越消耗性能。在完全可以开启循环然后用替换卡片操作代替的情况下,不推荐新增卡片。建议卡片列表大小保持在10以内以保证最佳性能。 以下为卡片列表大小在每次滑动时对性能的影响(指再次渲染耗时) 注:不同手机可能结果不同,但是耗时差距的原因是一样的 耗时(ms/毫秒) 10张卡片 100张卡片 1000张卡片 再次渲染1 10 12 116 再次渲染2 12 10 87 再次渲染3 17 16 145 再次渲染4 9 16 112 再次渲染5 9 18 90 平均 11.4 14.4 110
2020-04-20 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - 获取本地时间,少了8小时
在开发者工具里看,new Date() 获取的是本地时间,在手机的调试工具里看,比本地时间少8个小时,这是正常的吗
2020-05-06 - 如何禁止view滚动?(是view,不是scroll-view)
我现在在view中放了一个自定义组件tab-control,tab-control是固定高度,又放了一个scroll-view,实现效果是view的高度是屏幕的高度,让订单信息在scroll-view中滚动,但是view也会出现滚动效果,怎么禁止view滚动? <view class="orderInfo" > <e-tab-control titles="{{titles}}" active="active"/> <scroll-view class="sview" scroll-y> ... </scroll-view> </view>
2020-05-07 - 大招,大招,如何生成任意小程序的小程序码
我在前几天写过一篇文章介绍,如何生成小程序固定page的小程序码,具体链接如下 https://developers.weixin.qq.com/community/develop/article/doc/00048203e446b00b441a171a351813 当时以为仅仅可以生成自己小程序的小程序码,其实是没有这个限制的 前提条件 您已经注册了一个微信小程序,或者你可以登录别人小程序后台 ❤️ 下面我以京东小程序,京喜为例来生成京喜小程序的小程序码 第一步,登录小程序后台,点击右上角的工具 [图片] 占位 第二步,输入要生成小程序码的小程序名称,比如这里输入京喜 [图片] [图片] [图片] [图片] 第三步 在手机微信搜索京喜小程序,并进入我的页面 [图片] 占位 [图片] 内 点击右下角复制链接,这个时候就提取到京喜小程序我的页面的链接,然后返回小程序后台,输入 [图片] 占位 输入京喜小程序我的页面的链接,这个时候就可以生成京喜小程序我的页面的小程序码,扫码便可以直接进入 [图片] 占 [图片] 位 这个时候就可以去京喜小程序提取固定某个页面的链接,从而生成当前page的小程序码了。
2020-03-23 - 大数据量swiper滑动优化
问题场景 事情是这样的,我做了一个在线答题小程序,有一个顺序练习模块,每次顺序练习,都要把整个题库过一遍,每个题库题目数量不一,有的几百,有的上千,为方便讨论,我们假定某个题库1000道题目, 具体答题模块是用swiper来实现的,当swiper的数组很大的时候,setData会有三四秒的延迟,我们都知道setData的效率,但是没想到这么厉害。 问题描述问题不是swiper本身,而是setData https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html 官方资料https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html 功能演示 [视频] https://developers.weixin.qq.com/community/develop/article/doc/00040235334788f8651a168d951413 这个问题困扰了我几个周时间, 不敢梳头,因为白头发会掉了一地,不要问我为什么是白头发 不敢照镜子,因为黑眼圈更重了; 不敢出门走路,因为问题没有解决,抬不起头来。 [图片] 昨天我在群里发了一个有偿征求优化方案,小伙伴很积极,讨论了一个晚上,第二天就有朋友把优化的方案,递给我,今天中午验证有效,亲测。 [图片] 现在好了,问题得到圆满解决,心情都不一样了,原来处处都是美 [图片] 解决方案大家都能想到方案就是分页,虽然总数组长度为1000,但是每次渲染到swiper的可能只有3、5、7不等的小数组,这样通过setData传递到UI层的时候才不会卡。 但是分页的逻辑要我们自己来控制,怎么判断左滑动,怎么判断右滑动,滑动边界问题,很多细节,总之我为了这个问题花了一个周末没有解决, 就是在大方向明确的情况下,还是写不出来。 具体的实现方案晚点我整理下发出来,同时也希望想挑战的同学锻炼下。 在这里特别感谢群里的两位小伙伴 社区相关帖子 https://developers.weixin.qq.com/community/develop/article/doc/000ecafb3486f07000c92c3225c013 https://developers.weixin.qq.com/community/develop/doc/000e4c77da47208296f8b0b4c51800 感谢@~~娃娃 @~~ dinner
2020-03-25 - 给列表添加曝光统计【电商商品列表曝光统计】
场景: 电商平台商品推荐算法,是基于用户浏览商品进行推荐,介绍一下如何获取用户浏览列表的统计方法。 思路: 监听页面滚动 ——>判断用户浏览停留时间——>记录当前可视窗口的商品——>上报统计数据。 解析: 首先在页面滚动事件里监听,当用户停下来浏览超过1秒,获取当前屏幕视口有哪些商品,把获取到的商品记录下来,当用户离开页面上报曝光数据。 效果: [图片] 示例: https://developers.weixin.qq.com/s/YT6bJ7mx7NgE
2020-03-25 - 自定义navigationBar顶部导航栏,兼容适配所有机型(附完整案例)
前言 navigationBar相信大家都不陌生把?今天我们就来说说自定义navigationBar,把它改变成我们想要的样子(搜索框+胶囊、搜索框+返回按钮+胶囊等)。 思路 隐藏原生样式 获取胶囊按钮、状态栏相关数据以供后续计算 根据不同机型计算出该机型的导航栏高度,进行适配 编写新的导航栏 引用到页面 正文 一、隐藏原生的navigationBar window全局配置里有个参数:navigationStyle(导航栏样式),default=默认样式,custom=自定义样式。 [代码]"window": { "navigationStyle": "custom" } [代码] 让我们看看隐藏后的效果: [图片] 可以看到原生的navigationBar已经消失了,剩下孤零零的胶囊按钮,胶囊按钮是无法隐藏的。 二、准备工作 1.获取胶囊按钮的布局位置信息 我们用wx.getMenuButtonBoundingClientRect()【官方文档】获取胶囊按钮的布局位置信息,坐标信息以屏幕左上角为原点: [代码]const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); [代码] width height top right bottom left 宽度 高度 上边界坐标 右边界坐标 下边界坐标 左边界坐标 下面是官方给的示意图,方便大家理解几个坐标。 [图片] 2.获取系统信息 用wx.getSystemInfoSync()【官方文档】获取系统信息,里面有个参数:statusBarHeight(状态栏高度),是我们后面计算整个导航栏的高度需要用到的。 [代码]const systemInfo = wx.getSystemInfoSync(); [代码] 三、计算公式 我们先要知道导航栏高度是怎么组成的, 计算公式:导航栏高度 = 状态栏高度 + 44。 实例 【源码下载】 自定义导航栏会应用到多个、甚至全部页面,所以封装成组件,方便调用;下面是我写的一个简单例子: app.js [代码]App({ onLaunch: function(options) { const that = this; // 获取系统信息 const systemInfo = wx.getSystemInfoSync(); // 胶囊按钮位置信息 const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); // 导航栏高度 = 状态栏高度 + 44 that.globalData.navBarHeight = systemInfo.statusBarHeight + 44; that.globalData.menuRight = systemInfo.screenWidth - menuButtonInfo.right; that.globalData.menuBotton = menuButtonInfo.top - systemInfo.statusBarHeight; that.globalData.menuHeight = menuButtonInfo.height; }, // 数据都是根据当前机型进行计算,这样的方式兼容大部分机器 globalData: { navBarHeight: 0, // 导航栏高度 menuRight: 0, // 胶囊距右方间距(方保持左、右间距一致) menuBotton: 0, // 胶囊距底部间距(保持底部间距一致) menuHeight: 0, // 胶囊高度(自定义内容可与胶囊高度保证一致) } }) [代码] app.json [代码]{ "pages": [ "pages/index/index" ], "window": { "navigationStyle": "custom" }, "sitemapLocation": "sitemap.json" } [代码] 下面为组件代码: /components/navigation-bar/navigation-bar.wxml [代码]<!-- 自定义顶部栏 --> <view class="nav-bar" style="height:{{navBarHeight}}px;"> <input class="search" placeholder="输入关键词!" style="height:{{menuHeight}}px; min-height:{{menuHeight}}px; line-height:{menuHeight}}px; left:{{menuRight}}px; bottom:{{menuBotton}}px;"></input> </view> <!-- 内容区域: 自定义顶部栏用的fixed定位,会遮盖到下面内容,注意设置好间距 --> <view class="content" style="margin-top:{{navBarHeight}}px;"></view> [代码] /components/navigation-bar/navigation-bar.json [代码]{ "component": true } [代码] /components/navigation-bar/navigation-bar.js [代码]const app = getApp() Component({ properties: { // defaultData(父页面传递的数据-就是引用组件的页面) defaultData: { type: Object, value: { title: "我是默认标题" }, observer: function(newVal, oldVal) {} } }, data: { navBarHeight: app.globalData.navBarHeight, menuRight: app.globalData.menuRight, menuBotton: app.globalData.menuBotton, menuHeight: app.globalData.menuHeight, }, attached: function() {}, methods: {} }) [代码] /components/navigation-bar/navigation-bar.wxss [代码].nav-bar{ position: fixed; width: 100%; top: 0; color: #fff; background: #000;} .nav-bar .search{ width: 60%; color: #333; font-size: 14px; background: #fff; position: absolute; border-radius: 50px; background: #ddd; padding-left: 14px;} [代码] 以下是调用页面的代码,也就是引用组件的页面: /pages/index/index.wxml [代码]<navigation-bar default-data="{{defaultData}}"></navigation-bar> [代码] /pages/index/index.json [代码]{ "usingComponents": { "navigation-bar": "/components/navigation-bar/navigation-bar" } } [代码] /pages/index/index.js [代码]const app = getApp(); Page({ data: { // 组件参数设置,传递到组件 defaultData: { title: "我的主页", // 导航栏标题 } }, onLoad() { console.log(this.data.height) } }) [代码] 效果图: [图片] 好了,以上就是全部代码了,大家可以文中复制代码,也可以【下载源码】,直接到开发者工具里运行,记得appid用自己的或者测试哦! 下面附几张其它小程序的效果图,大家也可以尝试照着做: [图片][图片] 总结 本文写了自定义navigationBar的一些基础性东西,里面涉及组件用法、参数传递、导航栏相关。 由于测试环境有限,大家在使用时如果发现有什么问题,希望及时反馈,以供及时更新帮助更多的人! 大家有什么疑问,欢迎评论区留言!
2022-06-23 - 搞懂微信支付 v3 接口规则-【附Java源码】
简介 为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。 其实还要一个主要因素是「为了符合监管的要求」。 主要是为了符合监管的要求,保证更高的安全级别。《中华人民共和国电子签名法》、《金融电子认证规范》及《非银行支付机构网络支付业务管理办法》中规定 “电子签名需要第三方认证的,由依法设立的电子认证服务提供者提供认证服务。”,所以需使用第三方 CA 来确保数字证书的唯一性、完整性及交易的不可抵赖性。 支付宝支付也是如此,从之前的「普通公钥方式」新增了 「公钥证书方式」。今天的主角是微信支付 Api v3 这里就不展开讲支付宝支付了。 微信支付 Api v3 接口规则 官方文档 v2 与 v3 的区别 V3 规则差异 V2 JSON 参数格式 XML POST、GET 或 DELETE 提交方式 POST AES-256-GCM加密 回调加密 无需加密 RSA 加密 敏感加密 无需加密 UTF-8 编码方式 UTF-8 非对称密钥SHA256-RSA 签名方式 MD5 或 HMAC-SHA256 微信支付 Api-v2 版本详细介绍请参数之前博客 微信支付,你想知道的一切都在这里 干货多,屁话少,下面直接进入主题,读完全文你将 Get 到以下知识点 如何获取证书序列号 非对称密钥 SHA256-RSA 加密与验证签名 AES-256-GCM 如何解密 API 密钥设置 请登录商户平台进入【账户中心】->【账户设置】->【API安全】->【APIv3密钥】中设置 API 密钥。 具体操作步骤请参见:什么是APIv3密钥?如何设置? 获取 API 证书 请登录商户平台进入【账户中心】->【账户设置】->【API安全】根据提示指引下载证书。 具体操作步骤请参见:什么是API证书?如何获取API证书? 按照以上步骤操作后你将获取如下内容: apiKey API 密钥 apiKey3 APIv3 密钥 mchId 商户号 apiclient_key.pem X.509 标准证书的密钥 apiclient_cert.p12 X.509 标准的证书+密钥 apiclient_cert.pem X.509 标准的证书 请求签名 如何生成签名参数?官方文档 描述得非常清楚这里就不啰嗦了。 示例代码 构造签名串 [代码] /** * 构造签名串 * * @param method {@link RequestMethod} GET,POST,PUT等 * @param url 请求接口 /v3/certificates * @param timestamp 获取发起请求时的系统当前时间戳 * @param nonceStr 随机字符串 * @param body 请求报文主体 * @return 待签名字符串 */ public static String buildSignMessage(RequestMethod method, String url, long timestamp, String nonceStr, String body) { return new StringBuilder() .append(method.toString()) .append("\n") .append(url) .append("\n") .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 构造 HTTP 头中的 Authorization [代码]/** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethod} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType) throws Exception { // 构建签名参数 String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); // 获取商户私钥 String key = PayKit.getPrivateKey(keyPath); // 生成签名 String signature = RsaKit.encryptByPrivateKey(buildSignMessage, key); // 根据平台规则生成请求头 authorization return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); } /** * 获取授权认证信息 * * @param mchId 商户号 * @param serialNo 商户API证书序列号 * @param nonceStr 请求随机串 * @param timestamp 时间戳 * @param signature 签名值 * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 * @return 请求头 Authorization */ public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) { Map<String, String> params = new HashMap<>(5); params.put("mchid", mchId); params.put("serial_no", serialNo); params.put("nonce_str", nonceStr); params.put("timestamp", timestamp); params.put("signature", signature); return authType.concat(" ").concat(createLinkString(params, ",", false, true)); } [代码] 拼接参数 [代码] public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) { List<String> keys = new ArrayList<String>(params.keySet()); Collections.sort(keys); StringBuilder content = new StringBuilder(); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); String value = params.get(key); // 拼接时,不包括最后一个&字符 if (i == keys.size() - 1) { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"'); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value); } } else { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr); } } } return content.toString(); } [代码] 从上面示例来看我们还差两个参数 serial_no 证书序列号 signature 使用商户私钥对待签名串进行 SHA256 with RSA 签名 如何获取呢?不要着急,容我喝杯 「89年的咖啡」提提神。 获取证书序列号 通过工具获取 openssl x509 -in apiclient_cert.pem -noout -serial 使用证书解析工具 https://myssl.com/cert_decode.html 通过代码获取 [代码]// 获取证书序列号 X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream("apiclient_cert.pem 证书路径")); System.out.println("输出证书信息:\n" + certificate.toString()); System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16)); System.out.println("版本号:" + certificate.getVersion()); System.out.println("签发者:" + certificate.getIssuerDN()); System.out.println("有效起始日期:" + certificate.getNotBefore()); System.out.println("有效终止日期:" + certificate.getNotAfter()); System.out.println("主体名:" + certificate.getSubjectDN()); System.out.println("签名算法:" + certificate.getSigAlgName()); System.out.println("签名:" + certificate.getSignature().toString()); /** * 获取证书 * * @param inputStream 证书文件 * @return {@link X509Certificate} 获取证书 */ public static X509Certificate getCertificate(InputStream inputStream) { try { CertificateFactory cf = CertificateFactory.getInstance("X509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); cert.checkValidity(); return cert; } catch (CertificateExpiredException e) { throw new RuntimeException("证书已过期", e); } catch (CertificateNotYetValidException e) { throw new RuntimeException("证书尚未生效", e); } catch (CertificateException e) { throw new RuntimeException("无效的证书", e); } } [代码] SHA256 with RSA 签名 获取商户私钥 [代码] /** * 获取商户私钥 * * @param keyPath 商户私钥证书路径 * @return 商户私钥 * @throws Exception 解析 key 异常 */ public static String getPrivateKey(String keyPath) throws Exception { String originalKey = FileUtil.readUtf8String(keyPath); String privateKey = originalKey .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); return RsaKit.getPrivateKeyStr(RsaKit.loadPrivateKey(privateKey)); } public static String getPrivateKeyStr(PrivateKey privateKey) { return Base64.encode(privateKey.getEncoded()); } /** * 从字符串中加载私钥 * * @param privateKeyStr 私钥 * @return {@link PrivateKey} * @throws Exception 异常信息 */ public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { try { byte[] buffer = Base64.decode(privateKeyStr); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); return keyFactory.generatePrivate(keySpec); } catch (NoSuchAlgorithmException e) { throw new Exception("无此算法"); } catch (InvalidKeySpecException e) { throw new Exception("私钥非法"); } catch (NullPointerException e) { throw new Exception("私钥数据为空"); } } [代码] 私钥签名 [代码]/** * 私钥签名 * * @param data 需要加密的数据 * @param privateKey 私钥 * @return 加密后的数据 * @throws Exception 异常信息 */ public static String encryptByPrivateKey(String data, String privateKey) throws Exception { PKCS8EncodedKeySpec priPkcs8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey)); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); PrivateKey priKey = keyFactory.generatePrivate(priPkcs8); java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initSign(priKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signed = signature.sign(); return StrUtil.str(Base64.encode(signed)); } [代码] 至此微信支付 Api-v3 接口请求参数已封装完成。 执行请求 [代码]/** * V3 接口统一执行入口 * * @param method {@link RequestMethod} 请求方法 * @param urlPrefix 可通过 {@link WxDomain}来获取 * @param urlSuffix 可通过 {@link WxApiType} 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath apiclient_key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @param file 文件 * @return {@link String} 请求返回的结果 * @throws Exception 接口执行异常 */ public static Map<String, Object> v3Execution(RequestMethod method, String urlPrefix, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType, File file) throws Exception { // 构建 Authorization String authorization = WxPayKit.buildAuthorization(method, urlSuffix, mchId, serialNo, keyPath, body, nonceStr, timestamp, authType); if (method == RequestMethod.GET) { return doGet(urlPrefix.concat(urlSuffix), authorization, serialNo, null); } else if (method == RequestMethod.POST) { return doPost(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.DELETE) { return doDelete(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.UPLOAD) { return doUpload(urlPrefix.concat(urlSuffix), authorization, serialNo, body, file); } return null; } [代码] 网络请求库默认是使用的 Hutool 封装的一套 Java 工具集合来实现 GET 请求 [代码]/** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doGet(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] POST 请求 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doPost(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] DELETE 请求 [代码]/** * delete 请求 * * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doDelete(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.delete(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] 上传文件 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @param file 上传的文件 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doUpload(String url, String authorization, String serialNumber, String jsonData, File file) { return HttpRequest.post(url) .addHeaders(getUploadHeaders(authorization, serialNumber)) .form("file", file) .form("meta", jsonData) .execute(); } [代码] 构建 Http 请求头 [代码]private Map<String, String> getBaseHeaders(String authorization) { String userAgent = String.format( "WeChatPay-IJPay-HttpClient/%s (%s) Java/%s", getClass().getPackage().getImplementationVersion(), OS, VERSION == null ? "Unknown" : VERSION); Map<String, String> headers = new HashMap<>(3); headers.put("Accept", ContentType.JSON.toString()); headers.put("Authorization", authorization); headers.put("User-Agent", userAgent); return headers; } private Map<String, String> getHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", ContentType.JSON.toString()); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } private Map<String, String> getUploadHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", "multipart/form-data;boundary=\"boundary\""); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } [代码] 构建 Http 请求返回值 从响应的 HttpResponse 中获取微信响应头信息、状态码以及 body [代码]/** * 构建返回参数 * * @param httpResponse {@link HttpResponse} * @return {@link Map} */ private Map<String, Object> buildResMap(HttpResponse httpResponse) { Map<String, Object> map = new HashMap<>(); String timestamp = httpResponse.header("Wechatpay-Timestamp"); String nonceStr = httpResponse.header("Wechatpay-Nonce"); String serialNo = httpResponse.header("Wechatpay-Serial"); String signature = httpResponse.header("Wechatpay-Signature"); String body = httpResponse.body(); int status = httpResponse.getStatus(); map.put("timestamp", timestamp); map.put("nonceStr", nonceStr); map.put("serialNumber", serialNo); map.put("signature", signature); map.put("body", body); map.put("status", status); return map; } [代码] 至此已完成构建请求参数,执行请求。接下来我们就要实现响应数据的解密以及响应结果的验证签名 对应的官方文档 证书和回调报文解密 签名验证 验证签名 构建签名参数 [代码]/** * 构造签名串 * * @param timestamp 应答时间戳 * @param nonceStr 应答随机串 * @param body 应答报文主体 * @return 应答待签名字符串 */ public static String buildSignMessage(String timestamp, String nonceStr, String body) { return new StringBuilder() .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 证书和回调报文解密 官方文档文末有完整的源码这里就不贴了。贴一个示例大家参数一下 [代码]try { String associatedData = "certificate"; String nonce = "80d28946a64a"; String cipherText = "DwAqW4+4TeUaOEylfKEXhw+XqGh/YTRhUmLw/tBfQ5nM9DZ9d+9aGEghycwV1jwo52vXb/t6ueBvBRHRIW5JgDRcXmTHw9IMTrIK6HxTt2qiaGTWJU9whsF+GGeQdA7gBCHZm3AJUwrzerAGW1mclXBTvXqaCl6haE7AOHJ2g4RtQThi3nxOI63/yc3WaiAlSR22GuCpy6wJBfljBq5Bx2xXDZXlF2TNbDIeodiEnJEG2m9eBWKuvKPyUPyClRXG1fdOkKnCZZ6u+ipb4IJx28n3MmhEtuc2heqqlFUbeONaRpXv6KOZmH/IdEL6nqNDP2D7cXutNVCi0TtSfC7ojnO/+PKRu3MGO2Z9q3zyZXmkWHCSms/C3ACatPUKHIK+92MxjSQDc1E/8faghTc9bDgn8cqWpVKcL3GHK+RfuYKiMcdSkUDJyMJOwEXMYNUdseQMJ3gL4pfxuQu6QrVvJ17q3ZjzkexkPNU4PNSlIBJg+KX61cyBTBumaHy/EbHiP9V2GeM729a0h5UYYJVedSo1guIGjMZ4tA3WgwQrlpp3VAMKEBLRJMcnHd4pH5YQ/4hiUlHGEHttWtnxKFwnJ6jHr3OmFLV1FiUUOZEDAqR0U1KhtGjOffnmB9tymWF8FwRNiH2Tee/cCDBaHhNtfPI5129SrlSR7bZc+h7uzz9z+1OOkNrWHzAoWEe3XVGKAywpn5HGbcL+9nsEVZRJLvV7aOxAZBkxhg8H5Fjt1ioTJL+qXgRzse1BX1iiwfCR0fzEWT9ldDTDW0Y1b3tb419MhdmTQB5FsMXYOzqp5h+Tz1FwEGsa6TJsmdjJQSNz+7qPSg5D6C2gc9/6PkysSu/6XfsWXD7cQkuZ+TJ/Xb6Q1Uu7ZB90SauA8uPQUIchW5zQ6UfK5dwMkOuEcE/141/Aw2rlDqjtsE17u1dQ6TCax/ZQTDQ2MDUaBPEaDIMPcgL7fCeijoRgovkBY92m86leZvQ+HVbxlFx5CoPhz4a81kt9XJuEYOztSIKlm7QNfW0BvSUhLmxDNCjcxqwyydtKbLzA+EBb2gG4ORiH8IOTbV0+G4S6BqetU7RrO+/nKt21nXVqXUmdkhkBakLN8FUcHygyWnVxbA7OI2RGnJJUnxqHd3kTbzD5Wxco4JIQsTOV6KtO5c960oVYUARZIP1SdQhqwELm27AktEN7kzg/ew/blnTys/eauGyw78XCROb9F1wbZBToUZ7L+8/m/2tyyyqNid+sC9fYqJoIOGfFOe6COWzTI/XPytCHwgHeUxmgk7NYfU0ukR223RPUOym6kLzSMMBKCivnNg68tbLRJHEOpQTXFBaFFHt2qpceJpJgw5sKFqx3eQnIFuyvA1i8s2zKLhULZio9hpsDJQREOcNeHVjEZazdCGnbe3Vjg7uqOoVHdE/YbNzJNQEsB3/erYJB+eGzyFwFmdAHenG5RE6FhCutjszwRiSvW9F7wvRK36gm7NnVJZkvlbGwh0UHr0pbcrOmxT81xtNSvMzT0VZNLTUX2ur3AGLwi2ej8BIC0H41nw4ToxTnwtFR1Xy55+pUiwpB7JzraA08dCXdFdtZ72Tw/dNBy5h1P7EtQYiKzXp6rndfOEWgNOsan7e1XRpCnX7xoAkdPvy40OuQ5gNbDKry5gVDEZhmEk/WRuGGaX06CG9m7NfErUsnQYrDJVjXWKYuARd9R7W0aa5nUXqz/Pjul/LAatJgWhZgFBGXhNr9iAoade/0FPpBj0QWa8SWqKYKiOqXqhfhppUq35FIa0a1Vvxcn3E38XYpVZVTDEXcEcD0RLCu/ezdOa6vRcB7hjgXFIRZQAka0aXnQxwOZwE2Rt3yWXqc+Q1ah2oOrg8Lg3ETc644X9QP4FxOtDwz/A=="; AesUtil aesUtil = new AesUtil(wxPayV3Bean.getApiKey3().getBytes(StandardCharsets.UTF_8)); // 平台证书密文解密 // encrypt_certificate 中的 associated_data nonce ciphertext String publicKey = aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), cipherText ); // 保存证书 FileWriter writer = new FileWriter(wxPayV3Bean.getPlatformCertPath()); writer.write(publicKey); // 获取平台证书序列号 X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes())); return certificate.getSerialNumber().toString(16).toUpperCase(); } catch (Exception e) { e.printStackTrace(); } [代码] 验证签名 [代码]/** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param certInputStream 微信支付平台证书输入流 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(String signature, String body, String nonce, String timestamp, InputStream certInputStream) throws Exception { String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); // 获取证书 X509Certificate certificate = PayKit.getCertificate(certInputStream); PublicKey publicKey = certificate.getPublicKey(); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 公钥验证签名 * * @param data 需要加密的数据 * @param sign 签名 * @param publicKey 公钥 * @return 验证结果 * @throws Exception 异常信息 */ public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception { java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initVerify(publicKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8))); } [代码] 至此微信支付 Api-v3 接口已介绍完,如有疑问欢迎留言一起探讨。 完整示例 SpringBoot 参考资料 你真的了解 HTTPS 吗? WechatPay-API-v3
2021-03-02 - 【个人笔记】小程序开发关于小程序data的坑
今天在开发在线答题小程序的时候,遇到一个问题,记录下 问题描述我在答题之后,会更新状态,但是这个状态变更之后,没有反馈到UI层,只在逻辑层做了变更 看到这里,已经有人能猜到问题了 问题原因我在更新状态的时候,用了this.data.list=newlist,注意这种方式,确实能把逻辑层的data更新了,只是没有异步反馈到UI层 而不是下面 1 this.setData({ list: newlist }) 2 备注 后面开发的时候注意这种情况,也尽量避免这种更新状态的方式
2020-03-28 - [开盖即食]小程序Canvas官方新版API实战
[图片] [图片] 最近本人在开发一个新项目的时候,注意到官方在2.9.0开始支持了一个canvas 2D的新API,同时对webGL上支持也有了很大的改进,相信很多人用canvas的组件做一些分享海报,战绩和新闻帖功能。 [图片] 这里是新的引入方式。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html 那么新的canvas2D API有啥好处呢? 原本的API微信有做一定的修改,现在全面支持源生H5 JS的写法,迁移H5的老代码变成更加容易,学习成本更低 修复了一些诡异的BUG,例如原本在IOS早期版本写法顺序会导致clip()图片裁切失效等~ 性能上的优化和提升,复杂动画上帧数明显 举例写法上的一些改变: 1、设置font的写法: [代码]//原本(传值的写法) ctx.setFontSize(20); ctx.fillText('MINA', 100, 100) ctx.draw() //现在(和源生H5写法一致,赋值) ctx.font = "16px"; ctx.fillStyle = 'blue'; //可以直接写颜色,原本的不支持 //不需要 ctx.draw() [代码] 2、获取并添加图片写法: [代码]//原本 //使用的是 wx.getImageInfo的方法 wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { console.log(res); ctx.drawImage(res.path, 0, 0); ctx.draw(true); }, fail: function (res) { //失败回调 } }); //现在 //可以直接img.onload调用 const headerImg = canvas.createImage(); headerImg.src = headImage;//微信请求返回头像 headerImg.onload = () => { ctx.save(); ctx.beginPath()//开始创建一个路径 ctx.arc(38, 288, 18, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.drawImage(headerImg,0,0); ctx.closePath(); ctx.restore(); } [代码] 3、将canvas生成虚拟地址便于下载(重点): [图片] 由于官方文档没有写清楚,误导了挺多人的。这里canvas对象必须通过选择器获取,并获得对应的node节点。 [代码]async saveImg() { let self = this; //这里是重点 新版本的type 2d 获取方法 const query = wx.createSelectorQuery(); const canvasObj = await new Promise((resolve, reject) => { query.select('#posterCanvas') .fields({ node: true, size: true }) .exec(async (res) => { resolve(res[0].node); }) }); console.log(canvasObj); wx.canvasToTempFilePath({ //fileType: 'jpg', //canvasId: 'posterCanvas', //之前的写法 canvas: canvasObj, //现在的写法 success: (res) => { console.log(res); self.setData({ canClose: true }); //保存图片 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 }) // setTimeout(() => { // self.setData({show: false}) // }, 6000); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") } else { util.showToast("请截屏保存分享"); } }, complete(res) { wx.hideLoading(); console.log(res); } }) }, fail(res) { console.log(res); } }, this) }, [代码] 分享个canvas海报的代码片段: [图片] 片段名: PoCf4emw7TgE 片段link: https://developers.weixin.qq.com/s/PoCf4emw7TgE [图片] [图片] 总结,相对之前还要看官方文档的canvas自定义API,现在写起来更加的方便,老代码迁移起来得心应手,只要你之前会canvas,那么各种效果和动画,拿来就怼,没什么大问题~ 一些奇怪的问题(注意!!!) canvas 2d 目前(2020年4月3日)还不支持真机调试,会报错!!! IDE工具 1.02.2003190 直接保存新版本canvas的API图片是打不开的,但是直接用手机保存在相册是没问题的,请更新到1.02.2003250 最新版即可解决~ 一些老款手机用新的API保存图片会有报错问题,如华为NOTE10,请更新系统到能支持的最新,且微信也是,即可解决~ 部分Android设备诡异的闪退和报错 [图片] 这种有可能是代码写法的问题,比如: [代码]//缺省写法 会导致部分Android机器 闪退 ctx.font = "bold 16px"; ctx.fillStyle = "#000" //在canvas 2D的写法中,所以写法必须规范且完整 ctx.font = "normal bold 12px sans-serif"; ctx.fillStyle = '#707070'; [代码] 所以在canvas 2D 的环境,所以写法必须原始且规范,不能用缺省写法,不然就会有诡异的闪退/报错。 后续:官方在7.0.13的Android版本已修复。 https://developers.weixin.qq.com/community/develop/doc/00088c13e1437890692afd8d85ec00 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-05-09 - API Promise化
本文是我的小程序开发日记的其中一篇,刚兴趣的读者可以前往 GitHub 收看更多,欢迎star,感谢万分! 前言 众所周知,前端一大坑就是回调函数。 相信很多人是从[代码]async/await[代码]的温柔乡,掉到小程序重新写回调的大坑里的。 由于开发者工具新增了 增强编译 从而原生支持了[代码]async\await[代码],避免了我们仍需通过webpack等第三方打包工具实现。因此我们需要做的就是将官方API的 异步调用 方式改成 Promise的方式 即可。 分析与实践 大致上可以有两种思路,第一种就是,逐个函数封装: [代码]let promisify = func => args => new Promise((resolve, reject) => { func(Object.assign(args, { success: resolve, fail: reject, })) }) let _login = promisify(wx.login) // 将wx.login转成Promise形式的方法 _login().then(res => console.log) [代码] 这种方式比较麻烦,每次调用都需要手动转换。 劫持WX 第二种就类似[代码]Page[代码]封装那样,劫持[代码]wx[代码]对象,进行全局统一封装。但有一点比较棘手的是,需要分析清楚哪些是函数,哪些函数是异步而不是同步的,一开始我的思路是这样的: 同步方法是以[代码]Sync[代码]结尾的 通过[代码]typeof[代码]判断是否为函数 [代码]// promisify.js let originalWX = wx let props = Object.keys(wx) for (let name of props) { let fn = wx[name] if (typeof fn === 'function' && !name.endsWith('Sync')) { wx[name] = promisify(fn) } } [代码] 尝试封装之后,发现报错了。因为[代码]wx.drawCanvas[代码]只有[代码]getter[代码]没有[代码]setter[代码],无法给它赋值。相当于这个方法是[代码]readonly[代码]。 [图片] 既然存在没有[代码]setter[代码]的方法,那么我看有多少方法是有[代码]setter[代码]的: [代码]Object.keys(wx).filter(name => { let descriptor = Object.getOwnPropertyDescriptor(wx, name) return typeof descriptor.set === 'function' }) [代码] 结果是[代码][][代码],相当于无法改变[代码]wx[代码]对象的每个属性值。 [图片] 复制模式 虽然[代码]wx[代码]的属性都是[代码]readonly[代码],不能劫持[代码]wx[代码],但我发现[代码]wx[代码]是[代码]writable[代码]的。 那么可以采用复制模式,将它的所有异步方法拷贝一份并[代码]promisify[代码]之后赋值到新对象,最后再将整个对象赋值给[代码]wx[代码]即可: [代码]let props = Object.keys(wx) let jwx = {} for (let name of props) { let fn = wx[name] if (typeof fn === 'function' && !name.endsWith('Sync')) { jwx[name] = promisify(fn) } else { jwx[name] = fn } } wx = jwx [代码] 这种方式虽可行,但是挺冗余的,因为将很多可能没用上的方法也进行了[代码]promisify[代码]。 代理模式 熟悉ES新特性的读者应该知道[代码]Proxy[代码]。 它可以用来定义对象的自定义行为,顾名思义,就是给对象挂上[代码]Proxy[代码]之后,对这个属性的任何行为都可以被代理。 那么我们就可以给[代码]wx[代码]挂上代理: [代码]let originalWX = wx wx = new Proxy({}, { get(target, name) { if (name in originalWX ) { let fn = originalWX[name] let isSyncFunc = name.endsWith('Sync') // 同步函数 let isNotFunc = typeof fn !== 'function' // 非函数 if (isSyncFunc || isNotFunc) return fn return promisify(fn) } } }); [代码] 代理的方式虽解决了复制模式的冗余问题,但是仍有一个问题待解决:异步方法的判断。 在实践中,我发现并不是所有同步方法都是以[代码]Sync[代码]结尾的。比如:[代码]wx.getMenuButtonBoundingClientRect[代码]。 因此打算手动维护一个同步方法列表,将这项方法过滤掉: [代码]let syncFuncList = ['getMenuButtonBoundingClientRect'] // name为函数名 let isSync = name.endsWith('Sync') || syncFuncList.includes(name) [代码] 优化 考虑到要兼容已上线的小程序,若匆忙替换[代码]wx[代码],必会导致全局报错,因此可以如下处理: 当用户调用API时,如果传入了[代码]success[代码]、[代码]fail[代码]、[代码]complete[代码]等回调方法的话,则仍继续使用回调的方式继续执行。那么[代码]promisify[代码]可以如下优化: [代码]let originalWX = wx let hasCallback = obj => { let cbs = ['success', 'fail', 'complete'] return Object.keys(obj).some(k => cbs.includes(k)) } wx = new Proxy({}, { get(target, name) { if (name in originalWX ) { let fn = originalWX[name] let isSyncFunc = name.endsWith('Sync') // 同步函数 let isNotFunc = typeof fn !== 'function' // 非函数 if (isSyncFunc || isNotFunc) return fn return (obj) => { if (!obj) return fn() if (hasCallback(obj)) return fn(obj) return promisify(fn)(obj) } } } }); [代码] 由于本文的前提是开启 增强编译,而该模式下也新增支持[代码]Array.prototype.includes[代码],因此可以放心使用该ES7的新特性。 后续 由于发现了微信官方也提供了一个 API Promise化 的工具类库,因此增加了本章节。 通过阅读源代码,发现官方的工具类库提供两个方法:[代码]promisify[代码] 和 [代码]promisifyAll[代码] 其中[代码]promisify[代码]与前文的同名方法是几乎一致的。而[代码]promisifyAll[代码]则是接收两个参数,第一个是被封装的对象,第二个则是封装之后的对象,如下使用将和前文我提到的封装方式类似: [代码]import { promisifyAll } from 'miniprogram-api-promise'; let jwx = {} promisifyAll(wx, jwx) wx = jwx [代码] 另外还有一点需要提到的是,官方这个工具类库,判断是否为异步函数的方式是维护了一个异步方法列表,会存在遗漏新API的可能。 相当于我的做法是黑名单机制,而官方采用了白名单机制。 最后再提醒下,开发者工具记得打开 增强编译
2020-04-01 - WXS仿拼多多横向滚动条ScrollView组件
Demo: 最终实现效果如下 [图片] 接下来要实现的就是下面的,滚动条,显示横向拖动的进度。 Component代码: index.js Component({ properties: { customStyle: { type:String, }, // 根据Slot中的元素计算出的ScrollView总宽度 rpx scrollViewWidth: { type: Number, value: 0 }, // ScrollView的总高度,调整高度来控制显示行数 rpx scrollViewHeight: { type: Number, value: 0 }, // 滚动条的宽度rpx scrollBarWidth: { type: Number, value: 160 }, // 滚动条的高度 rpx scrollBarHeight: { type: Number, value: 10 }, // 滚动条滑块的宽度 rpx scrollBarBlockWidth: { type: Number, value: 100 }, // 滚动条样式 scrollBarStyle: { type: String, }, // 滚动滑块样式 scrollBarBlockStyle: { type: String, } }, data: { windowWidth: 375, // px px2rpxRatio: 0, windowWidth2ScrollViewWidthRatio: 0, scrollBarBlockLeft: 0, }, lifetimes: { attached: function() { this.data.windowWidth = wx.getSystemInfoSync().windowWidth this.setData({ px2rpxRatio: Number(750 / this.data.windowWidth).toFixed(3) }) console.log('px2rpxRatio', this.data.px2rpxRatio) this.setData({ windowWidth2ScrollViewWidthRatio: Number(this.data.windowWidth * this.data.px2rpxRatio / this.data.scrollViewWidth).toFixed(3) }) let scrollBarBlockWidth = Number(this.data.scrollBarWidth * this.data.windowWidth2ScrollViewWidthRatio).toFixed() if(scrollBarBlockWidth >= this.data.scrollBarWidth) { scrollBarBlockWidth = this.data.scrollBarWidth } this.setData({ scrollBarBlockWidth: scrollBarBlockWidth }) }, }, }) index.wxml <wxs module="wxs" src="./index.wxs"></wxs> <view style="{{ customStyle }}"> <scroll-view scroll-x="{{ true }}" bind:scroll="{{ wxs.onScroll }}" data-px2rpxRatio="{{ px2rpxRatio }}" data-scrollViewWidth="{{ scrollViewWidth }}" data-scrollBarWidth="{{ scrollBarWidth }}"> <view class="scroll-view" style="height: {{ scrollViewHeight }}rpx"> <slot/> </view> </scroll-view> <view class="scrollbar" style="width: {{ scrollBarWidth }}rpx; height: {{ scrollBarHeight}}rpx; margin: 0 auto; margin-top: 10rpx; {{ scrollBarStyle }}"> <view class="scrollbar-block" id="scrollbar" style="width: {{ scrollBarBlockWidth }}rpx; left: {{scrollBarBlockLeft}}rpx; {{ scrollBarBlockStyle }}"></view> </view> </view> index.wxs var onScroll = function(e, ownerInstance) { var scrollBarBlockLeft = (e.detail.scrollLeft * e.currentTarget.dataset.px2rpxratio / (e.currentTarget.dataset.scrollviewwidth)) * e.currentTarget.dataset.scrollbarwidth ownerInstance.selectComponent('#scrollbar').setStyle({left: scrollBarBlockLeft + 'rpx'}) } module.exports = { onScroll: onScroll }; index.wxss .scroll-view{ width: 100%; box-sizing: border-box; display: flex; flex-direction: column; flex-wrap: wrap; justify-content: flex-start; align-items: flex-start; align-content: flex-start; } .scrollbar { margin: 0 auto; background: black; position: relative; border-radius: 4rpx; overflow: hidden; } .scrollbar-block { height: 100%; position: absolute; top: 0; background: darkred; border-radius: 4rpx; } 以上就是组件的代码部分,下面来看一看在Page页中如何使用,代码也很简单: index.js //index.js Page({ data: { data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] } }) index.wxml <x-scroll-bar scroll-view-width="1200" scroll-view-height="400"> <view wx:for="{{ data }}" class="block" > {{ item }} </view> </x-scroll-bar> index.wxss .block{ flex-shrink: 0; flex-grow: 0; background: black; width: 200rpx; height: 200rpx; line-height: 200rpx; text-align: center; color: white; } .block:nth-child(even) { background: red; } 以上全部内容,组件代码已经在我的Github,可直接访问下载: https://github.com/foolstack-omg/fool-weapp
2020-04-08 - 纯CSS3的毫秒倒计时
1. 使用场景: 很多小程序需要使用倒计时功能,可以使用js做秒级倒计,但往往需要到达毫秒级,才能让用户有种紧迫感气氛,如果使用100毫秒setData一次,会是性能大幅度下降,导致页面卡顿。使用gif动图也是一种实现方式,但由于很多地方使用不同的样式倒计时,使用gif维护比较困难。 2. 技术核心: CSS3帧动画功能,动画的steps()功能 3. 效果演示: [图片] 4.代码片段: https://developers.weixin.qq.com/s/umMJwtmu7vga 5.温馨提示: 来个关注,不定期更新技术文章。
2020-04-09 - 网页端管理系统在小程序上的实现
接到一个需求,要把一套现有的网页端管理系统完整的复制到小程序上。开始我是拒绝的,想想网页端那些表格要在手机、特别是小程序上复现就头疼。最后甲方给了我一个可以接受的理由:有了小程序就不用做app了啊。 虽然有难度,但是工作还是要做的,有问题就一点一点来解决。先说一个优势,这个网页端是我最近刚重构过的,改成了前后端分离,接口上用了jwt做登录校验(关于jwt的介绍可以移步:【接口相关】聊一聊数据接口的登录态校验以及JWT),可以直接拿到小程序里来用。接下来说一下实际遇到的问题和我的解决方案。 菜单 这个简单,把网页端左侧菜单栏里筛选出常用的放到小程序的tabBar里就可以了,直接用原生的tabBar,没什么花头。 测试的时候发现了一个bug,已经提交给了官方:XS Max真机调试、预览,原生tabBar上的线不显示? 数据表格 网页端数据列表基本都是使用表格来展现,到了小程序端就不适合再用表格了。一方面是小程序没有原生的表格组件,另一方面是手机屏幕不适合展示很宽的表格(横屏什么的从来不在我的考虑范围内)。 我最终采用的解决方案是用卡片列表的形式来展现数据列表。下图是网页端表格和小程序里对应的卡片列表。 [图片] [图片] 模态框+表单 网页端列表中用到的各种表单基本是在弹出模态框里使用表单,这个在小程序上我改成了放到从底部弹出的半屏弹窗。具体效果对比直接看图,小程序里为了防止弹窗内容太多超出屏幕限制,弹窗加了最大高度限制。 [图片] [图片] 日历 由于需求需要,这个项目有一个通过日历展示一个月的日程安排,先看网页端的效果,这里用的是antd的Calendar日历组件。 [图片] 由于手机屏幕的限制,就算能在小程序页面上展现日历,也没办法合适的展现需要的内容。最终决定小程序端只展示某一日的内容,通过从底部弹出的半屏弹窗里显示的日历来切换日期。 [图片] 还是要有取舍 虽然经过各种修改后,绝大多数功能都改成了适合小程序端展现的方式,但是还是有一些功能实在是不适合放到小程序端,或者从功能上来说没必要放到小程序端,这些就只能放弃了。 [图片]
2020-04-10 - 微信合单支付demo - PHP
微信合单支付PHP版demo 1、获取API v3证书 /** * 获取证书 * @return mixed */ public function getCert() { $url = 'https://api.mch.weixin.qq.com/v3/certificates'; $timestamp = time(); $nonce = $this->nonce_str(); $body = ''; $sign = $this->sign($url, 'GET', $timestamp, $nonce, $body, $this->getPrivateKey($this->private_key), $this->mch_id, $this->serial_no); $header = [ 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $sign, 'Accept:application/json', 'User-Agent:' . $this->mch_id, ]; $result = $this->curl($url, '', $header, 'GET'); $result = json_decode($result, true); return $result['data']['0']['serial_no']; } 2、支付请求 /** * @return bool|mixed */ public function pay() { $url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/app'; $requestData = [ 'combine_appid' => $this->app_id, 'combine_mchid' => $this->mch_id, 'combine_out_trade_no' => 'app_pay_' . time(), 'scene_info' => [ 'device_id' => 'pay_device_id', 'payer_client_ip' => '127.0.0.1', ], 'time_start' => date('c', time()), 'time_expire' => date('c', time() + 7200), 'notify_url' => 'http://www.ttglad.com/notify.php', 'sub_orders' => [ [ 'mchid' => $this->mch_id, 'attach' => 'notify with attach', 'amount' => [ 'total_amount' => 100, 'currency' => 'CNY', ], 'out_trade_no' => 'sub_order_' . time(), 'sub_mchid' => $this->sub_mch_id, // 二级商户号 需要进件系统生成 'profit_sharing' => true, // 分账 'description' => '描述', ], ], ]; $header = $this->getCurlHeader($url, json_encode($requestData), 'POST'); $result = $this->curl($url, json_encode($requestData), $header, 'POST'); return json_decode($result, true); } 3、支付查询 /** * @return mixed */ public function payQuery() { $url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/out-trade-no/'; $url = $url . ''; // 支付单号 $method = 'GET'; $data = ''; $header = $this->getCurlHeader($url, $data, $method); $result = $this->curl($url, $data, $header, $method); return json_decode($result, true); } 4、退款 /** * @return mixed */ public function refund() { $requestData = [ 'sp_appid' => $this->app_id, 'sub_mchid' => $this->sub_mch_id, 'transaction_id' => '',// 支付三方流水 'out_refund_no' => '',// 退款单号 'notify_url' => 'http://www.ttglad.com/notify_refund.php', 'amount' => [ 'refund' => 100, 'total' => 100, 'currency' => 'CNY', ] ]; $header = $this->getCurlHeader($this->refundUrl, json_encode($requestData), 'POST'); $result = $this->curl($this->refundUrl, json_encode($requestData), $header, 'POST'); return json_decode($result, true); } 5、退款查询 /** * @return mixed */ public function refundQuery() { $url = 'https://api.mch.weixin.qq.com/v3/ecommerce/refunds/id/' . '' . '?sub_mchid=' . $this->sub_mch_id; $method = 'GET'; $data = ''; $header = $this->getCurlHeader($url, $data, $method); $result = $this->curl($url, $data, $header, $method); return json_decode($result, true); } 6、支付通知 public function notify() { $header = $this->getHeaders(); $body = $GLOBALS['HTTP_RAW_POST_DATA']; if (empty($header) || empty($body)) { throw new Exception('通知参数为空', 2001); } $timestamp = $header['WECHATPAY-TIMESTAMP']; $nonce = $header['WECHATPAY-NONCE']; $signature = $header['WECHATPAY-SIGNATURE']; $serialNo = $header['WECHATPAY-SERIAL']; if (empty($timestamp) || empty($nonce) || empty($signature) || empty($serialNo)) { throw new Exception('通知头参数为空', 2002); } $cert = $this->getCertBySerialNo($serialNo); $message = "$timestamp\n$nonce\n$body\n"; //校验签名 if (!$this->verify($message, $signature, $cert['plainCertificate'])) { throw new Exception('验签失败', 2005); } $decodeBody = json_decode($body, true); if (empty($decodeBody) || !isset($decodeBody['resource'])) { throw new Exception('通知参数内容为空', 2003); } $decodeBodyResource = $decodeBody['resource']; $decodeData = $this->decryptToString($decodeBodyResource['associated_data'], $decodeBodyResource['nonce'], $decodeBodyResource['ciphertext'], ''); $decodeData = json_decode($decodeData, true); if (empty($decodeData)) { throw new Exception('通知参数解密发生错误', 2004); } // todo 业务逻辑 } 7、其他方法 /** * 初始化参数 */ public function __construct() { parent::__construct(); // 微信支付 商户号 $this->mch_id = ''; // 二级商户号,需要走进件系统生成 $this->sub_mch_id = ''; // 微信支付 商户号绑定的appid $this->app_id = ''; // 商户私钥 $this->private_key = ''; // 商户证书序列号 // 如何查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao $this->serial_no = ''; // apiv3秘钥:https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/api-v3-mi-yao $this->mch_key = ''; } /** * @param $serialNo * @return mixed */ private function getCertBySerialNo($serialNo) { $url = 'https://api.mch.weixin.qq.com/v3/certificates'; $timestamp = time(); $nonce = $this->nonce_str(); $body = ''; $sign = $this->sign($url, 'GET', $timestamp, $nonce, $body, $this->getPrivateKey($this->private_key), $this->mch_id, $this->serial_no); $header = [ 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $sign, 'Accept:application/json', 'User-Agent:' . $this->mch_id, ]; $result = $this->curl($url, '', $header, 'GET'); $cert = json_decode($result, true); $return = []; if (!empty($cert['data'])) { foreach ($cert['data'] as $item) { if ($serialNo == $item['serial_no']) { $return = $item; break; } } } return $return; } /** * @param $url * @param $body * @param $method * @return array */ protected function getCurlHeader($url, $body, $method) { $timestamp = time(); $nonce = $this->nonce_str(); $sign = $this->sign($url, $method, $timestamp, $nonce, $body, $this->getPrivateKey($this->private_key), $this->mch_id, $this->serial_no); return [ 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $sign, 'Accept:application/json', 'User-Agent:' . $this->mch_id, 'Content-Type:application/json', 'Wechatpay-Serial:' . $this->getCert(), ]; } /** * @return string */ protected function nonce_str() { static $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < 32; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; } return $randomString; } /** * @param $key * @return bool|resource */ protected function getPrivateKey($key) { return openssl_get_privatekey($key); } /** * @param $key * @return resource */ protected function getPublicKey($key) { return openssl_get_publickey($key); } /** * @param $url * @param $http_method * @param $timestamp * @param $nonce * @param $body * @param $mch_private_key * @param $merchant_id * @param $serial_no * @return string */ protected function sign($url, $http_method, $timestamp, $nonce, $body, $mch_private_key, $merchant_id, $serial_no) { $url_parts = parse_url($url); $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")); $message = $http_method . "\n" . $canonical_url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n"; openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); $schema = 'WECHATPAY2-SHA256-RSA2048 '; $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $merchant_id, $nonce, $timestamp, $serial_no, $sign); return $token; } /** * @param $message * @param $signature * @param $merchantPublicKey * @return bool|int */ private function verify($message, $signature, $merchantPublicKey) { if (empty($merchantPublicKey)) { return false; } if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { throw new \RuntimeException("当前PHP环境不支持SHA256withRSA"); } $signature = base64_decode($signature); return openssl_verify($message, $signature, $this->getPublicKey($merchantPublicKey), 'sha256WithRSAEncryption'); } /** * @param $url * @param array $data * @param $header * @param string $method * @param int $time_out * @return mixed */ private function curl($url, $data = [], $header, $method = 'POST', $time_out = 3) { $curl = curl_init(); // 设置curl允许执行的最长秒数 curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_HTTPHEADER, $header); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_TIMEOUT, $time_out); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); if ($method == 'POST') { curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); } // 执行操作 $result = curl_exec($curl); curl_close($curl); return $result; } /** * @param $associatedData * @param $nonceStr * @param $ciphertext * @param $aesKey * @return bool|string */ private function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey = '') { if (empty($aesKey)) { $aesKey = $this->mch_key; } $ciphertext = \base64_decode($ciphertext); if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) { return false; } // ext-sodium (default installed on >= PHP 7.2) if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) { return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // ext-libsodium (need install libsodium-php 1.x via pecl) if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) { return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // openssl (PHP >= 7.1 support AEAD) if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) { $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE); $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE); return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData); } throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php'); } /** * @return array */ private function getHeaders() { $headers = array(); foreach ($_SERVER as $key => $value) { if ('HTTP_' == substr($key, 0, 5)) { $headers[str_replace('_', '-', substr($key, 5))] = $value; } if (isset($_SERVER['PHP_AUTH_DIGEST'])) { $header['AUTHORIZATION'] = $_SERVER['PHP_AUTH_DIGEST']; } elseif (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $header['AUTHORIZATION'] = base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']); } if (isset($_SERVER['CONTENT_LENGTH'])) { $header['CONTENT-LENGTH'] = $_SERVER['CONTENT_LENGTH']; } if (isset($_SERVER['CONTENT_TYPE'])) { $header['CONTENT-TYPE'] = $_SERVER['CONTENT_TYPE']; } } return $headers; } 8、注意事项 支付通知需要根据通知header里面的证书序号获取证书 合单支付同一个子单不允许在不同的父单支付 9、最近写了个支付的package,仅供参考:https://github.com/ttglad/payment 参考文档: 合单支付文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/combine.shtml接口规则:https://wechatpay-api.gitbook.io/wechatpay-api-v3/
2021-04-16 - 简单实现公众号回复跳转小程序
[图片] 如上图所示,我们想要实现这样的功能仅仅需要一串代码即可,很多资深大佬都懂,别喷我, <a data-miniprogram-appid="小程序appid" data-miniprogram-path="小程序路径" data-miniprogram-nickname="小程序名称" href="" data-miniprogram-type="text" data-miniprogram-servicetype="">炫洛科技</a> 修改上述代码中中文的字体要求即可,最后炫洛科技几个字为回复显示的字,即上图,商城首页的展示效果
2020-04-21 - 【好文】小程序动态换肤解决方案 - 本地篇
小程序动态换肤方案 – 本地篇 需求说明 在开发小程序的时候,尤其是开发第三方小程序,我们作为开发者,只需要开发一套模板,客户的小程序对我们进行授权管理,我们需要将这套模板应用到对方的小程序上,然后进行发版审核即可; 但是个别客户的小程序需要做 [代码]定制化配色方案[代码],也就是说,不同的小程序个体需要对页面的元素(比如:按钮,字体等)进行不同的配色设置,接下来我们来讨论一下怎么实现它。 方案和问题 一般来说,有两种解决方案可以解决小程序动态换肤的需求: 小程序内置几种主题样式,通过更换类名来实现动态改变小程序页面的元素色值; 后端接口返回色值字段,前端通过 [代码]内联[代码] 方式对页面元素进行色值设置。 当然了,每种方案都有一些问题,问题如下: 方案1较为死板,每次更改主题样式都需要发版小程序,如果主题样式变动不大,可以考虑这种; 方案2对于前端的改动很大,[代码]内联[代码] 也就是通过 [代码]style[代码] 的方式内嵌到[代码]wxml[代码] 代码中,代码的阅读性会变差,但是可以解决主题样式变动不用发版小程序的问题。 ps:我一直在尝试如何在小程序里面,通过js动态修改stylus的变量问题,这样就可以解决上面说的问题了,后期如果实现了,一定周知各位 本文先重点描述第一种方案的实现,文章末尾会贴上我的 [代码]github项目[代码] 地址,方便大家尝试。 前期准备 本文采用的是 [代码]gulp[代码] + [代码]stylus[代码] 引入预编译语言来处理样式文件,大家需要全局安装一下 [代码]gulp[代码],然后安装两个 [代码]gulp[代码] 的插件 [代码]gulp-stylus[代码](stylus文件转化为css文件) [代码]gulp-rename[代码](css文件重命名为wxss文件)。 gulp 这里简单贴一下gulpfile文件的配置,比较简单,其实就是借助 [代码]gulp-stylus[代码] 插件将 [代码].styl[代码] 结尾的文件转化为 [代码].css[代码] 文件,然后引入 [代码]gulp-rename[代码] 插件对文件重命名为 [代码].wxss[代码] 文件; 再创建一个任务对 [代码].styl[代码] 监听修改,配置文件如下所示: [代码]var gulp = require('gulp'); var stylus = require('gulp-stylus'); var rename = require('gulp-rename'); function stylusTask() { return gulp.src('./styl/*.styl') .pipe(stylus()) .pipe(rename(function(path) { path.extname = '.wxss' })) .pipe(gulp.dest('./wxss')) } function autosTask() { gulp.watch('./styl/*.styl', stylusTask) } exports.default = gulp.series(gulp.parallel(stylusTask, autosTask)) [代码] stylus 这里会分为两个文件,一个是主题样式变量定义文件,一个是页面皮肤样式文件,依次如下所示: 主题样式变量设置 [代码]// theme1 theme1-main = rgb(254, 71, 60) theme1-sub = rgb(255, 184, 0) // theme2 theme2-main = rgb(255, 158, 0) theme2-sub = rgb(69, 69, 69) // theme3 theme3-main = rgb(215, 183, 130) theme3-sub = rgb(207, 197, 174) [代码] 页面皮肤样式 [代码]@import './define.styl' // 拼接主色值 joinMainName(num) theme + num + -main // 拼接辅色值 joinSubName(num) theme + num + -sub // 遍历输出改变色值的元素类名 for num in (1..3) .theme{num} .font-vi color joinMainName(num) .main-btn background joinMainName(num) .sub-btn background joinSubName(num) [代码] 输出: [代码].theme1 .font-vi { color: #fe473c; } .theme1 .main-btn { background: #fe473c; } .theme1 .sub-btn { background: #ffb800; } .theme2 .font-vi { color: #ff9e00; } .theme2 .main-btn { background: #ff9e00; } .theme2 .sub-btn { background: #454545; } .theme3 .font-vi { color: #d7b782; } .theme3 .main-btn { background: #d7b782; } .theme3 .sub-btn { background: #cfc5ae; } [代码] 代码我写上了注释,我还是简单说明一下上面的代码:我首先定义一个主题文件 [代码]define.styl[代码] 用来存储色值变量,然后会再定义一个皮肤文件 [代码]vi.styl[代码] ,这里其实就是不同 [代码]主题类名[代码] 下需要改变色值的元素的属性定义,元素的色值需要用到 [代码]define.styl[代码] 预先定义好的变量,是不是很简单,哈哈哈。 具体使用 但是在具体页面中需要怎么使用呢,接下来我们来讲解一下 页面的 [代码]wxss[代码] 文件导入编译后的 [代码]vi.wxss[代码]文件 [代码]@import '/wxss/vi.wxss'; [代码] 页面的 [代码]wxml[代码] 文件需要编写需要改变色值的元素,并且引入变量 [代码]theme[代码] [代码]<view class="intro {{ theme }}"> <view class="font mb10">正常字体</view> <view class="font font-vi mb10">vi色字体</view> <view class="btn main-btn mb10">主色按钮</view> <view class="btn sub-btn">辅色按钮</view> </view> [代码] 页面 [代码]js[代码] 文件动态改变 [代码]theme[代码]变量值 [代码] data: { theme: '' }, handleChange(e) { const { theme } = e.target.dataset this.setData({ theme }) } [代码] 效果预览 [图片] 项目地址 项目地址:https://github.com/csonchen/wxSkin 这是本文案例的项目地址,为了方便大家浏览项目,我把编译后的wxss文件也一并上传了,大家打开就能预览,如果觉得好,希望大家都去点下star哈,谢谢大家。。。
2020-04-23 - 删除数组中的某个对象元素,根据对象中的某个属性删除该对象元素
wxml页面代码: <block wx:for="{{list}}" wx:key="index"> <view class="content"> <text>{{item.id}}</text> <text>{{item.name}}</text> <button size="mini" bindtap="delete" data-id="{{item.id}}">删除</button> </view> </block> js模块代码: Page({ data:{ list:[ { id: '1001', name: 'tom1' }, { id: '1002', name: 'tom2' }, { id: '1003', name: 'tom3' }] }, //删除数组中的某个对象元素,根据对象中的某个属性删除该对象元素 delete(e){ let eid = e.currentTarget.dataset.id let myList = this.data.list for (var i = 0; i < this.data.list.length; i++) { if (this.data.list[i].id == eid){ myList.splice(i,1) this.setData({ list: myList }) break } } } })
2020-04-26 - 滥用分享违规案例集锦
平台鼓励用户和朋友们分享优质小程序服务或内容,但诱导用户分享小程序且不顾及被分享者的体验,会造成骚扰并引发用户的反感及投诉,不利于营销活动生态长期发展。 [视频] 为协助微信小程序开发者们对“滥用分享”行为的理解,小编整理了如下关于“滥用分享”行为的常见问题供大家参考: 一、常见的“滥用分享”行为有哪些? 1、 强制滥用分享 小程序强制诱导用户分享后才能继续下一步操作。包括但不限于分享后方可解锁功能或能力,分享后方可查阅、下载图片或视频等。 违规案例:如下图测试类小程序,用户的测试结果需分享至微信群才能查看,开发者利用用户的好奇心强制诱导用户分享小程序内容,该分享方式既影响分享者体验又对被分享者造成骚扰。 [图片] 查看更多违规案例 2、无深度互动利益诱导分享 小程序分享后无好友互动即可获取利益,包括但不限于现金、礼品、积分等。“互动”是指被分享者需明确参与活动或业务流程,即在理解被分享内容基础上,主动进入相关活动或业务流程页面,完成点击等相关操作。 无互动分享获利违规案例:如下答题类小程序,无互动分享小程序内容即可获得一次续命机会,续命成功后,可继续答题。 [图片] 查看更多违规案例 浅度互动分享违规案例:如下小程序,当被分享者收到分享卡片页,访问小程序后分享者即可获利。但其实对于大部分被分享者来说,往往不清楚当前活动的内容,以致于给被分享者造成不好的体验。 [图片] gif动图示例 查看更多违规案例 平台提倡运营者在设计活动时,能充分考虑被分享者的感受,分享互动过程中需要明确告知预期及具有选择权。故平台通过“互动”规则, 指引开发者合理设计分享对目标用户有价值意义的服务或内容,减少骚扰,提升体验,与运营者共建活动生态。 3、暗示性诱导分享 小程序内存在通过分享按钮的红点提示、图示图标、浮层等方式,暗示用户进行点击分享。 违规案例:如下图短视频类小程序,在分享按钮上增加红点提示,误导用户点击分享。平台希望用户能因为小程序的优质内容主动进行分享,而不是小程序内的误导提示或利益诱导分享。 [图片] 查看更多违规案例 4、虚拟业务中被分享者可获利的拉新玩法类型 小程序内涉及虚拟业务利诱被分享者,以此达到拉新目的。 违规案例:如下图类小程序通过虚拟业务,利益诱导被分享者进入小程序。由于苹果相关规范,小程序内暂不支持虚拟业务利益诱导被分享者。 [图片] 5、未经允许披露好友关系链及用户隐私 小程序通过诱导分享助力砍价类活动,未经用户授权公开展示用户昵称/昵称头像,存在不当使用用户个人信息的违规行为。 违规案例:如下图售票类小程序购票加速活动,在用户没有经过二次确认的情况下,公开展示用户的微信昵称和头像。 [图片] 6、分享转化率低等过度营销分享违规类型 小程序存在过度营销分享行为:如果同一主体、关联主体或者同一帐号、关联帐号或平台认为有联合行动的多个帐号下的小程序分享量过大且分享转化率过低,将会被认为可能存在过度营销分享行为,相关小程序的跳转至APP的能力、分享能力等将会被自动限制,同时小程序关联的微信开放平台开发者帐号分享小程序能力也将被自动限制。 违规案例:如下小程序通过做任务领金币的形式,诱导用户分享小程序至微信好友、登录app等任务即可获得利益,诱导用户盲目地分享小程序内容,忽略小程序的实际分享转化率,并对被分享者造成骚扰。 [图片] 查看更多违规案例 7、诱导分享至朋友圈违规类型 小程序通过利益诱导分享至朋友圈,包括但不限于:现金奖励、实物奖品、虚拟奖品(红包、优惠券、代金券、积分、话费、流量、信息等)。 违规案例:如下小程序诱导用户在朋友圈炫图即可增加10能量,存在利益诱导分享至朋友圈行为。 [图片] 查看更多违规案例 二、作为微信小程序开发者,关于“滥用分享”要注意什么? 平台建议有以上“滥用分享”行为的小程序,尽快自查并整改。微信小程序完整的分享规则可具体参考《微信小程序平台运营规范》5.1 三、如果违规了,平台会怎样处理违规小程序? 1、对于恶意对抗类小程序(批量性专搞强制诱导):一经发现平台将以永久封号处理并拉黑主体。 2、对于有滥用分享的小程序:首次发现平台将下发站内信,给予3天整改时间,到期未完成整改平台将视情节严重程度对帐号进行分享能力封禁,下架直至封号处理。 四、如果被处罚了,开发者应该怎么办? 若收到平台下发的站内信,请开发者们重视,并在限期内完成整改后发起申诉。 我们希望开发者“节制”、合理地使用分享能力,优化用户体验,让用户形成自发传播的习惯。
2020-05-14 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - php获取微信公众号评论留言数据
官方文档地址:查看指定文章的评论数据(新增接口) https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1494572718_WzHIY 使用说明:这是我平时用于留言随机抽奖的部分代码,主要可以将评论的openid和留言内容等信息写入数据,供后续抽奖及下发红包使用。 1.获取access token 这部分不做讲解,参照我们之前发布的文章。 2.调用接口 [代码]$msg_data_id[代码][代码]=[代码][代码]$_GET[代码][代码][[代码][代码]'id'[代码][代码]];[代码][代码] [代码] [代码] [代码][代码]$page[代码][代码]=[代码][代码]$_GET[代码][代码][[代码][代码]'page'[代码][代码]];[代码][代码] [代码][代码]$page3[代码][代码]=[代码][代码]$page[代码][代码]+1;[代码][代码] [代码] [代码] [代码][代码]if[代码][代码]([代码][代码]$page[代码][代码]==1){[代码][代码] [代码] [代码] [代码][代码]$page2[代码][代码]=[代码][代码]'0'[代码][代码];[代码][代码] [代码] [代码] [代码][代码]}[代码][代码]else[代码][代码]{[代码][代码] [代码] [代码] [代码] [代码] [代码][代码]$page2[代码][代码]=[代码][代码]$page[代码][代码]*1;[代码][代码] [代码][代码]}[代码][代码] [代码] [代码]$data[代码] [代码]= '{[代码][代码]"msg_data_id"[代码][代码]:[代码][代码]"'.$msg_data_id.'"[代码][代码],[代码][代码]"index"[代码][代码]:[代码][代码]"0"[代码][代码],[代码][代码]"begin"[代码][代码]:[代码][代码]"'.$page2.'"[代码][代码],[代码][代码]"count"[代码][代码]:[代码][代码]"1"[代码][代码],[代码][代码]"type"[代码][代码]:[代码][代码]"0"[代码][代码]}';[代码][代码]$url[代码] [代码]= [代码][代码]"https://api.weixin.qq.com/cgi-bin/comment/list?access_token=$access_token"[代码][代码];[代码][代码]$result[代码] [代码]= https_request([代码][代码]$url[代码][代码], [代码][代码]$data[代码][代码]);[代码][代码] [代码] [代码]//var_dump($result);[代码][代码] [代码] [代码]//echo $result;[代码][代码] [代码] [代码]$obj[代码][代码]=json_decode([代码][代码]$result[代码][代码]);[代码][代码] [代码] [代码] [代码][代码]$data[代码][代码]=[代码][代码]$obj[代码][代码]->comment;[代码][代码] [代码][代码]$i[代码][代码]=0;[代码][代码] [代码][代码]foreach[代码] [代码]( [代码][代码]$data[代码] [代码]as[代码] [代码]$unit[代码] [代码])[代码][代码] [代码][代码]{[代码][代码] [代码][代码]$i[代码][代码]++;[代码][代码] [代码][代码]$arr[代码][代码][[代码][代码]$i[代码][代码]][[代码][代码]'user_comment_id'[代码][代码]]=[代码][代码]$unit[代码][代码]->user_comment_id;[代码][代码] [代码][代码]$arr[代码][代码][[代码][代码]$i[代码][代码]][[代码][代码]'content'[代码][代码]]=[代码][代码]$unit[代码][代码]->content;[代码][代码] [代码][代码]$arr[代码][代码][[代码][代码]$i[代码][代码]][[代码][代码]'openid'[代码][代码]]=[代码][代码]$unit[代码][代码]->openid;[代码][代码] [代码] [代码] [代码][代码]// echo $arr[$i]['openid'].'[代码][代码]';[代码][代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码] [代码][代码]}[代码][代码] [代码] [代码] [代码] [代码]function[代码] [代码]https_request([代码][代码]$url[代码][代码], [代码][代码]$data[代码] [代码]= null)[代码][代码]{[代码][代码] [代码][代码]$curl[代码] [代码]= curl_init();[代码][代码] [代码][代码]curl_setopt([代码][代码]$curl[代码][代码], CURLOPT_URL, [代码][代码]$url[代码][代码]);[代码][代码] [代码][代码]curl_setopt([代码][代码]$curl[代码][代码], CURLOPT_SSL_VERIFYPEER, FALSE);[代码][代码] [代码][代码]curl_setopt([代码][代码]$curl[代码][代码], CURLOPT_SSL_VERIFYHOST, FALSE);[代码][代码] [代码][代码]curl_setopt([代码][代码]$curl[代码][代码], CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);[代码][代码] [代码][代码]if[代码] [代码](![代码][代码]empty[代码][代码]([代码][代码]$data[代码][代码])){[代码][代码] [代码][代码]curl_setopt([代码][代码]$curl[代码][代码], CURLOPT_POST, 1);[代码][代码] [代码][代码]curl_setopt([代码][代码]$curl[代码][代码], CURLOPT_POSTFIELDS, [代码][代码]$data[代码][代码]);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]curl_setopt([代码][代码]$curl[代码][代码], CURLOPT_RETURNTRANSFER, 1);[代码][代码] [代码][代码]$output[代码] [代码]= curl_exec([代码][代码]$curl[代码][代码]);[代码][代码] [代码][代码]curl_close([代码][代码]$curl[代码][代码]);[代码][代码] [代码][代码]return[代码] [代码]$output[代码][代码];[代码][代码]}[代码]
2019-12-02 - Android真机不支持env(safe-area-inset-bottom)?
该变量是IOS 系统内核提供的,在IOS上正常使用;而安卓和开发工具上用的是 Chromium 内核,没有这个变量,所以不支持
2019-10-17 - 使用 MobX 来管理小程序的跨页面数据
在小程序中,常常有些数据需要在几个页面或组件中共享。对于这样的数据,在 web 开发中,有些朋友使用过 redux 、 vuex 之类的 状态管理 框架。在小程序开发中,也有不少朋友喜欢用 MobX ,说明这类框架在实际开发中非常实用。 小程序团队近期也开源了 MobX 的辅助模块,使用 MobX 也更加方便。那么,在这篇文章中就来介绍一下 MobX 在小程序中的一个简单用例! 在小程序中引入 MobX 在小程序项目中,可以通过 npm 的方式引入 MobX 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 引入 MobX : [代码]npm install --save mobx-miniprogram mobx-miniprogram-bindings [代码] (这里用到了 mobx-miniprogram-bindings 模块,模块说明在这里: https://developers.weixin.qq.com/miniprogram/dev/extended/functional/mobx.html 。) npm 命令执行完后,记得在开发者工具的项目中点一下菜单栏中的 [代码]工具[代码] - [代码]构建 npm[代码] 。 MobX 有什么用呢? 试想这样一个场景:制作一个天气预报资讯小程序,首页是列表,点击列表中的项目可以进入到详情页。 首页如下: [图片] 详情页如下: [图片] 每次进入首页时,需要使用 [代码]wx.request[代码] 获取天气列表数据,之后将数据使用 setData 应用到界面上。进入详情页之后,再次获取指定日期的天气详情数据,展示在详情页中。 这样做的坏处是,进入了详情页之后需要再次通过网络获取一次数据,等待网络返回后才能将数据展示出来。 事实上,可以在首页获取天气列表数据时,就一并将所有的天气详情数据一同获取回来,存放在一个 数据仓库 中,需要的时候从仓库中取出来就可以了。这样,只需要进入首页时获取一次网络数据就可以了。 MobX 可以帮助我们很方便地建立数据仓库。接下来就讲解一下具体怎么建立和使用 MobX 数据仓库。 建立数据仓库 数据仓库通常专门写在一个独立的 js 文件中。 [代码]import { observable, action } from 'mobx-miniprogram' // 数据仓库 export const store = observable({ list: [], // 天气数据(包含列表和详情) // 设置天气列表,从网络上获取到数据之后调用 setList: action(function (list) { this.list = list }), }) [代码] 在上面数据仓库中,包含有数据 [代码]list[代码] (即天气数据),还包括了一个名为 [代码]setList[代码] 的 action ,用于更改数据仓库中的数据。 在首页中使用数据仓库 如果需要在页面中使用数据仓库里的数据,需要调用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中,然后就可以在页面中直接使用仓库数据了。 [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad() { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 actions: ['setList'], // 将 this.setList 绑定为仓库中的 setList action }) // 从服务器端读取数据 wx.showLoading() wx.request({ // 请求网络数据 // ... success: (data) => { wx.hideLoading() // 调用 setList action ,将数据写入 store this.setList(data) } }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,可以在 wxml 中直接使用 list : [代码]<view class="item" wx:for="{{list}}" wx:key="date" data-index="{{index}}"> <!-- 这里可以使用 list 中的数据了! --> <view class="title">{{item.date}} {{item.summary}}</view> <view class="abstract">{{item.temperature}}</view> </view> [代码] 在详情页中使用数据仓库 在详情页中,同样可以使用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中: [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad(args) { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 }) // 页面参数 `index` 表示要展示哪一条天气详情数据,将它用 setData 设置到界面上 this.setData({ index: args.index }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,这个页面 wxml 中也可以直接使用 list : [代码]<view class="title">{{list[index].date}}</view> <view class="content">温度 {{list[index].temperature}}</view> <view class="content">天气 {{list[index].weather}}</view> <view class="content">空气质量 {{list[index].airQuality}}</view> <view class="content">{{list[index].details}}</view> [代码] 完整示例 完整例子可以在这个代码片段中体验: https://developers.weixin.qq.com/s/YhfvpxmN7HcV 这个就是 MobX 在小程序中最基础的玩法了。相关的 npm 模块文档可参考 mobx-miniprogram-bindings 和 mobx-miniprogram 。 MobX 在实际使用时还有很多好的实践经验,感兴趣的话,可以阅读一些其他相关的文章。
2019-11-01 - 使用WXS做搜索列表功能
使用WXS过滤功能做搜索列表功能 小程序搜索功能,通过关键字去搜索data里面的数组变量 list,如果根据输入框输入的文字实时搜索,频繁使用 this.setData 改变数据变量list ,这样多次调用 setData 去 改变原数据的方法,需要保存一份原数据和搜索数据,避免输入框文字框清空时,无法输出原数据,这种方法用到更多的变量,同时会降低小程序的体验度和增加性能消耗,而且额外保存一份原数据和搜索数据,处理起来也不方便。 在实践中,使用 wxs 的过滤功能可以更好的完成搜索功能需求。实现原理如下草图。 [图片] 示例代码: https://developers.weixin.qq.com/s/qJhhxJma7fcY <block wx:for="{{ list }}"> <block wx:if="{{ tools.search(index,inputVal) }}"> <navigator url="" class=“weui-cell” hover-class=“weui-cell_active”> <view class=“weui-cell__bd”> <view>索引{{index}}</view> </view> </navigator> </block> </block> [图片] [图片]
2019-11-08 - 用云开发快速制作精美互动打卡小程序丨实战
个人介绍 大家好,我是Zero,一名大四的前端开发爱好者,目前主要研究微信小程序和iOS开发。 这是第二次参加微信小程序应用开发赛,2018年我们设计了一款通过二维码寻找丢失物品的小程序《蝴蝶寻物》,获得了华北赛区三等奖。 今年,在小程序云开发功能的大力推广下,我决定采用云开发的方式,实现一个双人互动打卡互动的小程序《Mango Daily》(中文名称《芒果日常》)。(由于是个人开发者账号,所以暂时还未上架) [图片] 得力于云开发提供的API,本项目在较短的时间内就实现了比较理想的效果。 接下来,我想从本项目入手,讲讲我是如何依靠小程序 云开发平台将想法快速实现的。 1. 技术准备 在去年的项目中,我们采用ThinkPHP开发了一套API系统,其中需要实现小程序的授权登录,设置鉴权来保证数据安全等操作。整个过程只有我一名开发人员,所以大致就是“先搞定后端,其次搞定界面,最后进行联调”的一个过程。 后来在云 社区看到一篇文章:《1个开发如何撑起一个过亿用户的小程序》,觉得可以尝试一下新的开发方式。通读小程序云开发文档之后,发现并不需要学习新的技术就可以快速上手。 2. 开发 Mango Daily 使用的是小程序原生开发 云开发结合的方式进行开发的。 2.1 界面开发 界面没有使用第三方UI框架,而是自己将常用的模块封装成了组件。 [图片] 图中比较核心的模块包括 TabBar、Toast、Modal、Nav等。 [图片] 2.2 云开发 云开发包括云数据库,云函数和云存储。本项目中三个功能均使用到。 2.2.1 云数据库 云数据库是一个非关系型数据库,在实际开发中基本符合本项目的需求。部分表关联查询则是通过分步查询的方式代替。 云数据库已经实现了自动鉴权,可以保证数据的安全性。目前云数据库只支持以下几种权限: 所有用户可读,仅创建者可读写 仅创建者可读写 所有用户可读 所有用户不可读写 默认情况下是***仅创建者可读写***,所以在首次开发时,手动插入的测试数据并不一定可以在前端顺利读取,需要修改集合的权限。 云数据库的调用在前端代码中即可完成。但是从上面几种读写权限来看,并没有办法实现对另一个用户创建的数据进行修改或者删除的操作(当然这也是非常不可取的),于是云函数就派上用场了。 2.2.2 云函数 我理解的云函数,则是跑在云端的一个函数脚本文件。 在接触云开发之前,如果我们想要去调用微信公众平台提供的API(例如发起退款、发送模板消息等),则需要在后端代码去实现,然后只需要给前端返回一个JSON表示请求状态即可。或者想要去实现上述描述中,修改一条由他人创建的数据的功能时,都是有后端工程师去完成的。 在本次开发中,我深刻体会到了云函数的强大,以及微信公众平台工程师设计产品的严谨性。 Mango Daily用到了微信公众平台的模板消息功能,所以需要在合适的时机请求微信官方提供的API。 因为取消了后端的开发,所以一开始打算直接在小程序端去请求官方API。但是失败了。因为此请求涉及APPKEY等重要信息,禁止在小程序端代码中直接请求官方API。这样就可以通过云函数去代替先前的后端开发,最后将状态返回给小程序端即可。 例如想给新用户发送一条短信,以往的做法是客户端请求后端API,然后由后端完成发送短信操作。这里云函数就代替了后端开发。如果仅仅依靠小程序JS代码去发送短信,是非常不可取的。 另外,云函数对云数据库有更高的操作权限,所以想要修改、删除他人生成的数据时,云函数可以直接进行操作。 云函数还提供定时触发功能,不过在本项目中暂未涉及。 2.2.3 云存储 本次开发省去了使用其他服务商的存储服务,全部得力于云存储功能。云存储允许上传多种文件类型,像图片、音频等文件还可以直接在小程序端调用。这里我们使用云存储实现了文章插图的功能。 [图片] 2.3 优化 2.3.1 数据层封装 Mango Daily 数据操作进行了两次封装,一层是对云数据库API进行封装,第二层是每一个数据集合都对应一个Manager管理层。 以用户集合 User,Article 为例,项目中的结构如下: util |- db.js manager |- Article.js |- User.js db.js 是对云数据库API的封装,实现了增删查改等操作,以更新数据为例。 [代码]/** * 更新数据 */ const update = (collection, _id, data) => { return new Promise((resolve, reject) => { if (!exist(collection)) { reject(401, resCode[401]); } db.collection(collection).doc(_id).update({ data: data }).then(res => { resolve(res); }).catch((code, msg) => { reject(code, msg); }); }); } [代码] Article.js 是文章集合的管理类,同样实现了增删查改等操作,不过其是基于 db.js 进行扩展的。以更新文章操作为例: [代码]/** * 更新 */ const update = (_id, data) => { return new Promise((resolve, reject) => { db.update(collection, _id, data).then(res => { resolve(res); }).catch((code, msg) => { reject(db.errMsg); }); }); } [代码] 之所以封装两层,是想尽量减少Page对象中对云数据库的直接调用。这样在页面js文件中只需要调用某一个Manager提供的函数即可。 2.3.3 后台上传策略 Mango Daily还实现了发送模板消息的功能,这就涉及到了FromID的收集。目前FromID的收集大部分采用埋点的方式。 如果每次采集到新的FromID都直接上传到数据库存储,可能会造成网络资源的浪费,所以需要选择合适的时机上传数据。 在本项目中,每次采集到FromID,首先存到 globalData 中,当小程序进入后台状态时,再进行数据的上传。 app.js 中的实现: [代码]/** * 后台监听 */ onHide: function() { this.uploadFormID(); }, /** * 上传token */ uploadFormID: function() { let ids = this.globalData.formIds; if (ids.length == 0) { return ; } let formId = ids.pop(); this.push.upload(formId).then(_ => { console.log("上传formID:" , formId); this.uploadFormID(); }).catch(err => { console.log(err); }); }, [代码] 3. 维护 很遗憾,这一部分可能没有太多需要写的。 在18年的项目中,需要考虑数据库的维护问题。但是使用了云开发之后,Serverless的优点就表现出来了。我无须将太多的精力放在后端的维护上。 4. 总结 在本次项目开发中,我深刻体会到了云开发的便捷性。无须自己实现鉴权,对接第三方存储。数据方面,增删查改功能非常方便。云开发提供的种种便利,让我在有新创意的时候,优先选择小程序 云开发的方式去实现。 [代码]你好,你的小程序涉及用户自行生成内容的发布/分享/交流,属社交范畴,为个人主体小程序未开放类目,建议申请企业主体小程序 [代码] 另外,Mango Daily中的随笔功能属于用户自行生成内容功能,所以在上架的时候,个人开发者账号是不被允许的,所以在考虑上架产品的时候,请按照实际情况酌情考虑选择账号主体类型。 源码地址 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你想要了解更多关于云开发CloudBase相关的技术故事/技术实战经验,请扫码关注【腾讯云云开发】公众号~ [图片]
2019-11-11 - 腾讯云智服客服系统快速上手指南
一、注册登录 1、首次注册 进入腾讯云智服官网yzf.qq.com,点击首页“注册”按钮,使用微信进行扫码,同意腾讯云智服的登录授权。(注:此扫码微信号即超级管理员账号。) Step1:进入官网,点击注册微信扫码,在手机上点击同意授权。 Step2:填写企业相关信息,所有信息均可在后台【设置】→【企业设置】修改,完成后点击下一步。 (温馨提示:腾讯云智服承诺对您填写的信息绝对保密,未经用户许可腾讯不会向第三方公开、透露用户个人和公司信息。腾讯对相关信息采用专业加密存储与传输方式,保障您和企业信息的安全。) Step3:注册完成后,点击“开始体验”登录,开始设置您的专属客服平台。(每个微信号最多可注册 50 个云智服帐号,可轻松管理多个企业。) 2、登录 使用微信扫码即可登录腾讯云智服。 二、邀请成员加入 服务团队基本成员角色包括:管理员、客服专员、机器人训练师。所有受邀同事的角色和每个角色可用权限均可由管理员修改。 首先设置客服分组和该组客服最大接入用户数量,以保证服务质量。 设置入口:【设置】→【团队设置】→【客服管理】 新建自定义分组,设置分组的名称与最大接入数。 [图片] 客服分组设置完成后,就可以邀请您的同事加入了。 设置入口:【设置】→【客服管理】→【邀请成员】 [图片] 将“邀请成员”页面的二维码或链接发送给同事,让其填写信息后提交申请。 同事完成申请后,管理员进入【申请列表】中审核。选择该同事对应的角色(必选、多选)与组别。 若一开始邀请成员后未设置分组,该同事归入默认分组。可在默认分组中选择该同事,点击【编辑】修改成员姓名、组别和角色。 三、接入渠道 腾讯云智服支持接入渠道有微信渠道(公众号、小程序)和网站渠道,可在以上渠道接入人工服务与机器人服务。 3.1微信渠道接入 接入微信渠道后,可在公众号、小程序中便捷配置在线人工、机器人服务。 设置入口:【设置】→【渠道设置】→【微信渠道】 [图片] (1)公众号渠道接入 开始之前,您需要有已认证的公众号管理员账号。由于微信限制,仅支持微信认证公众号接入人工服务。详细接入设置 Step1 点击公众号授权,使用管理员微信扫码授权。 Step2 接入微信公众号渠道成功后,默认不接入机器人服务,接入默认分组人工服务,可在【设置机器人】、【设置人工】中修改。 Step3接入成功后可在公众号下留言,测试能否成功发送消息。 (2)小程序渠道接入 开始之前,您需要有小程序管理员账号,且小程序需有客服入口,客服入口需要在小程序内设置好,可参考公众号官方添加小程序按钮的指引。 Step1 点击小程序授权,使用小程序管理员个人微信扫码授权。 Step2 接入小程序渠道成功后,默认不接入机器人服务,接入默认分组人工服务,可在【设置机器人】、【设置人工】中修改。 Step3接入成功后可点击小程序客服入口留言,测试能否成功发送消息。 3.2网站渠道接入 网站提供两种部署方式:网站链接(URL链接)、内嵌插件(Web JS)。开发者通过地址调用或简短代码,可在网站中接入客服。网站详细接入设置 设置入口:【设置】→【渠道设置】→【网站设置】→【新增网站】 新建一个网站渠道,输入网站名称和使用场景。 两种方式接入网站渠道: (1)新建内嵌插件 在新建网站界面,点击【复制】内嵌插件代码粘贴到您的网站的</body>标签之前,即可完成部署。目前暂不支持内嵌插件自定义外观,可使用腾讯云智服内嵌插件默认样式、颜色外观及固定位置,支持自定义网站入口显示位置。 内嵌插件代码,示例如下: uid:为用户唯一标识,如没有可不填写 data:用户传递用户信息,最多支持五个,参数名分别为c1,c2,c3,c4,c5 selector: css选择器,用于替换默认的常驻客服入口 callback(type, data):回调函数,type表示事件类型,data表示事件相关数据 type支持的类型:newmsg有新消息,error腾讯云智服页面发生错误, close聊天窗口关闭 若您需要传送相关数据,可通过在链接后追加参数,包括用户id(uid)与自定义用户信息参数(c1、c2、c3、c4、c5)传递。 (2)新建网站链接 在新建网站界面,点击【复制】对应链接地址,用户点击该链接即可进入咨询。 若您需要传送相关数据,可通过在链接后追加参数,包括用户id(uid)与自定义用户信息参数(c1、c2、c3、c4、c5)传递。 部署聊天页链接,示例如下: [图片] 四、开始服务 进入【人工】,切换状态为“在线”后即可接入用户。(需确保当前客服所在组别和渠道设置的人工组别保持一致) 客服人员可使用PC工作台和小程序工作台服务用户。关注公众号:腾讯云智服,在底部菜单【工作台】进入小程序,即可接入用户开始服务。
2019-11-14 - 如何写出优雅的深复制
前言 无论在项目开发或者学习中,深拷贝已经是一个老生常谈的话题了,但是在实际中,如何优雅地写出深拷贝是我们值得思考的一个问题 内容 深拷贝 与 浅拷贝 深拷贝 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象 浅拷贝 如果一个对象有着原始对象属性值的一份精确拷贝。如果这个对象属性是基本类型,那么拷贝的就是基本类型的值,如果属性是引用类型,那么拷贝的就是内存地址。 区别 其实深拷贝和浅拷贝的主要区别就是其在内存中的存储类型不同。 堆和栈都是内存中划分出来用来存储的区域。 栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。 对于js中的基本数据类型,他们的值被以键值对的形式保存在栈中。 [图片] 与基本类型不同的是,引用类型的值被保存在堆内存中,对象的引用被保存在栈内存中,而且我们不可以直接访问堆内存,只能访问栈内存。所以我们操作引用类型时实际操作的是对象的引用。 [图片] 了解相关的基础知识后,我们话不多说,直奔主图 简单版 在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。 [代码] JSON.parse(JSON.stringify()); [代码] 这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。 基础版 如果是浅拷贝对象时,我们可以很容易的就写出代码 [代码]function clone(obj) { let cloneObj = {}; for (const key in obj) { cloneObj [key] = obj[key]; } return cloneObj ; }; [代码] 对于浅拷贝而言,只需要简单地将对象的每一个属性进行复制即可。然而,对于深拷贝而言,我们拷贝对象的话是需要知道目标对象的属性是否是基本数据类型以及对象的深度。这些我们可以通过递归的方法来实现。 [代码]/* * 作用: 深复制对象属性 */ function clone(obj) { if (typeof obj=== 'object') { let cloneObj = {}; for (const key in obj) { cloneObj [key] = clone(obj[key]); } return cloneObj ; } else { return obj; } }; [代码] 这时候,我们实现了一个基础的深复制,那么问题来了,对于数组,该如何实现呢? 加深版 在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了: [代码]function clone(obj) { if (typeof obj=== 'object') { let cloneObj = Array.isArray(obj) ? [] : {}; for (const key in target) { cloneObj[key] = clone(obj[key]); } return cloneObj; } else { return obj; } }; [代码] 在判断目标对象是引用类型时,则通过Array.isArray方法判断是否是数组,如果是则赋值为空数组,否则赋值为空对象。 循环引用 当对象子属性的值是父对象时,则递归的方法将不再适用。原因就是对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况,这将导致递归进入死循环导致栈内存溢出。 为了解决这个问题,我们可以通过WeakMap这种数据结构来实现。首先我们通过WeakMap来存储当前对象和拷贝对象的对应关系。当需要拷贝当前对象时,先去WeakMap中找,有则返回,无则set。 [代码]WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。 [代码] [代码]function clone(target, weakMap = new WeakMap()) { if (typeof target === 'object') { let cloneTarget = Array.isArray(target) ? [] : {}; if (weakMap.get(target)) { return weakMap.get(target); } weakMap.set(target, cloneTarget); for (const key in target) { cloneTarget[key] = clone(target[key], weakMap); } return cloneTarget; } else { return target; } }; [代码] 考虑一下性能,while循环的性能要比for on的要好,因此改造一下 [代码]function forEach(array, iteratee) { let index = 0; while (index < array.length) { iteratee(index, array[index]); index++; } return array; } function clone(target, weakMap = new WeakMap()) { if (typeof target === 'object') { const isArray = Array.isArray(target); let cloneTarget = Array.isArray(target) ? [] : {}; if (weakMap.get(target)) { return weakMap.get(target); } weakMap.set(target, cloneTarget); const keyList = isArray ? undefined : Object.keys(target); forEach( keyList || target , function(key, value){ if(keyList){ // 对象而言,其值才是他的key key = value; } cloneTarget[key] = clone(target[key], weakMap); }) return cloneTarget; } else { return target; } }; [代码] 其他数据类型 综上我们考虑到的只是普通的object以及array俩种数据类型,但引用类型并不单只有这俩个,还有很多。。。 判断是否是引用类型 在判断是否是引用类型时,我们可以通过typeof字段,此时我们还需要考虑typeof可能返回’function’字符串以及对象有可能是null的情况,因此可写出判断函数如下所示 [代码]function isObject(target) { const type = typeof target; return target !== null && (type === 'object' || type === 'function'); } [代码] 获取数据类型 我们可以使用toString来获取准确的引用类型: [代码]function getType(target) { return Object.prototype.toString.call(target); } [代码] [图片] [图片] 根据上面的返回的字符串,我们可以抽离出一些常用的数据类型以便后面使用: [代码]const mapTag = '[object Map]'; const setTag = '[object Set]'; const arrayTag = '[object Array]'; const objectTag = '[object Object]'; const boolTag = '[object Boolean]'; const dateTag = '[object Date]'; const errorTag = '[object Error]'; const numberTag = '[object Number]'; const regexpTag = '[object RegExp]'; const stringTag = '[object String]'; const symbolTag = '[object Symbol]'; [代码] 在上面的集中类型中,我们简单将他们分为两类: 可以继续遍历的类型 不可以继续遍历的类型 可继续遍历的类型 上面我们已经考虑的object、array都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有Map,Set等都是可以继续遍历的类型 这时候我们需要一个通过对象原型上的constructor属性获取构造函数,从而对要复制的对象进行初始化。方法如下: [代码]function getInit(target) { const Ctor = target.constructor; return new Ctor(); } [代码] 下面我们改写一下clone函数,让他兼容map,set。 [代码]const mapTag = '[object Map]'; const setTag = '[object Set]'; const arrayTag = '[object Array]'; const objectTag = '[object Object]'; const deepTag = [mapTag, setTag, arrayTag, objectTag]; function clone(target, weakMap = new WeakMap()) { // 克隆基本数据类型 if (!isObject(target)) { return target; } // 初始化 const type = getType(target); let cloneTarget; if (deepTag.includes(type)) { cloneTarget = getInit(target, type); } // 防止循环引用 if (weakMap.get(target)) { return weakMap.get(target); } weakMap.set(target, cloneTarget); // 克隆set if (type === setTag) { target.forEach(value => { cloneTarget.add(clone(value,weakMap)); }); return cloneTarget; } // 克隆map if (type === mapTag) { target.forEach((value, key) => { cloneTarget.set(key, clone(value,weakMap)); }); return cloneTarget; } // 克隆对象和数组 const keys = type === arrayTag ? undefined : Object.keys(target); forEach(keys || target, (value, key) => { if (keys) { key = value; } cloneTarget[key] = clone(target[key], weakMap); }); return cloneTarget; } [代码] 不可继续遍历的类型 其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理: Bool、Number、String、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象: [代码]function cloneOtherType(targe, type) { const Ctor = targe.constructor; switch (type) { case boolTag: case numberTag: case stringTag: case errorTag: case dateTag: return new Ctor(targe); case regexpTag: return cloneReg(targe); case symbolTag: return cloneSymbol(targe); default: return null; } } function cloneSymbol(targe) { return Object(Symbol.prototype.valueOf.call(targe)); } //克隆正则 function cloneReg(targe) { const reFlags = /\w*$/; const result = new targe.constructor(targe.source, reFlags.exec(targe)); result.lastIndex = targe.lastIndex; return result; } [代码] 克隆函数 对于克隆函数,实际上是没有太大的意义。。。因为不同的俩个对象使用同一个函数是没有任何问题的。 首先,我们可以通过prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的。 我们可以直接使用eval和函数字符串来重新生成一个箭头函数,注意这种方法是不适用于普通函数的。 [代码]function cloneFunction(func) { const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); if (func.prototype) { // 普通函数 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if (body) { if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } else { return null; } } else { // 箭头函数 return eval(funcString); } } [代码] 总结 综上,我们围绕深复制进行了解析,了解到了应该如何写出优雅的深复制,在实际开发中,可以根据不同的场景,合理的选择如何书写深复制。
2019-11-15 - 在线答题小程序相关分享
概述 本文主要用户整理我在社区发布的关于在线答题小程序的像个文章 小程序如何生成邀请码? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000ec4460402883a0c6a64e645b813 备注 如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充如有其它需要可以联系,我来补充补充
2020-06-11 - 微信小程序实现多图片的上传。
1.在微信小程序上,实现多图片的上传在wxml文件中实现页面布局。 [图片] [代码]<view style="width:100%;float:left;margin-top:25rpx;margin-left:40rpx;padding-bottom:120rpx;" class="weui-uploader__bd"> <view class="weui-uploader__files"> <block wx:for="{{pics}}" wx:for-item="image" wx:key="item"> <view class="weui-uploader__file"> <image src="../../../images/warehouse/scanCode_delete.png" wx:if="{{image!=''}}" bindtap="img_delete" class="image_view_css" data-index="{{index}}" data-sign="{{disabl}}"></image> <image class="weui-uploader__img" wx:if="{{image!=''}}" src="{{filters.updateImag(httpURL,deURL,image)}}" bindtap="previewImage"></image> </view> </block> </view> <view class="weui-uploader__input-box {{isShow?'true':'hideTrue'}}"> <view class="weui-uploader__input" bindtap="chooseImage" data-sign="{{disabl}}"></view> </view> </view> [代码] 2.这里页面样式的布局实现。 [图片] [图片] [图片] [代码].weui-uploader__bd { margin-bottom: -4px; margin-right: -9px; overflow: hidden; } .weui-uploader__file { float: left; margin-right: 15px; margin-bottom: 9px; } .image_view_css{ width:60rpx; height: 60rpx; position: absolute; z-index: 5; margin-left: 65px; margin-top: -25rpx } .weui-uploader__img { display: block; width: 79px; height: 79px; } .weui-uploader__input-box { float: left; position: relative; margin-right: 9px; margin-bottom: 9px; width: 77px; height: 77px; border: 1px solid #d9d9d9; } .weui-uploader__input-box:before, .weui-uploader__input-box:after { content: " "; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #d9d9d9; } .weui-uploader__input-box:before { width: 2px; height: 39.5px; } .weui-uploader__input-box:after { width: 39.5px; height: 2px; } .weui-uploader__input { position: absolute; z-index: 1; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; } .hideTrue { display: none } [代码] 3.在js层的实现中在在data变量中先定义好 pics: [], count: [1, 2, 3], isShow:true, 变量 1)onload的方法中首先要加载 [代码]isShow: (options.isShow == "true" ? true : false) [代码] 2)页面布局形成的为: [图片] 3)在实现图片的选择上传中点击空白的页面增加图片调用执行方法 [图片] 在 图片的上传后success成功返回值中在开发工具上是以http://开头返回图片文路径,手机上返回的是wxfile:// 开头的 [图片] [图片] 4)而实现图片的预览调用的方法是: [代码] previewImage: function (e) { var current = e.target.dataset.src wx.previewImage({ current: current, urls: this.data.pics }) }, [代码] 实现将图片上传到后台的服务器上,在js中调用的方法 //对保存了图片数组pics做一个for循环。 [代码] wx.uploadFile({ url: url + "/WxController/upload", filePath: pics[0], //这里微信的调用方法只能每次上传一张图片所以可以再调用方法前做对数组pics的一个迭代循环。 name: 'file', formData: { coNo: app.globalData.staff.coNo, lastFolder: 'ProdPaperWx' }, dataType: 'json', success: function (res) { if (res.statusCode == 200) { console.log("成功返回值:",res) } } }) [代码] [图片] 后台接口方法/WxController/upload 用java执行 [代码] public Map<String,Object> upload(HttpServletRequest request, @RequestParam(value = "file", required = false) MultipartFile file) throws IOException { Map<String,Object> map=new HashMap<String, Object>(); String msg="",ret="success",path="",type=null; request.setCharacterEncoding("UTF-8"); String coNo=request.getParameter("coNo"); String lastFolder =request.getParameter("lastFolder"); if(!file.isEmpty()){ String fileName = file.getOriginalFilename(); type = fileName.indexOf(".") != -1 ? fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()) : null; if(type!=null){ if("PNG".equals(type.toUpperCase())||"JPG".equals(type.toUpperCase())) { // 项目在容器中实际发布运行的根路径 String realPath=BaseController.getCustFilePath()+"image/"+coNo+ "/" + lastFolder + "/"; //这图片存放的路径采用了在指定的方式,在BaseController的getCustFilePath()方法中指定好了 File myFilePath = new File(realPath); if (!myFilePath.exists()){//文件夹不存在,生成一个文件夹 myFilePath.mkdirs(); } // 自定义的文件名称 String trueFileName=coNo+String.valueOf(System.currentTimeMillis())+"."+type; // 设置存放图片文件的路径 path=realPath+trueFileName; file.transferTo(new File(path));ret="success";msg=""; }else { msg="不是我们想要的文件类型,请按要求重新上传!";ret="error";path=""; } }else{ msg="文件类型为空!";ret="error";path=""; } }else { msg="没有找到相对应的文件!";ret="error";path=""; } map.put("ret",ret); map.put("msg",msg); map.put("path",path); return map; } [代码] 这里是BaseController类内的一个指定图片存放路径的方法。 [代码]public class BaseController { //客户文件存放路径 public static String getCustFilePath(){ return "D:/lqb/imge/"; } } [代码]
2019-11-15 - 小程序模板消息能力调整通知
小程序模板消息能力在帮助小程序实现服务闭环的同时,也存在一些问题,如: 1. 部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰; 2. 模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求。 为提升小程序模板消息能力的使用体验,我们对模板消息的下发条件进行了调整,由用户自主订阅所需消息。 一次性订阅消息 一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 [图片] (一次性订阅示例) 长期性订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。 调整计划 小程序订阅消息接口上线后,原先的模板消息接口将停止使用,详情如下: 1. 开发者可登录小程序管理后台开启订阅消息功能,接口开发可参考文档:《小程序订阅消息》 2. 开发者使用订阅消息能力时,需遵循运营规范,不可用奖励或其它形式强制用户订阅,不可下发与用户预期不符或违反国家法律法规的内容。具体可参考文档:《小程序订阅消息接口运营规范》 3. 原有的小程序模板消息接口将于 2020 年 1 月 10 日下线,届时将无法使用此接口发送模板消息,请各位开发者注意及时调整接口。 微信团队 2019.10.12
2019-10-13 - 实战分享: 小程序云开发玩转订阅消息(一)
[图片] 微信官方为提升小程序模板消息能力的使用体验,对模板消息的下发条件进行了调整。原有的小程序模板消息接口于 2020 年 1 月 10 日下线,届时将无法使用旧的小程序模板消息接口发送模板消息,取而代之的是新的一次性订阅消息和长期订阅消息。 订阅消息给小程序开发者带来了更好的触达用户的能力,在具体实施过程中,开发者如何把模板消息换成新的订阅消息,是否需要购买服务器来实现服务器鉴权,怎样才能在用户订阅之后一段时间后,给用户发送长期或一次性订阅消息呢? 小程序·云开发最近支持了通过云调用免 access_token 发送订阅消息,还新增支持了在定时触发器中实现云调用,这些能力可以帮助开发者轻松玩转小程序订阅消息。 我们今天会利用小程序·云开发进行一个小程序中实现订阅开课提醒的实战,帮助大家了解如何基于小程序·云开发快速接入小程序订阅消息。 [图片]整体时序图[图片]开课提醒订阅消息时序图环境准备注册小程序帐号[1]开通云开发服务[2]获取订阅消息模板 ID在微信小程序管理后台中,新增一个订阅消息的模板,这里我们新增了一个开课提醒的模板。 [图片]新增模板引导用户订阅微信小程序提供了[代码]wx.requestSubscribeMessage[代码] 接口来发起申请订阅权限界面。 [图片]微信申请订阅权限界面在 "订阅开课提醒" 的按钮上绑定 tap 事件,事件处理器我们这里用的 [代码]onSubscribe[代码] index.wxml [代码]<button class="btn" data-item="{{ item }}" bindtap="onSubscribe" hover-class="btn-hover" > 订阅开课提醒 </button> [代码]在 [代码]onSubscribe[代码] 函数内,我们会调用微信 API [代码]wx.requestSubscribeMessage[代码] 申请发送订阅消息权限,当用户在弹窗同意订阅之后,我们会收到 [代码]success[代码] 回调,将订阅的课程信息调用云函数 [代码]subscribe[代码] 存入云开发数据库,云函数 [代码]subscribe[代码] 的实现在下文会讲。 index.js [代码]onSubscribe: function(e) { // 获取课程信息 const item = e.currentTarget.dataset.item; // 调用微信 API 申请发送订阅消息 wx.requestSubscribeMessage({ // 传入订阅消息的模板id,模板 id 可在小程序管理后台申请 tmplIds: [lessonTmplId], success(res) { // 申请订阅成功 if (res.errMsg === 'requestSubscribeMessage:ok') { // 这里将订阅的课程信息调用云函数存入云开发数据 wx.cloud .callFunction({ name: 'subscribe', data: { data: item, templateId: lessonTmplId, }, }) .then(() => { wx.showToast({ title: '订阅成功', icon: 'success', duration: 2000, }); }) .catch(() => { wx.showToast({ title: '订阅失败', icon: 'success', duration: 2000, }); }); } }, }); [代码][代码] },[代码] 文章字数超出 50000 字,后半部分链接 《实战分享: 小程序云开发玩转订阅消息(二)》
2019-10-23 - 小程序订阅消息开发指南
2019年10月12日微信开放了小程序订阅消息的功能。按官方的说法,目前的模板消息在实现小程序服务闭环上存在缺陷: 1. 部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰;2. 模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求模板消息确实存在上述的硬伤,不利于小程序的用户留存和用户体验。为了解决这些问题,微信官方推出了用户订阅消息功能。我在微慕专业版上加了订阅消息的功能,并验证了这个功能。这个功能是否能都达到官方的预期,这个我感觉不那么乐观。这里我先说我的感受:目前的订阅消息还不完善,后续还有很大的优化空间。 目前,官方只开放了“一次性订阅消息”,尚未开放“长期性订阅消息”,因此我只尝试了“一次性订阅消息”。 一次性订阅消息:用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 订阅消息推送位置:服务通知 订阅消息下发条件:用户自主订阅 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面 以下我简单说明订阅消息的开发过程和使用体验。 一.订阅消息的开发1.获取订阅消息的模板ID 在微信小程序的管理后台,在左侧“功能”菜单,选择“订阅消息”,然后点击“添加” [图片] 然后选择你需要的消息模板,并配置关键词。 [图片] 配置完成后,如下图所示。 [图片] 值得关注的是,在配置好的模板详情页面里的“详细内容”很重要,这个就是开发订阅消息时需要遵循的消息格式,这个格式和模板消息有细微的差别 根据微慕小程序的需要,我选用了“新的评论提醒”和“内容更新提醒”这两个消息模版。前者用于提醒发表话题或文章的作者,有新的话题或文章评论,增强作者与读者之间的交流互动;后者是提醒订阅用户,小程序有新的文章发布,引导用户回归小程序。 订阅消息申请模板的时候,需要选择所属类目,只能选择当前小程序相关的类目模板,对于模板消息不需要选择对应类目。如果删除小程序类目,就会把订阅消息模板一起删除。因此删除类目要小心谨慎。 [图片] 2.触发用户订阅,获取下发的权限 触发用户订阅,微信小程序提供的api是: [代码]wx.requestSubscribeMessage[代码],用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 注意:微信小程序开发工具尚不支持此功能,在开发工具触发订阅的api,会提示: requestSubscribeMessage:fail 开发者工具暂时不支持此 API 调试,请使用真机进行开发 调用api的代码示例如下: [代码]wx.requestSubscribeMessage({[代码] [代码]tmplIds: ["模板A","模板B"],[代码] [代码]success: function (res) {[代码] [代码]//成功[代码] [代码]},[代码] [代码]fail(err) {[代码] [代码]//失败[代码] [代码]console.error(err);[代码] [代码]}[代码] [代码]})[代码] wx.requestSubscribeMessage(Object object) 的回调函数[代码]object.success [代码]参数有两个:errMsg和TEMPLATE_ID; 接口调用成功时errMsg值为’requestSubscribeMessage:ok’。TEMPLATE_ID是动态的键,即模板id,值包括’accept’、’reject’、’ban’。’accept’表示用户同意订阅该条id对应的模板消息,’reject’表示用户拒绝订阅该条id对应的模板消息,’ban’表示已被后台封禁。例如 { errMsg: “requestSubscribeMessage:ok”, zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: “accept”} 表示用户同意订阅zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE这条消息。 个人觉得这个动态键不是特别合理,代码处理起来有些麻烦,如果改成静态键的json格式比较方便处理,例如: [代码]{[代码] [代码] errMsg:"requestSubscribeMessage:ok",[代码] [代码] result: [[代码] [代码] { templateId:"zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE",[代码] [代码]status:"accept"[代码] [代码]}[代码] [代码] ][代码] [代码]}[代码] 在手机上调用此api方法会调出订阅消息的界面,如下图所示: [图片] 关于这个订阅消息的授权有几点要注意: 1) 在确认提示框里,如果用户选择“取消”表示拒绝(取消)订阅消息,选择“允许”表示用户订阅一次消息。 2) 如果用户不勾选“总是保持以上选择,不再询问”,那么每次用户触发都会弹出提示框。 3) 如果用户勾选“总是保持以上选择,不再询问”,那么将再也不会唤起这个对话框。同时,如果选择“取消”,那么以后每次调用这个api的时候,都会自动拒绝;如果选择“允许”,那么以后每次调用此api,都会自动允许授权。 目前小程序没有提供获取用户是否授权订阅消息的方法。通过wx.openSetting 方法无法获取用户是否授权消息订阅的信息,scope 列表没有订阅消息的内容。 如果想从自动拒绝转换到自动自动运行,需要打开小程序的设置去配置。设置方法:点击小程序右上角的三个点,打开如下对话框 [图片] 然后选择“设置”,在设置项里选择“订阅消息” [图片] [图片] 4)对于同一种消息,用户可以订阅多次,订阅多少次,就会收到多少次订阅消息,这个订阅次数是否有上限,官方没有说明,初步判断是不限的。但是,微信不会提供订阅的次数,因此需要在小程序的后端服务里存储用户订阅的次数。因此,我在微慕小程序专业版里,提供了一个给用户多次订阅的设置,并记录用户订阅的次数。 [图片] 如果用户需要某个消息服务,可以订阅多次,当然也可以在点击“订阅”的对话框里选择“取消”,“取消”一次也就减少一次订阅。 5)对于支付的场景,也需要用户确认是否订阅,这个我觉得不合理,支付后给用户一个订单推送消息应该是刚性需求,不需要再询问一遍用户是否订阅。 2.调用接口下发订阅消息 订阅消息下发的接口是小程序后台服务端调用:subscribeMessage.send,此方法类似下发模板消息的方法,详细调用说明见参考官方的链接: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html 订阅消息的下发接口方法和模板消息稍有不同, 模板消息的json格式如下 [代码]"data": {[代码] [代码]"keyword1": {[代码] [代码]"value": "内容1",[代码] [代码]"color": "#000"[代码] [代码]},[代码] [代码]"keyword2": {[代码] [代码]"value": "内容2",[代码] [代码]"color": "#000"[代码] [代码]}[代码] [代码]}[代码] 而订阅消息的json格式如下: [代码]"data": {[代码] [代码]"thing1": {[代码] [代码]"value": "内容"[代码] [代码]},[代码] [代码]"number2": {[代码] [代码]"value": 20[代码] [代码]}[代码] 订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,如果不按类型提交,会导致发送失败。同时如果是文本型的内容,字数也有限制,超过限制也会发送失败,但具体字数是多少,官方没有给出,同时中英文混合计算的长度也有差异,据我目前测试25个中文字符是可以的。希望官方能给出具体的字符长度限制的明确数字。 如果调用下发的次数大于用户的订阅次数,调用接口下发订阅消息会返回失败。报如下错误 [图片] 二.订阅消息使用心得1.订阅消息虽然把订阅的授权的交给了用户,但是也增加了用户使用难度,同时,一次性订阅只能收到一次,操作起来比较繁琐,如果不是刚需用户可能会首次就拒绝了这个服务,要想重新获取授权,需要用户自己打开小程序设置里去配置,颇为麻烦,小程序没有提供更简便的方法去唤起。 2.小程序的服务商为了获得更多给用户发送订阅消息的次数,肯定会想方设法去埋点引诱用户去点击订阅,这种诱导估计也是违规。 3.用户使用门槛和学习比较高,比如某个预约的服务,原来的场景是用户只要有提交表单,小程序就可以推送消息给用户,但是现在需要用户主动去订阅,无形中多了一步,如果用户不熟悉订阅消息或者直接点了“取消”,小程序就没法通知到用户了,用户可能因此错失服务,对商家和用户都是损失。 4.微信小程序将采用订阅消息,并逐步取消模板消息,虽然微信官方试图在方便用户和不打扰用户这两种选择里去寻求平衡,但订阅消息目前的模式恐怕无法达到这个期望,至少在我看来,无论对小程序的服务商,还是小程序的用户,都感到不方便。 update:2020年5月18日,日前订阅消息已经支持微信小程序开发工具。
2020-05-18 - 小程序生成分享图片方案分享
小程序生成图片分享朋友圈 小程序开发者都希望自己的小程序得以广泛传播,因为不少小程序都设计了很多转发激励行为,但分享小程序到朋友圈(或其他外部平台)一直是一个难题。一个常见的方案就是生成分享海报、分享图片。但生成分享图片在技术上却也是一个难题。 技术选型 目前常用技术方案基本分为三种: 使用 canvas 绘图并生成 使用后端绘图库进行绘制,返回给小程序端 使用服务端开一个浏览器进行 HTML 渲染,并截图返回给小程序端 第一种方案:要求较高,canvas 和纯 html 布局相去甚远,零基础学习成本较高,而且在不同的微信浏览器中效果不可预期,想短时间内做出精美可控的生成图片不容易。实操的时候发现了一个非常麻烦的事情:网络图片或者 base64 图片都无法直接在 canvas 里渲染显示,要先下载好传进去。 第二种方案:后端库可以完成较为简单的需求,但字体加载、阴影、圆角、透明等方案效果需要精调,如果文字需要截断或动态伸缩长度时并不容易处理。图片的截取和伸缩自适应也不灵活。而且选用这种方案相当于需要把 UI 布局的工作丢给后端工程师去解决,这不是他们擅长的范围,效果未必会好。 第三种方案:页面的绘制方面,纯前端技术即可完成,难度低,完成度高,但是需要在后端起一个 node 服务开启 puppeteer 去控制服务端 Chrome 浏览器。这种方案的缺点就是成本太高,我们和业界同行都测算过,结果差不多:4 核 16G 的服务器生成图片的 QPS 大概只有 10-20,相当于一秒钟较差情况只能生成 10 张图片,这对于突发的大量分享需求并不能满足,而且这种配置的服务器,不能部署其他服务,只跑这个服务就会用尽大部分资源。 费用上:只单单算 5M 带宽的服务器费用一个月就要 700+ 人民币,流量和图片托管费用另算。此方案的最小化实现:至少需要 1 核 2G 的服务器才能较为顺畅地完成一次顺利截图,但是还是要处理浏览器无响应假死等情况,较为复杂。但综合来看,这种方案是效果最好最为灵活的。 快海报小程序分享图生成服务 快海报 kuaihaibao.com 是专门提供小程序分享海报生成服务的,技术上用的就是上面所述的第三种方案,但是只需要调用他的 API 就可以完成,不需要开发者维护 puppeteer 和 headless Chrome,而且成本较低,一张分享图的最低生成成本是 0.01 元。 其实真正集成到自己的服务中时,平均成本要比这个低,因为有些生成的图片的二维码,如果不带用户个人信息(不给分享的用户返利)时,可以生成一次之后永久缓存起来,其他用户再分享同一个东西都用缓存好的图片,综合成本就降下来了。 算一下成本: 比方说一个刚起步的小程序日活 5000(对于刚起步的小程序其实已经很高了吧) 假设有 5% 的用户生成分享图 也就是每天生成 250 张分享图,一个月会生成 7500 张分享图 这样的话每个月成本就是 75 元人民币左右,相比 700+ 人民币的服务器成本省太多了。这是测算比较高的指标,而且是完全不应用缓存方案的情况。 如果你的小程序还处于冷启动的阶段: 日活 500 假设有 5% 的用户生成分享图 也就是每天生成 25 张分享图,一个月会生成 750 张分享图 每月成本 7.5 元。比 1 核 2G 的最小化自部署方案也要便宜。但带来的收益是无穷的,750 张分享图发到朋友圈,每张分享图 1000 受众浏览,一个月就是将近 750000 人次分享受众。 调用 API 首先去 https://kuaihaibao.com/ 注册账号,验证邮箱激活之后,其实就可以先测试用了,每个账号有 100 次测试额度,测试生成的图片带水印。 网站左侧的 [文档] 页面能找到集成文档,非常简单,一共就只有一个核心 API,通过 HTTP 调用的。 先在【开发】->【设置】中激活 token [图片] 目前支持三种生成方式: 直接传 URL 进行渲染 传 HTML 渲染 使用内置的模板进行选择 这里演示使用模板渲染,因为比较简单 打开 【开发】->【模板】中,找到自己喜欢的模版。因为我只想生成一个简单的分享图片,所以最简单的方式就是使用网站内置的模版,内置模板目前有 8 款,应该能满足大部分小程序的需求了,抽奖、打卡、图文、文字、电商都有,改一改文案和图片就可以了。 我选了这个抽奖模板: [图片] 按照 https://kuaihaibao.com/doc/docs/template/kzccda95.html 文档描述的 JSON 改成我需要的: [代码]{ "backgroundColor": "#fafafa", "backgroundImage": "", "user": { "avatar": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/girl_2.jpg", "nickname": "我是测试账号", "color": "#666" }, "tip": "邀请你来抽奖", "qrcode": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/sample_qr_0.png", "records": [ { "title": "一等奖", "desc": "2019 年 11 月 16 日 10:00 开奖", "image": "https://s3.cn-northwest-1.amazonaws.com.cn/res.weiyidan.com/production/10000002/4109f8e51a8f43b9816dbc8fe636e22a.jpeg" } ], "brand": "我的测试抽奖小程序", "slogan": "快来和我一起抽吧!", "metaColor": "#999" } [代码] 然后打开 Terminal 做一次请求试试: [代码]curl -X "POST" "https://api.kuaihaibao.com/services/screenshot" \ -H 'Authorization: Bearer 这里写你自己的 token' \ -H 'Content-Type: application/json; charset=utf-8' \ -d /pre>{ "template": "kzccda95", "data": { "qrcode": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/sample_qr_0.png", "records": [ { "title": "一等奖", "desc": "2019 年 11 月 16 日 10:00 开奖", "image": "https://s3.cn-northwest-1.amazonaws.com.cn/res.weiyidan.com/production/10000002/4109f8e51a8f43b9816dbc8fe636e22a.jpeg" } ], "tip": "邀请你来抽奖", "slogan": "快来和我一起抽吧!", "metaColor": "#999", "brand": "我的测试抽奖小程序", "backgroundImage": "", "backgroundColor": "#fafafa", "user": { "avatar": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/girl_2.jpg", "nickname": "我是测试账号", "color": "#666" } } }' [代码] 返回了结果: [代码]{ "success": true, "data": { "name": "iPhone 5", "image": "https://khb-test-oss.oss-cn-shanghai.aliyuncs.com/screenshot/4fa63f2a3605cbdece90c659cbccea619d9cf9fa?x-oss-process=style/test_watermark" } } [代码] 打开图片地址看看: [图片] 速度很快,图片很漂亮,只是中间带水印,充值后成为付费用户,再生成的图片水印就自动取掉了。 后端集成 这里参考快海报官方给的最佳实践的逻辑参考图: [图片] 所以后端只需要做一件事,就是提供一个 API 给客户端用,这个 API 被调用的时候去请求快海报的服务器,再把结果返回给小程序就好了。
2020-10-20 - 做了一个颜色选择器
edit at 11/12 代码传到了:https://github.com/eclipseglory/zasi-components , DEMO演示在文章结尾 小程序没有提供color-picker类似的组件,只能自己做。 可传统的RGB颜色选择器,真的腻了,而且在手机上也不是很操作,就跑网上搜了一圈,发现有一种圆环形的(基于HSV)我很喜欢: [图片] 我自诩对canvas2d和webgl很熟悉,做个这玩意儿很轻松,开始做!没想到痛苦开始了。 从上周5开始,一共做了三个版本: 1.纯canvas版本 2.canvas+组件版本 3.纯组件版本 纯canvas版本这个版本做了整整一天! [图片] 由于canvas绘制性能问题,特别是因为没有requestAnimationFrame可以调用,别说在真机上测试特别不流畅,就是在模拟器上也小卡小卡的。而且,在纯的canvas进行触摸定位等事件响应处理,计算起来太麻烦,bug不断,只能放弃了。 混合版本因为wxs模块是提供requestAnimationFrame接口的,所以我就想,使用canvas作为底部颜色环,上面就直接用view作为指针,这样,事件触发和处理比起纯canvas要简单得多,而且还能利用rAF回调页面接口去绘制其他canvas。 的确,我的想法得到了证实,这个混合版本比起第一个要流畅得多! 可就要完工的时候,我却发现,在真机上,cover-view的鼠标事件有很大问题,坐标值飘忽不定,也就是说拖动指针会发生鬼畜般的抖动!加上我不知道怎么debug到wxs模块中,于是跟个sb一样fix,找了半天也没找到问题在哪儿,直到我搜索时,返现有人也遇到和我一样的问题,我才安心了:这是小程序的问题。 动手改!既然cover-view有不行,那就不用它。 实际上canvas在该组件中的作用无非就是绘制一个圆环而已,如果我利用离屏canvas事先画好,然后保存成图片,再用image加载它,这样就可以避免使用canvas来显示圆环了,也就可以不用cover-view放到其顶部! 想法是好的,可是到了真机上,绘制保存出来的图片时好时坏: [图片] 只能放弃,又耽误我一天。 无canvas版本刚才说了,canvas在该组件中的作用,仅仅是绘制一个颜色环而已,除此之外真没什么用。 那我就用css模拟一个类似圆环就好了,精确到每一度一个颜色一点意义没有。 所以就利用css的background-image属性,做了4个四分之一圆弧,然后拼在一起,得到了一个彩色原版,再用一个小的view遮挡,让它们只露出一部分,圆环就做好了。 之前的代码都不用改,直接用新作的圆环views替换canvas的标签即可。主体框架和功能,不到一天就完成了,不得不说,比起纯的canvas绘制,要方便太多太多。 这是截图: [图片] 代码片段这里是 演示DEMO,要使用的话,复制里面的组件出来用就好。 有些代码我混淆过,但不耽误使用。 有问题找我
2019-11-12 - 小程序识别身份证,银行卡,营业执照,驾照
最近老是有同学问我小程序ocr识别的问题,就趁机研究了下,实现了小程序识别身份证,银行卡,驾照,营业执照,图片文字的功能。今天来给大家讲讲详细的实现流程。 先画一张流程图出来 [图片] 第一次看到这个流程图,可能有点萌,什么云开发,云函数。。。。 不要着急,我们接下来会一步步带大家实现。 先看下我们的页面和效果图。 [图片] 功能其实很简单,就是我们点对应的按钮后,去拍照或者去相册选择对应的图片。然后把图片上传到云存储,会有一个对应的图片url,然后把这个图片url传递到云函数,然后云函数里使用小程序的开发ocr能力,来识别图片,返回对应的信息回来。如下图所示,我们识别银行卡(身份证什么的就不演示了,涉及到石头哥个人隐私) [图片] 接下来就是代码的实现了。 一,首先要创建一个云开发的小程序项目 这里我前面文章有讲解过,就不再细说了,不会的同学去翻看下我之前的文章。或者看下我录制的 讲解视频 这里有一点需要注意的给大家说下 [图片] 二,创建一个简单的小程序页面 1,index.wxml如下 [图片] 2,index.js完整代码如下 [代码]Page({ //身份证 shenfenzheng() { this.photo("shenfenzheng") }, //银行卡 yinhangka() { this.photo("yinhangka") }, //行驶证 xingshizheng() { this.photo("xingshizheng") }, //拍照或者从相册选择要识别的照片 photo(type) { let that = this wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { // tempFilePath可以作为img标签的src属性显示图片 let imgUrl = res.tempFilePaths[0]; that.uploadImg(type, imgUrl) } }) }, // 上传图片到云存储 uploadImg(type, imgUrl) { let that = this wx.cloud.uploadFile({ cloudPath: 'ocr/' + type + '.png', filePath: imgUrl, // 文件路径 success: res => { console.log("上传成功", res.fileID) that.getImgUrl(type, res.fileID) }, fail: err => { console.log("上传失败", err) } }) }, //获取云存储里的图片url getImgUrl(type, imgUrl) { let that = this wx.cloud.getTempFileURL({ fileList: [imgUrl], success: res => { let imgUrl = res.fileList[0].tempFileURL console.log("获取图片url成功", imgUrl) that.shibie(type, imgUrl) }, fail: err => { console.log("获取图片url失败", err) } }) }, //调用云函数,实现OCR识别 shibie(type, imgUrl) { wx.cloud.callFunction({ name: "ocr", data: { type: type, imgUrl: imgUrl }, success(res) { console.log("识别成功", res) }, fail(res) { console.log("识别失败", res) } }) } }) [代码] 上面代码注释讲解的很清楚了,再结合我们的流程图,相信你可以看明白。 [图片] 三,重头戏来了,识别的核心代码是下面这个云函数 [图片] 云函数的完整代码也给大家贴出来 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { let { type, imgUrl } = event switch (type) { case 'shenfenzheng': { // 识别身份证 return shenfenzheng(imgUrl) } case 'yinhangka': { // 识别银行卡 return yinhangka(imgUrl) } case 'xingshizheng': { // 识别行驶证 return xingshizheng(imgUrl) } default: { return } } } //识别身份证 async function shenfenzheng(imgUrl) { try { const result = await cloud.openapi.ocr.idcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } //识别银行卡 async function yinhangka(imgUrl) { try { const result = await cloud.openapi.ocr.bankcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } //识别行驶证 async function xingshizheng(imgUrl) { try { const result = await cloud.openapi.ocr.vehicleLicense({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } [代码] 其实没什么特别的,就是用一个switch方法,根据用户传入的不同的type值,来实现不同的识别效果。 如用传入的type是‘ yinhangka’,我们就调用银行卡识别 [代码]try { const result = await cloud.openapi.ocr.bankcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } [代码] 进而把识别的结果返回给小程序端,如下图 [图片] 到这里我们就完整的实现了,小程序识别身份证,银行卡,行驶证的功能。至于别的更多的ocr识别,可以去看小程序官方文档,结合着我的这篇文章,相信你也可以轻松实现更多的图片识别。 [图片] 源码其实在上面都已经贴给大家了,如果你觉得不完整,想要完整的源码可以在文章底部留言或者私信我。
2019-10-30 - 订阅消息如果选择选择‘总是保持以上选择,"不再询问"后的设置问题
目前是选择‘总是保持以上选择,"不再询问"后,可以在设置中开启或拒绝接收,但不会再次拉起授权弹窗
2019-10-18 - 【开箱即用】分享一个3D环物展示的解决方案
概述 有时候我们需要立体展示一个物体时,可能需要用到以下效果。当然实现的效果可能有很多,这里就为大家介绍一个大神写的方案,希望能帮到大家! 利用小程序开放的接口模拟简单的3D环物功能。只需传入物品序列照片数组即可。 [图片] 截图来自小程序“白海豚保护区” 一、小程序插件 AppID:wx0f253bdf656bfa08 基础库要求:>= 2.4.3 文档链接:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx0f253bdf656bfa08 图片要求:网络URL链接 [图片] 二、兼容云开发 由于插件已经有段时间没有更新了,笔者在开发时又用到了小程序的云开发储存图片资源。拜原作者开源所赐,为了兼容云开发,我在开源的小程序插件代码中进行了部分修改。 开源代码:https://github.com/hiteochew/DimensionalShow-wxapp-plugin 修改方法: 将原代码中 downloadFile 方法替换为以下代码即可。 [代码]// 文件位置:plugin/api/util.js // 代码位置:第 124 行 function downloadFile(src) { return new Promise((resolve, reject) => { //云储存 wx.cloud.downloadFile({ fileID: src }).then(res => { resolve(res.tempFilePath); }).catch(error => { reject(err); }) }) } [代码] 结语 欢迎社区三连——关注点赞收藏!
2019-10-20 - 「笔记」订阅消息体验踩坑
前言 10月12日夜晚社区发了公告小程序模板消息能力调整通知,正式发布了 一次性订阅消息 这一能力,所以第一时间进行了体验。 本文主要是补充一下官方未提供的使用方法,和使用中与模板消息用法的不同。 文档地址 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html 使用方法 [代码]wx.requestSubscribeMessage({ tmplIds: ["模板id1","模板id2"], success: function (res) { //成功 }, fail(err) { //失败 console.error(err); } }) [代码] 第一个坑 如果不勾选红色方框内的内容,用户每次触发订阅消息功能都会弹出授权窗口,如果用户勾选了则不会出现弹窗。 [图片] 第二个坑 目前开发者工具(v1.02.191012)不支持调试,只能通过真机调试。 [图片] 第三个坑 微信不会为开发者保存订阅次数,需要自己在后台记录用户触发的次数。 超过次数调用接口下发订阅消息会返回失败。 [图片] 第四个坑 发送模板格式和原来的模板消息格式不一致,特别是data内的内容,订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,具体使用参考后台的模板详情。 模板消息的格式: [代码]"data": { "keyword1": { "value": "内容", "color": "#000" }, "keyword2": { "value": "内容", "color": "#000" } } [代码] 订阅消息的格式: [代码]"data": { "thing1": { "value": "内容" }, "number2": { "value": 20 } [代码] 第五个坑 订阅消息申请模板的时候,需要选择所属类目,而且只能是自己小程序相关类目,模板消息是不需要选择对应类目的。 如果删除小程序类目,则会把订阅消息模板一起删除,需谨慎操作。 [图片] 第六个坑 长期订阅消息只针对特定行业开放,所以普通开发者并无法使用。 结束 暂时就先总结这些,有其它坑再补充。
2019-10-13 - 微信对话开放平台使用教学视频(持续更新)
【整体介绍】 【初级技能配置】 【高级技能配置】 【末日生存游戏配置】
2019-09-25 - 【配置案例】利用系统随机回复 配置“猜拳游戏”技能
微信对话开放平台(后文简称平台)发布以来,已有不少公司及个人第一时间注册体验,平台官方也对微信对话开放平台的使用提供了详尽的文档及视频指导支持。但有用户反映平台及文档专业性过强,一时间无从下手。 下文整理了一份以配置猜拳技能为例的简易教程,为大家介绍简单问答型技能的配置方法,供大家参考学习。后续我们还将更新系列文章,揭开对话开放平台的神秘面纱,让大家逐步对微信对话开放平台有一个完整清晰的认识,以便节约大家的开发运营成本。 平台登录 访问微信对话开放平台官网(openai.weixin.qq.com) 在对话平台点击右上角的“登录”按钮后,使用个人微信扫码登录。 [图片] 创建机器人 当我们完成登录后,点击首页开始使用按钮就可以进入创建客服机器人页面,填写机器人信息。 [图片] 基本概念 再开始配置猜拳游戏之前我们需要了解可能用到的几个基本概念: 意图:意图是指用户在语音交互发出的主要请求或动作。例:当用户说“今天天气怎么样?”,用户的意图是了解今日天气 技能:技能是满足用户特定需求的一个应用,通过语音交互完成。例:当用户说“今天天气怎么样”,会命中天气查询的技能,天气是触发该技能的激活词。 用户问法:用户问法是指在语音交互过程中用户的指令、请求,也是用户对于意图的常用表达。我们最好能提供不同的用户说法,尽可能全面地覆盖不同地区不同用户的表达方式。 创建技能 平台创建的技能可分为问答型技能和任务型技能两大类,其中问答型技能只需在用户说法中配置用户提问的常用表达,并在机器人回答中配置对应的文本回复即可。本文将以技能“猜拳”为例,主要介绍此类问答型技能的配置方法。 首点击导航栏-控制台>>自定义对话技能>>普通技能>>创建技能 进入创建技能页面,填写技能名称—猜拳。 [图片] 新增问答 猜拳技能创建完成后,我们需要在技能页面下创建意图,点击新增问答,填写标准问题名称。 根据猜拳游戏的规则,我们考虑到用户将可能有开始游戏、出剪子\布\石头、退出游戏这五种问答情况,因此我们需要对应这五种情况创建五个意图。 [图片] 创建问答并点击确定后,将进入编辑页面,在这里,我们需要配置用户说法及对应的机器人回答。 添加用户问法 根据猜拳游戏的游戏规则,我们在此处添加用户在玩猜拳游戏时的常用表达,如:我出布\我出剪子等。因为考虑到不同用户的不同表达习惯以及地区表达差异性,我们需要尽可能全面配置不同的表达方式,以提高命中率。 这里有两种方法可以提供: 1、通过“( )、|、&& ”这种问法规则来进行更多的泛化用户问法 [图片] 2、通过调整机器人的AI模型(语义相似度阈值)来泛化用户问法 [图片] 机器人回答 机器人回答是指用户问法命中意图之后,开发者希望给出的回复话术。 由于猜拳游戏中用户问题的回答通常只有胜、负、平局三种结果,较为固定,不需要调用API资源,因此我们只需在下方编辑回复文本,配置诸如“我出石头!你赢了耶,小微要加油了!我们再来,你出剪刀、石头还是布?”这类的回答。 同时,这里的机器人回答支持添加多条回答,系统会随机选取一条进行回复。也正是如此,我们利用了机器人多条回复的随机性,实现猜拳游戏的不确定性。 [图片] 机器人调试 完成技能配置后,为了检查开发配置的技能,确保技能上线顺利运行,我们还需要用到【机器人调试】。在机器人调试中,我们可测试技能配置情况,预先体验技能发布后的真实对话效果。 使用机器人调试功能,我们需要首先在技能页点击右上角【机器人调试】按钮,打开测试对话框,输入用户问法,测试技能配置情况。 我们也同样可以在机器人调试页,点击json,查看技能和意图命中情况。如技能匹配失败或有误,可根据json数据来定位问题,重新进行技能修改及调试。 在猜拳游戏的机器调试中,我们选用了问法:“我出布”,机器人也随机给出了配置对应回答中的其中一条,确保游戏正常进行。 [图片] 技能发布与接入 当我们完成猜拳游戏的技能配置后,接下来需要做的就是技能的发布与上线,点击控制台的【发布】按钮,就可以执行发布操作,技能就开始生效应用啦。 服务接入(即关联微信公众号、小程序等)请参考平台说明文档: https://developers.weixin.qq.com/doc/aispeech/ACCOMPLISH.html [图片] 简易问答型技能配置指南就介绍到这儿了,接下来我们还将介绍复杂高级技能的配置方法,敬请期待!
2020-07-31 - 小程序毫秒级倒计时的一种优雅实现
在小程序上用毫秒级定时器来刷新页面或组件,会引发性能问题。 使用 [代码]canvas[代码] 又对字体样式无法进行很好的控制。 一种实现方案 使用秒级的定时器刷新页面或组件实现秒级的倒计时,制作毫秒倒计时的 GIF 并把图放到秒级倒计时后面。 毫秒倒计时 GIF:[图片] 小程序效果如下图所示: [图片] HAVE FUN 😃
2019-09-20 - 几行代码实现小程序云开发提现功能
先看效果: [图片] 纯云开发实现,下面说使用步骤: 一:开通商户的企业付款到领取功能 说明地址: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 使用条件 1、商户号(或同主体其他非服务商商户号)已入驻90日 2、截止今日回推30天,商户号(或同主体其他非服务商商户号)连续不间断保持有交易 使用条件是第一难,第二难在下面这里 [图片] 在网上找了很多,感觉是云开发这里的一个不完善地方,如果不填ip,会报这种错 [代码]{"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"NO_AUTH"} [代码] [代码]<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></return_msg> <mch_appid><![CDATA[wx383426ad9ffe1111]]></mch_appid> <mchid><![CDATA[1536511111]]></mchid> <result_code><![CDATA[FAIL]]></result_code> <err_code><![CDATA[NO_AUTH]]></err_code> <err_code_des><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></err_code_des> </xml> [代码] 云开发没有ip这个概念,所以这里有些无从下手,不过这里我采用了个替代方案,参考了社区帖子: https://developers.weixin.qq.com/community/develop/doc/00088cff3a40d87d80f7267b65b800 之后我也亲自验证了,基本上就是这几个,当然肯定不够,但是可以自己在逻辑上进行处理,ip以下: [代码]172.81.207.12 172.81.212.74 172.81.236.99 172.81.235.12 172.81.245.51 212.64.65.131 212.64.84.22 212.64.85.35 212.64.85.139 212.64.87.134 [代码] 接着,可以动手了 二、云开发部分 1、设置云存储 证书配置地址: [图片] 下载后有三个文件,我们只需要p12结尾的那个 [图片] 然后,将这个apiclient_cert.p12文件上传到你的云存储 [图片] 这里处理完了,我们只需要一个东西,就是fileID也就是常说的云存储ID(上图红框内容) 2、配置云函数 新建云函数ref云函数 [图片] 代码如下: [代码]const config = { appid: 'wx383426ad9ffe1111', //小程序Appid envName: 'zf-shcud', // 小程序云开发环境ID mchid: '1111111111', //商户号 partnerKey: '1111111111111111111111', //此处填服务商密钥 pfx: '', //证书初始化 fileID: 'cloud://zf-shcud.11111111111111111/apiclient_cert.p12' //证书云存储id }; const cloud = require('wx-server-sdk') cloud.init({ env: config.envName }) const db = cloud.database(); const tenpay = require('tenpay'); //支付核心模块 exports.main = async(event, context) => { //首先获取证书文件 const res = await cloud.downloadFile({ fileID: config.fileID, }) config.pfx = res.fileContent let pay = new tenpay(config,true) let result = await pay.transfers({ //这部分参数含义参考https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 partner_trade_no: 'bookreflect' + Date.now() + event.num, openid: event.userinfo._openid, check_name: 'NO_CHECK', amount: parseInt(event.num) * 100, desc: '二手书小程序提现', }); if (result.result_code == 'SUCCESS') { //如果提现成功后的操作 //以下是进行余额计算 let re=await db.collection('user').doc(event.userinfo._id).update({ data: { parse: event.userinfo.parse - parseInt(event.num) } }); return re } } [代码] 需安装的依赖:wx-server-sdk、tenpay 这里只是实现了简单原始的提现操作,关于提现后,比如防止重复提交,提现限额这些,在开源二手书商城上有完整流程,地址: https://github.com/xuhuai66/used-book-pro 这种办法,不是每次都能成功提现,小概率遇到ip未在白名单情况,还是希望,云开发团队能尽快出一个更好的解决方案吧
2019-09-21 - 创意小程序-可以自主创建成语接龙的社交应用
相信大家都玩过成语接龙的小游戏 但是此类小游戏,过于单一,接龙都是官方固定的那些玩法 基于此, 我做了一款小程序【八斗接龙】,在这里你可以自己创建接龙,创建好之后可以分享给你的朋友去玩,大家可以对此进行评论,还有过关用时的排名。 注:里面的难点就是方格的生成,以及每个方格里的汉字颜色的设置、更换 我不擅长运营,如果有对此小程序感兴趣,并且善于推广运营的话,可以联系我,找我合作。 废话不多说还是看视频吧 [视频] 扫码,体验一下吧 [图片]
2019-09-22 - 搭建一个https网站的全过程
概述:本着学习的目的,做了这个分享。自己切切实实的做完了整个流程,发现其中的坑也是蛮多的,当然自己的收获对应也是蛮多的。写下这个流程一方面为了加深自己的印象、可以在将来回顾一下,另一方面也是为了给有需要的人提供帮助~ 一、服务器准备 [代码]为了演示方便,我购买了一台腾讯云服务器 [代码] [图片] 安装的操作系统是centos 二、域名准备 1、域名注册 可以从万网上进行注册:https://wanwang.aliyun.com/ 2、域名备案 备案流程略复杂,这里只列了一个步骤简介 入口:https://beian.aliyun.com/ [图片] 域名的备案时间较长,建议大家提前准备起来。 3、域名解析 域名备案通过之后,为我们的网站准备一个子域名,入口:https://dns.console.aliyun.com/?spm=a2c1d.8251892.aliyun_sidebar.daliyun_sidebar_dns.37575b76kNuXEO#/dns/domainList [图片] 点击上图的解析设置 [图片] 将记录值填写我们刚刚买的服务器的公网ip 三、申请ssl证书 我是从腾讯提供的证书服务里申请的,腾讯申请入口 https://console.cloud.tencent.com/ssl 点击“申请免费证书” [图片] 这里选择了手动Dns验证 [图片] 申请完成后会有个表格,是说明如何Dns校验的,要求域名下添加一条解析记录 [图片] Dns校验 首先,在域名下添加一条txt记录 [图片] 然后,单机自助诊断旁边的“查询” [图片] 下载证书 [图片] 点击图中的下载即可 四、服务器安装软件 我用ssh连接服务器 登录服务器:ssh root@212.129.*.* , 然后输入密码(从腾讯云管理后台进行密码的设置和获取)进入服务器,然后安装软件,如下~ 1、git:用于代码管理 2、nvm:用于管理node版本 3、node:用于启动web服务 4、pm2:用于守护node进程 安装git [代码]yum install git -y [代码] 下载nvm [代码]git clone git://github.com/creationix/nvm.git ~/nvm [代码] 设置nvm 自动运行; [代码]echo "source ~/nvm/nvm.sh" >> ~/.bashrc source ~/.bashrc [代码] 查询node版本 [代码]nvm list-remote [代码] 安装node.js [代码]nvm install v10.16.3 [代码] 使用nodejs [代码]nvm use v10.16.3 [代码] 使用npm安装pm2 [代码]npm install -g pm2 [代码] 五、下载一个web项目 & 使用 pm2 启动 这里我使用了自己的一个github上的项目:https://github.com/myronliu/ssr-koa-react-redux.git 1、git clone https://github.com/myronliu/ssr-koa-react-redux.git 2、cd ssr-koa-react-redux 3、npm install 4、pm2 start server.js 服务启动之后如下图: [图片] 现在我们可以用IP(服务器的公网ip) + 端口号来进行访问了 [图片] 六、安装nginx 步骤 1: 添加 yum 源 Nginx 不在默认的 yum 源中,可以使用 epel 或者官网的 yum 源,本例使用官网的 yum 源。 sudo rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm 安装完 yum 源之后,可以查看一下。 sudo yum repolist 步骤 2: 安装 yum 安装 Nginx,非常简单,一条命令: sudo yum install nginx 常用nginx命令介绍 设置开机启动 $ sudo systemctl enable nginx 启动服务 $ sudo systemctl start nginx 停止服务 $ sudo systemctl restart nginx 重新加载,因为一般重新配置之后,不希望重启服务,这时可以使用重新加载。 $ sudo systemctl reload nginx 步骤3: 打开防火墙端口 sudo firewall-cmd --permanent --zone=public --add-service=http sudo firewall-cmd --permanent --zone=public --add-service=https sudo firewall-cmd --reload [图片] 步骤4: 配置nginx 找到并查看配置文件/etc/nginx/nginx.conf [图片] 步骤5: 配置我们自己的conf [图片] 编写为 [图片] 步骤6: 现在可以使用域名访问我们的网站了,目前是http协议的 [图片] 七、安装证书 将第三步下载的证书里选择Nginx下的两个文件上传到服务器的/etc/nginx/conf.d文件夹下 [图片] Mac上利用scp上传 scp /Users/xxx/Downloads/todo.xxx.com/Nginx/1_todo.xx.com_bundle.crt root@212.129.*.*:/etc/nginx/conf.d [图片] 八、创建https的conf文件 进入到/etc/nginx/conf.d下,执行命令:touch https.conf && vi https.conf [图片] 测试nginx配置 & 重启 [图片] 九、访问https协议的站点 [图片] 备注: 如有问题,欢迎指出! 如有侵权,联系删除~
2019-09-24 - PHP获取小程序openid,10行代码快速获取小程序openid
上一节教大家如何在本地运行php后台项目,并可以被小程序访问到,这一节就来给大家讲一个实际工作中常用的需求,微信小程序openid的获取。当然了,还是用我们的php做为后台。 老规矩,先看效果图 [图片] 通过上图我们可以看到我们成功的获取到了小程序的openid,而这里获取openid是借助php后台获取的。 我们的php核心代码,只有下面10行 [图片] 下面就来讲讲具体的实现步骤。 一,首先要搭建一个可以本地访问的php后台 这里我在上一节的文章已经讲过了,还不知道怎么配置本地php项目的同学可以去翻看我上篇文章:phpstorm运行本地PHP服务器,实现小程序可以访问的PHP后台 二,编写php代码 只需要创建一个如下图所示的简单的php文件就可以了。 [图片] 把代码给大家贴出来。要记得把下面的appid和appSecret换成你自己的。至于如果获取,我之前的文章也有讲过,去翻下我零基础入门小程序开发的文章即可。 [代码]<?php /** * 2019/9/21 20:34 * author: 编程小石头 * wechat:2501902696 * desc: 获取小程序用户openid */ getOpenid(); function getOpenid() { $code = $_GET['code'];//小程序传来的code值 $appid = 'wx4b55bb240aec2ee3';//小程序的appid $appSecret = '1f6f68884c1add6293cfa9b86e1f6bfd';// 小程序的$appSecret $wxUrl = 'https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code'; $getUrl = sprintf($wxUrl, $appid, $appSecret, $code);//把appid,appsecret,code拼接到url里 $result = curl_get($getUrl);//请求拼接好的url $wxResult = json_decode($result, true); if (empty($wxResult)) { echo '获取openid时异常,微信内部错误'; } else { $loginFail = array_key_exists('errcode', $wxResult); if ($loginFail) {//请求失败 var_dump($wxResult); } else {//请求成功 $openid = $wxResult['openid']; echo "获取openid成功成功:" . $openid; } } } //php请求网络的方法 function curl_get($url, &$httpCode = 0) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); //不做证书校验,部署在linux环境下请改为true curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); $file_contents = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $file_contents; } [代码] 代码就这么多,我们编写好以后,运行下项目。 [图片] 三,编写小程序代码 小程序的代码很简单,就是一个按钮,点击按钮的时候我们请求上面定义好的php后台项目。 小程序代码也很简单 [图片] [图片] 代码就上图这么点,我把code.js里的代码贴出来给大家 [代码]Page({ getCode() { let that = this; wx.login({ success(res) { console.log('code', res.code) that.getOpenid(res.code) } }) }, getOpenid(wxCode) { wx.request({ url: 'http://localhost:8080/Demo.php', data: { code: wxCode }, success(res) { console.log('获取成功', res) }, fail(res) { console.log('获取失败', res) } }) } }) [代码] 给大家解释下上面的代码,我们通过wx.login来获取code,然后通过wx.request调用我们定义的php后台。url里的 Demo.php必须和你的php文件名保持一致。 [图片] 效果 点击按钮以后,就可以成功的获取到我们所需的小程序openid了。效果图如下 [图片] 到这里我们就成功的实现了PHP获取小程序openid的功能了,是不是很简单。 后面我会写更多关于php和小程序的文章,敬请关注。
2019-09-21 - 小程序之日历签到积分
[图片] 该示例为纯手写代码,暂无插件,不多说直接上代码 我们的实现思路: JS部分 1、获取当前年月 const date = new Date(); cur_year = date.getFullYear(); cur_month = date.getMonth() + 1; const weeks_ch = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]; this.setData({ cur_year, cur_month, weeks_ch, }) 2、获取当月共多少天 getThisMonthDays: function (year, month) { return new Date(year, month, 0).getDate() }, 3、获取当月第一天星期几 getFirstDayOfWeek: function (year, month) { return new Date(Date.UTC(year, month - 1, 1)).getDay(); }, 4、计算当月1号前空了几个格子,把它填充在days数组的前面 calculateEmptyGrids: function (year, month) { var that = this; //计算每个月时要清零 that.setData({ days: [] }); const firstDayOfWeek = this.getFirstDayOfWeek(year, month); if (firstDayOfWeek > 0) { for (let i = 0; i < firstDayOfWeek; i++) { var obj = { date: null, isSign: false } that.data.days.push(obj); } this.setData({ days: that.data.days }); //清空 } else { this.setData({ days: [] }); } }, 5、绘制当月天数占的格子,并把它放到days数组中 calculateDays: function (year, month, sign) { var that = this; var isSign; const thisMonthDays = this.getThisMonthDays(year, month); for (var i = 1; i <= thisMonthDays; i++) { var obj = { date: i, isSign: ‘’ } for (var j = 0; j < sign.length; j++) { if (i == parseInt(sign[j].create_time.substr(8, 2))) { obj.isSign = true; break; } } that.data.days.push(obj); } this.setData({ days: that.data.days }); }, 6、切换控制年月,上一个月,下一个月 handleCalendar: function (e) { const handle = e.currentTarget.dataset.handle; const cur_year = this.data.cur_year; const cur_month = this.data.cur_month; if (handle === ‘prev’) { let newMonth = cur_month - 1; let newYear = cur_year; if (newMonth < 1) { newYear = cur_year - 1; newMonth = 12; } this.signRecord(newYear, newMonth); this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } else { if (cur_month + 1 > month) { this.setData({ imgType: ‘next.png’ }) } else { let newMonth = cur_month + 1; let newYear = cur_year; if (newMonth > 12) { newYear = cur_year + 1; newMonth = 1; } this.signRecord(newYear, newMonth); if (cur_month + 1 == month) { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘next.png’ }) } else { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } } } }, wxml部分: <view class=‘all’> <view class=“bar”> <!-- 上一个月 --> <view class=“previous” bindtap=“handleCalendar” data-handle=“prev”> <image src=‘https://www.***.com/weChatImg/pre.png’></image> </view> <!-- 显示年月 --> <view class=“date”>{{cur_year || “–”}} / {{filter.fill(cur_month) || “–”}}</view> <!-- 下一个月 --> <view class=“next” bindtap=“handleCalendar” data-handle=“next”> <image src=‘https://www.***.com/weChatImg/{{imgType}}’></image> </view> </view> <view class=‘xxian’> <image src=‘weChatImg/huan.png’></image> <image src=‘weChatImg/huan.png’></image> </view> <!-- 显示星期 --> <view class=“week”> <view wx:for="{{weeks_ch}}" wx:key="{{index}}" data-idx="{{index}}">{{item}}</view> </view> <view class=‘days’> <!-- 列 --> <view class=“columns” wx:for="{{days.length/7}}" wx:for-index=“i” wx:key=“i”> <view wx:for="{{days}}" wx:for-index=“j” wx:key=“j”> <!-- 行 --> <view class=“rows” wx:if="{{j/7 == i}}"> <view class=“rows” wx:for="{{7}}" wx:for-index=“k” wx:key=“k”> <!-- 每个月份的空的单元格 --> <view class=‘cell’ wx:if="{{days[j+k].date == null}}"> <text decode="{{true}}"> </text> </view> <!-- 每个月份的有数字的单元格 --> <view class=‘cell’ wx:else> <!-- 当前日期已签到 --> <view wx:if="{{days[j+k].isSign == true}}" style=‘color:#acacac’ class=‘cell’> {{days[j+k].date}} <image src=‘https://www.***.com/weChatImg/sgin.png’></image> </view> <!-- 当前日期未签到 --> <view wx:else> <text>{{days[j+k].date}}</text> </view> </view> </view> </view> </view> </view> </view> </view> 相信大家通过以上思路,再结合自己的需求应该可以自己做出符合自己心目中的日历插件或者签到
2019-09-11 - 利用wxs文件实现touch事件拖动
[图片] 图一:wxml文件中的touch事件绑定在js文件中,js文件中的对应的事件做出处理,然后修改x,y等相关的位置信息,并且渲染出来,问题在于:卡顿,原因:比如说js文件和wxml文件之间时通过一条管道进行通信的,那么在wxml文件触发touch事件时,开始向js文件传递数据,js接受到数据后进行处理,然后再传相关数据给wxml文件,这样一次touch事件就在这条管道中进行了两次传输,而touchmove事件的触发过于频繁,导致了管道堵塞,(塞车)。所以卡顿了 图二:通过微信给出的相关api,把wxml文件中的touch事件绑定到wxs文件中,然后再wxs文件中做出相应的修改,(修改样式),文档链接 这样做可以解决卡顿的问题,但是有个问题是,再wxs文件中修改的样式等级是最高的,也就是说通过wxs文件修改样式后,其他途径修改样式就行不通了,如果不实际情况允许的话,用这种方法做也是可以的。 图三:图三是把js,wxml,wxs三个文件形成一个圆环,wxml触发touch事件,把事件传递给wxs文件,wxs文件做相关处理,然后把结果传给js文件,js文件接收到数据后,再对wxml文件做相关的样式修改(通常都是修改x,y等值),这样既可以避免图一的卡顿,也可以避开图二的样式等级的问题,如果再在wxs文件中做相关节流的话,效果应该会更好,相关的操作,用到的api请点击这里 开发文档里面都有,就不11说明了 另外对于movable-view,我就不说, 一开始就是用的movable-view,问题多多,最后改用了图三的做法 第一次写文章,有问题的地方请多指教,纯属分享个人开发经验
2019-07-31 - 小程序自定义单页面、全局导航栏
摘要: 小程序开发技巧。 作者:小白 原文:小程序自定义单页面、全局导航栏 Fundebug经授权转载,版权归原作者所有。 需求 产品说小程序返回到首页不太方便,想添加返回首页按钮,UI说导航栏能不能设置背景图片,因为那样设计挺好看的。 [图片] 需求分析并制定方案 这产品和UI都提需求了,咱也不能反驳哈,所以开始调研,分析可行性方案;1、可以添加悬浮按钮。2、自定义导航栏。 添加悬浮按钮,是看起来是比较简单哈,但是感觉不太优雅,会占据页面的空间,体验也不太好。所以想了下第二种方案,自定义导航栏既可以实现产品的需求还可以满足UI的设计美感,在顶部空白处加上返回首页的按钮,这样和返回按钮还对称(最终如图所示,顶部导航栏是个背景图片,分两块组合起来)。 实现方案 一、实现的前提 1、首先查看文档,看文档里关于自定义导航栏是怎么规定的,有哪些限制;还有小程序自定义导航栏全局配置和单页面配置的微信版本和调试库的最低支持版本。 2、在app.json window 增加 navigationStyle:custom ,顶部导航栏就会消失,只保留右上角胶囊状的按钮,如何修改胶囊的颜色呢;胶囊体目前只支持黑色和白色两种颜色 在app.josn window 加上 “navigationBarTextStyle”:“white/black” 3、还要考虑加返回按钮和返回首页的按钮,适配不同的机型 先说下两种配置方法: ①全局配置navigationStyle: 调试基础库>=1.9.0 微信客户端>=6.6.0 app.json [代码]{ "usingComponents": { "navigationBar": "/components/navigationBar/navigationBar" }, "window": { "navigationStyle": "custom" } } [代码] ②单页面配置navigationStyle 调试基础库>=2.4.3 微信客户端版本>=7.0.0 自定义的页面.json [代码]{ "window": { "navigationStyle": "default" } } { "navigationStyle": "custom", "usingComponents": { "navigationBar": "/components/navigationBar/navigationBar" } } [代码] 两者的区别就是,全局配置放在app.json文件里,单页面配置放在自定义页面配置文件里。 二、实现的步骤 以下说下几个要点: 1、自定义导航栏文本,是否显示返回,是否显示返回首页,导航栏高度 2、statusBarHeight,用来获取手机状态栏的高度,这个需要在全局app.js中的onLaunch,调用wx.getSystemInfo获取,navigationBarHeight+默认的高度,这个是设定整个导航栏的高度, 3、还有注意的,在写样式距离和大小时建议都用px,因小程序右边的胶囊也是用的px,不是rpx。 4、因为自定义导航栏每个页面都要写,所以把导航栏封装了公共组件,这样只需要在每个页面引入即可。 如下是封装的导航栏组件: wxml [代码]<view class="navbar" style="{{'height: ' + navigationBarHeight}}"> <view style="{{'height: ' + statusBarHeight}}"></view> <view class='title-container'> <view class='capsule' wx:if="{{ back || home }}"> <view bindtap='back' wx:if="{{back}}"> <image src='/images/back.png'></image> </view> <view bindtap='backHome' wx:if="{{home}}"> <image src='/images/home.png'></image> </view> </view> <view class='title'>{{text}}</view> </view> </view> <view style="{{'height: ' + navigationBarHeight}};background: white;"></view> [代码] 这里有个需注意的问题,就是一般会出现自定义导航栏,下拉页面,导航栏也随着会下拉,这种问题是因为设置fixed后页面元素整体上移了navigationBarHeight,所以在此组件里设置一个空白view元素占用最上面的navigationBarHeight这块高度 wxss [代码].navbar { width: 100%; background-color: #1797eb; position: fixed; top: 0; left: 0; z-index: 999; } .title-container { height: 40px; display: flex; align-items: center; position: relative; } .capsule { margin-left: 10px; height: 30px; background: rgba(255, 255, 255, 0.6); border: 1px solid #fff; border-radius: 16px; display: flex; align-items: center; } .capsule > view { width: 45px; height: 60%; position: relative; .capsule > view:nth-child(2) { border-left: 1px solid #fff; } .capsule image { width: 50%; height: 100%; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); } .title { color: white; position: absolute; top: 6px; left: 104px; right: 104px; height: 30px; line-height: 30px; font-size: 14px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } [代码] js [代码]const app = getApp() Component({ properties: { text: { type: String, value: 'Wechat' }, back: { type: Boolean, value: false }, home: { type: Boolean, value: false } }, data: { statusBarHeight: app.globalData.statusBarHeight + 'px', navigationBarHeight: (app.globalData.statusBarHeight + 44) + 'px' }, methods: { backHome: function () { let pages = getCurrentPages() wx.navigateBack({ delta: pages.length }) }, back: function () { wx.navigateBack({ delta: 1 }) } } }) [代码] json [代码]{ "component": true, "usingComponents": {} } [代码] 最终还需要考虑下版本兼容的问题,毕竟还有一些用户,微信版本并没有更新到最新版本。 首先可以在app.js里面获取下当前用户的微信版本,做下版本比较,如果小于这个版本,设置个全局变量,也可以在组件写个方法,在不同的页面打开显示不同的顶部导航栏,或者可以控制是否显示导航栏,这里就不详细说了。 亲自试了下,在低于7.0版本的微信中,如果采用单页面自定义导航栏,会出现两个导航栏,这时候通过判断版本号不要再渲染自定义的导航栏组件了,在页面的配置文件里写上title名,还有相应的背景色,这样就会显示自带的导航栏了。 总结 小程序开发是有些坑的地方,从不支持自定义导航栏,到支持全局自定义导航栏,再到现在的支持单页面配置,可以看出在慢慢完善。还有底部tabbar,可自己选择配置的太少了,虽然也支持自定义,但是发现自定义写的底部导航组件体验并不好,每次打开页面都会重新渲染底部的按钮,如果全部写成在一个页面里的tab切换,虽然按钮每次不用重新加载了,但是业务多肯定不行,写到一个单页面里东西也太多了。 希望微信能够多添加或放开一些功能,让开发者更好的服务于产品,给用户更好的体验。
2019-06-22 - 小程序页面吸顶效果、右下角悬浮按钮等隐藏显示切换时不卡顿的实现方法
使用的api及页面方法 api:wx.createSelectorQuery、wx.createIntersectionObserver 页面方法:onPageScroll 为什么使用以上方法? wx.createSelectorQuery:主要解决页面渲染后保证所涉及的元素能百分百渲染到屏幕上,这里打包一个异步方法。 [代码]getElement(elm, component) { const _this = this; return new Promise((resolve, reject) => { let ss = setInterval(function() { let Qy = component ? _this.createSelectorQuery() : wx.createSelectorQuery(); let a = Qy.select(elm).boundingClientRect(function(res) { if (res) { clearInterval(ss) resolve(res) } }).exec(); }, 50); }); } [代码] wx.createIntersectionObserver与onPageScroll的作用: 单纯使用onPageScroll切换隐藏显示状态必然会高频率使用setData导致页面卡顿。如果只是在wx.createIntersectionObserver与onPageScroll中隐藏或者显示,即确保每个方法中只setData一次,那么卡顿的现象就不会出现。 以下wx.createIntersectionObserver仅作显示元素 [代码]onCreateIntersectionObserver(component,elm) { const _this = this; this.getElement(elm||".tr-fixed", component).then(res => { _this.setData({ fixed_top: res.top //记录一直显示的元素的top值 }); _this.IntersectionObserver = component ? _this.createIntersectionObserver() : wx.createIntersectionObserver() _this.IntersectionObserver.relativeTo(".top-transparent", { bottom: 2 }).observe(elm||'.tr-fixed', (res) => { //显示吸顶 const { fixed_show } = _this.data; if (fixed_show === false) { _this.setData({ fixed_show: true }); } //显示吸顶 }) }); } [代码] 上面代码中: .top-transparent是自定义参照区域。 .tr-fixed或elm为切换隐藏与显示的元素(事先写好顶部浮动,隐藏起来,这里并没有css仅作为监听对象) wxml基本代码: [代码]<view class="top-transparent">页面顶部透明参照元素</view> <view class="tr-fixed">一直显示的部分(滚动的scrollTop小于此元素的top值则隐藏,如果监测到与透明的参照元素交叉则显示)</view> <view class="fixed-view" wx:if="{{fixed_show}}">隐藏的部分(与一直显示的部分一模一样)</view> [代码] [代码].top-transparent{ position: fixed; top: 0; left: 0; right: 0; height: 20px; background: transparent;//透明 pointer-events: none; //保证此元素所有点击事件无效,即点击事件都穿透到比它层级低的元素上 } [代码] 以下onPageScroll仅作隐藏元素 [代码]onPageScroll(e) { const { fixed_top, fixed_show } = this.data // 隐藏吸顶头部 if (fixed_top != undefined && fixed_show) { if (e.scrollTop <= fixed_top) { this.setData({ fixed_show: false }) } } // 隐藏吸顶头部 }, [代码] 代码片段: https://developers.weixin.qq.com/s/oUhsfCmP76au
2019-08-14 - 小程序内用户帐号登录规范调整和优化建议
为更好地保护用户隐私信息,优化用户体验,平台将会对小程序内的帐号登录功能进行规范。本公告所称“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。具体规范要求如下: 1.服务范围开放的小程序 对于用户注册流程是对外开放、无需验证特定范围用户,且注册后即可提供线上服务的小程序,不得在用户清楚知悉、了解小程序的功能之前,要求用户进行帐号登录。 包括但不限于打开小程序后立即跳转提示登录或打开小程序后立即强制弹窗要求登录,都属于违反上述要求的情况; 以下反面示例,在用户打开小程序后立刻弹出授权登录页; [图片] 建议修改为如下正面示例形式:在体验小程序功能后,用户主动点击登录按钮后触发登录流程,且为用户提供暂不登录选项。 [图片] 2.服务范围特定的小程序 对于客观上服务范围特定、未完全开放用户注册,需通过更多方式完成身份验证后才能提供服务的小程序,可以直接引导用户进行帐号登录。例如为学校系统、员工系统、社保卡信息系统等提供服务的小程序; 下图案例为正面示例:校友管理系统,符合规范要求。 [图片] 3.仅提供注册功能小程序 对于线上仅提供注册功能,其他服务均需以其他方式提供的小程序,可在说明要求使用帐号登录功能的原因后,引导用户进行帐号注册或帐号登录。如ETC注册申请、信用卡申请; 如下反面示例,用户在进入时未获取任何信息,首页直接强制弹框要求登录注册ETC,这是不符合规范的。 [图片] 建议修改为如下正面示例所示形式:允许在首页说明注册功能后,提供登录或注册按钮供用户主动选择点击登录。 [图片] 4.提供可取消或拒绝登录选项 任何小程序调用帐号登录功能,应当为用户清晰提供可取消或拒绝的选项按钮,不得以任何方式强制用户进行帐号登录。 如下图所示反面示例,到需要登录环节直接跳转登录页面,用户只能选择点击登录或退出小程序,这不符合登录规范要求。 [图片] 建议修改为下图正面示例形式,在需帐号登录的环节,为用户主动点击登录,并提供可取消按钮,不强制登录。 [图片] 针对以上登录规范要求,平台希望开发者们能相应地调整小程序的帐号登录功能。如未满足登录规范要求,从2019年9月1日开始,平台将会在后续的代码审核环节进行规则提示和修改要求反馈。
2019-07-20 - 浅谈微信抽奖小程序!
说抽奖小程序之前,先整体了解下抽奖这一核心。从以下六方面分析 抽奖心理:抽奖在想什么? 抽奖假象:越多越多? 抽奖玩法:抽奖本质,能不能解决? 抽奖变现:或许天上真的会掉馅饼,但还是钱好 抽奖机会:还有没有必要做一个抽奖小程序? 抽奖意义:我们有一个锦鲤梦,游戏人生 抽奖是一个利他利己的行为,一来增加自己的中奖率,二来传播也为他人制造中奖机会。通过人与人之间的信任关系,间接为发起人活动背书,随之品牌营销,低成本的流量曝光。 哪里有福利,哪里有免费,哪里就有流量! 谁都可以参与,谁都可以发起。 一、抽奖心理你是否懂参与者,他们分别的诉求是什么? ▍抽奖者:我能中?有多少人参与了,什么时候开奖,及时通知开奖结果,活动真实性?简单易懂,奖品吸引力。 ▍发起者:怎么玩?才能更好的推广自己/公众号增粉/品牌服务/产品特性等,用户精不精准? 先明确身份(普通玩家,品牌方,运营,线下商家等) 再次挖掘需求(打品牌广告,提升销售?),工具是否支持 设置门槛,扩大影响力,ROI 上手教程(需求场景解决方案,与产品匹配) [图片] 二、抽奖假象▍抽奖越多越好? 听说现在有几百款抽奖小程序了,生存状况如何?先说出3个,有何差异? ▍玩法越多越好? 看到复杂的玩法,一方面难受,一方面欣慰 难受,大概率不会去玩 欣慰,营销玩法还挺丰富的,需求与利益结合 ▍功能越多越好? 如何把用户“困”在小程序,是对“美好生活”的向往? 后来才明白一句“你说你,想要逃,偏偏注定要……” 化简为繁,一蹴而就,谁在可歌可泣! 三、抽奖玩法简单来说,大的方向也只需要解决这三步。 提供奖品 开奖条件 发放奖品 [图片] ▍提供奖品: 实物,红包,知识付费等。 ▍开奖条件: 默认到达时间自动开奖,其次按人数,即开即中。 弱化开奖条件,因为一般都是按时间开奖。为用户提供最佳的解决方法,而非,先熟悉整个游戏规则,展示全部条件,全部玩法等。 ▍发奖方式: 发起者联系:让中奖人填写地址,电话,微信号等,适合快递实物,或添加中奖人再说明。 中奖人联系:留下发起人的手机,微信,二维码等,适合发放优惠券,红包等虚拟物品。 延伸: 单独自定义给中奖者一句话 一般平台自带文案,如果发起者还能单独设置一句,相当于多了一次互动营销机会,凸显人格化。 延长中奖人填写中奖信息时间 比如遇到节假日,参与人中奖人太多等。 ▍分享按钮: 是否允许参与者分享本次抽奖活动。之前「抽奖助手」把这个功能放在了“更多功能”里面,默认不开启,有的小程序选择了默认开启,我更倾向于前者。 在心理上,用户并不一定需要他人随意分享,他的赞助奖品。 在操作上,手动开启代表用户默认同意,手动关闭代表防止止损。 所以,应该把选择选择权交给用户,而不是替用户选择。让用户的利益与平台背后的利益一起成长,也多了裂变小程序的条件,多赢。在用户的关系链分享,垂直密集,越是喜欢抽奖的人,越会分享给抽奖的人,相互吸引。 有前置这个功能的必要。 ▍其他玩法: 线下活动/线上直播/年会抽奖玩法: 突出:公司,logo,主题,倒序抽奖 担心:繁琐,手忙脚乱,冷场,多人操作 在线互动——投屏,弹幕,背景音乐,活跃氛围 后台规则——简单,倒序(手动逐个开奖),重抽 延伸:摇一摇功能(摇越快,中奖率越高) 上滑抽奖:提示上拉,看下个奖品(固定栏展示:不断陷入) 参与条件:如性别,地区,(实名),达到一定抽奖次数,输入指定口令参与等 头像玩法: 列表:头像,昵称,码,中奖概率,排名(突出榜单,中奖概率) 宫格:头像,更多(一般都是这种,突出活动受欢迎) ▍小插曲:关于抽奖背景图片 如果平台能针对节假日提供2-3张背景图,不是更有助于发起活动吗?在每次小程序迭代上新,去旧图片。 这个小设计,花费不大,又让用户觉得产品很用心。 [图片] 四、抽奖变现自有广告:上首页(类似每日福利,自助福利) 增值服务:付费版(去除广告,突破参与人数限制,公号仅粉丝参与等) 电商合作:合作电商(购买商城东西当抽奖礼物),第三方电商优惠券导购等 互选广告:保证小程序内广告的调性,视觉统一性,比如开学期某当的的图书满减活动,做用户尽可能有兴趣的广告,增加了广告主的曝光/跳转/转化,成为平台的案例级,再次吸引广告主。位置:banner,开奖后短文案广告,商城广告等 官方广告:如先看视频后抽奖等 五、抽奖机会说抽奖就想到……那其他抽奖小程序还有机会吗? 头部小程序在教育你,我(品牌)等于抽奖(品类),稳固自己的地位,防守 做不了头部,后来者怎么办? 占据特性 垂直聚焦 开创品类 微商,公众号主,电商,专属游戏周边,线下门店等成了多款抽奖小程序的突破口。 不同人群的喜好,行为,用户社交关系链的特征不一样,复制之前的成功模式到自己的行业,有一定的借鉴意义。 谁能比较优雅、灵活地解决掉对应场景存在的问题,谁能满足差异化需求,打消参与顾虑,自然流量/广告主纷涌而至。 以下举3个例子 01. 点赞抽奖 公众号粉丝互动:点赞就能参与,抽红包 行业KOL & 节点效应:或许你喜欢的公号发起了一个活动,因为他的背书,而积极参与。如何帮助公众号主完成与粉丝的互动,做成案例,由此扩散,吸引一个又一个的公众号主,重合度引爆。这种推广方式,好过工具方主动介绍。 [图片] 02.锦鲤圈 京东平台导流:店铺抽奖 与 用户跳转到“京东购物”小程序店铺提升中奖率 通过锦鲤奖+人气奖,双福利邀好友,关注店铺,浏览该店铺商品,提升中奖率,多入口跳转到京东店铺。(方法很京东,也有小程序通过试玩应用,签到等任务来提升抽奖币,限定消耗抽奖币参与,换购等电商) [图片] 03.华硕+ 品牌宣传,拉新会员,导流门店 成为会员无门槛抽奖。导流到线下门店(线下门店优惠券),门店自助发布活动管理等 [图片] 不同的产品决定了其走向,有的是平台主导权,有的擅长调性,研究人性,有的擅长运营增长,有的正利用不公平,谁是分享王福利就向你倾斜。 抽奖再怎么玩法多样,用户有他自己的判断,同化的用户留下来! 六、抽奖意义如果说抽奖有什么社会意义,解决供需双方之间的需求。在诞生的那一刻,给我们带来短期或长期的价值,降低推广成本,短时间获取用户,尽管用户用完就跑…… 不管我们做什么,要提供给用户价值和好玩的东西,做对小程序本身有价值的事情。 感谢围观小程序测评系列《抽奖小程序》,作为小程序测评师,希望给大家带来一些启发,一起推送小程序更好地发展。
2019-09-04 - 8个月三轮融资近亿元,「小名片」如何挖掘微信商务社交潜力
作者:王隐 “我没有告诉您电话号码,也能打过来吗?” 在和「小名片」CEO杨芳贤微信语音时,由于网速不好,所以换成电话采访,但我并没有告诉他电话号码,杨芳贤还是拨了过来。 “上次我在微信上发名片给你,已经实现了名片交换 ,也就互留了电话号码。” 这是「小名片」比较典型的使用场景之一。 在加杨芳贤好友时,我在查看他名片的同时也创建了自己的名片,而这张名片会自动存在他的名片夹中,他可以直接在小程序中找到我的联系方式。 [图片] 名片一直都是小程序比较火爆的赛道,吸引了不少创业者,包括「加推」「递名片」等都获得了数额不菲的融资。「小名片」也是其中之一,量子程序请到了「小名片」CEO杨芳贤,和他聊了聊名片创业的诸多感悟。 [图片] 69天突破1000万用户, 产品逻辑大不同 此前, 杨芳贤一直在做IBOS协同办公平台,主打企业内部的协同办公。 2017年1月,杨芳贤开发的小程序「群应用」正式上线,仅69天就突破了1000万用户,发展非常迅猛。这让杨芳贤看到了小程序的巨大潜力。之后,to B的「小名片」诞生。 在杨芳贤看来,每一张名片背后都是一家企业,商务潜力非常大。“小程序寄生在微信上,微信已超10亿月活,几乎所有的商务人士都在微信上,借助微信的社交关系,名片可以快速地覆盖到大多数商业人士。” 名片,是切入to B市场的绝佳入口。 当时已上线的名片小程序不只「小名片」一家,但杨芳贤看来,自己的产品逻辑和别人不太一样。比如「小名片」的设计更加拟物化,微信中转发出去的小程序气泡卡片正好是纸质名片的样式,展示了姓名、电话,公司、职位等基础信息。 [图片] 更有特色的是「小名片」独有的指标体系。在转发出去的界面中,拟物化的卡片下方还有三个带有社交属性的数值系统:人气、收藏、赞,来增强用户的参与感和成就感。 数值系统设计精巧,非常巧妙地击中了商务社交场景中微妙的信任和攀比心理。人气越高、收藏阅读,是不是就意味着这个人的人脉更加广泛,和他做商业成功可能性更高?答案是肯定的。 所以杨芳贤发现,大量「小名片」用户通过四处转发、交换名片及点赞来提升自己名片上的社交数值,就像一个人的手机号,QQ号一样,使用时间越长,等级越高越值得信赖。每一个用户都成了「小名片」的推销员,雪球也就越滚越大。 值得一提的是,「小名片」的种子用户很多来自杨芳贤的上一个IBOS产品。这些早期积累的IBOS用户不仅来自互联网行业,还有很多传统企业,地域上除了一线城市,也跨越了很多二三线城市,所以「小名片」的用户画像就不仅限于某个地域和圈层,而是通过更广范围的渗透,进入各个行业,大大拓宽了自身的用户群。 [图片] 中国的SaaS为什么没做起来? 提升效率是痒点,营销获客才是痛点 “在中国做SaaS,如果仅仅是为中小微企业提升效率,往往叫好不叫座。” 在杨芳贤看来,微信群能解决的问题,为什么要用管理软件呢?很多小微企业可能就几个人,老板抬头一看就看明白了,用专业的汇报软件反而降低了工作效率。 对于海量的中小微企业,营销、展示、获客是痛点,是刚需,而提升效率只是痒点。 杨芳贤自述:“我发个人名片,往往是因为在线下或微信群已经跟他接触过了。只不过在群里,大家业务互相不了解, 我不知道你是做什么的,如果名片上能展示企业信息就完美了。” 「小名片」带有的销售雷达可以获得用户的访问轨迹及关注点,更精准地知道客户意愿,从而让销售人员主动出击。 [图片] “在一个500人的行业交流群中,你发一段公司介绍,可能只有50个人看到你的信息,有30个人对你感兴趣,但可能只有一个人联系你,这样的获客效率只有千分之二。 而如果把「小名片」丢到群里,「小名片」带有的销售雷达可以告诉销售人员,群里面是哪50个人查看过你的信息,然后哪30个人对你非常感兴趣,甚至可以告诉你,他对哪个产品,案例感兴趣。” 这样一来,「小名片」可以帮助用户在微信群里筛选客户,销售人员可以不再像以前那样盲目,可以针对这30个对你感兴趣的用户,主动跟他联系。 杨芳贤还为我们讲了一个「小名片」线下门店经典案例。 有一个客户做体重管理,他以前是一家国内知名健康管理集团的总裁助理,在北京,他开了好几家线下店。 2018年国庆,他当时去找工厂订了一批体脂秤。体脂秤其实成本很低,只要十几块钱一个,他最早是想通过做活动赠送,你只要到店我就送体脂秤给你。但这么做,1万个体脂秤只能带来1万名到店潜在客户。 而他用了「小官网」营销砍价功能后,定价218的体脂秤,邀请亲朋好友来砍价,每砍一刀可能砍个10块8块,最后一个体脂秤8块8毛就能拿到。 用户通过砍价有很强的参与感,线上支付后,到店的人数远超他赠送的体脂秤数量,并且到店用户的购买率非常高,最后营收了400多万,而他付出的活动成本只是「小官网」上5000块的使用费用。 「小名片」还打造了自己的产品矩阵,包括个人名片「群应用」,全员名片「小名片」,企业名片「小官网」,门店名片「随手逛」,产品矩阵拥有3000多万用户,覆盖企业用户超50万。 目前,「小名片」盈利主要来自付费版用户。 「小名片」的展示功能免费,但如果用到营销插件、销售雷达、以及留存客户的SCRM功能,可以直接在线上付费订阅。 [图片] 杨芳贤告诉我们,一个20人的企业订阅「小名片」,一年费用大概在五六千块钱。复购率方面,2018年10月第一批客户复购率接近70%。 目前,「小名片」团队有100多人,年收入不但可以cover掉公司的运营支出,并有盈利。在小程序创业团队里面,实属不易。 [图片] 名片跟风为哪般? 小程序名片的社交场景属性很强,2017年、2018年都大火过一阵,市面上一时间曾涌现出100多个同类型产品。 在杨芳贤看来,如此众多的名片产品,最后胜出的团队一定是对商务社交有着自己独到理解的。他把名片小程序的从业者分为三类: 一是all in赛道的团队,他们有用户,也拿到了巨额融资; 二是一些传统的SaaS厂商,他们入局更多是防御性,防止小程序名片从SRCRM切走自己客户; 三是跟风进来的中小团队,先简单的跟上风口,走一步看一步。 [图片] 层出不穷的竞争者中,「小名片」的核心价值何在? 入局早,积累的海量用户及沉淀下来的商务关系是「小名片」的核心壁垒,这是杨芳贤的答案。 2017年入局,当时「小名片」享受到了小程序红利,而如今红利消失,从零再做到100万用户级的名片小程序并不容易,「小名片」接下来要做的就是将积累下来的用户运营好,并将他们从用户转化为客户。 「小名片」也有自己的“代理商”,不过杨芳贤更愿意称他们为“服务商”,因为小名片会将已有的存量用户开放给服务商,与服务商一起将存量用户转换成客户。 [图片] 没有刚需场景、没有商业闭环的小程序 可能活不过这个冬天 尽管2018年的经济形势不是太好,但「小名片」过去的8个月里还是拿到了三轮总额近亿元融资,投资方为北大创投、泰有资本、洪泰资本、基因资本、考拉资本多家知名投资机构。 金沙江创投朱啸虎认为,小程序流量红利只有2018年一年。杨芳贤并不认可,他认为“小程序真正的流量红利只有2017年,如果说把小游戏当成小程序的一个类目,那2017年和2018年上半年都有红利。” 而在展望2019年时,杨芳贤反复提到了场景的重要性。 “一定要有刚需的场景,如果这个场景以前在线上或线下相对低效,但通过你的小程序,它能明显地提升效率,或者比过去做了很好的延伸,这样的话我觉得有机会。” 面对层出不穷的“追随者”们,杨芳贤认为构建商业闭环尤为重要。 “如果你没有商业闭环,这个事情太容易被复制了,从长远的角度来看,它也没有价值,因为小程序的用户不是你的。 ” 在杨芳贤看来,微信小程序的用户要的是解决他的问题,用户今天用了九宫格中任何一个小程序,明天微信换成另外一个,用户是无感的。并且小程序所有的场景入口都在微信里面,用户只认微信这个平台。 但如果用户付费成了你的客户,这时你提供的就不仅仅是简单的功能,而是有价值的服务,客户和你之间就形成了深度连接,没那么容易绕过你,你就有机会利用小程序成就自己。
2019-02-21 - 一个简单的抽奖转盘游戏
在一个项目中要做一个游戏,在这个过程中做了一个简单的9宫格抽奖游戏。大体思路是,点击开始按钮,游戏开始。由一个逐步递增参数 drawStep 来控制格子的背景颜色的改变,游戏停止的位置参数 stopPosition 由随机函数生成,以此来控制格子停止的位置。游戏转动一圈是8个格子,5圈就是40.easeTime参数模拟格子转动的缓步启动和缓步停止。 [代码]<!doctype html> <html> <head> <meta charset="gbk"> <title>简单抽奖游戏</title> <style type="text/css"> *{margin:0; padding:0;} table{width:400px; height:500px; margin:0 auto; text-align:center;} td{border:1px solid #900;} .run{background:#F00;} </style> </head> [代码] [代码]<body> <table> <tr><td class="gif_icon gif_0">奖品1</td><td class="gif_icon gif_1">奖 品2</td><td class="gif_icon gif_2">奖品3</td></tr> <tr><td class="gif_icon gif_7">奖品8</td><td><button>开始 </button></td><td class="gif_icon gif_3">奖品4</td></tr> <tr><td class="gif_icon gif_6">奖品7</td><td class="gif_icon gif_5">奖 品6</td><td class="gif_icon gif_4">奖品5</td></tr> </table> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"> </script> <script type="text/javascript"> $(function(){ var drawTimer=null; var drawStep=-1; var easeTime=2;//缓动计时 var stopPosition=1; $('button').click(function(){ stopPosition=Math.floor(Math.random()*8+1); drawTimer=setTimeout(drawRun,easeTime*100); }); function drawRun(){//抽奖游戏滚动 if(drawStep>= ( 40+stopPosition ) ){ $('.gif_'+(drawStep%8)).css('background','#f00'); drawStep=stopPosition; easeTime=2; msg(drawStep+1); clearTimeout(drawTimer); return; } if(drawStep>=(36+stopPosition)){ easeTime+=1; }else{ if(easeTime<=2){ easeTime=2; } easeTime--; } drawStep++; $('.gif_icon').each(function(index){ $(this).css('background','#fff'); }); $('.gif_'+(drawStep%8)).css('background','#f00'); drawTimer=setTimeout(drawRun,easeTime*70); } function msg(num){ alert('恭喜您抽中了奖品'+num); } }) </script> </body> </html> </pre> [代码] 代码演示:http://github.crmeb.net/u/LXT
2019-08-12 - 微信小程序评论/留言功能,附:前端+后端代码+视频讲解!
前端界面:[图片] [图片] [代码]<!-- 表单 -->[代码][代码]<[代码][代码]form[代码] [代码]bindsubmit[代码][代码]=[代码][代码]"formSubmit"[代码][代码]>[代码][代码]<[代码][代码]input[代码] [代码]type[代码][代码]=[代码][代码]"text"[代码] [代码]name[代码][代码]=[代码][代码]"liuyantext"[代码] [代码]placeholder[代码][代码]=[代码][代码]'输入留言内容'[代码] [代码]class[代码][代码]=[代码][代码]"input-style"[代码][代码]/>[代码][代码]<[代码][代码]button[代码] [代码]formType[代码][代码]=[代码][代码]"submit"[代码] [代码]class[代码][代码]=[代码][代码]"btn"[代码] [代码]wx:if[代码][代码]=[代码][代码]"{{nickName == empty}}"[代码] [代码]open-type[代码][代码]=[代码][代码]"getUserInfo"[代码] [代码]bindgetuserinfo[代码][代码]=[代码][代码]"bindGetUserInfo"[代码] [代码]bindtap[代码][代码]=[代码][代码]'login'[代码][代码]>授权登录</[代码][代码]button[代码][代码]>[代码][代码]<[代码][代码]button[代码] [代码]formType[代码][代码]=[代码][代码]"submit"[代码] [代码]class[代码][代码]=[代码][代码]"btn"[代码] [代码]wx:else>留言</[代码][代码]button[代码][代码]>[代码][代码]<[代码][代码]input[代码] [代码]type[代码][代码]=[代码][代码]"text"[代码] [代码]name[代码][代码]=[代码][代码]"nickname"[代码] [代码]value[代码][代码]=[代码][代码]'{{nickName}}'[代码] [代码]style[代码][代码]=[代码][代码]"display:none;"[代码][代码]/>[代码][代码]<[代码][代码]input[代码] [代码]type[代码][代码]=[代码][代码]"text"[代码] [代码]name[代码][代码]=[代码][代码]"headimg"[代码] [代码]value[代码][代码]=[代码][代码]'{{avatarUrl}}'[代码] [代码]style[代码][代码]=[代码][代码]"display:none;"[代码][代码]/>[代码][代码]</[代码][代码]form[代码][代码]>[代码] [代码]<[代码][代码]view[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{re}}"[代码] [代码]wx:key[代码][代码]=[代码][代码]"re"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"result"[代码][代码]>{{item.result}}</[代码][代码]view[代码][代码]>[代码][代码]</[代码][代码]view[代码][代码]>[代码][代码]<[代码][代码]view[代码] [代码]style[代码][代码]=[代码][代码]"text-align:center;font-size:14px;color:#ccc;margin-top:20px;"[代码][代码]>以下是留言内容</[代码][代码]view[代码][代码]>[代码][代码]<[代码][代码]view[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{liuyanlist}}"[代码] [代码]wx:key[代码][代码]=[代码][代码]"liuyanlist"[代码] [代码]class[代码][代码]=[代码][代码]"liuyanview"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"headimg"[代码][代码]><[代码][代码]image[代码] [代码]src[代码][代码]=[代码][代码]"{{item.headimg}}"[代码][代码]></[代码][代码]image[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"nickname_liuyantext"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"nickname"[代码][代码]>{{item.nickname}} <[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"time"[代码][代码]>{{item.lytime}}</[代码][代码]view[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"text"[代码][代码]>{{item.liuyantext}}</[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<!-- 占位符 -->[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]style[代码][代码]=[代码][代码]"width:100%;height:10px;"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码]</[代码][代码]view[代码][代码]>[代码] [代码]//index.js[代码][代码]//获取应用实例[代码][代码]const app = getApp()[代码][代码]Page({[代码][代码] [代码][代码]/**[代码][代码] [代码][代码]* 页面的初始数据[代码][代码] [代码][代码]*/[代码][代码] [代码][代码]data: {[代码][代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]//授权登录[代码][代码] [代码][代码]login: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]// 查看是否授权[代码][代码] [代码][代码]wx.getSetting({[代码][代码] [代码][代码]success(res) {[代码][代码] [代码][代码]if[代码] [代码](res.authSetting[[代码][代码]'scope.userInfo'[代码][代码]]) {[代码][代码] [代码][代码]// 已经授权,可以直接调用 getUserInfo 获取头像昵称[代码][代码] [代码][代码]wx.getUserInfo({[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log(res.userInfo),[代码][代码] [代码][代码]that.setData({[代码][代码] [代码][代码]nickName: res.userInfo.nickName,[代码][代码] [代码][代码]avatarUrl: res.userInfo.avatarUrl,[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]bindGetUserInfo(e) {[代码][代码] [代码][代码]console.log(e.detail.userInfo)[代码][代码] [代码][代码]},[代码] [代码] [代码][代码]formSubmit: [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'已留言'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'success'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]liuyantext = e.detail.value.liuyantext; [代码][代码]//获取表单所有name=liuyantext的值 [代码][代码] [代码][代码]var[代码] [代码]nickName = e.detail.value.nickname; [代码][代码]//获取表单所有name=nickName的值 [代码][代码] [代码][代码]var[代码] [代码]headimg = e.detail.value.headimg; [代码][代码]//获取表单所有name=headimg的值 [代码][代码] [代码][代码]wx.request({[代码][代码] [代码][代码]url: [代码][代码]'http://localhost/liuyanserver/liuyan.php?liuyantext='[代码] [代码]+ liuyantext + [代码][代码]'&nickname='[代码] [代码]+ nickName + [代码][代码]'&headimg='[代码] [代码]+ headimg,[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]liuyantext,[代码][代码] [代码][代码]nickName,[代码][代码] [代码][代码]headimg[代码][代码] [代码][代码]},[代码][代码] [代码][代码]header: { [代码][代码]'Content-Type'[代码][代码]: [代码][代码]'application/json'[代码] [代码]},[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log(res.data)[代码][代码] [代码][代码]that.setData({[代码][代码] [代码][代码]re: res.data,[代码][代码] [代码][代码]})[代码][代码] [代码][代码]wx.hideToast();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码] [代码] [代码][代码]onPullDownRefresh: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]wx.showNavigationBarLoading();[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码] [代码][代码]wx.request({[代码][代码] [代码][代码]url: [代码][代码]'http://localhost/liuyanserver/loadliuyan.php'[代码][代码],[代码][代码] [代码][代码]headers: {[代码][代码] [代码][代码]'Content-Type'[代码][代码]: [代码][代码]'application/json'[代码][代码] [代码][代码]},[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//将获取到的json数据,存在名字叫list的这个数组中[代码][代码] [代码][代码]that.setData({[代码][代码] [代码][代码]liuyanlist: res.data,[代码][代码] [代码][代码]//res代表success函数的事件对,data是固定的,liuyanlist是数组[代码][代码] [代码][代码]})[代码][代码] [代码][代码]// 隐藏导航栏加载框[代码][代码] [代码][代码]wx.hideNavigationBarLoading();[代码][代码] [代码][代码]// 停止下拉动作[代码][代码] [代码][代码]wx.stopPullDownRefresh();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码] [代码] [代码][代码]//加载最新数据[代码][代码] [代码][代码]onLoad: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码] [代码][代码]wx.request({[代码][代码] [代码][代码]url: [代码][代码]'http://localhost/liuyanserver/loadliuyan.php'[代码][代码],[代码][代码] [代码][代码]headers: {[代码][代码] [代码][代码]'Content-Type'[代码][代码]: [代码][代码]'application/json'[代码][代码] [代码][代码]},[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]//将获取到的json数据,存在名字叫list的这个数组中[代码][代码] [代码][代码]that.setData({[代码][代码] [代码][代码]liuyanlist: res.data,[代码][代码] [代码][代码]//res代表success函数的事件对,data是固定的,liuyanlist是数组[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码] [代码]/**index.wxss**/[代码][代码].input-style{[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]90%[代码][代码];[代码][代码] [代码][代码]height[代码][代码]: [代码][代码]50px[代码][代码];[代码][代码] [代码][代码]border[代码][代码]:[代码][代码]1px[代码] [代码]solid[代码] [代码]#ccc[代码][代码];[代码][代码] [代码][代码]margin[代码][代码]:[代码][代码]10px[代码] [代码]auto[代码][代码];[代码][代码]}[代码] [代码].btn{[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]88%[代码][代码];[代码][代码] [代码][代码]margin[代码][代码]:[代码][代码]5px[代码] [代码]auto[代码][代码];[代码][代码]}[代码] [代码].liuyanview{[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]90%[代码][代码];[代码][代码] [代码][代码]margin[代码][代码]: [代码][代码]10px[代码] [代码]auto[代码][代码];[代码][代码]}[代码] [代码].result{[代码][代码] [代码][代码]text-align[代码][代码]: [代码][代码]center[代码][代码];[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]14px[代码][代码];[代码][代码] [代码][代码]color[代码][代码]: [代码][代码]#f00[代码][代码];[代码][代码] [代码][代码]margin-top[代码][代码]: [代码][代码]20px[代码][代码];[代码][代码]}[代码] [代码].headimg{[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]45px[代码][代码];[代码][代码] [代码][代码]height[代码][代码]: [代码][代码]45px[代码][代码];[代码][代码] [代码][代码]border-radius: [代码][代码]100%[代码][代码];[代码][代码]}[代码] [代码].headimg image{[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]45px[代码][代码];[代码][代码] [代码][代码]height[代码][代码]: [代码][代码]45px[代码][代码];[代码][代码] [代码][代码]border-radius: [代码][代码]100%[代码][代码];[代码][代码]}[代码] [代码].nickname_liuyantext{[代码][代码] [代码][代码]width[代码][代码]: calc([代码][代码]100%[代码] [代码]- [代码][代码]55px[代码][代码]);[代码][代码] [代码][代码]float[代码][代码]: [代码][代码]right[代码][代码];[代码][代码] [代码][代码]margin-top[代码][代码]:[代码][代码]-45px[代码][代码];[代码][代码]}[代码] [代码].nickname_liuyantext .nickname{[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]15px[代码][代码];[代码][代码] [代码][代码]color[代码][代码]: [代码][代码]#999[代码][代码];[代码][代码]}[代码] [代码].nickname_liuyantext .nickname .time{[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]11px[代码][代码];[代码][代码] [代码][代码]color[代码][代码]: [代码][代码]#999[代码][代码];[代码][代码] [代码][代码]float[代码][代码]: [代码][代码]right[代码][代码];[代码][代码]}[代码] [代码].nickname_liuyantext .text{[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]16px[代码][代码];[代码][代码] [代码][代码]color[代码][代码]: [代码][代码]#666[代码][代码];[代码][代码]}[代码] 以上是前端部分 后端有两个文件。 点击下载后端,后端需要修改自己的数据库配置! 视频:https://www.lanzous.com/i1w1deb 后端:https://www.lanzous.com/i1w19pi
2018-09-16 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: 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 - 10行代码实现小程序支付功能!丨实战
前面给大家讲过一个借助小程序云开发实现微信支付的,但是那个操作稍微有点繁琐,并且还会经常出现问题,今天就给大家讲一个简单的,并且借助官方支付api实现小程序支付功能。 传送门: 借助小程序云开发实现小程序支付功能 老规矩,先看本节效果图 [图片] 我们实现这个支付功能完全是借助小程序云开发实现的,不用搭建自己的服务器,不用买域名,不用备案域名,不用支持https。只需要一个简单的云函数,就可以轻松的实现微信小程序支付功能。 核心代码就下面这些: [图片] 一、创建一个云开发小程序 关于如何创建云开发小程序,这里我就不再做具体讲解。不知道怎么创建云开发小程序的同学,可以去翻看腾讯云云开发公众号内菜单【技术交流-视频教程】中的教学视频。 创建云开发小程序有几点注意的 1.一定不要忘记在app.js里初始化云开发环境。 [图片] 2.创建完云函数后,一定要记得上传 二、创建支付的云函数 1.创建云函数pay [图片] [图片] 三、引入三方依赖tenpay 我们这里引入三方依赖的目的,是创建我们支付时需要的一些参数。我们安装依赖是使用里npm 而npm必须安装node,关于如何安装node,我这里不做讲解,百度一下,网上一大堆。 1.首先右键pay,然后选择在终端中打开 [图片] 2.我们使用npm来安装这个依赖。 在命令行里执行 npm i tenpay [图片] [图片] [图片] 安装完成后,我们的pay云函数会多出一个package.json 文件 [图片] 到这里我们的tenpay依赖就安装好了。 四、编写云函数pay [图片] 完整代码如下 [代码]//云开发实现支付 const cloud = require('wx-server-sdk') cloud.init() //1,引入支付的三方依赖 const tenpay = require('tenpay'); //2,配置支付信息 const config = { appid: '你的小程序appid', mchid: '你的微信商户号', partnerKey: '微信支付安全密钥', notify_url: '支付回调网址,这里可以先随意填一个网址', spbill_create_ip: '127.0.0.1' //这里填这个就可以 }; exports.main = async(event, context) => { const wxContext = cloud.getWXContext() let { orderid, money } = event; //3,初始化支付 const api = tenpay.init(config); let result = await api.getPayParams({ out_trade_no: orderid, body: '商品简单描述', total_fee: money, //订单金额(分), openid: wxContext.OPENID //付款用户的openid }); return result; } [代码] 一定要注意把appid,mchid,partnerKey换成你自己的。 到这里我们获取小程序支付所需参数的云函数代码就编写完成了。 不要忘记上传这个云函数。 [图片] 出现下图就代表上传成功 [图片] 五、写一个简单的页面,用来提交订单,调用pay云函数。 [图片] 这个页面很简单: 1.自己随便编写一个订单号(这个订单号要大于6位) 2.自己随便填写一个订单价(单位是分) 3.点击按钮,调用pay云函数。获取支付所需参数。 下图是官方支付api所需要的一些必须参数。 [图片] 下图是我们调用pay云函数获取的参数,和上图所需要的是不是一样。 [图片] 六、调用wx.requestPayment实现支付 下图是官方的示例代码: [图片] 这里不在做具体讲解了,把完整代码给大家贴出来 [代码]// pages/pay/pay.js Page({ //提交订单 formSubmit: function(e) { let that = this; let formData = e.detail.value console.log('form发生了submit事件,携带数据为:', formData) wx.cloud.callFunction({ name: "pay", data: { orderid: "" + formData.orderid, money: formData.money }, success(res) { console.log("提交成功", res.result) that.pay(res.result) }, fail(res) { console.log("提交失败", res) } }) }, //实现小程序支付 pay(payData) { //官方标准的支付方法 wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, //统一下单接口返回的 prepay_id 格式如:prepay_id=*** signType: 'MD5', paySign: payData.paySign, //签名 success(res) { console.log("支付成功", res) }, fail(res) { console.log("支付失败", res) }, complete(res) { console.log("支付完成", res) } }) } }) [代码] 到这里,云开发实现小程序支付的功能就完整实现了。 实现效果 1.调起支付键盘 [图片] 2.支付完成 [图片] 3.log日志,可以看出不同支付状态的回调 [图片] 上图是支付成功的回调,我们可以在支付成功回调时,改变订单支付状态。 下图是支付失败的回调: [图片] 下图是支付完成的状态: [图片] 到这里我们就轻松的实现了微信小程序的支付功能了,是不是很简单啊。 源码地址: https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-08-15 - 完美解决小程序session问题
小程序不像web浏览器有cookie机制,在默认使用cookie存sessionid的机制下,后台将无法正常使用session功能,如果正确使用session呢,提供两个方案。 [代码]1、将sessionid通过url进行传递 用户每次登录成功后将生成的sessionid值使用参数回传到客户端, 客户端接到sessionid后保存到本地, 在发起网络请求的底层接口中默认自动带上sessionid=本地存储的sessionid值。 需要配合服务器一起更改,服务器后端默认使用cookie机制 2、无缝对接cookie, 将服务器的set-cookie值保存到本地,再请求的时候模拟浏览器头部信息并带上保存的cookie信息 1)保存cookie值: _XHR('login',{'code':res.code}).then(function( ret ){ ret.header["Set-Cookie"] != undefined && wx.setStorageSync("cookie", ret.header["Set-Cookie"]); }); 2)请求的时候自动带上cookie信息 var header={}; header = { 'content-type': 'application/x-www-form-urlencoded' }; var cookie = wx.getStorageSync("cookie"); if( url != 'login' && !isNull( cookie ) ){ header['cookie'] = cookie; } 将header 赋值到 request的header内 wx.request({ url: qryDomian + url + '.html', data: _data, method: 'POST', header: header, dataType:'json' ...... 第二种方案服务器无需做任务操作。[代码]
2019-05-20 - getCurrentPages()的用法
getCurrentPages()的用法 getCurrentPages()是个好东西,今天来说说他的用法。 先看看官方文档: [路由 · 小程序]:https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/route.html [图片] getCurrentPages() 函数用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。 简单说,就是可以获取到当前小程序的页面栈 那么,获取到页面栈,有什么用处呢? 1、判断页面栈是否超过10级,超过10级,将不能打开新页面(主要是不用用navigateTo方式打开)。 2、可以修改某个页面栈的data数据,或者方法。 这里给大家分享一个实际应用场景:仅发起者可分享。 有些投票、通知、抽奖、签到等,发起者会在私密的圈子内进行,比如,仅会员群才能参与的抽奖、公司内部的通知公告、班级内部的投票等。发起者是不希望别人分享出去,那么小程序里面要怎么做? 说到分享,在小程序内,应该是想到onShareAppMessage这个方法。只要page.js中有这个方法,不管你是否在内部写了代码逻辑,小程序默认就是可以分享的,如果没有这个方法,小程序右上角的“…”就不会出现“转发”的选项。 问题是,是否允许分享,一般都是小程序内的一个开关设置项,可以看下图: [图片] 用户在加载内容时,需要先从服务端获取到这个开关状态,再决定是否出现“转发”的选项。 此时,我们默认不给page添加onShareAppMessage方法,这样,你转发出去的小程序卡片,别人将无法通过长按进行分享(群聊无法长按分享,私聊还是个坑,看下图)。 [图片] [图片] 然后再动态设置当前page的onShareAppMessage方法,用this,或者getCurrentPages()都能解决,看下图: [图片] 目前私聊的卡片,长按依然可以转发,似乎不是很完美,但是,功能基本实现了。 如果想让私聊卡片的转发无效,你也可以变通一下,比如做个限群成员可见功能,即使私聊卡片被转发,可以判断小程序场景值,不展示内容即可~ 1、用wx.getLaunchOptionsSync() 获取小程序启动时的参数: [图片] 2、判断群聊和私聊的场景值: [图片] 3、如果是微信私聊中打开,给用户提示即可~ [图片] 欢迎各位一起讨论技术问题:mianhuabingbei
2019-02-20 - 微信人脸核身接口能力
一、能力背景 近年来,国家在医疗挂号、APP注册、快递收寄、客运、运营商等多领域规定,需要用户实名才可办理业务,预计后续也会有越来越多的此类法规。因此,微信参照公安部“互联网+”可信身份认证服务平台标准,依托腾讯公司及微信的生物识别技术,建立微信“实名实人信息校验能力” ,即通过人脸识别+权威源比对,校验用户实名信息和本人操作(简称微信人脸核身)。 目前接口限定主体及行业类目开放公测,提供给资质符合要求的业务方,在合适的业务场景内使用。目前仅支持持二代身份证的大陆居民。 由于人脸核身功能涉及到用户的敏感、隐私信息,因此调用此接口的小程序,需要满足一定的条件。即:小程序的主体以及类目,需要在限定的类目范围内,且与小程序的业务场景一致。开展的业务也需要是国家相关法规、政策规定的需要“实名办理”的相关业务(其他未在范围内的业务,则暂不支持)。 以下为接口接入及开发的详细内容。如开发中遇到任何疑问,可以点击此处通过社区反馈,将有工作人员跟进回复。 文档第四部分【再次获取核验结果api】,有助于提高业务方安全性,请务必接入! 现阶段微信人脸核验能力,针对小程序,开放的主体类目范围包含: 小程序一级类目 小程序二级类目 小程序三级类目 使用人脸核验接口所需资质 物流服务 收件/派件 / 《快递业务经营许可证》 物流服务 货物运输 / 《道路运输经营许可证》(经营范围需含网络货运) 教育 学历教育(学校) / (2选1):1、公立学校:由教育行政部门出具的审批设立证明 或 《事业单位法人证书》;2、私立学校:《民办学校办学许可证》与《民办非企业单位登记证书》 医疗 公立医疗机构 / 《医疗机构执业许可证》与《事业单位法人证书》 医疗 互联网医院 / 仅支持公立医疗机构互联网医院(2选1):1、卫生健康部门的《设置医疗机构批准书》;2、 《医疗机构执业许可证》(范围均需含“互联网诊疗”或名称含“互联网医院”等相关内容 医疗服务 三级私立医疗机构 / 仅支持三级以上私立医疗机构,提供《医疗机构执业许可证》、《营业执照》及《医院等级证书》 政务民生 所有二级类目 / 仅支持政府/事业单位,提供《组织机构代码证》或《统一社会信用代码证》。 金融业 银行 / (2选1):1、《金融许可证》; 2、《金融机构许可证》。 金融业 信托 / (2选1):1、《金融许可证》; 2、《金融机构许可证》。 金融业 公募基金 / (4选1):1、《经营证券期货业务许可证》且业务范围必须包含“基金”;2、《基金托管业务许可证》; 3、《基金销售业务资格证书》;4、《基金管理资格证书》。 金融业 证券/期货 / 《经营证券期货业务许可证》 金融业 保险 / (8选1):1、《保险公司法人许可证》;2、《经营保险业务许可证》;3、《保险营销服务许可证》;4、《保险中介许可证》;5、《经营保险经纪业务许可证》;6、《经营保险公估业务许可证》或《经营保险公估业务备案》;7、《经营保险资产管理业务许可证》 ;8、《保险兼业代理业务许可证》。 金融业 消费金融 / 银监会核准开业的审批文件与《金融许可证》与《营业执照》 金融业 汽车金融/金融租赁 / 仅支持汽车金融/金融租赁主体,同时提供:1、《营业执照》(公司名称包含“汽车金融” /“金融租赁”;营业范围包含“汽车金融”/“金融租赁”业务);2、《金融许可证》或银保监会及其派出机构颁发的开业核准批复文件。 交通服务 网约车 快车/专车/其他网约车 (自营性网约车)提供《网络预约出租汽车经营许可证》。(网约车平台)提供与网约车公司的合作协议以及合作网约车公司的《网络预约出租汽车经营许可证》。 交通服务 航空 / (航司)提供《公共航空运输企业经营许可证》。(机场)提供《民用机场使用许可证》或《运输机场使用许可证》。 交通服务 公交/地铁 / 提供公交/地铁/交通卡公司《营业执照》 交通服务 水运 / (船企)提供《水路运输许可证》。(港口)提供《港口经营许可证》 交通服务 骑车 / 仅支持共享单车,提供共享单车公司《营业执照》 交通服务 火车/高铁/动车 / 仅支持铁路局/公司官方,提供铁路局/公司《营业执照》 交通服务 长途汽车 / (2选1):1、《道路运输经营许可证》(经营范围需含客运);2、官方指定联网售票平台(授权或协议或公开可查询文件)。 交通服务 租车 / 运营公司提供《备案证明》与对应公司《营业执照》,且营业执照中包含汽车租赁业务 交通服务 高速服务 / 仅支持ETC发行业务,(2选1):1、事业单位主体,需提供《事业单位法人证书》;2、官方指定的发行单位(一发单位),需提供“官方授权或协议,或公开可查询的文件”; 生活服务 生活缴费 / (供电类)提供《电力业务许可证》与《营业执照》,且《营业执照》且经营范围含供电。(燃气类)提供《燃气经营许可证》与《营业执照》,且《营业执照》且经营范围含供气。(供水类)提供《卫生许可证》与《营业执照》。(供热类)提供《供热经营许可证》与《营业执照》,且《营业执照》且经营范围含供热。 IT科技 基础电信运营商 / (2选1):1、基础电信运营商:提供《基础电信业务经营许可证》;2、运营商分/子公司:提供营业执照(含相关业务范围)。 IT科技 转售移动通信 / 仅支持虚拟运营商,提供《增值电信业务许可证》(业务种类需含通过转售方式提供移动通信业务) 旅游服务 住宿服务 / 仅支持酒店,提供《酒店业特种行业经营许可证》 商业服务 公证 / 仅支持公证处,提供《公证处执业许可证》或《事业单位法人证书》 社交 直播 / (2选1):1、《信息网络传播视听节目许可证》;2、《网络文化经营许可证》(经营范围含网络表演)。 如对以上类目或资质有疑问,可点击参考小程序“非个人主体开放的服务类目”,详细了解小程序开放的服务类目及对应资质。 二、准备接入 (请在小程序发布后,再提交人脸核身接口申请) 满足第一节中描述的类目和主体的小程序,可申请微信人脸核验接口。目前微信人脸核身接口已改为线上自助申请方式,需按照如下图例指引,进行接口申请: 第一步:请通过mp.weixin.qq.com登录小程序账号在后台“功能-人脸核身”的路径,点击开通按钮—— [图片] 第二步:仔细查阅《人脸识别身份信息验证服务条款》后,点击“同意并下一步”—— [图片] 第三步:请正确填写服务信息,并上传该小程序类目下所要求的资质—— [图片] 第四步:请按照业务实际需求填写使用人脸接口的场景和用途—— [图片] 第五步:请完善测试信息和联系人—— [图片] 第六步:提交后请耐心等待1-3个工作日的审核期,审核结果将以站内信通知—— 如申请期间遇到问题,可联系腾讯工作邮箱 wx_city@tencent.com,将会有相关工作人员进一步指引。 三、接口文档: (一)接口描述 名称: wx.startFacialRecognitionVerify(OBJECT) 功能:请求进行基于生物识别的人脸核身 验证方式:在线验证 兼容版本: 一闪:android 微信7.0.22以上版本, iOS 微信7.0.18以上版本 建议在微信官网升级至最新版本 (二)参数说明 1、OBJECT参数说明: 参数 类型 必填 说明 name String 是 姓名 idCardNumber String 是 身份证号码 success Function 否 调用成功回调 fail Function 否 调用失败回调 complete Function 是 调用完成回调(成功或失败都会回调) 2、CALLBACK返回参数 参数 类型 说明 errMsg String 错误信息 errCode Number 错误码 verifyResult String 本次认证结果凭据,第三方可以选择根据这个凭据获取相关信息 注 1:传递用户姓名和身份证有两种方式 业务方没有用户实名信息,用户需要在前端填写身份证和姓名,那么前端直接通过jsapi 调用传递 name 和 idCardNumber。 业务方已经有用户实名信息,后台通过微信提供的 api(详情见文档后面“上传姓名身份证后台 api”)上传用户身份证姓名和身份证,api 返回 user_id_key 作为凭证传给前端,前端再调用 jsapi,用户姓名、身份证信息不需要经过前端,参数只需要传递 userIdKey。Tips:使用该功能需要小程序基础库版本号>=1.9.3。 3、回调结果说明 回调结果请参考以下释义: [图片] [图片] [图片] 4、示例代码 [图片] [图片] (三)上传用户姓名身份证的后台api 1、API说明 1.1说明 业务方上传用户姓名和身份证,获取用户凭证,把凭证给到前端通过 jsapi 调用。 Tips :使用该功能需要小程序基础库版本号>=1.9.3。 1.2请求URL https://api.weixin.qq.com/cityservice/face/identify/getuseridkey?access_token={ac cess_token} 1.3请求方式 POST 2、请求数据格式 [代码]Json { "name" : “张三”, "id_card_number" : "452122xxxxxxx43215" } [代码] 请求示例 [代码]#!/bin/bash TOKEN='xxxxxxxxxxxx' URL='https://api.weixin.qq.com/cityservice/face/identify/getuseridkey' JSON='{ "name": "张三", "id_card_number": "452344xxxxxxxxxxxxx234"}' curl "${URL}?access_token=${TOKEN}" -d "${JSON}" [代码] 参数说明 json 字段 中文显示 是否必传 name 姓名 是 id_card_number 身份证号码 是 out_seq_no 业务方唯一流水号 否 3、返回数据 参数 类 型 说明 errcode int 错误码 errmsg string 错误信息 user_id_key string 用于后台交互表示用户姓名、身份证的凭证 expires_in uint32 user_id_key 有效期,过期需重新获取 [代码]{ "errcode" : 0, "errmsg" : "ok", "user_id_key" : "id_key_xxxx", "expires_in": 3600 } [代码] 4、后台消息推送 如果业务方传入out_seq_no,核身完成后会通过消息推送回调给业务方的服务器,如果回调业务方失败,会在5s尽力推送,超过5s不再推送。 参数说明 参数 类 型 说明 ToUserName string 小程序原始ID FromUserName string 事件消息openid CreateTime uint32 消息推送时间 MsgType string 消息类型 Event string 事件类型 openid string 核身用户的openid out_seq_no string 业务方唯一流水号 verify_result string 核身返回的加密key(凭据) 返回示例 [代码]{ "ToUserName": "gh_81fxxxxxxxx", "FromUserName": "oRRn15NUibBxxxxxxxxx", "CreateTime": 1703657835, "MsgType": "event", "Event": "face_identify", "openid": "oRRn15NUibBxxxxxxxxx", "out_seq_no": "test1234", "verify_result": "XXIzTtMqCxwOaawoE91-VNGAC3v1j9MP-5fZJxv0fYT4aGezzvYlUb-n6RWQa7XeJpQo0teKj8mGE4ZcRe1JI3GqzADBYORBu613rKjKAFfEXTXw_bu1bs7MnmPOpguS" } [代码] 四、再次获取核验结果api 此接口是前端完成人脸核身后,基于前端返回的凭据,通过后台api再次进行核验结果和身份信息的校验,有助于提高安全性,请务必接入! 前端获取结果不可信,存在被篡改的风险,为了保障请求结果安全性,请务必对identify_ret、id_card_number_md5、name_utf8_md5字段进行校验! (一)API说明 1、说明 人脸核身之后,开发者可以根据jsapi返回的verify_result向后台拉取当次认证的结果信息。 2、请求URL https://api.weixin.qq.com/cityservice/face/identify/getinfo?access_token={access_token} 3、请求方式 POST 4、请求格式 json (二)请求数据说明 1、请求 参数 类型 是否必填 描述 verify_result String 是 jsapi返回的加密key(凭据) 2、数据返回 HTTP 头如下 Date: Mon, 06 Feb 2017 08:12:58 GMT Content-Type: application/json; encoding=utf-8 Content-Length: 85 Connection: close json示例 [代码]{ "errcode" : 0, [代码] [代码]"errmsg" : "ok", "identify_ret" : 0, "identify_time" : 1486350357 "validate_data": "8593" [代码] [图片] (三)返回参数说明 1、返回参数 注:errcode和identify_ret同时为0,代表本次认证成功。 参数 类型 描述 errcode int 错误码, 0表示本次api调用成功 errmsg string 本次api调用的错误信息 identify_ret int 人脸核身最终认证结果 identify_time uint32 认证时间 validate_data string 用户读的数字(如是读数字) openid string 用户openid user_id_key string 用于后台交互表示用户姓名、身份证的凭证 finish_time uint32 认证结束时间 id_card_number_md5 string 身份证号的md5(最后一位X为大写) name_utf8_md5 string 姓名MD5 2、错误码对应信息 errcode 备注 84001 非法identity_id 84002 用户信息过期 84003 用户信息不存在 五、小程序辅助接口:检查设备是否支持人脸检测 1、接口名称 接 口 :wx.checkIsSupportFacialRecognition(OBJECT) 功能:检查设备是否支持人脸检测 2、接口说明和使用 小程序调用该接口,可以检测当前手机设备是否具备支持人脸检测的能力,可与以上接口分开使用,为了用户体验,建议调用后对手机设备不支持的用户做对应功能处理。 3、接口说明和使用 01 OBJECT 参数说明: 参数 类型 是否必填 描述 success Function 否 调用成功回调 fail Function 否 调用失败回调 complete Function 是 调用完成回调(成功或失败都会回调) checkAliveType Number 否 人脸核验的交互方式,默认读数字(见表 2) 表 2:checkAliveType 的值和对应的解释: 参数 解释 2 先检查是否可以屏幕闪烁,不可以则自动为读数字 02 CALLBACK 返回参数 参数 类型 说明 errMsg Boolean 错误信息 errCode Number 错误码 03 回调结果说明 回调类型 ErrCode 说明 sucess 0 支持人脸采集 fail 10001 不支持人脸采集:设备没有前置摄像头 fail 10002 不支持人脸采集:没有下载到必要模型 fail 10003 不支持人脸采集:后台控制不支持 回调结果说明仅对Android生效,iOS不返回errcode。 04 示例代码 [图片] 六、安全性说明 为保障业务可用性以及安全性,请详细研读微信人脸核身接口相关基础说明及安全说明文档:https://docs.qq.com/doc/DTFB0YWFIdGV6amly 备注:如开发中遇到任何疑问,可以点击此处通过社区反馈,将有工作人员跟进回复。 七、案例展示及补充说明 安徽医科大学第二附属医院,微信人脸核验登录: 安徽医科大学第二附属医院,是三级甲等综合医院。其小程序为用户提供挂号、门诊费用、住院费用、检查报告、体检等医疗服务,同时也提供停车、餐饮等便民服务,是医疗小程序中完整的案例。 小程序使用了微信人脸核验能力作为登录的核验。满足医院管理要求,也满足国家对于实名就医的管理规则。 案例实现的截图效果如下: [图片] [图片] 针对近期少数小程序方面反馈的两类问题,也在本课程进行补充说明。 1、本接口的开放范围,即:可支持的主体类目,是否可以扩大? 说明:基于本接口整体使用范围的评估、相关法规的参考、监管策略的理解执行等,暂时未立刻进行扩大开放范围的工作。 但我们会持续基于不同行业的法规、政策及监管要求等,逐一进行研究考量,以便确认如何扩大开放范围。 2、小程序如果涉及用户本人的生物特征采集,(如本人人脸照片、人脸视频),或涉及采集用户本人生物特征信息并开展人脸核验功能,则存在被驳回的情况? 说明:近两年“人脸识别”技术在社会上掀起了热潮。人脸识别虽然作为摆脱“中间媒介”或“承载载体”的一种直接技术手段,解决了部分政务、交通、医疗、零售等证明“操作者是本人”的问题,但也因此,引入了新的更大的安全风险。 一是,虚假安全风险。 身份认证领域的安全三因素包括“我知道什么”、“我拥有什么”、“我的特征是什么”,通用的安全做法,是要双因素认证(2FA),人脸识别技术如仅凭“我的特征是什么”这一个因素,则容易被攻破或利用。表象给用户以安全的感觉,但实际并不能达到安全效果。 二是,信息泄漏的风险。 越来越多的组织或个人,在并非必需用户敏感信息、生物特征的情况下,采集并存储此类信息。在信息加密、传输、存储过程中,容易暴漏更多的网络节点,使得此类信息有更大的风险被网络黑客拦截、窃听、窃取,或直接被脱库。 三是,消除风险的难度大。 以往基于“中间媒介”或“承载载体”的方式,如出现丢失、被冒用、恶意盗用等风险,可以通过挂失、更换、使用新载体或新媒介等方式,快速排除一定的风险。C端主动,B端主动,都能解决一部分问题。但人脸识别做为更直接的方式,一旦出现冒用、盗用,受害者将面临更大的财产及人生安全风险,且C端用户更多时候无法主动消除风险。 基于以上问题风险,加之国家出台《网络安全法》、《用户隐私保护条例》等法律法规标准,网信办、公安部、工信部及市场监管总局等四部委发起的app获取隐私整治,结合平台安全、用户敏感隐私信息保护要求及监管,针对部分暂无相关法规或要求,需要采集或生物认证方式进行身份核验的,或以“追热点”或“尝鲜”为目的,采集用户生物特征或进行身份核验的,进行严格审核,必要时不予以支持。
星期一 15:19 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28 - 视频教程:小程序中的同步、异步
[视频]
2019-07-05 - 微信小程序用户授权弹窗,获取用户信息。用户拒绝授权时,引导用户去重新授权
我们在开发小程序时,如果想获取用户信息,就需要获取用的授权,如果用户误点了拒绝授权,我们怎么样去正确的引导用户重新授权呢。今天就来给大家讲讲如果正确的引导用户授权。 老规矩,先看效果图 [图片] 从上图可以看出,我们在用户点击拒绝授权时,我们会弹出一个提示框,提示用户去设置页重新授权,当用户去授权页重新授权以后,我们再回到首页,点击获取用户信息时,就可以成功的获取到用户信息了。 如下图蓝色框里,就是我们成功的获取的用户信息。 [图片] 一,我们获取用户信息的时候需要用户授权 我们点击获取用户信息时,通常会弹出如下提示框,如果用户点击了取消,就再也没有办法通过点击授权按钮获取用户信息了。 [图片] 所以接下来我们要做的就是在用户拒绝了授权时,引导用户去设置页重新授权。 把获取用户授权的代码先贴给大家 [代码]<button open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 授权获取头像昵称 </button> [代码] 二,检测用户是否授权 我们在用户点击了上面定义的button按钮后,做权限检测。代码如下。 [代码] getUserInfo: function(e) { let that = this; // console.log(e) // 获取用户信息 wx.getSetting({ success(res) { // console.log("res", res) if (res.authSetting['scope.userInfo']) { console.log("已授权=====") // 已经授权,可以直接调用 getUserInfo 获取头像昵称 wx.getUserInfo({ success(res) { console.log("获取用户信息成功", res) that.setData({ name: res.userInfo.nickName }) }, fail(res) { console.log("获取用户信息失败", res) } }) } else { console.log("未授权=====") that.showSettingToast("请授权") } } }) }, [代码] 给大家简单解析下。 wx.getSetting :用来获取用户授权列表 if (res.authSetting[‘scope.userInfo’]) 代码用户授权成功,如果用户没有授权,就代表授权失败。 在授权失败时,我们调用that.showSettingToast()方法 三,showSettingToast方法如下 [代码] // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) } [代码] 这方法做的就是引导用户去设置页。 四,我们的设置页 [图片] 我们的设置页其实很简单,只有上图这么一段代码。 [图片] 五,去系统设置页。 我们上面第四步的button按钮,点击以后,就会去系统设置页。 [图片] 可以看到系统设置页,有一个开关,当用户点击开关时,就可以重新授权啦。 [图片] 重新授权成功以后,我们回到首页,就可以成功的获取到用户信息了。 [图片] 到这里我们就成功的实现了引导用户授权的功能了。 把index.wxml和index.js代码贴出来给大家 index.wxml [代码]<!--index.wxml--> <button open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 授权获取头像昵称 </button> <text>{{name}}</text> [代码] index.js [代码]//index.js Page({ getUserInfo: function(e) { let that = this; // console.log(e) // 获取用户信息 wx.getSetting({ success(res) { // console.log("res", res) if (res.authSetting['scope.userInfo']) { console.log("已授权=====") // 已经授权,可以直接调用 getUserInfo 获取头像昵称 wx.getUserInfo({ success(res) { console.log("获取用户信息成功", res) that.setData({ name: res.userInfo.nickName }) }, fail(res) { console.log("获取用户信息失败", res) } }) } else { console.log("未授权=====") that.showSettingToast("请授权") } } }) }, // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, }) [代码] 有任何关于编程的问题都可以留言或者私信我,我看到后会及时解答 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 有任何关于小程序的问题可以加我微信:2501902696(备注小程序) 视频讲解: https://edu.csdn.net/course/detail/9531
2019-07-26 - 云函数获取用户手机号吗?
小程序通过云函数获得用户手机号码? 思路解析, [图片] 了解了小程序的加密方式,我们就可以自己去解密我们需要的信息。如:最困住我们的用户手机号码? 官方是有案例的,想更多学习可以给与参考,但是估计要多看几遍,有node基础的就比较好理解一些。 [图片] 下面是官网地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud 下面开始说我自己的方法; 1.首先构建云函数,需要两个云函数,一个用来解密,session_key,一个用来解密加密手机号码; [代码]//云函数:getSession; // 云函数入口文件 const cloud = require('wx-server-sdk') //npm install request-promise 通过终端下载npm install wx-server-sdk , const rp = require('request-promise'); cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const _JSCODE = event.code const AccessToken_options = { method: 'GET', url: 'https://api.weixin.qq.com/sns/jscode2session', qs: { appid: '', //你的小程序appid; secret: '', //你的秘钥 grant_type: 'authorization_code', js_code: _JSCODE }, json: true }; const resultValue = await rp(AccessToken_options); return { resultValue } } [代码] 下载好需要的两个包,就可以对云函数初始化,执行npm init 有个起名字的环节,用过node的都知道,默认index.js,有个选择 [代码]package name: (gettoken) index.js version: (1.0.0) description: git repository: keywords: author: license: (ISC) About to write to D:\projects\zy_face_id_wxs\server\getToken\package.json: { "name": "index.js", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "request-promise": "^4.2.4", "wx-server-sdk": "^0.8.1" }, "devDependencies": {}, "description": "" } Is this OK? (yes) yes [代码] 这是初始化,终端的代码; 右键点击上传并部署到云端; 下面是客户端的代码; [代码] getPhoneNumber(e) { if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showModal({ content: '不能获取手机号码', showCancel: false }) return; } wx.showLoading({ title: '获取手机号中...', }) console.log(e) wx.login({ success(res) { if (res.code) { console.log(res.code) console.log(e.detail.iv) console.log(e.detail.encryptedData) wx.cloud.callFunction({ name: 'getSession', //调用云函数获取session_key; data: { code: res.code, }, success: res => { wx.hideLoading() // console.log(res.result.resultValue) var data = res.result.resultValue console.log(data) console.log(data.session_key) //获取到了session_key的值; }, fail: error => { console.log(error) } }) } } }) }, [代码] 2.构建第二个云函数 GetWX; [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // const requestpromise = require('request-promise'); var WXBizDataCrypt = require('./RdWXBizDataCrypt') // 用于手机号解密 cloud.init() exports.main = async (event, context) => { const session_key = event.session_key //appid写入你自己的appid,session_key 用第一个云函数的返回值; const pc = new WXBizDataCrypt(appid, session_key ) // -解密第一步 const data = pc.decryptData(event.encryptedData, event.iv) // 解密第二步 return { data } } [代码] 这个同样执行上面的步骤,npm install wx-server-sdk 和初始化npm init; 重点来了,解密所需要的js文件。 [图片]` 这两个文件已经上传到我的网盘里面,需要的请下载; https://pan.baidu.com/s/1VrS1gX_Bw3dKaZkQnNzy2A 提取码:cxnh; 格式和图片的保持一致,并上传到云端。 3.开始前端调用了; 代码如下; [代码]getPhoneNumber(e) { if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showModal({ content: '不能获取手机号码', showCancel: false }) return; } wx.showLoading({ title: '获取手机号中...', }) console.log(e) wx.login({ success(res) { if (res.code) { console.log(res.code) console.log(e.detail.iv) console.log(e.detail.encryptedData) wx.cloud.callFunction({ name: 'getSession', //调用云函数获取session_key; data: { code: res.code, }, success: res => { wx.hideLoading() var data = res.result.resultValue console.log(data) console.log(data.session_key) //获取到了session_key的值; const session_key=data.session_key wx.cloud.callFunction({ name:'getWX', //解析秘文,获得手机号码; data:{ session_key: session_key, encryptedData: e.detail.encryptedData, iv: e.detail.iv, }, success:res=>{ console.log(res) }, fail:err=>{ console.log(err) } }) }, fail: error => { console.log(error) } }) } } }) }, [代码] 4.输出的结果: [图片] 遇到问题了在私信问我,一一回答。
2019-07-17 - 复制任意微信小程序页面路径
以下以微信小程序“虎牙直播”为例,演示如何复制微信小程序页面的路径。 1.进入小程序的“关于虎牙直播”页面 [图片] 2.点击右上角的“…”进入“更多资料”页面 [图片] [图片] [图片] 3.复制AppID:wx74767bf0b684f7d3 4.进入小程序后台输入appid并搜索,然后点下一步 [图片] 5.鼠标移动到“获取更多页面路径”,在弹出窗口输入当前登陆的小程序的任意开发者微信号,然后点击开启,出现顶部的“开启入口成功”就可以使用手机访问“虎牙直播”任意页面进行复制了 [图片] 6.某个直播间的页面路径:pages/main/liveRoom/index.html?anchorUid=1678113423&source=search[图片] PS:复制出来的页面路径在小程序里使用的时候记得删除 .html 才能正常访问。
2020-01-16 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 小程序如何生成海报分享朋友圈
摘要: 小程序开发必备技能啊… 原文:小程序如何生成海报分享朋友圈 作者:小白 Fundebug经授权转载,版权归原作者所有。 项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。 需求 利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。 生成的海报如下: [图片] 需求分析 1、利用小程序官方提供的api可以直接分享转发到微信群打开小程序 2、利用小程序生成海报保存图片到相册分享到朋友圈,用户长按识别二维码关注公众号或者打开小程序来达到裂变的目的 实现方案 一、分析如何实现 相信大家应该都会有类似的迷惑,就是如何按照产品设计的那样绘制成海报,其实当时我也是不知道如何下手,认真想了下得通过canvas绘制成图片,这样用户保存这个图片到相册,就可以分享到朋友圈了。但是要绘制的图片上面不仅有文字还有数字、图片、二维码等且都是活的,这个要怎么动态生成呢。认真想了下,需要一点一点的将文字和数字,背景图绘制到画布上去,这样通过api最终合成一个图片导出到手机相册中。 二、需要解决的问题 二维码的动态获取和绘制(包括如何生成小程序二维码、公众号二维码、打开网页二维码) 背景图如何绘制,获取图片信息 将绘制完成的图片保存到本地相册 处理用户是否取消授权保存到相册 三、实现步骤 这里我具体写下围绕上面所提出的问题,描述大概实现的过程 ①首先创建canvas画布,我把画布定位设成负的,是为了不让它显示在页面上,是因为我尝试把canvas通过判断条件动态的显示和隐藏,在绘制的时候会出现问题,所以采用了这种方法,这里还有一定要设置画布的大小。 [代码]<canvas canvas-id="myCanvas" style="width: 690px;height:1085px;position: fixed;top: -10000px;"></canvas> [代码] ②创建好画布之后,先绘制背景图,因为背景图我是放在本地,所以获取 <canvas> 组件 canvas-id 属性,通过createCanvasContext创建canvas的绘图上下文 CanvasContext 对象。使用drawImage绘制图像到画布,第一个参数是图片的本地地址,后面两个参数是图像相对画布左上角位置的x轴和y轴,最后两个参数是设置图像的宽高。 [代码]const ctx = wx.createCanvasContext('myCanvas') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) [代码] ③创建好背景图后,在背景图上绘制头像,文字和数字。通过getImageInfo获取头像的信息,这里需要注意下在获取的网络图片要先配置download域名才能生效,具体在小程序后台设置里配置。 获取头像地址,首先量取头像在画布中的大小,和x轴Y轴的坐标,这里的result[0]是我用promise封装返回的一个图片地址 [代码]let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36; //绘制的头像在画布上的位置 ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 [代码] 这里举个例子说下如何绘制文字,比如我要绘制如下这个“字”,需要动态获取前面字数的总宽度,这样才能设置“字”的x轴坐标,这里我本来是想通过measureText来测量字体的宽度,但是在iOS端第一次获取的宽度值不对,关于这个问题,我还在微信开发者社区提了bug,所以我想用另一个方法来实现,就是先获取正常情况下一个字的宽度值,然后乘以总字数就获得了总宽度,亲试是可以的。 [图片] [代码]let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); [代码] ④绘制公众号二维码,和获取头像是一样的,也是先通过接口返回图片网络地址,然后再通过getImageInfo获取公众号二维码图片信息 ⑤如何绘制小程序码,具体官网文档也给出生成无限小程序码接口,通过生成的小程序可以打开任意一个小程序页面,并且二维码永久有效,具体调用哪个小程序二维码接口有不同的应用场景,具体可以看下官方文档怎么说的,也就是说前端通过传递参数调取后端接口返回的小程序码,然后绘制在画布上(和上面写的绘制头像和公众号二维码一样的) [代码]ctx.drawImage('小程序码的本地地址', x轴, Y轴, 宽, 高) [代码] ⑥最终绘制完把canvas画布转成图片并返回图片地址 [代码] wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath // 返回的图片地址保存到一个全局变量里 that.setData({ showShareImg: true }) wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) [代码] ⑦保存到系统相册;先判断用户是否开启用户授权相册,处理不同情况下的结果。比如用户如果按照正常逻辑授权是没问题的,但是有的用户如果点击了取消授权该如何处理,如果不处理会出现一定的问题。所以当用户点击取消授权之后,来个弹框提示,当它再次点击的时候,主动跳到设置引导用户去开启授权,从而达到保存到相册分享朋友圈的目的。 [代码]// 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } [代码] 总结 至此所有的步骤都已实现,在绘制的时候会遇到一些异步请求后台返回的数据,所以我用promise和async和await进行了封装,确保导出的图片信息是完整的。在绘制的过程确实遇到一些坑的地方。比如初开始导出的图片比例大小不对,还有用measureText测量文字宽度不对,多次绘制(可能受网络原因)有时导出的图片上的文字颜色会有误差等。如果你也遇到一些比较坑的地方可以一起探讨下做个记录,下面附下完整的代码 [代码]import regeneratorRuntime from '../../utils/runtime.js' // 引入模块 const app = getApp(), api = require('../../service/http.js'); var ctx = null, // 创建canvas对象 canvasToTempFilePath = null, // 保存最终生成的导出的图片地址 openStatus = true; // 声明一个全局变量判断是否授权保存到相册 // 获取微信公众号二维码 getCode: function () { return new Promise(function (resolve, reject) { api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => { console.log(res, '获取微信公众号二维码') if (res.code == 200) { console.log(res.content, 'codeUrl') resolve(res.content) } }).catch(err => { console.log(err) }) }) }, // 生成海报 async createCanvasImage() { let that = this; // 点击生成海报数据埋点 that.setData({ generateId: '点击生成海报' }) if (!ctx) { let codeUrl = await that.getCode() wx.showLoading({ title: '绘制中...' }) let code = new Promise(function (resolve) { wx.getImageInfo({ src: codeUrl, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) Promise.all([headImg, code]).then(function (result) { const ctx = wx.createCanvasContext('myCanvas') console.log(ctx, app.globalData.ratio, 'ctx') let canvasWidthPx = 690 * app.globalData.ratio, canvasHeightPx = 1085 * app.globalData.ratio, avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36, //绘制的头像在画布上的位置 codeurl_width = 80, //绘制的二维码宽度 codeurl_heigth = 80, //绘制的二维码高度 codeurl_x = 588, //绘制的二维码在画布上的位置 codeurl_y = 984, //绘制的二维码在画布上的位置 wordNumber = that.data.wordNumber, // 获取总阅读字数 // nameWidth = ctx.measureText(that.data.wordNumber).width, // 获取总阅读字数的宽度 // allReading = ((nameWidth + 375) - 325) * 2 + 380; // allReading = nameWidth / app.globalData.ratio + 325; allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; console.log(wordNumber, wordNumber.toString().length, allReading, '获取总阅读字数的宽度') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制 ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.setFontSize(28); // 文字字号 ctx.fillText(that.data.currentChildren.name, 103, 78); // 绘制文字 ctx.font = 'normal bold 44px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(wordNumber, 325, 153); // 绘制文字 ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('打败了全国', 26, 190); // 绘制文字 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#faed15'); // 文字颜色 ctx.fillText(that.data.percent, 154, 190); // 绘制孩子百分比 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('的小朋友', 205, 190); // 绘制孩子百分比 ctx.font = 'normal bold 32px sans-serif'; ctx.setFillStyle('#333333'); // 文字颜色 ctx.fillText(that.data.singIn, 50, 290); // 签到天数 ctx.fillText(that.data.reading, 280, 290); // 阅读时长 ctx.fillText(that.data.reading, 508, 290); // 听书时长 // 书籍阅读结构 ctx.font = 'normal normal 28px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].count, 260, 510); ctx.fillText(that.data.bookInfo[1].count, 420, 532); ctx.fillText(that.data.bookInfo[2].count, 520, 594); ctx.fillText(that.data.bookInfo[3].count, 515, 710); ctx.fillText(that.data.bookInfo[4].count, 492, 828); ctx.fillText(that.data.bookInfo[5].count, 348, 858); ctx.fillText(that.data.bookInfo[6].count, 212, 828); ctx.fillText(that.data.bookInfo[7].count, 148, 726); ctx.fillText(that.data.bookInfo[8].count, 158, 600); ctx.font = 'normal normal 18px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].name, 232, 530); ctx.fillText(that.data.bookInfo[1].name, 394, 552); ctx.fillText(that.data.bookInfo[2].name, 496, 614); ctx.fillText(that.data.bookInfo[3].name, 490, 730); ctx.fillText(that.data.bookInfo[4].name, 466, 850); ctx.fillText(that.data.bookInfo[5].name, 323, 878); ctx.fillText(that.data.bookInfo[6].name, 184, 850); ctx.fillText(that.data.bookInfo[7].name, 117, 746); ctx.fillText(that.data.bookInfo[8].name, 130, 621); ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像 ctx.draw(false, function () { // canvas画布转成图片并返回图片地址 wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath that.setData({ showShareImg: true }) console.log(res.tempFilePath, 'canvasToTempFilePath') wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) }) }) } }, // 保存到系统相册 saveShareImg: function () { let that = this; // 数据埋点点击保存学情海报 that.setData({ saveId: '保存学情海报' }) // 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } }, [代码]
2019-06-15 - 小打卡|如何组件化拆分一个200+页面的小程序
大家好,我是小打卡的前端唐驰。刚才金轩正同学分享了基于原生小程序底层架构,在此基础上我为大家分享下如何拆分一个200+页面的小程序,主要通过以下几点来聊一聊小打卡在组件化路上的一些实践 1.背景 2.组件与方案 3.组件间通讯 4.基于组件我们做了哪些事 [图片] [图片] 1. 其实一开始小打卡是没有引入组件化的,因为微信最开始是不支持组件化的。当时js代码已经4k+行了,各种功能代码,有用的没有用的,不知道干什么的代码就躺在那里,一动不动。举个例子,一个头像点击跳转的逻辑搜索了下,遍布在各个页面。修改起来可想而知的胆战心惊。另一个原因就是当时由于业务功能直线上升,很快我们就遇到了代码包超包了。在微信还没有实现分包之前,我们就只能一个一个页面的去review剔除代码,效率极低。这也是促成我们决定寻求出路的原因之一。可是删代码删功能是不能解决问题,期间我们也考虑过h5的方式,跑了demo之后却发现h5方式的多次渲染, 与加载首页白屏,尽管有各种服务端渲染方案,但是我们一致觉得为了用户体验,放弃了。 [图片] 2. 对于小打卡来说,我们不能再任由项目裸奔了,需要一种开发方式来进行约束,主要是有几个诉求: 在之前的项目上,为了方便。功能与功能之间的耦合程度极其的高,各种为了使用方便而随意修改某一个方法。 1.降低页面上各个功能点的耦合程度 我们不希望同一个功能点同样的代码在页面肆意copy,这样带来了极高的维护成本。以至后面无法维护。并且功能的复用不希望是copy,前端与后端不同的是不仅是单单的逻辑复用、更有布局、样式等。 2.提供代码的可复用性、可维护性 对于一个程序员来说,如果你打开一个代码文件。映入眼帘的是密密麻麻的代码,行数达到好几千K行,我相信大家的第一反应是抗拒的,更别说去修改代码,天知道会改出什么问题。 3.降低单一文件的复杂度 4.如果是公共功能的化我们还希望它能够有自己的作用域,保持自己的独立性。 [图片] 3. 根据以上几点,我们用一个页面举例,如何去拆分一个页面,首先我们需要有以下几点认识: 决定一个页面如何组件化的前提是该页面的功能是否是有全局都需要的功能模块 功能模块是否需要与页面其他模块强耦合 单个功能模块逻辑是否过于复杂(占用代码空间过大)——>单纯是为了页面代码的可读性。 不是全拆成组件就是最好的,不能为了组件而组件化 [图片] 4. 说了这么多,其实我们应该首先应该了解下,组件的特性? 专一性(一个组件只干一件事情,或者某一类事情。)功能的高度内聚 比如说右侧的feed集上的头像、它是一个组件、就负责显示头像跟跳转,其他的事它都不参与 可配置(能够适应通过设置属性值的方式来输出不同的东西)输入影响部分输出 然后我们同时可以设置头像组件上的size属性来设置头像在不同页面下的大小样式 生命周期(组件可以在自身或者说所在页面的生命周期内可以做不同的事情)比如可以在组件生成的时候进行数的初始化、属性值的类型校验、组件销毁时并同时销毁定时器等其他任务 事件传递 (既然要让组件与页面保持独立性,那么组件与页面的通讯交互就得需要一个标准) 右侧的feed组件其实是一个组件集合、我们通过组合不同的组件然后就形成了feed组件。就跟搭积木一样、只需要引入组件就行了。特别方便。 [图片] 5. 说到组件,那么小程序早期的不支持自定义组件开发这就很让人头疼、同样的feed组件我们经历了几乎三个版本的大改动、从最开始的直接写在页面里,后台使用template方式、再到后来的自定义组件方式。所以我们的演进步骤就成了page->template->component, 这儿列了一个表格对比了下几种组件化方式的对比。 可以看到,include的方式其实是最鸡肋的,include的方式其实实际意义上我理解成更多的是代码的切割,并且还不能将(template、wxs)分出去、所以这种方式我们直接pass掉了, 而template的方式其实是我们曾经主力使用的方式、到现在我们也还在使用、相对于include来说,template有了独立的作用域、虽然css、跟js还是与页面共享的。但是已经可以做一些比较简单的事情了。 对于component来说,完完全全的组件,满足了组件的所有要求。 [图片] 6. 先说说template的方式吧,举个列子,这个是我们的使用template构建的头像组件。跟写页面的方式很像、同样是js、wxss、wxml组成。用名称来命名。但是由于微信当时没有很方的方式去引用这些文件,或者说没有一种方法可以打包供我们很方便的使用。但是比起之前直接copy代码的方式、这样通过引用的方式使用其实感觉已经好了很多了。 [图片] [图片] 7. 具体的使用方式我画了张图,对应组件内文件与页面文件的对应方式、这里对于js的引用其实我们是做了一些小动作, 我们在调用Page方法前做了一次page方法与组件方法的check,因为在page代码里我们不能保证所有的方法名不会跟组件内的方法名不会冲突,所以我们做了这个一个检查、 然后mix函数还做了另一个事情就是将page方法与组件方法合并。然后对于mix函数其实我们还可以做很多事情、、比如规范生命周期回调函数放在一个对象内,然后我们自己定义的方法放在另一个对象里,就跟vue一样。 But,在经历了一段template组件化的时间后,我们又觉得这个方式还是有点烂,为什么呢?在使用时仍然不能避免引入众多的文件、虽然我们对js文件做了处理,但是wxss的样式仍然会被污染、js与page仍然共享作用域。并不能成为一个真正的标准组件。好在后来,微信上了自定义组件的功能,接下来聊聊这个标准的微信自定义组件吧。 8. 微信提供了自定义组件的功能后我们也第一时间跟进了,相对于template这种方式来说,现在是真正的独立于页面存在。使用也比之前更为方便与简洁,右图是我们对component的一个项目目录划分。我们将component划分为了公共组件与页面组件、为什么会有页面组件, 1.是为了降低页面代码的复杂度 2.为了好看。 公共组件就不说了,一定是最基础、最通用的组件。 [图片] 9. 转向component方式后有一个问题逐渐便凸显出来了,由于组件的独立作用域,组件间的通讯就成了一个问题,接下来聊一聊组件的事件传递。微信最开始的时候提供了一种triggerEvent的方式,可是这样的方式似乎并不能满足我们某些场景下的需求。后来又提供了page下selectComponent方法来直接操作组件内部的属性与方法。然后还有就是基于我们自己的事件广播机制。这几种方式构成了小打卡现目前最主要的组件与page、组件与组件间的数据交互方式 [图片] 10. 先来说说triggerEvent模式,微信在自定义组件上可以自定义监听函数。我们在组件内将需要向外抛出的事件统一通过this.triggerEvent(‘invoke’,{handler:’fun’,data:{}})这个方法来执行。其中invoke对应了我们绑定在组件标签上的监听函数。而将需要外部执行的方法与数据通过数据的方式传给监听函数。而在page上面我通过统一的监听回调函数去自动执行需要执行的方法、这里的trigger与event都不要我们去手写在组件与page创建的时候底层就已经帮我们预置了,我们只需要关注业务开发就行。这是对于一部分需要page与组件交互的模式。而对于我们想直接操作组件方法而不需要反馈的模式就得使用selectComponent的模式 [图片] 11. 一个简单的列子:全局的toast组件。在需要弹出toast的时候我们想直接调用就行、不用在通过传值给组件、然后由组件来执行显示或隐藏。这类组件我们在组件目录里新增了一个lib的文件。在page里只需要引入这个lib文件然后就可以直接调用toast组件。lib主要是对this.selectCompent与执行逻辑的一个封装。 [图片] 12. 事件发布订阅模式:基于底层的eventBus。简化后我们用在了组件与组件之间的通讯上、特点是简单。 [图片] 13. 解决了组件间的通讯问题,可是对于公共组件的引用仍然让我们觉得麻烦与不畅快、所以我们构建了全局通用模版、它是干什么的呢。它提供给了一些基础的全局组件、比如自定义导航头、toast、loading等等。小打卡所有的页面都通过slot的方式插入到这个模版组件x-page下面。这样就解决了我们需要在每个页面引入公共组件的问题。另一个问题使用自定义导航栏的时定位起点会有状态栏下移动到屏幕左上方。会造成布局的错误。通过x-page可以很好解决这个问题而不用重新布局。并且通信问题也不用担心,都是由x-page组件作为中台来对内对外进行分发与执行。 [图片] [图片] [图片] 14. 通过以上小打卡的开发模式就基本形成。要做的事情还有很多,更多组件的玩儿法,对于现在或者将来我们正在做的。 是构建小打卡的组件与基础sdk的仓库。 拆分组件开发与业务开发。 通过npm包管理的方式来应对越来越多的小程序平台的开发。 或者通过形成小程序插件的方式供其他小伙伴使用。 [图片] [图片] 以上就是我今天分享的内容。谢谢。
2019-04-26 - 微信小程序三种授权登录的方式
经过一段时间对微信小程序的研发后 总结出以下三种授权登录的方式,我给他们命名为‘一次性授权’‘永久授权’‘不授权’ 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 - 「激励式视频广告」向非游戏类小程序流量主开放
[图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片]
2019-04-16 - 发送短信验证码后60秒倒计时
微信小程序发送短信验证码后60秒倒计时功能,效果图: [图片] 完整代码 index.wxml [代码]<!--index.wxml-->[代码][代码]<view class=[代码][代码]"container"[代码][代码]>[代码][代码] [代码][代码]<view class=[代码][代码]"section"[代码][代码]>[代码][代码] [代码][代码]<text>手机号码:</text>[代码][代码] [代码][代码]<input placeholder=[代码][代码]"请输入手机号码"[代码] [代码]type=[代码][代码]"number"[代码] [代码]maxlength=[代码][代码]"11"[代码] [代码]bindinput=[代码][代码]"inputPhoneNum"[代码] [代码]auto-focus />[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{send}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]bindtap=[代码][代码]"sendMsg"[代码][代码]>发送</text>[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{alreadySend}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]>{{second+[代码][代码]"s"[代码][代码]}}</text>[代码][代码] [代码][代码]</view>[代码][代码]</view>[代码] index.wxss [代码]/**index.wxss**/[代码][代码].userinfo {[代码][代码] [代码][代码]display[代码][代码]: flex;[代码][代码] [代码][代码]flex-[代码][代码]direction[代码][代码]: column;[代码][代码] [代码][代码]align-items: [代码][代码]center[代码][代码];[代码][代码]}[代码][代码].section {[代码][代码]display[代码][代码]: flex;[代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]padding[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]border-bottom[代码][代码]: [代码][代码]1[代码][代码]rpx [代码][代码]solid[代码] [代码]#CFD8DC[代码][代码];[代码][代码]}[代码][代码] [代码] [代码]text {[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]200[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码]button {[代码][代码] [代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码].sendMsg {[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]12[代码][代码];[代码][代码] [代码][代码]margin-right[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]padding[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]height[代码][代码]: inherit;[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]80[代码][代码]rpx;[代码][代码]}[代码]index.js [代码]//index.js[代码][代码]//获取应用实例[代码][代码]const app = getApp()[代码][代码] [代码] [代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]send: true,[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]phoneNum: [代码][代码]''[代码][代码] [代码][代码]},[代码][代码] [代码][代码]// 手机号部分[代码][代码] [代码][代码]inputPhoneNum: function (e) {[代码][代码] [代码][代码]let phoneNum = e.detail.value[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]phoneNum: phoneNum[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]sendMsg: function () {[代码][代码] [代码][代码]var phoneNum = this.data.phoneNum;[代码][代码] [代码][代码]if(phoneNum == [代码][代码]''[代码][代码]){[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'请输入手机号码'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码],[代码][代码] [代码][代码]duration: [代码][代码]2000[代码][代码] [代码][代码]})[代码][代码] [代码][代码]return ;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]//此处省略发送短信验证码功能[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]alreadySend: true,[代码][代码] [代码][代码]send: false[代码][代码] [代码][代码]})[代码][代码] [代码][代码]this.timer()[代码][代码] [代码][代码]},[代码][代码] [代码][代码]showSendMsg: function () {[代码][代码] [代码][代码]if (!this.data.alreadySend) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]hideSendMsg: function () {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: false,[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]buttonType: [代码][代码]'default'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]timer: function () {[代码][代码] [代码][代码]let promise = new Promise((resolve, reject) => {[代码][代码] [代码][代码]let setTimer = setInterval([代码][代码] [代码][代码]() => {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: this.data.second - [代码][代码]1[代码][代码] [代码][代码]})[代码][代码] [代码][代码]if (this.data.second <= [代码][代码]0[代码][代码]) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]resolve(setTimer)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码], [代码][代码]1000[代码][代码])[代码][代码] [代码][代码]})[代码][代码] [代码][代码]promise.then((setTimer) => {[代码][代码] [代码][代码]clearInterval(setTimer)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码]})[代码]完整的短信验证码登录实例参考: https://blog.csdn.net/zuoliangzhu/article/details/81219900
2019-04-17 - 教你简单实现小程序的一键登录、当前用户、用户安全功能
一键登录 传统开发小程序登录后端工作: 客户端调用wx.login()后获取的code,后端人员拿到code和微信换取openid和session_key; 除此之外后端人员还需要根据openid生成session_token作为服务器和小程序之间做登录态校验保证安全性,客户端每次请求需要带上session_token,后端需要作校验; 还需要提供接口给客户端保存用户信息(头像、名称等),或者根据openid去和微信换取用户信息; 中间的过程的调试还需要花费大量的时间 利用bmob的小程序sdk的实现一键登录: 以上后端开发工作全省了!!! [代码] wx.login({ success: function (res) { var user = new Bmob.User();//实例化 user.loginWithWeapp(res.code).then(function (user) { if (user.get("nickName")) { //更新缓存中的openid wx.setStorageSync('openid', user.get("openid")) } else { //*************保存用户其他信息,比如昵称头像之类的***************** wx.getUserInfo({ success: function (result) { var nickName = result.userInfo.nickName; var avatarUrl = result.userInfo.avatarUrl; var u = Bmob.Object.extend("_User"); var query = new Bmob.Query(u); // 这个 id 是要修改条目的 id,你在生成这个存储并成功时可以获取到,请看前面的文档 query.get(user.id, { success: function (result) { // 自动绑定之前的账号 result.set('nickName', nickName); result.set("userPic", avatarUrl); result.set("openid", openid); result.save(); } }); } }); //*************保存用户其他信息,比如昵称头像之类的end***************** } }, function (err) { console.log(err, 'errr'); }); } }); } [代码] user用户对象中的loginWithWeapp方法使用当前使用小程序的微信用户身份注册或登录,成功后用户的 session 会在设备上持久化保存,之后可以使用 Bmob.User.current() 获取当前登录用户 如果该用户是第一次使用此应用,调用登录 API 会创建一个新的用户,你可以在 控制台 >应用> 数据 中的 _User表中看到该用户的信息,如果该用户曾经使用该方式登录过此应用,再次调用登录 API 会返回同一个用户。 登录后获取到一个用户数据对象,包括用户唯一标识objectId、openid、nickName、avatarUrl、authData,authData包括session_token登录校验,小程序sdk封装了请求带上了缓存authData的session_token,所以session_token这部分 不需要bmob的开发者考虑; 当前用户 如果用户每次打开App的时候都要求登录无疑是令人感到厌烦的,你可以通过缓存当前的Bmob.User对象来避免这个问题。 无论你使用任何注册或者登录方法,用户都会在localStorage中储存,你可以把缓存作为一个session对待,并且自动假设用户已经登录了。 你可以操作**Bmob.User.current()**来获取当前用户的信息 [代码]var currentUser = Bmob.User.current(); [代码] 用户安全 Bmob.User类默认就是受保护的,在Bmob.User中保存的数据只能被那个用户所修改。默认地,数据仍然可以被任意客户端所读取。这样就是说,有些Bmob.User对象被认证后是可以修改的,其他的仍然是只读的。 特别的,你不能调用save或者delete方法除非Bmob.User经过了认证,就比如调用过了logIn或者signUp方法,这样保证只有用户能改动他们自身的数据。 怎么样,都很简单吧! Bmob地址:www.bmob.cn
2019-04-15 - 小程序管理后台新增页面收录设置的开关
小程序管理后台新增页面收录设置的开关,开发者可根据业务需要进行设置: 1. 小程序管理后台-设置-基本设置-页面收录设置,可对你的小程序帐号进行收录的开启和关闭的设置。 2. 更新 微信开发者工具 ,可对 sitemap 进行特定页面的配置,可参考 小程序开发文档。 3. 此设置默认开启。若小程序中存在不适合展示的内容,或开发者不希望使用微信展示其小程序,建议开发者自行关闭该设置。查看 详情 。
2019-04-01 - 高仿网易严选的微信小程序商城
高仿网易严选的微信小程序商城(微信小程序客户端) [代码]界面高仿网易严选商城 测试数据采集自网易严选商城 服务端api基于Node.js+ThinkJS+MySQL 计划添加基于Vue.js的后台管理系统、PC版、Wap版 [代码] demo下载地址:http://t.cn/EJJeeo3 [图片][图片] [图片] [图片] [图片] 功能列表 [代码]首页 [代码] [代码]分类首页、分类商品、新品首发、人气推荐商品页面 [代码] [代码]商品详情页面,包含加入购物车、收藏商品、商品评论功能 [代码] [代码]搜索功能 [代码] [代码]专题功能 [代码] [代码]品牌功能 [代码] [代码]完整的购物流程,商品的加入、编辑、删除、批量选择,收货地址的选择,下单支付 [代码] [代码]会员中心(订单、收藏、足迹、收货地址、意见反馈) .... [代码] demo下载地址:https://github.com/tumobi/nideshop-mini-program
2019-03-26 - 小程序中使用防抖函数
这几天看了很多关于防抖函数的博客,我是在微信小程序中使用,在此总结一下关于防抖函数的知识。 为什么需要防抖函数? 防抖函数适用的是【有大量重复操作】的场景,比如列表渲染之后对每一项进行操作。 函数代码: [代码]var timer; debounce: function (func, wait) { return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; }, [代码] 参数: func:需要防抖的函数; wait:number类型,setTimeout的时间参数; 代码分析: 命名一个叫做debounce的函数,参数有两个(func,wait),return一个函数,内容为清除计时器,然后设置计时器,计时器的意思是:在wait时间后执行func。 清除计时器是整个函数的核心,因为防抖需要不停地清除计时器,最后在计时器结束后触发func来达到效果。 防抖函数的调用方法 example: [代码]this.debounce(this.函数名,3000)() [代码] 在使用这个函数的时候我遇到了一些问题: 因为微信小程序中很多地方都需要使用this.setData,如果对于this指向的理解不深入的话,很容易出现以下情况: 1:this==undefined; 2:Error:data is not defined; 等等一些列关于this的问题。 解决方法: [代码]this.debounce(this.函数名.bind(this),3000)() [代码] 使用bind(this)把this指向传到函数内部就解决了。
2019-03-19 - 使用云服务开发抢蛋糕小游戏的经验谈(附防作弊机制)
作者:于振宇 我们在 LeanCloud 成立五周年之际,发布了一款名为《LeanCloud 周年游戏》的微信小游戏。 游戏玩起来很简单,参与者要在 15 秒内从迅速掉落的蛋糕和炸弹中点中尽可能多的蛋糕来得分,蛋糕有好几种,分值也不一样,而误点到炸弹就要扣分。游戏一结束参与者能在排行榜中看到自己的名次,我们给前 50 名都设置了奖品。 游戏截图: [图片] 排行榜截图: [图片] 没玩过的朋友可以搜索「LeanCloud 周年游戏」体验一下。 这个项目开发周期大概为一周,包含客户端开发 2 天 + 服务端 1 天 + 调试 2 天。 接下来我会从客户端、服务端、作弊检测三方面来梳理关键的技术细节,希望能够为游戏开发者或感兴趣的朋友提供一些思路。 在开发环境方面,客户端主要使用了 Cocos Creator 来编辑构建「微信小游戏」项目,服务端使用了 LeanCloud 的云存储、云引擎和排行榜等服务,这些我都会在后面详细介绍。 客户端 先说引擎和编辑器。选取 Cocos Creator 的原因是当在编辑器中构建不同平台项目时,它的友好程度一直都比较好,而且 LeanCloud 也为 Cocos Creator 做了适配。我们游戏的玩法比较简单,无需过多解释,所以接下来我会从客户端资源、状态机、暂停、LeanCloud SDK 和微信这些方面来展开描述。 资源 在游戏运行过程中,加载资源、实例化、销毁 Node 等任何耗时操作都可能造成游戏卡顿,影响体验,特别是在低端机器上这种现象会更加明显。所以我们应该对资源进行预加载或者预实例化。 对于加载资源,通常是在场景切换时,对旧场景资源进行卸载,并对新场景资源进行预加载。 在 Cocos 中,通过 cc.loader 可以很方便地对单个资源、资源列表和资源目录进行加载和缓存。而对于 Node 的实例化和销毁,则要根据 Node 的生命周期进行区分。如果频繁生成和销毁的 Node,我们可以在加载阶段通过对象池技术预先实例化一部分,这样当在游戏过程中需要实例化 Node 时,就不需要实例化,而是从对象池中获取;在不需要时,不进行销毁操作,而是放回至对象池中等待下次使用。如弹幕游戏中的飞机和子弹等。在我们的游戏中,我们也对生成的蛋糕应用了「对象池」技术来避免游戏中可能出现的卡顿。庆幸的是,Cocos 已经提供了这项功能。 状态机 在游戏运行过程中,游戏主体(或角色)都会有很多的状态,比如英雄的空闲、移动、攻击、死亡等,因此通常会引入「状态机」模式对游戏对象进行设计。我们为抢蛋糕游戏引入了 machina 库作为状态机的框架,将整个游戏主体划分为初始化、准备、进行中、结束四个状态。 通过状态机,我们可以更加清楚地跟踪游戏在过程中的变化,并可以通过事件在不同的状态下做出不同的处理。 暂停 在游戏过程中,我们经常会需要暂停游戏,比如在抢蛋糕游戏结束时不再生成新的蛋糕和位置移动。 不同的游戏引擎的暂停方式有所不同。通过 Cocos 的文档,我们找到了引擎提供的 cc.director.pause() / cc.director.resume() 接口,但是尝试之后发现很多局限性,比如在暂停之后 Widget 适配会不起作用,ScrollView 拖拽不回弹等情况。 于是我们决定通过 Component.update(dt) 生命周期和状态机在游戏中自行控制 Node 的更新。主要思路是在全局游戏的 update() 生命周期里,将更新事件交由状态机,只有在游戏进入「进行中」状态时才处理更新事件,而在其他状态下则忽略更新事件。 更新过程为先获取场景下的所有 CakeCtrl 对象,调用自定义 onUpdate(dt) 方法进行更新(注意不是 update(dt) 生命周期方法)。 [代码]// 游戏状态: play: { ... update: function (dt) { const cakeCtrls = this._scene.getComponentsInChildren(CakeCtrl); cakeCtrls.forEach((cakeCtrl) => { cakeCtrl.onUpdate(dt); }); } ... }, [代码] LeanCloud SDK LeanCloud SDK 在大部分平台都做了适配,可以很方便地接入 LeanCloud 云服务。 开发者在使用 Cocos Creator 时一般在浏览器进行调试开发,当完成后再发布到微信环境。但不同环境下 LeanCloud SDK 略有不同,为了方便使用,你可以通过封装来隐藏加载不同版本 SDK 的细节。 比如在浏览器环境下,加载 leancloud-storage 库;而发布在微信小游戏环境下,则加载 leancloud-storage/dist/av-weapp-min.js 库。 [代码]if (cc.sys.browserType === cc.sys.BROWSER_TYPE_WECHAT_GAME) { AV = require("leancloud-storage/dist/av-weapp-min.js"); } else { AV = require("leancloud-storage"); } [代码] 另外,如果我们需要使用微信授权登录,为了方便在浏览器下调试,我们也可以将 login() 封装成不同的实现,统一逻辑层调用。 比如在浏览器环境下,使用账号 + 密码方式登录;而在微信小游戏环境下,使用微信授权登录。 [代码]login() { return new Promise((resolve, reject) => { // 微信登录 if (cc.sys.browserType === cc.sys.BROWSER_TYPE_WECHAT_GAME) { AV.User.loginWithWeapp() .then(user => { ... }) .catch(error => { reject(error); }); } else { // 使用默认账号登录,开发调试使用 AV.User.logIn("1if7jp52qx9771hllat1rvfqt", "123") .then(user => { ... }) .catch(error => { reject(error); }); } }); }, [代码] 更多关于 SDK 的文档,请访问 LeanCloud 文档。 微信 因为我们的游戏在排行榜中需要获取玩家的头像和昵称,所以需要使用到微信的获取用户信息(昵称、头像)接口。这里要吐槽一下,微信新版的 SDK 已经不允许用「弹框授权」来直接获取信息了,而需要使用「固定类型的」微信小程序按钮获取。但是这一机制有对微信旧版 SDK 又不可用,所以我们需要根据微信版本,确定通过哪种机制拿到微信授权。 如果是旧版本的微信,则可以直接调用获取用户信息接口;而如果是新版本的微信,则需要渲染出微信授权按钮,通过按钮的点击事件再获取。这里你可能需要面对小程序的渲染和 Cocos 的渲染机制不一致的问题。 所以,这里还用到了一个小窍门——将微信小程序的授权按钮设置为透明,覆盖到 Cocos 场景中的按钮之上,当按钮被点击时,系统会先将点击事件传递到微信小程序,在微信小程序回调中处理完成之后再交由游戏中处理。 服务端 在服务端开发中,我们主要使用了 LeanCloud 的云存储、云引擎和排行榜服务。 存储 在存储方面,主要使用了 3 张表: _User:存储用户信息,LeanCloud 内置表。 UserInfo:存储用户的详细信息,用于邮寄奖励。 Game:存储玩家每局游戏的数据。 为了保证游戏安全,只有用户信息是通过 LeanCloud 存储 SDK 直接操作的。而游戏相关的数据,都是通过 LeanCloud SDK 请求到云引擎中处理后保存的。参考文档 云引擎 云引擎是 LeanCloud 推出的服务端托管平台。通常比较关键的数据,我们推荐不要使用 SDK 直接操作,而是通过云引擎进行操作。参考文档 在抢蛋糕游戏中,为了保证游戏安全,我们在游戏结束后并没有在客户端直接通过 LeanCloud SDK 上传分数到排行榜,而是将游戏参数发送到云引擎,通过云引擎分析后再确定是否写入到排行榜。具体流程: 游戏开始,向服务端请求游戏数据,服务端会返回本局游戏的 id 和蛋糕数据;而对于「多次」作弊的玩家,将不返回游戏数据。 游戏结束,客户端将本局游戏的参数提交给服务端,包括:本局游戏 id、分数、蛋糕点击数量、时间戳、签名、蛋糕点击索引序列。 服务端对游戏数据进行合法性检测,如果通过则更新排行榜,否则丢弃并标记用户作弊(作弊检测方法会在后面有详细介绍)。 排行榜 排行榜是 LeanCloud Play 为游戏开发者提供的一项新的服务。它除了能提供方便的数据更新接口,还提供了排行榜成绩更新、榜单管理等配置。参考文档 在抢蛋糕游戏中,除了使用常规的「更新玩家成绩」之外,还用到了对作弊玩家进行「榜单移除」的操作。 更新玩家成绩 [代码]... // 提交分数 scoreInLeaderBoard = calcScoreInLeaderBoard(score); AV.Leaderboard.updateStatistics(currentUser, { free: scoreInLeaderBoard }) 标记作弊玩家,并移除榜单成绩 /** * 标记用户作弊 * @param {*} user 用户 */ function markUser(user) { let cheat = user.get("cheat") ? user.get("cheat") : 0; console.log(`cheat: ${cheat}`); cheat += 1; user.set("cheat", cheat); user.save(); if (cheat > MAX_CHEAT_COUNT) { // 如果超过最大作弊次数,则清除榜单 AV.Leaderboard.deleteStatistics(user, ["score"]) .then(() => { console.log(`remove ${user} statistics`); }) .catch(console.error); } } [代码] 作弊检测 对于面向程序员制作的游戏,我们猜测到大家可能会通过技术手段来获取更高分数。为了增加大家破解的趣味性,我们也提供了一些作弊检测机制供大家突破——通过运行时作弊检测和离线数据分析生成了最终的榜单数据。 运行时作弊检测 具体过程: 在游戏开始时,客户端向服务端发起开始请求,服务端随机生成本局游戏的蛋糕序列(共 200 个,游戏频率为每 0.1s 生成 1 个),将当前时间戳、用户、蛋糕序列保存至 Game 对象。 将 Game 对象 id和蛋糕序列下发至客户端,客户端根据蛋糕序列生成蛋糕,在游戏过程中,记录玩家点击蛋糕索引。 游戏结束后,将 Game 对象 id、分数、每种蛋糕点击的数量、结束时间戳、签名(md5(id + score + timestamp))和蛋糕点击索引序列发送给服务端。 服务端接收到参数后,对数据进行校验。 校验包括: 提交参数是否完整(基础检测) 分数和蛋糕点击数量是否匹配(逻辑检测) 分数和蛋糕索引是否匹配(逻辑检测) 服务端重新计算签名是否匹配(防止修改明文参数) 验证游戏时长是否合理(超过 2 倍游戏时长,则认为玩家可能是在分析请求) 对于检测到作弊的玩家,本局游戏成绩将不会更新排行榜,并记录 1 次作弊,超过 10 次作弊的玩家,将不能请求到游戏开始时的数据。 离线数据分析 运行时作弊检测并不足以抵挡住广大开发者破解的热情,很快就有用户梳理清楚了协议参数。所以在运行时检测后,我们又默默记下了用户的参数,用于离线分析。 校验包括: 验证游戏时长是否小于 1 倍游戏时长(游戏至少需要 18 秒完成,15秒游戏 + 3秒倒计时,有些同学竟然 2 秒就把游戏结束请求发来了) 蛋糕点击索引是否有重复(逻辑判断,有些 Android 插件可以让游戏卡住,使同一个蛋糕被点击 N 次,则会叠加多次分数) 蛋糕点击索引是否超过允许最大值(排行榜中有位 500+ 分的朋友通过解包,分析协议,通过模拟请求,顺利通过了上述检测,但是竟然在请求中把 200 个蛋糕索引全部赋值了,而正常游戏中最多只能点击到 150 个,即 15 秒 x 每秒 10 个) 致命缺陷 这类游戏是没办法防住按键(触摸)精灵的。如果通过「图像识别 + 自动点击脚本」可以轻松点击完所有的蛋糕并有效避开炸弹,则可以通过上述检测。 有人说如果服务端运算可不可以,思路是屏幕点击的坐标交由服务端运算,但是对于按键精灵类的脚本还是无法避免,并且还会增加项目的开发量(服务端要对不同分辨率和坐标做一些处理)。如果其他同学有办法做更有效的检测,希望能反馈到 LeanCloud 论坛,大家共同讨论。 以上便是我们此次开发小游戏的心得体会,希望能对大家有所帮助。
2019-04-28 - 开发实战分享|小程序扫码获取图书信息(内附详细教程)
作者:祈澈姑娘 小程序扫码实现读取isbn,获取图书的各种信息 接触到云函数已经有一段时间了,之前一直在看api,现在自己跟着网络上的资料和视频学习,受到豆瓣读书系列的启发,决定用云函数做一个项目,获取图书信息并存入云数据库。 基本流程 [图片] 1.用户端小程序调用 wx.scanCode接口,获取到ISBN码。 2.使用ISBN码调用云函数,在请求云函数的时候,云函数会请求豆瓣的API,获取豆瓣图书信息。 3.图书信息请求到之后,会将其中无用的信息返回给小程序中,小程序中再拿出获取到的信息,创建图书条目。 4.将对应的数据直接存储到云开大的数据库里面。 具体步骤 下面对该项目的步骤进行一些具体的讲解以及部分关键代码的呈现。 一、扫一扫获取图书ISBN码 二、准备环境、安装依赖 1.安装Node.js准备环境 2.在cmd打开云函数目录中,安装依赖 三、编写云函数代码 1.在云函数中用获取到的ISBN传参 2.编写用户端(小程序端代码) 3.编写云函数端代码 四、调用豆瓣API获取具体数据 五、将获取到的API数据存入云数据库里面 1.初始化 2.添加数据 六、云数据库读取的数据显示在小程序端列表里 1.获取res.data 2.设置界面相关数据 3.显示和布局 4.小程序wxml界面(主要demo) 七、【云开发】首页列表跳转详情页 1.新建一个详情页 2.按钮跳转事件 3.跳转到具体详情页 4.关于详情页的一些代码 一、扫一扫获取图书ISBN码 用户端小程序调用 wx.scanCode接口,获取到图书ISBN码(图书条形码),在办公室找了一圈,找到了一本图书ISBN码,可以自动忽略我这渣渣的像素。 [图片] 关键代码 [代码]// pages/scanCode/scanCode.js Page({ /** * 页面的初始数据 */ data: { }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function () { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { }, scanCode: function (event) { console.log(1) // 允许从相机和相册扫码 wx.scanCode({ onlyFromCamera:true, scanType:['barCode'], success:res=>{ console.log(res.result) }, fail:err=>{ console.log(err); } }) } }) [代码] ok,获取到信息: [图片] 二、准备环境、安装依赖 1.安装Node.js准备环境 安装nodejs,准备好环境,这一步就不细说了,没有安装的可以自行百度,不知道有没有安装的可以输入 node -v 查看一下。 2.在cmd打开云函数目录中,安装依赖 输入命令: [代码]npm install --production [代码] 依赖安装成功之后,文件里面多会出现 [代码]package-lock.json[代码]这个文件。 三、编写云函数代码 1.在云函数中用获取到的ISBN传参 云函数API: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-client-api/functions/callFunction.html 通过看文档可以学会,在云函数里,我们可以通过传递一份data来获取这里面的数据,然后再通过event来拿到对应的数据。 复制这个api里面的方法: [图片] 打开实战一里面写的小程序端的扫码的js界面,把这个方法放在 [代码]success[代码]里面。 要调用的云函数的名称 [代码]name[代码]要改成成实战二教程里面建立的云函数[代码]bookinfo[代码]: [图片] 传递的参数是 [代码]isbn[代码],结果是扫码得到的[代码]result[代码]: [图片] 2.编写用户端(小程序端代码) 将 [代码]result[代码]的结果打印出来,ok,用户端(小程序端)代码写好了。 用户端(小程序端)代码写完了,就这些: [代码]// pages/scanCode/scanCode.js Page({ /** * 页面的初始数据 */ data: { }, scanCode: function(event) { console.log(1) // 允许从相机和相册扫码 wx.scanCode({ onlyFromCamera: true, scanType: ['barCode'], success: res => { console.log(res.result) // wx.cloud.callFunction({ // 要调用的云函数名称 name: 'bookinfo', // 传递给云函数的参数 data: { isbn: res.result }, success: res => { console.log(res) }, fail: err => { console.error(res) } }) }, fail: err => { console.log(err); } }) } }) [代码] 3.编写云函数端代码 打开 [代码]bookinfo[代码]里面的 [代码]index.js[代码],将 [代码]event[代码]结果打印出来,请求云函数,将云函数之中的 [代码]isbn[代码]返回回来。 [代码]// 云函数入口文件 // const cloud = require('wx-server-sdk') // cloud.init() // 云函数入口函数 //var rp = require('request-promise') exports.main = async (event, context) => { console.logI(event); return event.isbn // var res = rp('https://api.douban.com/v2/book/isbn/' + event.isbn).then(html => { // return html; // }).catch(err => { // console.log(err) // }) //return res // const wxContext = cloud.getWXContext() // return { // event, // openid: wxContext.OPENID, // appid: wxContext.APPID, // unionid: wxContext.UNIONID, // } } [代码] 上传并且部署云函数。 测试一下,云函数调用成功,返回的结果(控制台打印)是isbn: [图片] 四、调用豆瓣API获取具体数据 在网上找了一下,找到了一个可以用的豆瓣API: https://api.douban.com/v2/book/isbn/:9787111128069 打开云函数文件夹,index.js里面编写代码,引用request promise: [代码]var rp = require('request-promise') [代码] 自定义的isbn,使用一个+号来连接,在传递一个catch来处理错误情况: [代码]var res = rp( 'https://api.douban.com/v2/book/isbn/'+event.isbn).then(html=>{ return html;}).catch(err=>{ console.log(err)}) [代码] [代码]returnres[代码]res就是对应的html,将html传给用户端后,上传云函数。 继续测试一下,拿到这个条形码的信息了(书本的信息): [图片] [图片] 对于这些信息,进一步处理,拿到自己想要的信息。 打开小程序端scanCode.js: [代码] //进一步的处理方法 var bookString=res.result; console.log(JSON.parse(bookString)) [代码] [图片] 看到了整本图书上面的所有信息,修改这些信息,存入云数据库之中即可。 五、将获取到的API数据存入云数据库里面 1.初始化 使用数据库的时候,首先要进行初始化: 云开发数据库文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/init.html 打开云开发控制台创建一个集合books。 打开小程序端js,初始化数据库: [代码] //云数据库初始化 const db = wx.cloud.database({}); const book = db.collection('books'); [代码] 2.添加数据 js代码流程: [代码]// pages/scanCode/scanCode.js Page({ data: { }, scanCode: function (event) { console.log(1) // 允许从相机和相册扫码 wx.scanCode({ onlyFromCamera: true, scanType: 'barCode', success: res => { console.log(res.result) wx.cloud.callFunction({ // 要调用的云函数名称 name: 'bookinfo', // 传递给云函数的参数 data: { isbn: res.result }, success: res => { // console.log(res) //进一步的处理 var bookString = res.result; console.log(JSON.parse(bookString)) //云数据库初始化 const db = wx.cloud.database({}); const book = db.collection('books') db.collection('books').add({ // data 字段表示需新增的 JSON 数据 data: JSON.parse(bookString) }).then(res => { console.log(res) }).catch(err => { console.log(err) }) }, fail: err => { console.error(res) } }) }, fail: err => { console.log(err); } }) } }) [代码] 六、云数据库读取的数据显示在小程序端列表里 1.获取res.data 参考的读取api,请点击: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/read.html 初始化实例和book方法: [代码] //云数据库初始化 const db = wx.cloud.database({}); const book = db.collection('books') [代码] 复制API这段代码获取多个记录的数据的方法,放在项目到onload方法之中。 打印在控制台: [图片] 2.设置界面相关数据 拿到res.data之后,要赋值给page实例里面的data,所以在data里面设置一个默认的空数组: [图片] 创建一个变量来保存页面page示例中的this,方便后续使用,也可以使用箭头函数来打印一下this,看是不是page示例: [代码]const db = wx.cloud.database({}); const cont = db.collection('books'); Page({ data: { book\_list:[] }, onLoad: function(options) { // 创建一个变量来保存页面page示例中的this, 方便后续使用 var _this=this; db.collection('books').get({ success: res =>{ console.log(res.data); console.log(this); } }) }, }) [代码] [图片] 直接使用this来设置data: [图片] 3.显示和布局 使用组件库引入,可以省略自己写很多代码的样式,简单方便,当然也可以自己写:https://youzan.github.io/vant-weapp/#/card 因为数据不止一条,循环,所以要用到小程序框架的列表渲染: https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/list.html 写好之后 wxml如下: [代码]<text>私家书柜</text> <view wx:for="{{book\\_list}}"> <van-card num="2" price="2.00" desc="描述信息" title="商品标题" /> </view> [代码] 4.小程序wxml界面(主要demo) wxml: [代码]<view wx:for="{{book\\_list}}"> <van-card num="2" price="{{item.price}}" desc="{{item.author}}" title="{{item.title}}" thumb="{{item.image }}" /> </view> [代码] js: [代码]const db = wx.cloud.database({}); const cont = db.collection('books'); Page({ data: { book_list:[] }, onLoad: function(options) { // 创建一个变量来保存页面page示例中的this, 方便后续使用 var _this=this; db.collection('books').get({ success: res =>{ console.log(res.data[0]); this.setData({ book_list:res.data }) } }) }, }) [代码] ok,云数据库读取的数据显示在小程序端列表里。 七、【云开发】首页列表跳转详情页 1.新建一个详情页 打开app.json, [代码]"pages/details/details",[代码],自动生成了一个详情页: [图片] 2.按钮跳转事件 打开首页列表页代码,绑定详情按钮跳转事件。 wxml: [代码]<view wx:for="{{book\\_list}}"> <van-card num="2" price="{{item.price}}" desc="{{item.author}}" title="{{item.title}}" thumb="{{item.image }}"> <view slot="footer"> <van-button size="mini" bind:click="viewitem">详情按钮</van-button> </view> </van-card> </view> [代码] [图片] 继续写js里面的绑定事件,在控制台打印一下event,方便后续测试: [代码]viewitem: function(event) { console.log(event) } [代码] 3.跳转到具体详情页 要在云开发里面写一个特定的id,打开云开发控制台,数据库,需要用到这个下划线是_id的字段: [图片] 给这个字段设置一个值,[代码]data-id="{{item._id}}"[代码]: [图片] 点击按钮,可以看到,点击不同的列表,打印的是不同的id,通过不同的id就可以看到不同的内容了: [图片] 4.关于详情页的一些代码 初始化db的实例: [代码]const db = wx.cloud.database({}); [代码] 打开云函数文档里面的读取数据api: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/read.html 复制此段读取数据记录的代码,放在onload里面: [图片] 可以看到,具体数据已经打印过来了: [图片] 这个时候还没有将数据传递到一个具体的页面实例中: [图片] 所以,success开始改成使用箭头函数,进入页面的时候,可以看到appdata里面的book: [图片] 具体展示:在wxml里面写上想要拿到的数据,ok,详情页面展示的数据: [图片] 效果如下: [图片] 这样,我们就完成了利用云开发扫码读取ISBN码并获取图书各种信息的全部步骤啦~
2019-04-17 - 小程序实现大转盘,九宫格抽奖,带跑马灯效果
基本实现功能 1,小程序仿天猫超市大转盘 2,九宫格转盘抽奖 3,积分抽奖 4,抽到的积分随机生成 5,抽奖结果可以同步到服务器(小程序云开发后台) 老规矩先看效果图 [图片] 简单说一下实现原理. 我们借助js的定时器,来执行一个加法。比如我们设置一个上限300,每过一定时间执行一次,然后我们再做一个随机数,这个随机数不停的++,直到总数大于300.就代表抽奖结束。核心代码如下。 [代码] //开始抽奖 startGame: function() { if (this.data.isRunning) return this.setData({ isRunning: true }) var _this = this; var indexSelect = 0 var i = 0; var timer = setInterval(function() { indexSelect++; let randomNum = Math.floor(Math.random() * 10) * 10; //可均衡获取0到90的随机整数 i += randomNum; if (i > 300) { //去除循环 clearInterval(timer) //获奖提示 let jifen = 1; let selectNum = _this.data.indexSelect console.log("选号:" + selectNum ); if (selectNum===0) { jifen = 2; } else if (selectNum === 1) { jifen = 3; } else if (selectNum === 2) { jifen = 4; } else if (selectNum === 3) { jifen = 5; } else if(selectNum === 4) { jifen = 6; } else if(selectNum === 5) { jifen = 8; } else if (selectNum === 6) { jifen = 10; } wx.showModal({ title: '恭喜您', content: '获得了' + jifen + "积分", showCancel: false, //去掉取消按钮 success: function(res) { if (res.confirm) { _this.setData({ isRunning: false }) } } }) } indexSelect = indexSelect % 8; _this.setData({ indexSelect: indexSelect }) }, (200 + i)) } [代码] 完整源码可以加我微信,如果有关于小程序的问题,可以加我微信2501902696(备注小程序)
2019-03-05 - 轻松生成小程序分享海报
小程序海报组件 https://github.com/jasondu/wxa-plugin-canvas 需求 小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小的压力;所以使用小程序的canvas是一个不错的选择,但由于canvas水比较深,坑比较多,还有不同海报需要重现写渲染流程,导致代码冗余难以维护,加上不同设备版本的情况不一样,因此小程序海报生成组件的需求十分迫切。 在实际开发中,我发现海报中的元素无非一下几种,只要实现这几种,就可以通过一份配置文件生成各种各样的海报了。 海报中的元素分类 [图片] 要解决的问题 单位问题 canvas隐藏问题 圆角矩形、圆角图片 多段文字 超长文字和多行文字缩略问题 矩形包含文字 多个元素间的层级问题 图片尺寸和渲染尺寸不一致问题 canvas转图片 IOS 6.6.7 clip问题 关于获取canvas实例 单位问题 canvas绘制使用的是px单位,但不同设备的px是需要换算的,所以在组件中统一使用rpx单位,这里就涉及到单位怎么换算问题。 通过wx.getSystemInfoSync获取设备屏幕尺寸,从而得到比例,进而做转换,代码如下: [代码]const sysInfo = wx.getSystemInfoSync(); const screenWidth = sysInfo.screenWidth; this.factor = screenWidth / 750; // 获取比例 function toPx(rpx) { // rpx转px return rpx * this.factor; } function toRpx(px) { // px转rpx return px / this.factor; }, [代码] canvas隐藏问题 在绘制海报过程时,我们不想让用户看到canvas,所以我们必须把canvas隐藏起来,一开始想到的是使用display:none; 但这样在转化成图片时会空白,所以这个是行不通的,所以只能控制canvas的绝对定位,将其移出可视界面,代码如下: [代码].canvas.pro { position: absolute; bottom: 0; left: -9999rpx; } [代码] 圆角矩形、圆角图片 由于canvas没有提供现成的圆角api,所以我们只能手工画啦,实际上圆角矩形就是由4条线(黄色)和4个圆弧(红色)组成的,如下: [图片] 圆弧可以使用canvasContext.arcTo这个api实现,这个api的入参由两个控制点一个半径组成,对应上图的示例 [代码]canvasContext.arcTo(x1, y1, x2, y2, r) [代码] 接下来我们就可以非常轻松的写出生成圆角矩形的函数啦 [代码]/** * 画圆角矩形 */ _drawRadiusRect(x, y, w, h, r) { const br = r / 2; this.ctx.beginPath(); this.ctx.moveTo(this.toPx(x + br), this.toPx(y)); // 移动到左上角的点 this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y)); // 画上边的线 this.ctx.arcTo(this.toPx(x + w), this.toPx(y), this.toPx(x + w), this.toPx(y + br), this.toPx(br)); // 画右上角的弧 this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br)); // 画右边的线 this.ctx.arcTo(this.toPx(x + w), this.toPx(y + h), this.toPx(x + w - br), this.toPx(y + h), this.toPx(br)); // 画右下角的弧 this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h)); // 画下边的线 this.ctx.arcTo(this.toPx(x), this.toPx(y + h), this.toPx(x), this.toPx(y + h - br), this.toPx(br)); // 画左下角的弧 this.ctx.lineTo(this.toPx(x), this.toPx(y + br)); // 画左边的线 this.ctx.arcTo(this.toPx(x), this.toPx(y), this.toPx(x + br), this.toPx(y), this.toPx(br)); // 画左上角的弧 } [代码] 如果是画线框就使用[代码]this.ctx.stroke();[代码] 如果是画色块就使用[代码]this.ctx.fill();[代码] 如果是圆角图片就使用 [代码]this.ctx.clip(); this.ctx.drawImage(***); [代码] clip() 方法从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)。 多段文字 如果是连续多段不同格式的文字,如果让用户每段文字都指定坐标是不现实的,因为上一段文字的长度是不固定的,这里的解决方案是使用[代码]ctx.measureText[代码] (基础库 1.9.90 开始支持)Api来计算一段文字的宽度,记住这里返回宽度的单位是px(坑),从而知道下一段文字的坐标。 超长文字和多行文字缩略问题 设置文字的宽度,通过[代码]ctx.measureText[代码]知道文字的宽度,如果超出设定的宽度,超出部分使用“…”代替;对于多行文字,经测试发现字体的高度大约等于字体大小,并提供lineHeight参数让用户可以自定义行高,这样我们就可以知道下一行的y轴坐标了。 矩形包含文字 这个同样使用[代码]ctx.measureText[代码]接口,从而控制矩形的宽度,当然这里用户还可以设置paddingLeft和paddingRight字段; 文字的垂直居中问题可以设置文字的基线对齐方式为middle([代码]this.ctx.setTextBaseline('middle');[代码]),设置文字的坐标为矩形的中线就可以了;水平居中[代码]this.ctx.setTextAlign('center');[代码]; [图片] 多个元素间的层级问题 由于canvas没有Api可以设置绘制元素的层级,只能是根据后绘制层级高于前面绘制的方式,所以需要用户传入zIndex字段,利用数组排序(Array.prototype.sort)后再根据顺序绘制。 图片尺寸和渲染尺寸不一致问题 绘制图片我们使用[代码]ctx.drawImage()[代码]API; 如果使用[代码]drawImage(dx, dy, dWidth, dHeight)[代码],图片会压缩尺寸以适应绘制的尺寸,图片会变形,如下图: 在基础库1.9.0起支持[代码]drawImage(sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)[代码],sx和sy是源图像的矩形选择框左上角的坐标,sWidth和sHeight是源图像的矩形选择框的宽度和高度,如下图: [图片] 如果绘制尺寸比源图尺寸宽,那么绘制尺寸的宽度就等于源图宽度;反之,绘制尺寸比源图尺寸高,那么绘制尺寸的高度等于源图高度; 我们可以通过[代码]wx.getImageInfo[代码]Api获取源图的尺寸; canvas转图片 在canvas绘制完成后调用[代码]wx.canvasToTempFilePath[代码]Api将canvas转为图片输出,这样需要注意,[代码]wx.canvasToTempFilePath[代码]需要写在[代码]this.ctx.draw[代码]的回调中,并且在组件中使用这个接口需要在第二个入参传入this(坑),如下 [代码]this.ctx.draw(false, () => { wx.canvasToTempFilePath({ canvasId: 'canvasid', success: (res) => { wx.hideLoading(); this.triggerEvent('success', res.tempFilePath); }, fail: (err) => { wx.hideLoading(); this.triggerEvent('fail', err); } }, this); }); [代码] IOS 6.6.7 clip问题 在IOS 6.6.7版本中clip方法连续裁剪图片时,只有第一张有效,这是微信的bug,官方也证实了(https://developers.weixin.qq.com/community/develop/buglist) 关于获取canvas实例 我们可以使用[代码]wx.createCanvasContext[代码]获取小程序实例,但在组件中使用切记第二个参数需要带上this,如下 [代码]this.ctx = wx.createCanvasContext('canvasid', this); [代码] 如何使用组件 https://github.com/jasondu/wxa-plugin-canvas
2019-02-22 - 小程序性能和体验优化方法
[图片] 小程序应避免出现任何 JavaScript 异常 出现 JavaScript 异常可能导致小程序的交互无法进行下去,我们应当追求零异常,保证小程序的高鲁棒性和高可用性 小程序所有请求应响应正常 请求失败可能导致小程序的交互无法进行下去,应当保证所有请求都能成功 所有请求的耗时不应太久 请求的耗时太长会让用户一直等待甚至离开,应当优化好服务器处理时间、减小回包大小,让请求快速响应 避免短时间内发起太多的图片请求 短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术或在屏幕外的图片使用懒加载 避免短时间内发起太多的请求 短时间内发起太多请求会触发小程序并行请求数量的限制,同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等 避免 setData 的数据过大 setData工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 常见的 setData 操作错误 频繁的去 setData Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层 染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时 每次 setData 都传递大量新数据 由setData的底层实现可知,数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程 后台态页面进行 setData 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行 避免 setData 的调用过于频繁 setData接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 避免将未绑定在 WXML 的变量传入 setData setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗 合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差 避免渲染界面的耗时过长 渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 避免执行脚本的耗时过长 执行脚本的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要确认并优化脚本的逻辑 对网络请求做必要的缓存以避免多余的请求 发起网络请求总会让用户等待,可能造成不好的体验,应尽量避免多余的请求,比如对同样的请求进行缓存 wxss 覆盖率较高,较少或没有引入未被使用的样式 按需引入 wxss 资源,如果小程序中存在大量未使用的样式,会增加小程序包体积大小,从而在一定程度上影响加载速度 文字颜色与背景色搭配较好,适宜的颜色对比度更方便用户阅读 文字颜色与背景色需要搭配得当,适宜的颜色对比度可以让用户更好地阅读,提升小程序的用户体验 所有资源请求都建议使用 HTTPS 使用 HTTPS,可以让你的小程序更加安全,而 HTTP 是明文传输的,存在可能被篡改内容的风险 不使用废弃接口 使用即将废弃或已废弃接口,可能导致小程序运行不正常。一般而言,接口不会立即去掉,但保险起见,建议不要使用,避免后续小程序突然运行异常 避免过大的 WXML 节点数目 建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长 避免将不可能被访问到的页面打包在小程序包里 小程序的包大小会影响加载时间,应该尽量控制包体积大小,避免将不会被使用的文件打包进去 及时回收定时器 定时器是全局的,并不是跟页面绑定的,当页面因后退被销毁时,定时器应注意手动回收 避免使用 css ‘:active’ 伪类来实现点击态 使用 css ‘:active’ 伪类来实现点击态,很容易触发,并且滚动或滑动时点击态不会消失,体验较差 建议使用小程序内置组件的 ‘hover-*’ 属性来实现 滚动区域可开启惯性滚动以增强体验 惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 [代码]-webkit-overflow-scrolling: touch[代码] 的样式
2019-03-15 - 【微信小程序】性能优化
为什么要做性能优化? 一切性能优化都是为了体验优化 1. 使用小程序时,是否会经常遇到如下问题? 打开是一直白屏 打开是loading态,转好几圈 我的页面点了怎么跳转这么慢? 我的列表怎么越滑越卡? 2. 我们优化的方向有哪些? 启动加载性能 渲染性能 3. 启动加载性能 1. 首次加载 你是否见过小程序首次加载时是这样的图? [图片] 这张图中的三种状态对应的都是什么呢? 小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:[代码]下载小程序代码包[代码]、[代码]加载小程序代码包[代码]、[代码]初始化小程序首页[代码]。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。 2. 加载顺序 小程序加载的顺序是如何? 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 [图片] 通过2,我们知道了,问题1中第一张图是[代码]资源准备[代码](代码包下载);第二张图是[代码]业务代码的注入以及落地页首次渲染[代码];第三张图是[代码]落地页数据请求时的loading态[代码](部分小程序存在) 3. 控制包大小 提升体验最直接的方法是控制小程序包的大小,这是最显而易见的 勾选开发者工具中“上传代码时,压缩代码”选项; 及时清理无用的代码和资源文件(包括无用的日志代码) 减少资源包中的图片等资源的数量和大小(理论上除了小icon,其他图片资源从网络下载),图片资源压缩率有限 从开发者的角度看,控制代码包大小有助于减少小程序的启动时间。对低于1MB的代码包,其下载时间可以控制在929ms(iOS)、1500ms(Android)内。 4. 采用分包加载机制 根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载; [图片] 使用分包时需要注意代码和资源文件目录的划分。启动时需要访问的页面及其依赖的资源文件应放在主包中。 5 采用分包预加载技术 在4的基础上,当用户点击到子包的目录时,还是有一个代码包下载的过程,这会感觉到明显的卡顿,所以子包也不建议拆的太大,当然我们可以采用子包预加载技术,并不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转; [图片] 这种基于配置的子包预加载技术,是可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;是灵活可控的 6. 采用独立分包技术 目前很多小程序[代码]主包+子包[代码](2M+6M)的方式,但是在做很多运营活动时,我们会发现活动(红包)是在子包里,但是运营、产品投放的落地页链接是子包链接,这是的用户在直达落地时,必须先下载主包内容(一般比较大),在下载子包内容(相对主包,较小),这使得在用户停留时间比较短的小程序场景中,用户体验不是很好,而且浪费了很大部分流量; [图片] 可以采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源; 7. 首屏加载的优化建议 7.1 提前请求 异步请求可以在页面onLoad就加载,不需要等页面ready后在异步请求数据;当然,如果能在前置页面点击跳转时预请求当前页的核心异步请求,效果会更好; 7.2 利用缓存 利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新,这不仅优化了性能,在无网环境下,用户也能很顺畅的使用到关键服务; 7.3 避免白屏 可以在前置页面将一些有用的字段带到当前页,进行首次渲染(列表页的某些数据–> 详情页),没有数据的模块可以进行骨架屏的占位,使用户不会等待的很焦虑,甚至走了; 7.4 及时反馈 及时的对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了,无响应 渲染性能优化 1. 小程序渲染原理 双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。 [图片] 分析这个流程不难得知:页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。 [图片] 2. 避免使用不当setData 在数据传输时,逻辑层会执行一次[代码]JSON.stringify[代码]来去除掉[代码]setData[代码]数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将[代码]setData[代码]所设置的数据字段与[代码]data[代码]合并,使开发者可以用[代码]this.data[代码]读取到变更后的数据。因此,为了提升数据更新的性能,开发者在执行[代码]setData[代码]调用时,最好遵循以下原则: 2.1 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; [图片] 2.2 数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用[代码]setData[代码]来设置这些数据; [图片] 2.3 与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下 [图片] 提升数据更新性能方式的代码示例 [代码]Page({ onShow: function() { // 不要频繁调用setData this.setData({ a: 1 }) this.setData({ b: 2 }) // 绝大多数时候可优化为 this.setData({ a: 1, b: 2 }) // 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外 this.setData({ myData: { a: '这个字符串在WXML中用到了', b: '这个字符串未在WXML中用到,而且它很长…………………………' } }) // 可以优化为 this.setData({ 'myData.a': '这个字符串在WXML中用到了' }) this._myData = { b: '这个字符串未在WXML中用到,而且它很长…………………………' } } }) [代码] 利用setData进行列表局部刷新 在一个列表中,有[代码]n[代码]条数据,采用上拉加载更多的方式,假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果 解决方法 1、可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来) 2、说到重点了,就是利用[代码]setData[代码]局部刷新 [代码]> a.将点赞的`id`传过去,知道点的是那一条数据, 将点赞的`id`传过去,知道点的是那一条数据 [代码] [代码]<view wx:if="{{!item.status}}" class="btn" data-id="{{index}}" bindtap="couponTap">立即领取</view> [代码] [代码]> b.重新获取数据,查找相对应id的那条数据的下标(`index`是不会改变的) > c.用setData进行局部刷新 [代码] [代码]this.setData({ list[index] = newList[index] }) [代码] 其实这个小操作对刚刚接触到微信小程序的人来说应该是不容易发现的,不理解setData还有这样的写法。 2.4 切勿在后台页面进行setData 在一些页面会进行一些操作,而到页面跳转后,代码逻辑还在执行,此时多个[代码]webview[代码]是共享一个js进程;后台的[代码]setData[代码]操作会抢占前台页面的渲染资源; [图片] [图片] 3. 用户事件使用不当 视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。 1.去掉不必要的事件绑定(WXML中的[代码]bind[代码]和[代码]catch[代码]),从而减少通信的数据量和次数; 2.事件绑定时需要传输[代码]target[代码]和[代码]currentTarget[代码]的[代码]dataset[代码],因而不要在节点的[代码]data[代码]前缀属性中放置过大的数据。 [图片] 4. 视图层渲染原理 4.1首次渲染 初始渲染发生在页面刚刚创建时。初始渲染时,将初始数据套用在对应的WXML片段上生成节点树。节点树也就是在开发者工具WXML面板中看到的页面树结构,它包含页面内所有组件节点的名称、属性值和事件回调函数等信息。最后根据节点树包含的各个节点,在界面上依次创建出各个组件。 [图片] 在这整个流程中,时间开销大体上与节点树中节点的总量成正比例关系。因而减少WXML中节点的数量可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。 简化WXML代码的例子 [代码]<view data-my-data="{{myData}}"> <!-- 这个 view 和下一行的 view 可以合并 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> <text> <!-- 这个 text 通常是没必要的 --> {{myText}} </text> </view> </view> <!-- 可以简化为 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> {{myText}} </view> [代码] 4.2 重渲染 初始渲染完毕后,视图层可以多次应用[代码]setData[代码]的数据。每次应用[代码]setData[代码]数据时,都会执行重渲染来更新界面。初始渲染中得到的data和当前节点树会保留下来用于重渲染。每次重渲染时,将[代码]data[代码]和[代码]setData[代码]数据套用在WXML片段上,得到一个新节点树。然后将新节点树与当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新、哪些节点需要添加或移除。最后,将[代码]setData[代码]数据合并到[代码]data[代码]中,并用新节点树替换旧节点树,用于下一次重渲染。 [图片] 在进行当前节点树与新节点树的比较时,会着重比较[代码]setData[代码]数据影响到的节点属性。因而,去掉不必要设置的数据、减少[代码]setData[代码]的数据量也有助于提升这一个步骤的性能。 5. 使用自定义组件 自定义组件的更新只在组件内部进行,不受页面其他不能分内容的影响;比如一些运营活动的定时模块可以单独抽出来,做成一个定时组件,定时组件的更新并不会影响页面上其他元素的更新;各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用。 [图片] 6. 避免不当的使用onPageScroll 每一次事件监听都是一次视图到逻辑的通信过程,所以只在必要的时候监听pageSrcoll [图片] 总结 小程序启动加载性能 控制代码包的大小 分包加载 首屏体验(预请求,利用缓存,避免白屏,及时反馈 小程序渲染性能 避免不当的使用setData 合理利用事件通信 避免不当的使用onPageScroll 优化视图节点 使用自定义组件
2019-03-07 - 小程序/小游戏后端获取当前appid、版本号和接口环境控制
写在前面的话 我们的后端架构是基于微服务的平台化架构,提供了一套通用的接口,所有的小程序/小游戏都走这套接口,由负载均衡统一分发,这样就需要区分是哪个应用的哪个版本访问的接口,以便准确的处理数据。 解决方案 一、请求时将 [代码]appid[代码] 和版本信息放入参数中提交给后端。 优点:简单,直接。 缺点:只能将信息传给后端,而后端无法控制小程序/小游戏端逻辑。 二、使用 wx.getAccountInfoSync 接口获取账号信息。 优点:官方提供的API。 缺点:里面只有 [代码]appid[代码],没有版本信息,存在和 [代码]方案一[代码] 同样的问题且不支持小游戏。 三、后端获取Referer信息,从里面获取信息。 优点:wx.request 中 [代码]header[代码] 内 [代码]Referer[代码] 的值是默认生成不可以设置的且格式固定。 缺点:无法区分开发版本、体验版本和审核版本, [代码]方案一[代码] 的问题依然存在。 四、自己维护版本信息,提供一个远程的版本信息同步接口。 优点:可扩展性高。 缺点:开发时需要定义版本信息,小程序/小游戏每次打开时都需要同步版本信息。 最佳方案 我们选择了 [代码]方案三[代码] 配合 [代码]方案四[代码] 来实现,主要原因是因为 wx.request 中 [代码]header[代码] 内 [代码]Referer[代码] 的值是默认生成不可以设置的且格式固定,并且里面包含了 [代码]appid[代码] 和 [代码]版本号[代码] [小程序自己的版本号,并不是我们自己定义的版本号,且只有线上版本的] 的信息。小程序/小游戏端无需增加参数后端即可获取这些信息。 [代码]Referer[代码] 的格式固定为: [代码]https://servicewechat.com/{ appid }/{ version }/page-frame.html [代码] 其中 [代码]{ appid }[代码] 为小程序的 [代码]appid[代码],[代码]{ version }[代码] 为小程序的版本号,版本号为 [代码]0[代码] 表示为开发版本、体验版本以及审核版本,版本号为 [代码]devtools[代码] 表示为开发者工具,其余为正式版本。 以上描述摘自官方文档 网络 的章节 开发版本、体验版本以及审核版本的 [代码]version[代码] 都为 0 ,然而在开发过程中后端分为 [代码]dev[代码] 、[代码]test[代码] 、[代码]uat[代码] 、[代码]prod[代码] 等不同的环境,只使用 [代码]方案三[代码] 并不能完美的解决问题,这种方式前端并不能获取到当前版本的状态,无法动态的切换访问的环境,如果都去通过负载均衡去分发,反而给负载均衡带来了压力,所有环境都部署在外网也不现实。 于是我们想到了一个解决办法,也就是 [代码]方案四[代码],自己维护版本号。通过版本状态来判断当前版本需要请求的环境。 实现思路 小程序/小游戏分为开发版本、体验版本、审核版本和线上版本。 开发版本是由开发者进行上传才可以访问的,开发者工具模拟器、预览和真机调试的不会出现在这里。 体验版本是由管理员选定某个人提交的开发版本作为体验版的。 审核版本也是由管理员选定某个人提交的开发版本进行提交审核的。 线上版本是审核版本审核通过后,管理员进行发布的。 不同角色的人员如何访问对应的版本 角色 环境 版本 小程序/小游戏的访问方式 开发 dev 使用开发者工具模拟器、预览或真机调试进行访问 测试 test 开发版本 使用 [代码]小程序助手[代码] 小程序,访问不同开发人员上传的版本 验收 uat 体验版本 扫描体验版二维码或使用 [代码]小程序助手[代码] 小程序进行访问体验版 审核 prod 审核版本 只有审核人员可以访问 用户 prod 线上版本 发布后全网可以访问 小程序/小游戏端定义自己的版本号,新版本开发时,版本号进行升级,同时后端数据库中添加当前开发中的小程序/小游戏版本号,并标识当前这个版本号的状态为 [代码]dev[代码] 开发阶段,提交测试时将版本号的状态修改为 [代码]test[代码] 测试阶段,用户验收时将版本号的状态修改为 [代码]uat[代码] 验收阶段,后续版本的做法以此类推。 后端提供一个接口,小程序/小游戏端打开后,首先调用这个接口进行状态同步,参数为当前的版本号,接口返回版本的状态。小程序/小游戏中通过硬编码的方式,根据状态来判断当前需要访问的环境地址。 需要后端先上线版本号管理的功能。 每个环境中的接口 [代码]pathname[代码] 是固定不变的,会变动的只有 [代码]origin[代码]。 举例: [代码]生产环境中的一个接口 https://examplp.com/api/login 验收环境中的一个接口 http://10.10.10.10:8080/api/login 测试环境中的一个接口 http://192.168.1.100:8080/api/login 开发环境中的一个接口 http://localhost:8080/api/login [代码] 其中 [代码]/api/login[代码] 为 [代码]pathname[代码] 是固定不变的,变动的只有前面的域名也就是 [代码]origin[代码],小程序/小游戏端只需要根据版本号的状态去修改 [代码]origin[代码] 就可以了。 注意:使用这套方案时,一定要防止版本信息同步接口过度依赖而影响主业务的事情发生,请做好兼容处理。 功能扩展 通过这套流程不但可以动态的切换小程序/小游戏端访问不同环境的后端接口,还可以动态的停止线上的某个版本。 接口再扩展一个 [代码]openid[代码] 的参数,也可以指定某个人的访问环境。比如测试需要使用线上版本将接口转到测试环境等。
2019-02-20 - 区分开发版,体验版以及正式版
在开发过程中,后端总会需要区分线上版和开发版,一直以来小程序官方都没有一个API可以支持识别版本类型,所以一直采用的方案是全局定义APIHost,手动切换. 最近发现一个方案,暂时观察是可行的. 小程序做网络请求时,开发者服务器可以在header头里获取referer字段,通常格式为: "https://servicewechat.com/(小程序ID)/(X)/page-frame.html" 其中X的值随着版本的不同在变化, 开发版及体验版:0, 小程序开发工具:devtool, 线上版:>0 可以在做小程序第一个网络请求前去请求一个特定API,用以获取APIHost及当前版本信息
2018-12-17 - 3D视觉效果轮播图 单排显示3个切换
滑动切换轮播图 当前轮播图比其他两张大一点 视觉突出点 有种3D效果 效果图见底下 代码片段: https://developers.weixin.qq.com/s/OEZSHbmy7X6v 效果图: [图片] 欢迎吐槽
2019-02-15 - 目前为止最全的微信小程序项目实例
wx-gesture-lock 微信小程序的手势密码 WXCustomSwitch 微信小程序自定义 Switch 组件模板 WeixinAppBdNovel 微信小程序demo:百度小说搜索 shitoujiandaobu 小程序:石头剪刀布(附代码说明) audiodemo 微信小程序开发之视频播放器 Video 弹幕 弹幕颜色自定义 star 微信小程序开发之五星评分 switchCity 微信小程序开发之城市选择器 城市切换 huadong_del 微信小程序滑动删除效果 jianhang_menu 微信小程序开发之圆形菜单 仿建行圆形菜单 xiaoxiaoxiao_lazyload 实现微信小程序图片懒加载特效 kangaiduowei 微信小程序:康爱多微商城:学习界面设计 tianmao_dazhuanpan 小程序实现大转盘 仿天猫抽奖 跑马灯效果(有图有源码) weapp-meirong 微信小程序学习用demo推荐:美容商城;列表,预约 baisi 微信小程序仿百思不得姐 weapp-one 仿 「ONE · 一个」 的微信小程序 netmusic-app 仿网易云音乐APP的微信小程序 a_takeaway 微信小程序的外卖demo sideslip 微信小程序『侧边栏滑动』特效 wx_plo 微信小程序之仿微信漂流瓶 kwonWhere 微信小程序-知亦行 audiodemo 微信小程序开发之视频播放 弹幕 弹幕颜色自定义 wxChart 微信小程序图标插件 guoku 微信小程序-果库 snake 微信小程序-贪吃蛇小程序 douban_movie 微信小程序-仿豆瓣电影 RecordDemo 麦克风动画 shishanggou 实现了包括常用组件,ajax获取数据,模板使用,路由等的使用,下拉刷新数据;
2019-02-12 - 安卓video视频重播【已解决】经验分享
自己写的代码片段 https://developers.weixin.qq.com/s/wo1dA9mQ746o 安卓video视频重播缓存问题和下载保存到手机实例 解决方案 给url 后缀加个 传参 我代码片段是用时间戳 这样保证他不会因为地址相同而不重新缓存 这样也可以说是手动清除缓存 例如:https://www.xxx.com/xxx.mp4?filekey='121212121' (传参字段可以自定义,后面等于时间戳即可) video重播有问题即可解决 问题描述: 视频刚加载一点,然后拖进度到最后,等它播放完,点重播,视频会卡到视频最后一帧的画面,除非重新打开 就是视频缓存导致的问题 重播 安卓手机会自动播放缓存内容 但是安卓的缓存又好像只有最后十几秒的 (如果视频较短 只有十几秒 不存在重播问题 高于一分钟基本缓存都是 最后十几秒)【官方说出现这个问题是只有小米手机出现过,我的手机是小米6 出现问题的】 论坛另一个码友碰到此类坑 详情戳 https://developers.weixin.qq.com/community/develop/doc/000684eb41c3f0ecd8e72adb05b000 后面我开发时候也碰到此类问题 只有安卓才会这样 我发现新浪微博小程序也有这个bug 一直找不到解决办法 后面查看小程序官方组件展示 我发现里面的 video组件视频不会有此问题 然后我以为是我写的代码不对 就去翻看源码 发现它的视频地址是这样的 srcplay: 'http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400' 和我的视频地址不一样 我的是 https://www.xxx.com/xxx.mp4 这样的 所以我分析一波 再加上我知道是安卓缓存问题 所以我得出解决方案 最后 不得不说句 小程序太牛逼了 是真的牛逼 [图片] 备注 代码片段中需要填入的视频地址 [图片]
2019-02-19 - 【技巧】利用canvas生成朋友圈分享海报
大家好,上次给大家讲了函数防抖和函数节流https://developers.weixin.qq.com/community/develop/doc/0002c892fb80a8326bf70f56d5bc04 今天给大家分享一下利用canvas生成朋友圈分享海报 由于小程序的限制,我们不能很方便地在微信内直接分享小程序到朋友圈,所以普遍的做法是生成一张带有小程序分享码的分享海报,再将海报保存到手机相册,有两种方法可以生成分享海报,第一种是让后台生成然后返回图片链接,这一种方法比较简单,只需要传后台所需要的参数就行了,今天给大家介绍的是第二种方法,用canvas生成分享海报。 首先先来看下效果: [图片] 主要步骤: 1. 把海报样式用标签先写好,方便画图时可以比对 2. 用canvas进行画图,canvas要注意定好宽高 3. canvas利用wx.canvasToTempFilePath这个api将canvas转化为图片 4. 将转化好的图片链接放入image标签里 5. 再利用wx.saveImageToPhotosAlbum保存图片 这里有几个坑点需要注意下: 1. 用canvas进行画图的时候要注意画出来的图的大小一定要是你用标签写好那个样式的两倍大小,比如你的海报大小是400*600的大小,那你用canvas画的时候大小就要是800*1200,宽高可以写在样式里,如果你画出来的图跟你海报图是一样的大小的话生成的图片是会很模糊的,所以才需要放大两倍。 2. 画图的时候要注意尺寸的转化,如果你是用rpx做单位的话,就要对单位进行转化,因为canvas提供的方法都是经px为单位的,所以这一点要注意一下,px转rpx的公式是w/750*z*2,w是手机屏幕宽度screenWidth,可以通过wx.getSystemInfo获取,z是你需要画图的单位,2就是乘以两倍大小。 3. 图片来源问题,因为canvas不支持网络图片画图,所以你的图片要么是固定的,如果不是固定的,那就要用wx.downloadFile下载后得到一个临时路径才行 4. 小程序码问题,小程序需要后台请求接口后返回一个二进制的图片,因为二进制图片canvas也是不支持的,所以也是要用wx.downloadFile下载后得到一个临时路径,或者可以叫后台直接返回一个小程序码的路径给你 5. 这里保存的时候是有个授权提醒的,如果拒绝的话再次点击就没有反应了,所以这里我做了一个判断是否有授权的,如果没有就弹窗提醒,确认的话会打开设置页面,确认授权后再次返回就行了,这里有个坑注意下,就是之前拒绝后再进入设置页面确认授权返回页面时保存图片会不成功,官方还没解决,我是加了个setTimeOut处理的,详情可以看这里https://developers.weixin.qq.com/community/develop/doc/000c46600780f0fa68d7eac345a400 代码实现: [图片] 这里图片我先用的是网上的链接,实际项目中是后台返回的数据,这个可以自行处理,这里只是为了演示方便,生成临时路径的方法我这里是分别定义了一个方法,其实可以合成一个方法的,只是生成小程序码时如果要传入参数要注意一下。 绘图方法是drawImg,这里截一部分,详细的可以看代码片段 [图片] 不足: 由于在实际项目中返回的图片宽高是不固定的,但是canvas画出来的又需要固定宽高,所以分享图会有图片变形的问题,使用drawImage里的参数也不能解决,如果各位有比较好的方案可以一起讨论一下。 代码片段: https://developers.weixin.qq.com/s/3pcsjDmS7M5Y 系甘先,得闲饮茶
2019-01-23 - 云开发生成小程序二维码,有需要的进。
真机测试已通过。 小程序端: wx.cloud.callFunction({ name: 'getQRCode', data: { scene: 'scene', page: 'pages/index/index', width: 180 } }).then(res => { let qr = "data:image/png;base64," + wx.arrayBufferToBase64(res.result) }) 云函数:getQRCode: const secret = 'your secret' const rp = require('request-promise') exports.main = async (event, context) => { let opt = { method: 'GET', url: 'https://api.weixin.qq.com/cgi-bin/token', qs: { appid: event.userInfo.appId, secret, grant_type: 'client_credential' }, json: true } let res = await rp(opt) opt = { method: 'POST', url: 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=' + res.access_token, body: { 'page': event.page, 'width': event.width, 'scene': event.scene }, json: true, encoding: null } return await rp(opt) }
2018-12-07 - 自定义导航栏所有机型的适配方案
写在前面的话 大家看到这个文章时一定会感觉这是在炒剩饭,社区中已经有那么多分享自定义导航适配的文章了,为什么我还要再写一个呢? 主要原因就是,社区中大部分的适配方案中给出的大小是不精确的,并不能完美适配各种场景。 社区中大部分文章给到的值是 iOS -> 44px , Android -> 48px 思路 正常来讲,iOS和Android下的胶囊按钮的位置以及大小都是相同且不变的,我们可以通过胶囊按钮的位置和大小再配合 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 来计算出导航栏的位置和大小。 小程序提供了一个获取菜单按钮(右上角胶囊按钮)的布局位置信息的API,可以通过这个API获取到胶囊按钮的位置信息,但是经过实际测试,这个接口目前存在BUG,得到的值经常是错误的(通过特殊手段可以偶尔拿到正确的值),这个接口目前是无法使用的,等待官方修复吧。 下面是我经过实际测试得到的准确数据: 真机和开发者工具模拟器上的胶囊按钮不一样 [代码]# iOS top 4px right 7px width 87px height 32px # Android top 8px right 10px width 95px height 32px # 开发者工具模拟器(iOS) top 6px right 10px width 87px height 32px # 开发者工具模拟器(Android) top 8px right 10px width 87px height 32px [代码] [代码]top[代码] 的值是从 [代码]statusBarHeight[代码] 作为原点开始计算的。 使用上面数据中胶囊按钮的高度加 [代码]top[代码] * 2 上再加上 [代码]statusBarHeight[代码] 的高度就可以得到整个导航栏的高度了。 为什么 [代码]top[代码] * 2 ?因为胶囊按钮是垂直居中在 title 那一栏中的,上下都要有边距。 扩展 通过胶囊按钮的 [代码]right[代码] 可以准确的算出自定义导航的 [代码]左边距[代码]。 通过胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]右边距[代码] 。 通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]windowWidth[代码] - 胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]width[代码] 。 再扩展 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 每个机型都不一样,刘海屏得到的数据也是准确的。 如果是自定义整个页面,iPhone X系列的刘海屏,底部要留 [代码]68px[代码] ,不要问我为什么! 代码片段 https://developers.weixin.qq.com/s/Q79g6kmo7w5J
2019-02-25 - 【优化】利用函数防抖和函数节流提高小程序性能
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/doc/000cc0b94ac5f8dcf4e7666475b804 今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行js代码的一种手段,因为是js实现的,所以在小程序里也是适用的。 首先先来理解一下两者的概念和区别: 函数防抖(debounce)是指事件在一定时间内事件只执行一次,如果在这段时间又触发了事件,则重新开始计时,打个很简单的比喻,比如在打王者荣耀时,一定要连续干掉五个人才能触发hetai kill '五连绝世'效果,如果中途被打断就得重新开始连续干五个人了。 函数节流(throttle)是指限制某段时间内事件只能执行一次,比如说我要求自己一天只能打一局王者荣耀。 这里也有个可视化工具可以让大家看一下三者的区别,分别是正常情况下,用了函数防抖和函数节流的情况下:http://demo.nimius.net/debounce_throttle/ 适用场景: 函数防抖 搜索框搜索联想。只需用户最后一次输入完,再发送请求 手机号、邮箱验证输入检测 窗口resize。只需窗口调整完成后,计算窗口大小。防止重复渲染 高频点击提交,表单重复提交 函数节流 滚动加载,加载更多或滚到底部监听 搜索联想功能 实现原理 函数防抖 [代码]const _.debounce = (func, wait) => {[代码][代码] [代码][代码]let timer;[代码] [代码] [代码][代码]return[代码] [代码]() => {[代码][代码] [代码][代码]clearTimeout(timer);[代码][代码] [代码][代码]timer = setTimeout(func, wait);[代码][代码] [代码][代码]};[代码][代码]};[代码] 函数节流 [代码]const throttle = (func, wait) => {[代码][代码] [代码][代码]let last = 0;[代码][代码] [代码][代码]return[代码] [代码]() => {[代码][代码] [代码][代码]const current_time = +[代码][代码]new[代码] [代码]Date();[代码][代码] [代码][代码]if[代码] [代码](current_time - last > wait) {[代码][代码] [代码][代码]func.apply([代码][代码]this[代码][代码], arguments);[代码][代码] [代码][代码]last = +[代码][代码]new[代码] [代码]Date();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]};[代码][代码]};[代码] 上面两个方法都是比较常见的,算是简化版的函数 lodash中的 Debounce 、Throttle lodash中已经帮我们封装好了这两个函数了,我们可以把它引入到小程序项目了,不用全部引入,只需要引入debounce.js和throttle.js就行了,链接:https://github.com/lodash/lodash 使用方法可以看这个代码片段,具体的用法可以看上面github的文档,有很详细的介绍:https://developers.weixin.qq.com/s/vjutZpmL7A51 系甘先,得闲饮茶
2019-01-14 - 小程序 地区选择 四联动
----------在线API版 代码片段 https://developers.weixin.qq.com/s/b56aoYm77p4e 请将 https://fts.jd.com/ 加入合法域名 ------离线数据 版 https://developers.weixin.qq.com/s/cn676Ymb7x4o 由于数据1M多 不能上传代码片段 所以压缩 导入 解压 libs/area.zip 地区数据来源于 京东 有港澳台 钓鱼岛 等等 大部分地区只能三联动
2018-12-27