- 微信开发者工具Git not found. Install it or configure it 解决办法
微信开发者工具Git not found. Install it or configure it using the ‘git.path’ setting解决办法 [图片] 微信开发者工具窗口界面依次选择 设置-》 编辑器设置-》 更多及工作区的编辑器设置-》 扩展-》 GIT-》 PATH-》 在setting.json中编辑 [代码] "git.path": "/usr/local/git/bin/git" #根据自己git的安装目录配置 [代码]
2020-09-10 - 云开发环境静态H5页面,跳转到不同小程序不同页面的实现
客户有需求,要短信或者其它场景中的链接静态H5页面,触发打开自家的小程序,官方也提供了 静态网站 H5 跳小程序技术文档(https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/staticstorage/jump-miniprogram.html ),不过里面只描述跳转到小程序本身绑定的云开发环境,那么是多个小程序共享的云开发环境如何实现跳转呢,该文档并没有说。 为每个小程序购买一个云开发环境当然是可以,然而,成本似乎似乎太高,不合算,为了给客户节省成本,就给一个小程序买了云开发环境,按照跨账号环境共享设置,实现共享给同主体下的不同小程序(参考:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/)。 接下来我把最终结果和解决方案,贴出来共享。 场景:首先同公司名下有4个小程序:A、B、C、D,只为A小程序购买了最基础的云开发环境19块,开通静态网站,添加跨账号共享功能,实现访问同一个静态H5页面携带不同参数,打开B、C、D不同小程序的指定页面,无论是微信里还是在手机浏览器均可以跳转。 实现代码: 1、云函数public编写(参考并改进 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/staticstorage/jump-miniprogram.html ) // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() switch (event) { case 'getUrlScheme': { return getUrlScheme(event) } } return 'action not found' } async function getUrlScheme(event) { if(event.appid) { var clb = cloud.openapi({ appid: event.appid }); } else{ var clb = cloud.openapi; } return clb.urlscheme.generate({ jumpWxa: { path: event.path, query: event.query, }, // 如果想不过期则置为 false,并可以存到数据库 isExpire: false, // 一分钟有效期 expireTime: parseInt(Date.now() / 1000 + 60), }) } 2、静态H5页面:jump.html <html> <head> <title>H5打开小程序</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"> <script> window.onerror = e => { console.error(e);alert('发生错误:' + e);} </script> <!-- 调试用的移动端 console --> <!-- <script src="https://cdn.bootcss.com/eruda/1.2.4/eruda.min.js"></script> --> <!-- <script>eruda.init();</script> --> <script> function getQueryParam(key) { const reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)', 'i'); const r = window.location.search.substr(1).match(reg); if (r != null) { return decodeURI(r[2]); } return null; } //设置 资源环境ID以及绑定的appid var resAppId = '环境宿主Appid';// <!-- replace --> var resEnv = '资源环境ID'; // <!-- replace --> //资源方其它小程序组【AppID,原始id,名称,缺省打开路径】 var appIDs = [ ['wxe6ddf673521da8f0','gh_e4025e37c422','小程序A','pages/webview'], // <!-- replace --> ['wxb1abf1b1fe25f8c6','gh_a4cbe6b9f17f','小程序B',''], // <!-- replace --> ['wx24911b4d9b6971c9','gh_e544c578a3ef','小程序C',''], // <!-- replace --> ['wx6fecac42503a957b','gh_81a6106a84ce','小程序D',''] // <!-- replace --> ]; ////////////////////////////// var launchIdx = getQueryParam('id') || 0; var pagepath = launchIdx== 0 ? appIDs[launchIdx][3] : ""; pagepath = pagepath ? pagepath+(getQueryParam('url') ? "?url="+encodeURIComponent(getQueryParam('url')):"") : ""; pagepath = pagepath ? pagepath : (getQueryParam('page') ? getQueryParam('page'):""); </script> <!-- weui 样式 --> <link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.4.1/weui.min.css" /> <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <script src="https://res.wx.qq.com/open/js/cloudbase/1.1.0/cloud.js"></script> <style>.hidden{display:none}.full{position:absolute;top:0;bottom:0;left:0;right:0}.public-web-container{display:flex;flex-direction:column;align-items:center}.public-web-container p{position:absolute;top:25%}.public-web-container a{position:absolute;bottom:40%}.wechat-web-container{display:flex;flex-direction:column;align-items:center}.wechat-web-container p{position:absolute;top:40%}.wechat-web-container wx-open-launch-weapp{position:absolute;bottom:40%;left:0;right:0;display:flex;flex-direction:column;align-items:center}.desktop-web-container{display:flex;flex-direction:column;align-items:center}.desktop-web-container p{position:absolute;top:40%} </style> </head> <body> <div class="page full"> <div id="public-web-container" class="hidden"> <p>正在唤起微信小程序...</p> <a id="public-web-jump-button" href="javascript:" class="weui-btn weui-btn_primary weui-btn_loading" onclick="openWeapp()"> <span id="public-web-jump-button-loading" class="weui-primary-loading weui-primary-loading_transparent"><i class="weui-primary-loading__dot"></i></span>点击唤起小程序</a> </div> <div id="wechat-web-container" class="hidden"> <script> document.write('<p>请点击下方按钮</p>'); document.write('<wx-open-launch-weapp id="launch-btn" username="'+appIDs[launchIdx][1]+'" path="'+pagepath+'">'); document.write(' <template><button style="width: 240px; height: 45px; text-align: center; font-size: 17px; display: block; margin: 0 auto; padding: 8px 24px; border: none; border-radius: 4px; background-color: #07c160; color:#fff;">打开微信小程序</button></template>'); document.write('</wx-open-launch-weapp>'); </script> </div> <div id="desktop-web-container" class="hidden"><p class="font-size:26px;">请在手机上打开本链接</p></div> </div> <script> function docReady(fn) { if (document.readyState === 'complete' || document.readyState === 'interactive') { fn();} else {document.addEventListener('DOMContentLoaded', fn);} } docReady(async function() { var ua = navigator.userAgent.toLowerCase() var isWXWork = ua.match(/wxwork/i) == 'wxwork'; var isWeixin = !isWXWork && ua.match(/MicroMessenger/i) == 'micromessenger'; var isMobile = isDesktop = false; if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|IEMobile)/i)) { isMobile = true } else { isDesktop = true } var isAndroid = ua.indexOf('android') > -1 || ua.indexOf('Adr') > -1; var isOS = ua.indexOf('iPhone') > -1 || ua.indexOf('iPad') > -1 || ua.indexOf('Mac') > -1; if (isWeixin) { var containerEl = document.getElementById('wechat-web-container'); containerEl.classList.remove('hidden'); containerEl.classList.add('full', 'wechat-web-container'); var launchBtn = document.getElementById('launch-btn'); launchBtn.addEventListener('ready', function (e) { console.log('开放标签 ready'); }); launchBtn.addEventListener('launch', function (e) { console.log('开放标签 success'); }); launchBtn.addEventListener('error', function (e) { console.log('开放标签 fail', e.detail);}); wx.config({ debug: false, appId: appIDs[launchIdx][0], // <!-- replace --> timestamp: 0, // 必填,填任意数字即可 nonceStr: 'nonceStr', // 必填,填任意非空字符串即可 signature: 'signature', // 必填,填任意非空字符串即可 jsApiList: ['chooseImage'], // 必填,随意一个接口即可 openTagList:['wx-open-launch-weapp'], // 填入打开小程序的开放标签名 }) } else if (isDesktop) { // 在 pc 上则给提示引导到手机端打开 var containerEl = document.getElementById('desktop-web-container') containerEl.classList.remove('hidden') containerEl.classList.add('full', 'desktop-web-container') } else { //腾讯云开发的免鉴权调用 var containerEl = document.getElementById('public-web-container') containerEl.classList.remove('hidden') containerEl.classList.add('full', 'public-web-container') var c = new cloud.Cloud({ identityless: true, resourceAppid: resAppId, // 资源方宿主 小程序的AppID resourceEnv: resEnv, // 资源方环境ID }) await c.init(); window.c = c; var buttonEl = document.getElementById('public-web-jump-button') var buttonLoadingEl = document.getElementById('public-web-jump-button-loading') try { await openWeapp(() => { buttonEl.classList.remove('weui-btn_loading'); buttonLoadingEl.classList.add('hidden'); }) } catch (e) { console.log('error',e) buttonEl.classList.remove('weui-btn_loading') buttonLoadingEl.classList.add('hidden'); throw e } } } ) async function openWeapp(onBeforeJump) { var c = window.c const res = await c.callFunction({ name: 'public', // 宿主环境中的云函数,注意开启权限 data: { action: 'getUrlScheme', appid: appIDs[launchIdx][0], path : pagepath }, }) if (onBeforeJump) { onBeforeJump(); } location.href = res.result.openlink; } </script> </body> </html> 调用方式:https://xxxx.xxx.xxx/jump.html?id=1&path=my/xwt-apply/xwt-apply (修改成你云环境静态网页的域名) id=1 表示jump.html页面中小程序B,path 表示小程序B中的具体页面。 你可以尝试修改成自己的,实现传参跳转到不同小程序页面。希望对你有帮助!
2023-07-10 - Skyline|长列表也可以丝滑~
[图片] [图片] 对于长列表出现的白屏、卡顿、界面跳动等问题,小程序提供了新 scroll-view 来解决这一系列问题。我们先来看看效果~ 快速滚动效果对比我们通过一组长列表来展示新旧 scroll-view 在快速滚动下的效果对比。 当长列表快速滚动时,旧 scroll-view 容易出现白屏的情况,新 scroll-view 则不会出现白屏。 左:旧 scroll-view、右:新 scroll-view [视频] 在安卓机器快速滚动过程中,旧 scroll-view 反应卡顿,容易出现手指离开操作时,滚动动画还在进行。 而新 scroll-view 则可以保持界面滚动效果跟随手指,停止滚动则缓慢结束动画效果。 左:旧 scroll-view、右:新 scroll-view ,测试机型:Xiaomi MIX 3 [视频] 反向滚动效果对比在对话等场景下,反向滚动是常见的功能,旧 scroll-view 并没有提供反向滚动的能力,我们来看看旧 scroll-view 下是怎么完成反向滚动的~ 在对话数据在加载的时候,对话列表需要在更新完列表数据之后,再使用 scroll-into-view 或者 scroll-top 来保持当前滚动位置,因为设置滚动位置会有延迟,所以容易出现 界面跳动 的情况。 // .js // scroll-view 滚动到顶部时触发 bindscrolltoupper() { // 先更新列表数据 this.setData({ recycleList: getnewList() }, () => { // 更新完数据后再设置滚动位置 this.setData({ scrollintoview: scrollintoview }) }) } 为了解决界面跳动的问题,社区上也有通过翻转的方法来解决,将 scroll-view 与 scroll-view 的子元素进行翻转。 // .wxss .reserve { transform: rotateX(180deg); } // .wxml 然而进行翻转之后,会遇到手指滚动方向与列表滚动方向相反、scroll-into-view 属性无效等问题。 为了帮开发者们解决反向滚动类列表的一系列问题,新 scroll-view 直接提供了 reverse 属性支持反向滚动的能力,滚动效果更加顺滑。 左:旧 scroll-view、右:新 scroll-view(图片加载期间,GIF 渲染较慢) [视频] 怎么接入新 scroll-view ?新的 scroll-view 使用起来很简单,主要有以下两个步骤: 修改小程序配置scroll-view 增加 type="list"// app.json // "renderer": "skyline" 开启之后所有页面会变成自定义导航,可参考 https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 实现自定义导航 // 也可在 page.json 中配置 "renderer": "skyline" 逐个页面开启 { ... "lazyCodeLoading": "requiredComponents", "renderer": "skyline" } // page.json { ... "disableScroll": true, "navigationStyle": "custom" } // page.wxml ... // 反向滚动 新的 scroll-view 从安卓 8.0.28 / iOS 8.0.30 开始支持,如需兼容低版本需要进行兼容处理。 wx.getSkylineInfo({ success(res) { if (res.isSupported) { // 使用新版 scroll-view } else { // 使用旧版 scroll-view } } }) 如需体验长列表效果,可在微信开发者工具导入该代码片段即可体验:https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 更多接入详情请参考文档 怎么做到的?大家肯定好奇为什么新 scroll-view 可以解决这个头疼的问题呢? 我们来对比一下新旧 scroll-view 有什么区别就可以知道答案啦~ 旧 scroll-view 逻辑层与渲染层的通信需要通过 JSBridge 进行转换,需要一定的时间开销渲染采用异步分块光栅化,当渲染赶不上滚动的速度,来不及渲染则会出现白屏渲染大量节点内存占用高,需要开发者自行优化只渲染在屏节点,开发成本高新 scroll-view 逻辑层与渲染层的通信无需通过 JSBridge 进行转换,减少了大量通信时间开销渲染采用同步光栅化,滚动与渲染在同一线程,不会出现白屏针对长列表进行优化,只渲染在屏节点,内存占用低,减少了一些渲染耗时,且开发接入成本低[图片] 除此之外,新 scroll-view 后续将提供 type="custom" 支持 sticky 吸顶效果、网格布局、瀑布流布局等能力便于开发者接入使用~
2023-08-03 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 如何实现动态生成好友分享图
本文章采用官方最新的 Canvas.createImage() 来实现下动态生成好友分享图,可以拿来即用。展示效果如下(其中蓝框中文案和红框头像为插入的文本、图像。背景图也支持动态更换): [图片] 小程序demo案例:https://developers.weixin.qq.com/s/nJtr4QmL7RD3 一、市面案例缺陷:翻阅了目前市面上的小程序动态生成好友分享图,大部分还是使用已废弃的『wx.createSelectorQuer』接口来实现。目前小程序已无法很好支持。 二、主要有以下几个关键点需要注意下: 做好友分享图要考虑5:4的比例使用 wx.getImageInfo 一定要考虑图片失败的场景,然后采用兜底图片。相同的逻辑在complete中执行。要区分 wx.createImage ,这个是小游戏用来创建图片对象的。小程序要用Canvas.createImage()。也不是使用 new Image!!!使用的时候一定要在 canvas 类型中注明 type 是 2d 的 canvas[图片] 三、优化知识点: 如何用 async in image loading:https://stackoverflow.com/questions/46399223/async-await-in-image-loading ----- 采用await img.decode() 或者 img.onload = () => resolve() 如何隐藏Canvas:https://developers.weixin.qq.com/community/develop/doc/1aadfacdd9f38584881e0c50db2bcda1 ----- position:fixed;left:100%;
2023-06-19 - 小程序用户头像昵称获取规则调整公告
更新时间:2022年11月9日由于 PC/macOS 平台「头像昵称填写能力」存在兼容性问题,对于来自低于2.27.1版本的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称。 更新时间:2022年9月28日考虑到近期开发者对小程序用户头像昵称获取规则调整的相关反馈,平台将接口回收的截止时间由2022年10月25日延期至2022年11月8日24时。 调整背景在小程序内,开发者可以通过 wx.login 接口直接获取用户的 openId 与 unionId 信息,实现微信身份登录,支持开发者在多个小程序或其它应用间匿名关联同一用户。 同时,为了满足部分小程序业务中需要创建用户的昵称与头像的诉求,平台提供了 wx.getUserProfile 接口,支持在用户授权的前提下,快速使用自己的微信昵称头像。 但实践中发现有部分小程序,在用户刚打开小程序时就要求收集用户的微信昵称头像,或者在支付前等不合理路径上要求授权。如果用户拒绝授权,则无法使用小程序或相关功能。在已经获取用户的 openId 与 unionId 信息情况下,用户的微信昵称与头像并不是用户使用小程序的必要条件。为减少此类不合理的强迫授权情况,作出如下调整。 调整说明自 2022 年 10 月 25 日 24 时后(以下统称 “生效期” ),用户头像昵称获取规则将进行如下调整: 自生效期起,小程序 wx.getUserProfile 接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。自生效期起,插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回:生效期后发布的插件新版本,通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响,但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称:如业务需获取用户头像昵称,可以使用「头像昵称填写能力」(基础库 2.21.2 版本开始支持,覆盖iOS与安卓微信 8.0.16 以上版本),具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求:对于来自低版本的基础库与微信客户端的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称,开发者可继续使用以上能力做向下兼容。对于上述 3,wx.getUserProfile 接口、wx.getUserInfo 接口、头像昵称填写能力的基础库版本支持能力详细对比见下表: [图片] *针对低版本基础库,兼容处理可参考 兼容文档 请已使用 wx.getUserProfile 接口的小程序开发者和已使用 wx.getUserInfo 接口的插件开发者尽快适配。小游戏不受本次调整影响。 最佳实践小程序可在个人中心或设置等页面使用头像昵称填写能力让用户完善个人资料: [图片] 微信团队 2022年5月9日
2023-09-26 - 小程序链接生成与使用规则调整公告
各位开发者: 为确保小程序链接合理使用,自 2022 年 4 月 11 日起,URL Scheme 和 URL Link (以下统称为 “链接” )接口能力规则将进行以下调整: 每个 URL Scheme 或 URL Link 有效期最长 30 天,均不再支持永久有效的链接、不再区分短期有效链接与长期有效链接;链接生成后,若在微信外打开,用户可以在浏览器页面点击进入小程序。每个独立的链接被用户访问后,仅此用户可以再次访问并打开对应小程序,其他用户无法再次通过相同链接打开该小程序;单个小程序每天生成链接数(URL Scheme 和 URL Link 总数)上限为 50 万条。 对于上述 1,在开发层面,相应的服务端接口 urlscheme.generate 和 urllink.generate 将进行以下调整: is_expire 值固定为 true,可不再传该值,若传值为 false 也与 true 一样会生成到期失效链接;若 expire_type 传值为 0,需注意 expire_time 传值的时间戳不超过 30 天,即该参数最长传值有效期为 30 天;若 expire_type 传值为 1,需注意 expire_interval 传值范围为 [1, 30],即该参数最长传值间隔天数为 30。详细对比见下表: [图片] 已使用该后端接口的开发者可以不进行任何修改,不会出现返回异常。若传值超过新规则合法值,或声明使用永久有效的链接,则均会被赋最长有效期值(30天);需注意以上新规则生效后的有效期和访问规则变化。 在本次规则调整生效前已经生成的链接,也将自动生效以下规则: 如果有效期超过30天或长期会被降级为30天有效,开始时间从调整日期开始计算;在调整生效后,只能被1个用户访问。 当前已使用微信云开发 静态网站H5跳小程序 与 短信跳小程序、微信服务平台短信服务为用户提供链接的功能不受影响,但同样适用以上规则。 微信团队 2022年3月9日 相关QAQ1:每天下发的短信量级超过50万条,不够用怎么办? A1:可将生成 scheme 的时机改为在用户打开 H5 时再生成: [图片]
2023-09-26 - 关于V3电商收付通的添加分账接收方有总量限制吗?
文档里并未看到限制,论坛里有人说2万上限。求证。
2021-03-22 - 解锁小程序中使用SVG新姿势
SVG 的优势 清晰度: 可以进行放大,而不失真 更小的文件体积 可扩展性,可以动态颜色 动效 可以添加动效 在小程序中使用 目前小程序 的image标签已经支持了 svg 的显示 [代码] <image src="./xx.svg"/> [代码] 如何动态的改变 svg 属性呢? 大体思路:把svg转成 base64 然后通过 image标签 src设置图片,再动态赋值svg颜色 把svg转成base64 如下一个svg 代码文件 [代码]<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="24 24 48 48"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" from="0" to="360" dur="1400ms"></animateTransform><circle cx="48" cy="48" r="20" fill="none" stroke="#eeeeee" stroke-width="2" transform="translate\(0,0\)"><animate attributeName="stroke-dasharray" values="1px, 200px;100px, 200px;100px, 200px" dur="1400ms" repeatCount="indefinite"></animate><animate attributeName="stroke-dashoffset" values="0px;-15px;-125px" dur="1400ms" repeatCount="indefinite"></animate></circle></svg> [代码] 转成base64,其实就是 对这个svg进行 encodeURIComponent 得到 如下代码 [代码]%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20viewBox%3D%2224%2024%2048%2048%22%3E%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20repeatCount%3D%22indefinite%22%20from%3D%220%22%20to%3D%22360%22%20dur%3D%221400ms%22%3E%3C%2FanimateTransform%3E%3Ccircle%20cx%3D%2248%22%20cy%3D%2248%22%20r%3D%2220%22%20fill%3D%22none%22%20stroke%3D%22%23eeeeee%22%20stroke-width%3D%222%22%20transform%3D%22translate%5C(0%2C0%5C)%22%3E%3Canimate%20attributeName%3D%22stroke-dasharray%22%20values%3D%221px%2C%20200px%3B100px%2C%20200px%3B100px%2C%20200px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3Canimate%20attributeName%3D%22stroke-dashoffset%22%20values%3D%220px%3B-15px%3B-125px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3C%2Fcircle%3E%3C%2Fsvg%3E [代码] 拼接base64 [代码] data:image/svg+xml;charset=utf-8,encodeURIComponent后的代码 [代码] 在对应svg属性上动态设置颜色,比如这里用到的是填充颜色 在js文件 data中定义 color 状态 在wxml中动态渲染 [代码] <image src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20viewBox%3D%2224%2024%2048%2048%22%3E%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20repeatCount%3D%22indefinite%22%20from%3D%220%22%20to%3D%22360%22%20dur%3D%221400ms%22%3E%3C%2FanimateTransform%3E%3Ccircle%20cx%3D%2248%22%20cy%3D%2248%22%20r%3D%2220%22%20fill%3D%22none%22%20stroke%3D%22%23{{color}}%22%20stroke-width%3D%222%22%20transform%3D%22translate%5C(0%2C0%5C)%22%3E%3Canimate%20attributeName%3D%22stroke-dasharray%22%20values%3D%221px%2C%20200px%3B100px%2C%20200px%3B100px%2C%20200px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3Canimate%20attributeName%3D%22stroke-dashoffset%22%20values%3D%220px%3B-15px%3B-125px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3C%2Fcircle%3E%3C%2Fsvg%3E" /> [代码] [代码]注意:这里的颜色 由于是已经被编码了,所以# 已经被转义了 %23, 直接写颜色数字即可[代码] 当然你也可以 去掉%23 自己实现一个内部方法 [代码] if (color && color.startsWith('#')) { return `%23${color.slice(1)}`; } [代码] 这样其实就实现了 svg的动态渲染,可是这种写法,写在wxml中 不是特别的优雅,那么如何重构下让我们的代码看起来更优雅呢? 把 svg 单独存放 支持动态返回 动态赋值 image src 属性 svg 动态函数 loading.svg.js 文件 [代码]export const loadingSvg = (color='#ddd') =>{ const svgXml = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="24 24 48 48"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" from="0" to="360" dur="1400ms"></animateTransform><circle cx="48" cy="48" r="20" fill="none" stroke="${color}" stroke-width="2" transform="translate\(0,0\)"><animate attributeName="stroke-dasharray" values="1px, 200px;100px, 200px;100px, 200px" dur="1400ms" repeatCount="indefinite"></animate><animate attributeName="stroke-dashoffset" values="0px;-15px;-125px" dur="1400ms" repeatCount="indefinite"></animate></circle></svg>` return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}` } [代码] 逻辑层引入,setData [代码] onLoad(){ const { loadingSvg } = require('./loading.svg.js') const svgImg = loadingSvg('#eee') this.setData({svgImg}) }, [代码] 渲染层使用 [代码] <image src="{{svgImg}}"/> [代码] github 使用案例 demoFormpSvg
2022-04-30 - svg简单图标在小程序中的使用分享
项目中经常会使用不同颜色的小图标,需要在项目中放置不同颜色的图标图片,有时候涉及图标大小和压缩问题。抽空研究了下svg代码在小程序中如何使用,整理了一个基础组件,以后项目中遇到使用频率高的小图标,用它就比较方便了。项目代码在git平台公开,可克隆下载。 svg代码在小程序中展示: 使用background-image(url('data:image/svg+xml,svg转换后代码'))进行小图标的展示; svg代码符号转换成十六进制的ascii码: “<”替换成“%3C”,“>”替换成“%3E”; fill=color更新图标颜色,color支持英文单词和16进制的颜色码,16进制颜色码“#”替换成“%23”; svg代码通过iconfont平台复制 [图片] js代码: class SVGCON { constructor() { } svgXml(n, c) { let name = n; let data = ''; let casualData = this[name](); let newArray = []; let color = 'black'; let newFill = ''; // 颜色转换 if (c && c.indexOf('#') >= 0) { color = c.replace('#', '%23'); } else if (c) { color = c; } newFill = "fill=" + "'" + color + "'"; // 更新颜色,加入fill=color(svg去掉fill=color相关代码) // 查找svg中的path数量 newArray = casualData.split('>'); casualData = ''; for (let i = 0; i < newArray.length; i++) { if (i == newArray.length - 1) { } else { newArray[i] = newArray[i] + ' ' + newFill + ">"; } casualData = casualData + newArray[i]; } // 转换成svg+xml data = casualData; data = data.replace('<', '%3C'); data = data.replace('>', '%3E'); data = 'data:image/svg+xml,' + data; //双引号展示不出来,需要转换成单引号 data = data.replace(/\"/g, "'"); return data; } arrowRight() { //向右箭头 return '复制的svg代码' } loading() { //加载中 return '复制的svg代码' } setting() { //设置 return '' } } export { SVGCON }; wxml组件代码: <view class="m-icon mini-class" style='background-image:url("{{backgroundImage}}")'></view> 开发全局基础组件 创建存放全局组件的目录components,开发完成组件之后在app.js配置全局组件 [图片] 页面直接使用组件 <mini-icon mini-class="icon" icon="arrow-top" color="{{color}}" /> 微信扫码预览效果: [图片] 代码托管于微信开发者-代码管理: https://git.weixin.qq.com/yukiyuki/yuki_svg.git 以上是关于svg代码在小程序中作为基础组件的使用的内容。有表达或者总结的不对的地方,请多指教,谢谢!
2021-02-19 - 微信支付商户经营工具-「企业付款到零钱」产品介绍及开通使用过程中的问题说明
1、功能说明 企业付款到零钱是一种由微信支付商户号直接付钱至用户微信零钱的能力,资金到账速度快,使用及查询方便,主要用来解决合理的商户对用户付款需求,支持平台操作及接口调用两种方式。 产品特点 免费:不收取付款手续费,节省企业成本。 灵活:可通过页面或接口发起付款,灵活满足企业不同场景的付款需求。 友好:通过openid即可实现付款,用户授权即可,体验更好。 快速:在发起后,及时到账用户零钱。通过微信消息触达,用户及时获知入账详情。 安全:提供多种安全工具,满足不同场景安全需求。如:按需调整付款额度;支持收款账户限制;支持安全防刷,拦截恶意用户、小号、机器号码;支持自定义大额通知等。 2、适用场景 费用报销:公司小额报销 员工福利:日常节假日福利金发放 保险理赔:小额保险款项发放 用户奖励:邀请奖励 佣金发放:一般适用于产品分销小额佣金的发放 3、开通条件 结算周期为T+1的商户,需满足三个条件: 1)商户号入驻满90天 2)截止今日往回推30天连续不间断保持有交易。 3)保持正常健康交易 其他说明 1、连续30天交易无金额限制,请保持正常交易。 2、同一主体下,若有一个商户号满足企业付款到零钱开通条件,其余商户号也一样可以开通,没有30天/90天的限制。 4、为什么会有30天/90天的限制 此项规定是根据中国人民银行文件银发〔2016〕261号文件的通知 详见第二项的第十二条说明 [图片] 更多可以看公告原文:点我查看 5、企业付款到零钱的限额说明 针对同一个商户,所有付款来源加总限制(商户平台&接口): 1)付款给同一个用户(实名用户):单日、单笔上限:2W元(单笔最多2W,当日最多也2W); 2)日付款总金额:100W元 ; 3)单笔付款最低额度:0.3元(范围:企业付款api、商户平台企业付款) 备注:开发文档的链接:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 6、写在最后 其实你们更想知道的是「何为健康交易」,那就简单透露一点点吧 1)符合行业属性的交易(自己领悟,你那么聪明一定可以悟到的) 2)符合用户消费习惯的交易(自己领悟,你那么聪明一定可以悟到的) 如果有问题可以跟帖回复,也可以私信,欢迎交流沟通 最后的最后,告诫开通了朋友千万不要拿去做坏事哦 本文还会更新其他内容,可以收藏一下下~~~ 更多商户相关文档可查看:https://developers.weixin.qq.com/community/develop/article/doc/000ce0be104fe8db37fbf478b5b813
2021-04-28 - 电子发票
小程序开票 https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/E_Invoice/Vendor_and_Invoicing_Platform_Mode_Instruction.html 如果商户接入模式为“商户+开票平台模式”:s_pappid由开票平台通过邮件线下方式提供 这个怎么邮件线下提供?
2020-10-10 - 三分钟,用云开发实现域名重定向
作者:布道师 鱼皮 今天分享域名重定向小知识,以及在腾讯云云开发 CloudBase 中实现域名重定向的实践。 痛点的诞生之前,我开发了一个编程导航网站,将网站放到了腾讯云云开发上,用云托管(容器)的方式部署和维护。还购买了一个域名[代码]code-nav.cn[代码],并且在云开发后台[代码]访问服务[代码]中,将该域名的子域名[代码]www.code-nav.cn[代码]和存放网站文件的容器相关联,配置如图: [图片] 然后,大家就能通过网址[代码]www.code-nav.cn[代码]访问该网站了。 [图片] 但是,很快,我就发现了一个严重的问题。 有不少同学想要访问我的网站,但是,由于他们输入的网址是[代码]code-nav.cn[代码],省略了网址前缀 [代码]www[代码],导致网站无法访问。也让我流失了一批用户。 初战-域名配置要解决这个问题,其实很简单,之前是配置[代码]www[代码]子域名指向容器,那在云开发后台再加一条配置,直接将购买的域名(父域名)[代码]code-nav.cn[代码]也指向容器,不就成了么? 配置如下: [图片] 这样,无论用户是否输入[代码]www[代码]前缀,都能够访问到我们的网站啦! [图片] 看似非常完美,但目前,网站其实还存在一定问题。 现存问题首先,带 www 与不带 www 其实是两个不同的网址,虽然对用户来说,感觉是访问了同一个网站。但对于搜索引擎,小蜘蛛们会把他们识别为两个不同的网站,并且分别收录这两个路径下网站的内容,导致权重分散。虽然对流量小的网站来说影响不大,但对于大站点,这是必须要处理的问题。 此外,访问[代码]code-nav.cn[代码](不带 www)的用户反映,网站上的数据无法加载。这是因为,腾讯云云开发的 WEB 安全域名限制,只有在白名单内的域名才允许访问云资源(数据、文件等),因此,还要在[代码]安全配置[代码]中,补上[代码]code-nav.cn[代码]域名。 [图片] 虽然现在访问正常了,但如果业务中还有一些和 www 网址强相关的逻辑,比如判断用户访问的网址必须是[代码]www.code-nav.cn[代码]才允许登录,那么你还要去修改代码,考虑稍有不周,就会导致一些功能出现问题。 为解决这些问题,我们可以使用[代码]重定向[代码]技术。 重定向重定向是一个很广泛的概念,即通过各种方法将各种网络请求重新定个方向转到其它位置,比如网页重定向、域名重定向、数据报文重定向等。 在网站开发中,重定向的应用场景太多了,比如用户未登录时,将它输入的网址自动跳转为登录页;用户访问旧版网址时,自动跳转到新版网页。重定向不仅是导游,也是一名霸道的保安。 因此,很多大站点都会采用重定向技术。比如访问谷歌[代码]google.com[代码],按 F12 查看开发者控制台,可以看到网址通过 302 重定向,自动跳转为了[代码]www..com[代码]。 [图片] 那问题就来了,啥是 302 重定向? 不妨看一看常见的重定向 HTTP 状态码。 重定向 HTTP 状态码和重定向有关的 HTTP 状态码主要是 301、302、303、307、308,最常用的是 301 和 302,可以看看 MDN 官方对它们的解释。 301 是永久重定向(Moved Permanently)说明请求的资源已经被 永久 移动到了由 Location 头部指定的 url 上,是固定的不会再改变,搜索引擎会根据该响应修正。 而 302 是暂时性转移(Moved Temporarily,或者 Found),表明请求的资源被 暂时 移动到了由 Location 头部指定的 URL 上。浏览器会重定向到这个 URL, 但是搜索引擎不会对该资源的链接进行更新。 虽然 301 和 302 都能够将用户输入的网址 A, 改为重定向后的网址 B,但他们还是有区别的: 搜索引擎区别: 301 表示原地址 A 的资源已被移除,永远无法访问,搜索引擎抓内容时会将网址 A 全部替换为 B;而 302 表示网址 A 还活着,搜索引擎会在抓取网址 B 新内容的同时,保留网址 A 的记录。安全性: 302 跳转有网站劫持的风险,导致网站被盗用。再战 — 云开发重定向实践了解重定向之后,来试试怎么实现重定向,以及如何在云开发中实现域名重定向。 实现重定向的方式有很多,很大程度上依赖于你使用的 web 服务器,比如 Nginx、Apache、Tomcat 等,一般在服务器中添加几条配置即可。 我的编程导航网站是以容器的方式,部署在云开发提供的云托管功能上的。我把开发好的网站文件和提供 web 服务的 Nginx 服务器一起打包,做成了容器,于是,可以将每个容器当成一个小服务器,独立运行。 [图片] 要支持重定向,只需要修改下 Nginx 的配置。比如这里我选择给整个网站添加 301 永久重定向,配置文件如下: server { listen 80; # gzip config gzip on; ... root /usr/share/nginx/html; include /etc/nginx/mime.types; # 添加重定向 if ($http_host ~ "^code-nav.cn") { rewrite ^(.*) https://www.code-nav.cn permanent; } } 不必手写和记忆 Nginx 配置,直接使用可视化界面生成即可: [图片] 详情参见这篇文章:轻松搞定 Nginx 配置代码的神器!其他的服务器配置也可以自行查阅文档,这里不再赘述。 然后,在云托管上创建新版本,发布新的容器,就大功告成啦! [图片] 查看下效果,访问[代码]code-nav.cn[代码],网站重定向到了[代码]www.code-nav.cn[代码],完美! [图片] 最后,回顾下在腾讯云云开发中实现域名重定向的完整过程,包括如下步骤: 在 访问服务 中添加父域名到网站的指向(云托管等)在 安全配置 中添加父域名到白名单中在 web 服务器中添加重定向配置新建版本,部署发布整个流程还是非常简单的~ 有需要的同学快去试试吧! 产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流群、最新资讯关注微信公众号【腾讯云云开发】
2021-06-10 - 用户关注公众号之后,我在用户管理里面搜索不到用户信息?
用户关注了我的公众号,但是用户管理里面搜索不到该用户,但是我开发得程序能获取到用户得openid,但是用户信息无法获取。
2021-01-15 - 实战:图片处理服务之快速压缩模版
前言 在昨天发布的《实战:如何降低云开发服务器成本? 》文章,评论区有提到需要「关于cloudbase的扩展能力-图像处理-快速缩略模板的用法」今天我就来和大家分享一下具体用法和效果。 安装 地址:https://console.cloud.tencent.com/tcb/env/overview 选中「扩展能力」菜单下面的「扩展应用」 [图片] 选择「图片处理」服务进行「安装」 [图片] 安装过程一直「下一步」就行没有需要配置的地方,需要等待几分钟 [图片] 查看文档 安装完成之后我们就可以使用了 首先看下文档:https://cloud.tencent.com/document/product/876/42103 [图片] 找到我们要用的「快速压缩模版」。 地址:https://cloud.tencent.com/document/product/460/6929 使用方法,直接在图片后面来评价参数即可。 [图片] 实战使用 通常使用在列表场景中,本来就不要高清图,所以可以进行压缩也不会影响用户体验。 [图片] 我们找一个图片链接放在浏览器上来看 [图片] 然后使用下快速压缩模版拼接参数 ?imageView2/1/w/100/h/100 [图片] 把两张图片下载下来对比一下大小 [图片] 压缩后小了54倍 总结 这样以来不仅让用户能够更快的加载出图片,并且还能降低服务器资源成本。
2020-12-02 - UNI-APP使用云开发跨全端开发实战讲解
UNI-APP 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。 本文为大家讲解如何采用云开发官方JS-SDK,接入云开发后端服务并支持UNI-APP全部端(不止于微信小程序) JS-SDK和UNI-APP适配器1.JS-SDK和适配器云开发官方提供的@cloudbase/js-sdk,主要用来做常规WEB、H5等应用(浏览器运行)的云开发资源调用,也是目前最为完善的客户端SDK。 目前市面上大部分的轻应用、小程序包括移动应用APP都是采用JS来作为开发语言的,所以我们可以对TA进行轻微改造,就可以轻松使用在各种平台中。 但是单独改造SDK包会有些许风险,比如在原SDK包升级时需要重新构造,就造成了无穷无尽的麻烦,改造成本相当大。 官方的产品小哥哥深知这种不适和痛苦,所以在@cloudbase/js-sdk 中提供一套完整的适配扩展方案,遵循此方案规范可开发对应平台的适配器,然后搭配 @cloudbase/js-sdk 和适配器实现平台的兼容性。 不了解的小伙伴肯定会有些茫然,我来用浅显的语言解释一下,就是@cloudbase/js-sdk 将底层的网络请求以及相关基础需求以接口的形式暴露出来,我们按照平台的特殊API来补充这些接口,sdk就可以根据这些补充的接口,无障碍的运行在平台中了。 如果我们想在UNI-APP中使用@cloudbase/js-sdk ,底层网络请求你需要来补充,因为sdk原本是适应浏览器的,TA不知道UNI-APP怎么对外发请求,所以你需要将uni.request 方法补充到TA暴露的接口中。补充完毕后,@cloudbase/js-sdk 就可以在UNI-APP中活泼的运行了。 我们将所有的uni方法全部补充到JS-SDK暴漏的接口中去,就形成了一个完整的适配器,我们将其成为uni-app适配器。 2.UNI-APP适配器UNI-APP的整体接口都是公开透明的,我们在开发UNI-APP时也都遵照同一套接口标准。所以小编已经将uni-app适配器制作完毕,大家只需要在使用时接入适配器就可以了。 我们在项目目录main.js中引入云开发JS-SDK,然后接入我们的UNI-APP适配器即可。 import cloudbase from '@cloudbase/js-sdk' import adapter from 'uni-app/adapter.js' cloudbase.useAdapters(adapter); cloudbase.init({ env: '',//云开发环境ID appSign: '',//凭证描述 appSecret: { appAccessKeyId: 1,//凭证版本 appAccessKey: ''//凭证 } }) 移动应用登录凭证云开发SDK在使用过程中,向云开发服务系统发送的请求都会需要验证请求来源的合法性。 我们常规 Web 通过验证安全域名,而由于 UNI-APP 并没有域名的概念,所以需要借助安全应用凭证区分请求来源是否合法。 登录云开发 CloudBase 控制台,在安全配置页面中的移动应用安全来源一栏:[图片] 点击“添加应用”按钮,输入应用标识:uni-app(也可以输入其他有标志性的名称),需要注意应用标识必须是能够标记应用唯一性的信息,比如微信小程序的 appId 、移动应用的包名等。[图片] 添加成功后会创建一个安全应用的信息,如下图所示:[图片] 我们需要保存一下上图中的版本(示例为1)、应用标识(示例为uni-app)、以及点击获取到的凭证(示例为demosecret) 在项目目录中,我们将main.js中的init部分补全 import cloudbase from '@cloudbase/js-sdk' import adapter from 'uni-app/adapter.js' cloudbase.useAdapters(adapter); cloudbase.init({ env: 'envid',//云开发环境ID,保证与你操作登录凭证一致 appSign: 'uni-app',//凭证描述 appSecret: { appAccessKeyId: 1,//凭证版本 appAccessKey: 'demosecret'//凭证 } }) 如此,你就可以正常的进行云开发的登录使用了。 需要注意以下4点: 你需要设置uni-app的各端安全域名为:request:tcb-api.tencentcloudapi.com、uploadFile:cos.ap-shanghai.myqcloud.com、download:按不同地域配置使用此种方法接入云开发是全端支持,并不会享有微信小程序生态的一些便利,微信小程序开发还是需要依赖正常请求调用过程(将云开发作为服务器来对待),但你可以判断wx来使用wx.cloud来兼容。使用云开发的匿名登录时,受各端实际情况影响,可能不能作为常久唯一登录id,需要根据自身业务建立统一账户体系,具体可使用自定义登录来进行。UNI-APP支持WEB网页端上线时,需要将网页域名配置到云开发安全域名中(防止WEB下载文件导致跨域)示例代码详解示例项目中已经基本构建了uni-app使用云开发的各种流程代码。 在页面中进行匿名登录: // index.vue import cloudbase from '@cloudbase/js-sdk' export default { data() { return { title: '登录中' } }, onLoad() { cloudbase.auth().anonymousAuthProvider().signIn().then(res => { this.title = '匿名登录成功' }).catch(err => { console.error(err) }) } } 调用云函数并收到返回结果: import cloudbase from '@cloudbase/js-sdk' export default { methods: { call: function() { cloudbase.callFunction({ name: "test", data: { a: 1 } }).then((res) => { console.log(res) }); } } } 操作数据库: import cloudbase from '@cloudbase/js-sdk' export default { methods: { database: function() { cloudbase.database().collection('test').get().then(res => { console.log(res) }) } } } 实时数据库监听: import cloudbase from '@cloudbase/js-sdk' export default { methods: { socket: function() { let ref = cloudbase.database().collection('test').where({}).watch({ onChange: (snapshot) => { console.log("收到snapshot", snapshot); }, onError: (error) => { console.log("收到error", error); } }); } } } 上传文件(框架限制,WEB端无法操作): import cloudbase from '@cloudbase/js-sdk' export default { methods: { upload: function() { uni.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album'], success: function(res) { console.log(res.tempFilePaths[0]) cloudbase.uploadFile({ cloudPath: "test-admin.png", filePath: res.tempFilePaths[0], onUploadProgress: function(progressEvent) { console.log(progressEvent); var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); } }).then((result) => { console.log(result) }); } }); } } } 下载文件(需要注意地域域名,配置安全域名): import cloudbase from '@cloudbase/js-sdk' export default { methods: { download: function() { cloudbase.downloadFile({ fileID: "cloud://demo-env-1293829/test-admin.png" }).then((res) => { console.log(res) }); } } } 部署步骤将项目下载后使用HBuilderX打开。按照获取移动安全凭证的指引,填写至mian.js相应处。打开目录命令行,npm i执行安装依赖。打开云开发控制台,开启匿名登录。新建一个默认的云函数,名称为test(逻辑内容直接返回event即可)新建一个数据库,名称为test(随便添加几个记录,设置权限为所有人可读)调整项目pages/index/index.vue中,21行代码,在登录成功后调用相应函数。以下是WEB端运行时展示:[图片] 关于uni-app适配器在util/adapter中,只进行了简单的测试,保证可用性,后续请关注官网获取最新适配器依赖此方法有别与uniCloud,是直接使用uni请求底层,依赖官方JS-SDK进行云开发服务的交互处理,在使用时注意区别。项目地址:https://github.com/AceZCY/UNI-for-CloudBase
2020-12-08 - 2020小程序云开发技术峰会——重新定义开发
[图片] [图片]
2020-11-18 - 微信小程序base64图片 canvas 画布 drawImage 实现分享海报
在微信小程序中 canvas drawImage API 传入的第一个参数是图片资源路径,这个参数通常由wx.chooseImage 或 wx.getImageInfo 获取图片信息来获得。 而 base64 格式图片数据,无法被 getImageInfo 直接调用,以下是解决方案: 首先使用 wx.base64ToArrayBuffer 将 base64 数据转换为 ArrayBuffer 数据 使用 FileSystemManager.writeFile 将 ArrayBuffer 数据写为本地用户路径的二进制图片文件 此时的图片文件路径在 wx.env.USER_DATA_PATH 中, wx.getImageInfo 接口能正确获取到这个图片资源并 drawImage 至 canvas 上 以下是具体的转换代码: getImgCode(url) { //自己接口获取base64图片 let res = await this.$http.post(’/WeixinMin/getEqr’, { pages: url }) if (res.code == 1) { const fsm = wx.getFileSystemManager(); const FILE_BASE_NAME = ‘tmp_base64src’; const filePath = [代码]${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}[代码]; //base64 数据转换为 ArrayBuffer 数据 const buffer = wx.base64ToArrayBuffer(res.data.image); fsm.writeFile({ filePath: filePath, data: buffer, encoding: ‘binary’, success: () => { console.log('写入成功, 路径: ', filePath); }, fail: err => { console.log(“失败******”, err); }, }); } }
2020-06-08 - 电商收付通,不能执行分账,提示“订单处理中,请稍后重试”
如果收款的二级商户是企业类型的,支付完成后可以立即执行分账。但是换另外一个二级商户(个人类型),执行分账就一直报错,支付后过了几天也不行 {"code":"INVALID_REQUEST","message":"订单处理中,请稍后重试"} 不知道是什么情况,可以帮忙解答下吗?
2020-09-07 - 如何使用Node.js的Buffers
为什么要有Buffers? 在纯[代码]JavaScript[代码]开发中,unicode编码的字符串也够好用的了,并不需要直接处理二进制数据(straight binary data)。在浏览器环境,大部分数据都是字符串的形式,这是足够的。然而,Node.js是服务器环境,必须要处理TCP流还有文件系统的读取和写入流,这就让[代码]JavaScript[代码]需要处理纯二进制数据了。 其实,要解决这个问题直接使用字符串也是可以的,这也是Node.js一开始的做法。然而,这样的做法有许多问题,也很慢。 所以,记住了,别使用二进制字符串(binary strings),用buffers代替它! 什么是Buffers? 在Node.js里,Buffers是专门设计来处理原始二进制数据的,是Buffer这个类的实例。 每个buffer在V8引擎外都有内存分配。Buffer操作起来和包含数字的数组一样,但是不像数组那样自由设置大小的。并且buffer拥有一系列操作二进制数据的方法。 另外,buffer里的“数字”代表的是byte并且限制大小是0到255(2^8-1) 在哪里可以看到buffers 一般情况,buffer经常可以在读取二进制数据流的时候看到,比如[代码]fs.createReadStream[代码] 用法: 创建buffer 有许多方法可以生成新的buffers: [代码]var buffer = new Buffer(8); [代码] 这个buffer是未初始化的,且包含8个字节(bytes)。 [代码]var buffer = new Buffer([ 8, 6, 7, 5, 3, 0, 9]); [代码] 这个buffer用一个数组的内容来初始化。记住了,数组里的数字表示的是字节(bytes) [代码]var buffer = new Buffer("I'm a string!", "utf-8") [代码] 通过第二个参数来指定编码(默认是utf-8)的字符串来初始化buffer。utf-8是在Node.js里最常用的编码,但是buffer还支持其他编码: “ascii”:这个编码方式很快,但是只限制ascii字符集。而且这个编码会将null转换成空格,而不像utf-8编码。 “ucs2”:一种双字节,小端存储的编码。可以编码一个unicode的子集。 “base64”:Base64字符串编码。 “binary”:这个“二进制字符串”前面提到过,这个编码即将被弃用,避免使用这个。 写入buffer 创建一个buffer: [代码]> var buffer = new Buffer(16); [代码] 开始写入字符串: [代码]> buffer.write("Hello", "utf-8") 5 [代码] [代码]buffer.write[代码]的第一个参数是写入buffer的字符串,而第二个参数是这个字符串的编码方式。如果字符串的编码是utf-8,那么这个参数是多余的。 [代码]buffer.write[代码]返回5,这代表我们写入了5个字节到这个buffer。事实上,“Hello“这个字符串也刚好是5个字符。这是因为刚好每个字符都是8位(bits)。这对补全字符串很重要: [代码]> buffer.write(" world!", 5, "utf-8") 7 [代码] 当[代码]buffer.write[代码]有3个参数的时候,第二个参数代表是偏移量,或者说是buffer开始写入的位置。 读取buffer toString: 这个方法可能是读取buffer最通用的方法了,因为很多buffer都包含文本: [代码]> buffer.toString('utf-8') 'Hello world!\u0000�k\t' [代码] 再一次,第一个参数代表编码方式。这里可以看到并没有用完整个buffer。幸运的是,我们知道写入了多少字节到这个buffer,我们可以简单地增加参数去割开这个字符串: [代码]> buffer.toString("utf-8", 0, 12) 'Hello world!' [代码] 独立字节: 你可以看到用类似数组的语法来设置独立位(individual bits) [代码]> buffer[12] = buffer[11]; 33 > buffer[13] = "1".charCodeAt(); 49 > buffer[14] = buffer[13]; 49 > buffer[15] = 33 33 > buffer.toString("utf-8") 'Hello world!!11!' [代码] 在这个例子里,手动地设置剩余的字节,这样就代表了“utf-8”编码的“!”和“1“字符了。 更多有趣用法 Buffer.isBuffer(object) 这个方法是检测一个对象是否是buffer,类似于[代码]Array.isArray[代码] Buffer.byteLength(string, encoding) 通过这个方法,你可以获取字符串(默认utf-8编码)的字节数。这个长度和字符串的长度(string length)不一样,因为很多字符需要更多的字节,例如: [代码]> var snowman = "☃"; > snowman.length 1 > Buffer.byteLength(snowman) 3 [代码] 这个unicode的雪人只有两个字符,却占了3个字节。 buffer.length 这个是buffer的长度,也代表分配了多少内存。这个不等于buffer内容的大小,因为buffer有可能是没满的,比如: [代码]> var buffer = new Buffer(16) > buffer.write(snowman) 3 > buffer.length 16 [代码] 在这个例子里,我们只写入了3个字符,但是长度依然是16,因为这是已经初始化了的。 buffer.copy(target, targetStart=0, sourceStart=0, sourceEnd=buffer.length) [代码]buffer.copy[代码]允许拷贝一个buffer的内容到另一个buffer。 第一个参数表示目标buffer,就是要写入内容的buffer。 另外一个参数是指定需要拷贝到目标buffer的开始位置。看个例子: [代码]> var frosty = new Buffer(24) > var snowman = new Buffer("☃", "utf-8") > frosty.write("Happy birthday! ", "utf-8") 16 > snowman.copy(frosty, 16) 3 > frosty.toString("utf-8", 0, 19) 'Happy birthday! ☃' [代码] 在这个例子,拷贝了含有3个字节长度的“snowman”buffer到“forsty”buffer。 其中forsty一开始写入了前16个字节,而snowman有3个字节长,因此结果就是19个字节长。 buffer.slice(start, end=buffer.length) 这个方法的API可以说和[代码]Array.prototype.slice[代码]是一样的。 不过其中一个特别重要的区别是:这个slice方法不是简单地返回一个新的buffer,也不仅仅是内存中子集的引用。这个slice会改变原来的buffer!举例: [代码]> var puddle = frosty.slice(16, 19) > puddle.toString() '☃' > puddle.write("___") 3 > frosty.toString("utf-8", 0, 19) 'Happy birthday! ___' [代码] 完。 原文链接:https://docs.nodejitsu.com/articles/advanced/buffers/how-to-use-buffers/
2020-03-11 - 搞懂微信支付 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 - 小程序多级分销知多少
微信小程序开发者对于小程序生态内的多级分销存在疑惑,今天小编就跟大家一起来梳理下关于多级分销的几个关键点,帮助大家了解哪些在平台内是违规分销行为。 什么是违规分销?三级或三级以上的分销模式就属于违规分销。微信生态拒绝违规分销行为。 二级分销模式:A(厂家)→B(代理) 三级分销模式:A(厂家)→B(代理)→C(代理) 对违规分销行为有初步的了解后,我们来看看微信小程序中常见的具体违规分销案例: l以电商小程序为载体,通过培养用户粉丝,制定分级佣金的形式进行多级分销的行为 [图片] l以“无实物商品”、“虚拟币”多层级分销盈利的庞氏骗局 [图片] l通过购买实物商品获得会员资格发展下线,并以直接或间接发展人员数量和销售业绩作为计酬依据。 [图片] l打着“国家扶持”“民族资产解冻”“民族大业”等旗号的“国家工程” [图片] l以共享经济、物联网、中国梦等名义,大肆发展下线会员牟取暴利 [图片] l免费旅游”“边旅游边赚钱”的旅游骗局,以巨额回报为噱头的金融诈骗 [图片] l以少量资金投入,通过不断的拆分裂变,获得高额盈利吸引用户参与。 [图片]
2020-03-18 - 子商户号支付问题求解
微信服务商子商户号在调用统一下单接口的时候,提示:sub_mch_id与sub_appid不匹配。 但是实际上,不管是微信小程序后台还是微信子商户后台上面都已经显示已经成功绑定。 微信商户后台 [图片] 微信小程序后台[图片] 微信商户后台 [图片]
2018-07-31 - 小程序1rpx边框不完美解决方案
在小程序开发中,1rpx边框随处可见, 像上图UI给的设计稿,如果只是简单使用[代码]border: 1rpx solid red;[代码]的话,在不同的机型上会有不同的表现 [图片] 表现IOS 机型上[图片] Android机型上[图片] 由图片可以看出, IOS机型上会有边框缺失(然而经常出现缺不能稳定复现), 而Android机型上边框比较粗 原因上面这两种表现形式很难联系到一起 首先先看IOS边框缺失的问题,借鉴网络上前辈们的经验 当父元素的高度为奇数,容易出现上下边框缺失,同理宽度为奇数,容易出现左右边框缺失解决办法是在边框内部添加一个1rpx的元素或者伪元素, 撑开内部使父元素的宽高是偶数。 然而我们发现这种方案在Iphone 6等2倍屏可以生效, 但放在如Iphone X等3倍屏下面就很飘了, 还是经常会出现边框缺失的情况, 这种情况下再去把父元素改为2和3共同的倍数就非常不现实了。 再回过头看导致边框缺失的具体原因是啥。 在这之前需要了解下高分屏的物理像素和虚拟像素的概念 简单来说物理像素是设备的实际像素 虚拟像素是设备的坐标点, 可以简单理解为css像素 而rpx类似rem,渲染后实际转换成px之后可能存在小数,在不同的设备上多多少少会存在渲染的问题。而1rpx的问题就更加明显,因为不足1个物理像素的话,在IOS会进行四舍五入,而安卓好像统一向上取整,这也是上面两种设备表现不同的原因。 解决方法我们采用的方法是采用translate:scale(0.5)的方法对边框进行缩放 具体的代码如下 .border1rpx, .border1rpx_before{ position: relative; border-width: 0rpx !important; padding: 0.5rpx; z-index: 0; } .border1rpx::after, .border1rpx_before::before{ content: ""; border-style: inherit; border-color: inherit; border-radius: inherit; box-sizing: border-box !important; position: absolute; border-width: 2rpx !important; left: 0; top: 0; width: 200% !important; height: 200% !important; transform-origin: 0 0; transform: scale(0.5) !important; z-index: -1; } .border1rpx-full { margin: -1rpx; } 给.border1rpx的元素设置边框宽度为0给::after伪元素宽高为两倍,边框设置2rpx,边框其他样式继承元素的设置然后再缩放0.5来达到边框为1rpx的效果 用法基础用法给相应的元素添加border1rpx的class即可, (.borde1rpx说:我们不生产边框,只是边框的搬运工,要显示边框样式的话还需要在元素上自行设置) 圆角边框圆角边框需要自行设置相应伪元素::before 或 ::after的border-raduis值为预期的2倍, 如原本想要设置10rpx的圆角,需要设置[代码].xxx::after{border-raduis: 20rpx;}[代码] 边框内部填充由于设计原因,目标元素会留1rpx的padding用于显示伪元素的边框,如果内部元素是填充的,正常会看到填充元素和目标元素有小部分间隙,此时需要给填充元素添加.border1rpx_full来解决 注意点此方案默认使用::after伪元素实现边框,如果目标元素的after被占用(如iconfont),请使用[代码].border1rpx_before[代码]如单独设置边框(如上边框), [代码]border: 1rpx solid red;border-width: 1rpx 0 0 0;[代码]不能被正确继承,请使用简写[代码]border-top: 1rpx solid red;[代码]由于设计原因,目标元素请最少设置1rpx的padding用于显示边框,(上面的样式已经有了默认的padding,不写也可以, 只是不要用padding:0覆盖)请自行测试点击功能是否正常,防止层级关系导致元素区域被伪元素覆盖
2020-07-23 - createSelectorQuery方法获取canvas节点导致内存泄漏
使用lottie动画过程发现,多次播放动画,使用时间过长导致程序闪退;进一步定位发现createSelectorQuery方法获取canvas节点后页面关闭内存并未释放;操作步骤、可复现demo如下; 操作步骤: 在A页面点击按钮打开B页面;点击B页面的按钮触发wx.createSelectorQuery().selectAll('#c1').node(res => {}).exec() (c1为canvas标签的id)返回A页面,B页面销毁但内存并未释放;反复进行1-3步骤;可以观察到内存一直在上升;操作视频:https://cdn.kaishuhezi.com/kstory/activity_flow/video/b5501115-95b4-41e8-b477-f2ffef2c4467.mp4 复现手机:魅族16th [图片] 代码片段:https://developers.weixin.qq.com/s/cJPBgWma7rgH
2020-04-28 - ios 上重复跳转到某页面并用canvas画图时会导致运行内存不足或意外退出
ios上多次跳转到某页面,并用canvas 2d画图,当跳转次数较多时小程序会提示“运行内存不足,请重新打开该小程序”或“小程序意外退出,请稍候重试”,部分设备在这时再次进去小程序或打开其他小程序也会提示该报错,只有杀掉微信进程后才能正常使用 目前用了几台ios设备都能重现, 只是出现问题的打开页面次数会有所不同 例如: iphone 6s - 打开70~80次会出错 iphone 7p - 打开110~130次会出错 ipad air - 打开25~50次会出错 ipad pro - 打开200次以上会出错 实际项目中,因为还调用其他的canvas api,出错会更加频繁,可能仅十几次就报错了 [视频]
2020-04-23 - 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 - 小程序API的异步优雅用法
先上代码,写一个全局的 [代码]wx.$promisify()[代码] 方法 [代码]wx.$promisify = (method, opts, ...params) => new Promise((resolve, reject) => wx[method]( { ...opts, success: resolve, fail: reject }, ...params ) ) [代码] 举一个几乎大家都会用到的登录为例(虽然图1我已经改进了很多次,但嵌套问题还是很刺眼) [图片] 改进后,多层嵌套变扁平了 [图片]
2020-05-20 - 小程序图片裁剪插件 image-cropper
之前的插件类目没有了导致搜不到了,重新发个文章。 image-cropper 一款高性能的小程序图片裁剪插件,支持旋转。 [图片] 优势 [代码]1.功能强大。[代码] [代码]2.性能超高超流畅,大图毫无卡顿感。[代码] [代码]3.组件化,使用简单。[代码] [代码]4.点击中间窗口实时查看裁剪结果。[代码] ㅤ 初始准备 1.json文件中添加image-cropper [代码] "usingComponents": { "image-cropper": "../image-cropper/image-cropper" }, "navigationBarTitleText": "裁剪图片", "disableScroll": true [代码] 2.wxml文件 [代码]<image-cropper id="image-cropper" limit_move="{{true}}" disable_rotate="{{true}}" width="{{width}}" height="{{height}}" imgSrc="{{src}}" bindload="cropperload" bindimageload="loadimage" bindtapcut="clickcut"></image-cropper> [代码] 3.简单示例 [代码] Page({ data: { src:'', width:250,//宽度 height: 250,//高度 }, onLoad: function (options) { //获取到image-cropper实例 this.cropper = this.selectComponent("#image-cropper"); //开始裁剪 this.setData({ src:"https://raw.githubusercontent.com/1977474741/image-cropper/dev/image/code.jpg", }); wx.showLoading({ title: '加载中' }) }, cropperload(e){ console.log("cropper初始化完成"); }, loadimage(e){ console.log("图片加载完成",e.detail); wx.hideLoading(); //重置图片角度、缩放、位置 this.cropper.imgReset(); }, clickcut(e) { console.log(e.detail); //点击裁剪框阅览图片 wx.previewImage({ current: e.detail.url, // 当前显示图片的http链接 urls: [e.detail.url] // 需要预览的图片http链接列表 }) }, }) [代码] 参数说明 属性 类型 缺省值 取值 描述 必填 imgSrc String 无 无限制 图片地址(如果是网络图片需配置安全域名) 否 disable_rotate Boolean false true/false 禁止用户旋转(为false时建议同时设置limit_move为false) 否 limit_move Boolean false true/false 限制图片移动范围(裁剪框始终在图片内)(为true时建议同时设置disable_rotate为true) 否 width Number 200 超过屏幕宽度自动转为屏幕宽度 裁剪框宽度 否 height Number 200 超过屏幕高度自动转为屏幕高度 裁剪框高度 否 max_width Number 300 裁剪框最大宽度 裁剪框最大宽度 否 max_height Number 300 裁剪框最大高度 裁剪框最大高度 否 min_width Number 100 裁剪框最小宽度 裁剪框最小宽度 否 min_height Number 100 裁剪框最小高度 裁剪框最小高度 否 disable_width Boolean false true/false 锁定裁剪框宽度 否 disable_height Boolean false true/false 锁定裁剪框高度 否 disable_ratio Boolean false true/false 锁定裁剪框比例 否 export_scale Number 3 无限制 输出图片的比例(相对于裁剪框尺寸) 否 quality Number 1 0-1 生成的图片质量 否 cut_top Number 居中 始终在屏幕内 裁剪框上边距 否 cut_left Number 居中 始终在屏幕内 裁剪框左边距 否 [代码]img_width[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置宽度,高度自适应) 图片宽度 否 [代码]img_height[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置高度,宽度自适应) 图片高度 否 scale Number 1 无限制 图片的缩放比 否 angle Number 0 (limit_move=true时angle=n*90) 图片的旋转角度 否 min_scale Number 0.5 无限制 图片的最小缩放比 否 max_scale Number 2 无限制 图片的最大缩放比 否 bindload Function null 函数名称 cropper初始化完成 否 bindimageload Function null 函数名称 图片加载完成,返回值Object{width,height,path,type等} 否 bindtapcut Function null 函数名称 点击中间裁剪框,返回值Object{src,width,height} 否 函数说明 函数名 参数 返回值 描述 参数必填 upload 无 无 调起wx上传图片接口并开始剪裁 否 pushImg src 无 放入图片开始裁剪 是 getImg Function(回调函数) [代码]Object{url,width,height}[代码] 裁剪并获取图片(图片尺寸 = 图片宽高 * export_scale) 是 setCutXY X、Y 无 设置裁剪框位置 是 setCutSize width、height 无 设置裁剪框大小 是 setCutCenter 无 无 设置裁剪框居中 否 setScale scale 无 设置图片缩放比例(不受min_scale、max_scale影响) 是 setAngle deg 无 设置图片旋转角度(带过渡效果) 是 setTransform {x,y,angle,scale,cutX,cutY} 无 图片在原有基础上的变化(scale受min_scale、max_scale影响) 根据需要传参 imgReset 无 无 重置图片的角度、缩放、位置(可以在onloadImage回调里使用) 否 GitHub https://github.com/wx-plugin/image-cropper/tree/master 如果有什么好的建议欢迎提issues或者提pr
2021-12-15 - 拎包哥回归之作,wifi打印机对接 [拎包入住,即抄即用]
[图片] 距离上一次冒泡已经是两个月前了,上一篇文章还是挂人的水帖,这次拎包哥带来的是市面上已经成熟存在的wifi打印机。 我浏览了一遍社区,但却没啥教程,为啥大家都藏着掖着,难道。。。这里有啥大咪咪? 其实不然,代码真的很简单(看上去有点长),大家耐心走个流程(粘贴)就完事儿了。 ------------------------------------------------------------------------------------ 0.首先,买部飞鹅云的wifi打印机(没广告费),大概300RMB左右(不清楚wifi打印机对比蓝牙打印机优势的可以先百度一下) [图片] 这里吐槽一下智联云wifi打印机 不要被他们的噱头:“多送10卷纸”所蒙蔽了双眼(这就是我踩过的坑哈哈)。服务态度。。。真的一言难尽,每次打服务热线过去都是播完一整首的广场舞歌曲后,机器人告诉我无人接听做结束。下载api发现也不能ctrl c+ ctrl v(菜)直接使用,然后我进QQ群去咨询,半天也没人鸟我。综上所述我才退的货去买的飞鹅云,粘贴api代码,机器立马(一两天)就跑起来了。 1.打开网址:http://admin.feieyun.com/,注册,然后添加打印机 [图片] [图片] 填入这两个信息(在打印机底部)就够了 [图片][图片] 2.在飞鹅开放平台注册开发者账号(放心,不要钱的) 打开个人中心,进行个人实名认证(不需要企业认证) [图片] 认证成功后你会得到自己的开发者信息 [图片] 3.粘贴官方的api到自己的小程序(我直接粘贴过来了,方便讲解) 要改的只有这几个地方: 3.1认证通过后得到的开发者信息 + 打印机底部的SN编号 var USER = "xxxxxxxxxxxxxxxxx"; //必填,飞鹅云 http://admin.feieyun.com/ 管理后台注册的账号名 var UKEY = "xxxxxxxxxxxxxxxxx"; //必填,这个不是填打印机的key,是在飞鹅云后台注册账号后生成的UKEY var SN = 'xxxxxxxxxxxxxxxxx'; //必填,打印机编号,打印机必须要在管理后台先添加 USER和UKEY在这里 [图片] SN在这里(密钥不用管) [图片] 3.2 去掉注释 这里的1代表一次打印1张,如此类推。。。 // print_r(SN,orderInfo,1); 3.3 修改orderInfo里的数据到自己想要的格式 ps. 记得勾选这个 [图片] 官方算法 或者你可以直接跑起来先爽一下,但自己的数据还是要有算法来适应打印纸。 var orderInfo; orderInfo = '小程序测试打印'; orderInfo += '名称 单价 数量 金额'; orderInfo += '--------------------------------'; orderInfo += '饭 10.0 10 10.0'; orderInfo += '炒饭 10.0 10 10.0'; orderInfo += '蛋炒饭 10.0 100 100.0'; orderInfo += '鸡蛋炒饭 100.0 100 100.0'; orderInfo += '西红柿炒饭 1000.0 1 100.0'; orderInfo += '西红柿蛋炒饭 100.0 100 100.0'; orderInfo += '西红柿鸡蛋炒饭 15.0 1 15.0'; orderInfo += '备注:加辣'; orderInfo += '--------------------------------'; 我的算法 orderInfo += '名称 单价 数量 金额';orderInfo += '--------------------------------';for (var i in list) { if (list[i].name.length < 15) { aLength = list[i].name.match(reg).length * 2 a = list[i].name.concat(' '.repeat(15 - aLength)) } if (list[i].price.toString().length < 6) { b = list[i].price.toString().concat(' '.repeat(6 - list[i].price.toString().length)) } if (list[i].num.toString().length < 5) { c = list[i].num.toString().concat(' '.repeat(5 - list[i].num.toString().length)) } if (list[i].sinTtlPrice.toString().length < 5) { d = list[i].sinTtlPrice.toString().concat(' '.repeat(5 - list[i].sinTtlPrice.toString().length)) } orderInfo += a + b + c + d + ''} 完整官方小程序api 只是一个方法,不用node xx包,我等手残党福音! [图片] [图片] //微信小程序https请求实例,根据自己的需求条件触发函数,推送订单打印test(e) { //USER和UKEY在飞鹅云( http://admin.feieyun.com/ )管理后台的个人中心可以查看 var USER = "xxxxxxxxxxxxxxxxx"; //必填,飞鹅云 http://admin.feieyun.com/ 管理后台注册的账号名 var UKEY = "xxxxxxxxxxxxxxxxx"; //必填,这个不是填打印机的key,是在飞鹅云后台注册账号后生成的UKEY var SN = 'xxxxxxxxxxxxxxxxx'; //必填,打印机编号,打印机必须要在管理后台先添加 //以下URL参数不需要修改 var HOST = "api.feieyun.cn"; //域名 var PATH = "/Api/Open/"; //接口路径 var STIME = new Date().getTime();//请求时间,当前时间的秒数 var SIG = hex_sha1(USER + UKEY + STIME);//获取签名 //标签说明: //单标签: //"<BR>"为换行,"<CUT>"为切刀指令(主动切纸,仅限切刀打印机使用才有效果) //"<LOGO>"为打印LOGO指令(前提是预先在机器内置LOGO图片),"<PLUGIN>"为钱箱或者外置音响指令 //成对标签: //"<CB></CB>"为居中放大一倍,"<B></B>"为放大一倍,"<C></C>"为居中,<L></L>字体变高一倍 //<W></W>字体变宽一倍,"<QR></QR>"为二维码,"<BOLD></BOLD>"为字体加粗,"<RIGHT></RIGHT>"为右对齐 //拼凑订单内容时可参考如下格式 //根据打印纸张的宽度,自行调整内容的格式,可参考下面的样例格式 var orderInfo; orderInfo = '<CB>小程序测试打印</CB><BR>'; orderInfo += '名称 单价 数量 金额<BR>'; orderInfo += '--------------------------------<BR>'; orderInfo += '饭 10.0 10 10.0<BR>'; orderInfo += '炒饭 10.0 10 10.0<BR>'; orderInfo += '蛋炒饭 10.0 100 100.0<BR>'; orderInfo += '鸡蛋炒饭 100.0 100 100.0<BR>'; orderInfo += '西红柿炒饭 1000.0 1 100.0<BR>'; orderInfo += '西红柿蛋炒饭 100.0 100 100.0<BR>'; orderInfo += '西红柿鸡蛋炒饭 15.0 1 15.0<BR>'; orderInfo += '备注:加辣<BR>'; orderInfo += '--------------------------------<BR>'; orderInfo += '合计:xx.0元<BR>'; orderInfo += '送货地点:广州市南沙区xx路xx号<BR>'; orderInfo += '联系电话:13888888888888<BR>'; orderInfo += '订餐时间:2014-08-08 08:08:08<BR>'; orderInfo += '<QR>http://www.dzist.com</QR>';//把二维码字符串用标签套上即可自动生成二维码 // ***接口返回值说明*** //正确例子:{"msg":"ok","ret":0,"data":"123456789_20160823165104_1853029628","serverExecutedTime":6} //错误:{"msg":"错误信息.","ret":非零错误码,"data":null,"serverExecutedTime":5} // console.log(orderInfo); //打开注释可测试 // print_r(SN,orderInfo,1); var hexcase = 0; var chrsz = 8; function hex_sha1(s) { return binb2hex(core_sha1(AlignSHA1(s))); } function core_sha1(blockArray) { var x = blockArray; var w = Array(80); var a = 1732584193; var b = -271733879; var c = -1732584194; var d = 271733878; var e = -1009589776; for (var i = 0; i < x.length; i += 16) { var olda = a; var oldb = b; var oldc = c; var oldd = d; var olde = e; for (var j = 0; j < 80; j++) { if (j < 16) w[j] = x[i + j]; else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1); var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), safe_add(safe_add(e, w[j]), sha1_kt(j))); e = d; d = c; c = rol(b, 30); b = a; a = t; } a = safe_add(a, olda); b = safe_add(b, oldb); c = safe_add(c, oldc); d = safe_add(d, oldd); e = safe_add(e, olde); } return new Array(a, b, c, d, e); } function sha1_ft(t, b, c, d) { if (t < 20) return (b & c) | ((~b) & d); if (t < 40) return b ^ c ^ d; if (t < 60) return (b & c) | (b & d) | (c & d); return b ^ c ^ d; } function sha1_kt(t) { return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : (t < 60) ? -1894007588 : -899497514; } function safe_add(x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } function rol(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } function AlignSHA1(str) { var nblk = ((str.length + 8) >> 6) + 1, blks = new Array(nblk * 16); for (var i = 0; i < nblk * 16; i++) blks[i] = 0; for (i = 0; i < str.length; i++) blks[i >> 2] |= str.charCodeAt(i) << (24 - (i & 3) * 8); blks[i >> 2] |= 0x80 << (24 - (i & 3) * 8); blks[nblk * 16 - 1] = str.length * 8; return blks; } function binb2hex(binarray) { var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; var str = ""; for (var i = 0; i < binarray.length * 4; i++) { str += hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8 + 4)) & 0xF) + hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8)) & 0xF); } return str; } // 打印订单方法:Open_printMsg function print_r(SN, orderInfo, TIMES) { wx.request({ url: 'https://' + HOST + PATH, data: { user: USER,//账号 stime: STIME,//当前时间的秒数,请求时间 sig: SIG,//签名 apiname: "Open_printMsg",//不需要修改 sn: SN,//打印机编号 content: orderInfo,//打印内容 times: TIMES,//打印联数,默认为1 }, method: "POST", header: { "content-type": "application/x-www-form-urlencoded" }, success: function (res) { console.log(res.data) } }) }} 好了,大咪咪披露完了。可能还有什么纰漏,没关系,评论里见吧! [图片] -=====================成果图========================= 应大家要求放上成果,图有点歪,当是治治大家的颈椎病吧。 [图片] [图片] ================更新于2020/7/22=========================
2022-03-10 - 如何只使用一个云函数搞定一个庞大而复杂的系统
吐槽 翻遍社区的文章,关于云开发的干货,少之又少,大部分都还是官方文档的搬来搬去,没啥营养,是时候放出一点技术"干货"了(有经验的开发者都能想到的方案)! 正题 小程序云开发的云函数的最大限制是 [代码]50[代码] 个,假设每个接口都使用 [代码]1[代码] 个云函数的话,有 [代码]10[代码] 张表,每张表都有 [代码]增删改查[代码] 四个接口,那么就会有 [代码]40[代码] 个接口,再加上一些其他接口,差不多刚刚好够用,那如果有 [代码]20[代码] 张表,甚至更多的表、更多的接口呢?对于中小型的小程序来说足够使用,那如果一个非常庞大而复杂的系统该怎么办呢? 而且每一个云函数的运行环境是独立的,想要共享一些数据也不是特别方便,那么有没有什么办法突破这样的限制呢? 其实解决方案很简单,只需要一点点的 [代码]OOP[代码] 思想和利用 [代码]JavaScript[代码] 的特性,一个云函数就可以搞定所有的接口。 具体的实现请往下看。 思路 云函数的运行环境是 [代码]Nodejs[代码] , 那么使用的语言就是 [代码]JavaScript[代码] ,可以充分的利用 [代码]JavaScript[代码] 的特性。 [代码]JavaScript[代码] 中的 [代码]属性访问表达式[代码] 有两种语法 [代码]expression . identifier expression [ expression ] [代码] 第一种写法是一个表达式后跟随一个句点 [代码].[代码] 和一个标识符。表达式指定对象,标识符则指定需要访问的属性的名称。 第二种写法是使用方括号 [代码][][代码],方括号内是另一个表达式(这种方法适用于对象和数组)。第二个表达式指定要访问的属性的名称或者代表要访问数组元素的索引。 不管使用哪种形式的属性访问表达式,在 [代码].[代码] 和 [代码][][代码] 之前的表达式总是会首先计算。 虽然 [代码].[代码] 的写法更加简单,但这种方式只适用于要访问的属性名称的合法标识符,并需要准确知道访问的属性的名字,如果属性的名称是一个保留字或者包含空格和标点符号,或者是一个数字(对于数组来说),则必须使用方括号 [代码][][代码] 的写法。当属性名是通过运算得出的值而不是固定值的时候,这时也必须使用方括号 [代码][][代码] 写法。 感谢社区大神 @卢霄霄 提供参考资料,详见 [代码]JavaScript权威指南[代码] (犀牛书)4.4章节。 可以使用 [代码][][代码] 的形式来完成动态的属性访问。具体实现请往下看。 实现 上面说了太多废话了,下面直接开干吧。 新建云函数 在云开发目录中新建一个云函数,我这里命名为 [代码]cloud[代码]。 打开 [代码]index.js[代码] 文件你会看到下面这段代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } [代码] 这个云函数仅作为入口使用,上面提到了云函数的运行环境是 [代码]Nodejs[代码] 那么 [代码]Nodejs[代码] 的特性也是可以使用的,这里主要用到的是全局对象 [代码]global[代码],详见文档 在文件中,写入一些必要的全局变量,主要还是云数据库方面的,方便后面使用。 在初始化后面插入代码 [代码]global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate [代码] 这样就可以在同一个云函数环境中直接访问这些全局变量。 创建公共类 然后新建一个文件夹,我这里命名为 [代码]controllers[代码] ,这个文件夹用于存放所有的接口。 在 [代码]controllers[代码] 中新建一个 [代码]base-controller.js[代码] 文件,创建一个叫做 [代码]BaseController[代码] 的类,用于提供一些公用的方法。 内容如下: [代码]class BaseController { /** * 调用成功 */ success (data) { return { code: 0, data } } /** * 调用失败 */ fail (erroCode = 0, msg = '') { return { erroCode, msg, code: -1 } } } module.exports = BaseController [代码] 看到这里大家可能有点没看懂在做什么,那么请继续往下看。 创建接口 假设创建一些要操作用户相关的的接口,可以在 [代码]controllers[代码] 文件夹中新建一个 [代码]user-controller.js[代码] 的文件,创建一个名为 [代码]UserController[代码] 的类,并继承上面的 [代码]BaseController[代码] 类,内容如下: [代码]const BaseController = require('./base-controller.js') class UserController extends BaseController { // ... } module.exports = UserController [代码] 可以在这个类中编写所有关于 [代码]user[代码] 的接口方法。 编写接口 假设要分页查询用户信息,可以在 [代码]UserController[代码] 类中创建一个 [代码]list[代码] 方法。 代码如下: [代码]async list (data) { const { pageIndex, pageSize } = data let result = await db.collection('users') .skip((pageIndex - 1) * pageSize) .limit(pageSize) .get() .then(result => this.success(result.data)) .catch(() => this.fail([])) return result } [代码] 由于上面已经定义了全局变量 [代码]db[代码] 所以在 [代码]UserController[代码] 中无需引入 [代码]wx-server-sdk[代码] 引入接口类 写到这里接口已经完成了,还需要再引入这些接口类才可以进行访问。在 [代码]index.js[代码] 中引入 [代码]user-controller.js[代码] [代码]const User = require('./controllers/user-controller.js') [代码] 然后创建一个 [代码]api[代码] 变量,[代码]new[代码] 一个 [代码]User[代码] 实例 [代码]const api = { user: new User() } [代码] 在 [代码]main[代码] 方法中调用 [代码]UserController[代码] 中的方法。 [代码]exports.main = async (event, context) => { const { data } = event let result = await api['user']['list'](data) return result } [代码] 写到这里基本已经完成了接口的调用,但想要一个云函数动态调用所有接口还需要做一些改动。 动态调用接口 刚开始的时候介绍了 [代码]属性访问表达式[代码],限制稍微改动一下 [代码]main[代码] 方法 [代码]exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } [代码] 在小程序调用云函数时,需要传入 [代码]controller[代码]、[代码]action[代码] 和 [代码]data[代码] 参数即可 [代码]const result = await wx.cloud.callFunction({ name: 'cloud', data: { controller, action, data } }) [代码] 完整 [代码]index.js[代码] 文件的代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const User = require('./controllers/user-controller.js') const api = { user: new User() } // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate // 云函数入口 exports.main = async (event, context) => { exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } } [代码] 其他实现 云开发官方团队打造的轮子 tcb-router
2020-05-26 - 将小程序原生异步函数promisify后,在async/await中使用
目前,小程序中支持使用async/await有三种模式: 1、不勾选es6转es5,不勾选增强编译;该模式是纯es7的async/await,需要基础库高版本。 2、勾选es6转es5,勾选增强编译;一般是因为调用了第三方的es5插件,通过增强编译支持async/await。 3、勾选es6转es5,不勾选增强编译;手工引入runtime.js支持async/await。 据最近更新情况,原生的函数已经大部分同时原生支持同步化了,不需要本方案转化了,直接加上await即可;比如wx.chooseImage、wx.showModal。。。具体有哪些,可以自己试。 如果只是wx.request的同步化,可参考: https://developers.weixin.qq.com/community/develop/article/doc/0004cc839407a069f77a416c056813 app.js代码: function promisify(api) { return (opt, ...arg) => { return new Promise((resolve, reject) => { api(Object.assign({}, opt, { success: resolve, fail: reject }), ...arg) }) } } App({ globalData: {}, chooseImage: promisify(wx.chooseImage), request: promisify(wx.request), getUserInfo: promisify(wx.getUserInfo), onLaunch: function () { }, }) 某page的index.js代码: const app = getApp() testAsync: async function(){ let res = await app.chooseImage() console.log(res) res = await app.request({url:'url',method:'POST',data:{x:0,y:1}}) console.log(res) }, [图片]
2020-10-20 - 【新能力】小程序订单管理功能来了
小程序全新上线了订单管理功能!所有小程序购物订单一个页面统一管理。 最近微信公开课小助手收到很多商家、开发者关于“如何接入订单管理”的问询。 目前,小程序面向商家的交易保障正在内测中,成功接入的商家将获得小程序卡片背书标识、交易保障官方组件和搜索加权等权益,欢迎广大商家和服务商扫描下方二维码报名。我们将根据商家报名情况,逐步开放内测。 [图片] 集中管理小程序订单 头天买东西,隔天就忘记。你的购物记录,小程序订单管理都帮你记好了—— 点击“发现-小程序-右上角…”,就可以进入小程序订单页面一次性搞定查询购物记录,接入交易保障的商家订单已经支持退款售后、联系客服、向微信求助解决争议等后续操作。在小程序买的东西,这里一目了然。 [图片] 3月13日起,7.0.10版本以上的微信用户陆续可以获得上述功能,在接入交易保障的商家小程序上看到对应标识 派爷年前下单的生发剂,终于快到货了。 更安心买买买 全新的小程序订单页面里,有一个特别的标志,绿色的“保”,就代表了你下单的商家接入了小程序的交易保障能力。 在这类商家的小程序下单,可以轻松享受商家认证、交易监管、无忧售后及48小时发货的购物权益。点击进入订单详情即可查看物流状态、联系客服、申请退货退款、查看争议/售后进度等等,买买买可以更安心了。 [图片] 有了“保”字护身符,给爸妈、朋友安利好东西,也更稳了。 有争议及时处理 买买买的快落,不应该被有争议的订单影响。今天开始,如果和接入小程序交易保障的商家产生交易争议,简单几步就可以维权。 快速发起争议流程 进入“小程序订单页面-相应订单-订单详情”后点击右上角即可发起争议处理流程,商家会在48小时内处理;如果商家与你没有能达成一致,微信平台将及时介入核实处理。 [图片] 低成本投诉 如果在购物过程中没有留下争议证明(如商品页面截图等)也不要紧,在经过认证的商家店铺里,可以不用举证直接发起争议解决流程,举证及核实将会由商家和微信来完成。 维权“直通车” 对于超出平台调解范围的争议,我们正在探索多种解决方案,包括在征得争议双方同意的情况下,协助将争议递交至互联网法院进行调解处理或申请立案,保障各方的合法权益。 目前,微信正就保障用户权益和广州互联网法院尝试合作,后续,我们也将继续拓展和其他法院的沟通合作,让更多用户也能享受到这一维权“直通车”。 虽说买卖不成情意在,但应有的权益还是要保障的。
2020-04-29 - 浅谈小程序的错误处理
其实,错误(异常)处理在任何编程语言里,都是不可避免的。正确处理异常,是一个程序/应用保持健壮的关键。 现实 从小程序的API文档可以看出,每个异步方法都支持传入一个[代码]fail[代码]方法,用于异常处理,例如: [代码]wx.login({ success (res) { console.log(res) }, fail(err) { // handle error } }) [代码] 会存在这样一种情况:开发人员会因为惰性直接忽略这个参数;而测试人员由于无法mock这些错误情况,导致测试用例没有覆盖,最终可能会因此流失用户。 还有一种情况,当这些API调用是在非关键流程上。若调用成功,则继续执行;若调用失败,直接忽略也不会影响。 对于第一种情况,除了在fail里做异常处理以外,别无他法。 本文将进一步讨论第二种情况。 分析 官方提供的API,在发生异常时,均会通过回调函数[代码]fail[代码]回传错误信息。如果我们能采集这些数据,进行统计分析能有这些作用: 为后续技术优化提供指导方向 了解用户设备的兼容性,预防踩重复的坑 由于官方提供的API,所有的异步方法都需要手动传入[代码]fail[代码],因此手动给每个方法传入[代码]fail[代码]可能是不可行的。 另外,小程序的更新频率很高,每隔一段时间就会出现许多新的API。 因此,最佳的实践即是封装全局对象[代码]wx[代码] 实践 封装[代码]wx[代码]的方案有很多,这里就列出两种比较常规的方案: 较安全的方案:在全局变量[代码]global[代码]上新增方法(如:[代码]global.wx[代码]) 较激进的方案:劫持[代码]wx[代码],直接在[代码]wx[代码]上动刀 两种方案可有利弊,要看如何权衡。以下我将以第二种方案举例: [代码]// global.js let originalWX = wx; wx = new Proxy({}, { // [0] get(target, name) { if (name in originalWX ) { let isSyncFunction = name.endsWith('Sync'); // 同步函数 [1] let isNotFunction = typeof originalWX[name] !== 'function'; // 非函数 [2] if (isSyncFunction || isNotFunction) return originalWX[name]; return function(obj) { if (typeof obj === 'object') { // [3] let originalFail = function() {}; if ('fail' in obj) { originalFail = obj.fail; } obj.fail = function() { // todo 上报数据到后端 [4] console.log('hijack success'); originalFail(); }; } return originalWX[name](obj); }; } } }); [代码] 代码注释: [0]: 这里使用的是ES6提供的[代码]Proxy[代码]代理对象;会有一定的兼容性,如果需要兼容更低版本的机型,可采用其他方案(感兴趣的人多的话,后续补上) [1]:前文也提到,只有异步方法才会有回调,因此同步方法直接返回原[代码]wx[代码]的方法 [2]:非函数;wx对象里有非函数的值,如 wx.env [3]:wx对象里的函数,可能传入非对象参数。如:wx.canIUse [4]:请看下一章节 [图片] 进阶 其实上述的代码,还不是最终版本。因为数据上报部分,还依赖后端提供接口。 按理说,日志系统也算是通用的服务。我很早前就在思考,为什么微信官方不提供呢?细心的读者可能会反驳说,微信有提供类似的功能:wx.reportMonitor(业务数据监控上报接口)。 其实,用过的读者应该了解,这个接口是非实时的,不能算是日志服务。 如果你有不定时翻看微信小程序开发文档的习惯的话,你总会有这样的感觉:时不时就新增了一个特性,塞在了一个不容易发现的角落。接下来要讲的新特性,就是官方提供的 实时日志: [代码]var log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null log && log.info.apply(log, arguments) [代码] 所有的日志,都可以通过 小程序管理后台 查看。 访问路径:[ 开发->运维中心->实时日志 ] [图片]
2020-03-23 - 开发了一个完整的小程序,用的是云开发。涉及的技术比较多,就不一一写了。看看有没有你需要的技术,给我留言,我会一一解答。
一,云开发端: 1,图片和文字的内容安全检测。 2,将数据导出Excel表, 3,定时触发器(定时器), 4,订阅消息。结合定时触发器,可实现定时推送。 5,生成数据表在云端的好处 6,生成带参数小程序码 7,模糊查询 8,数据实时更新 二,小程序端: 1,用的是原生开发 2,自定义导航栏,且可监听点击右上键箭头,可配置,可弹窗。 3,分享生成的canvas图片 4,统一报错处理, 5,请求封装,状态统一处理。 6,过期头像检测 7,小程序端没啥写的,自己去看页面吧。 三,其它: 1,接入广告流程 四:小程序体验(带有参数的小程序码 + canvas绘图) [图片]
2020-05-08 - 【笔记】小程序微信支付踩过的坑
最近一段时间一直在做跟微信支付相关的内容,遇到很多问题,说实话上周已经调试通过了一个微信支付流程,同样的配方,同样的味道,这次调通竟然花费了一周时间 总结下经常遇到的问题 1、签名失败, 这个问题真的折磨人 我的经历是这样的,证书和密钥都给我我配置好之后,支付一直报错,我找相关人员把密钥变更了,然后又试,开始报签名失败的问题。 试了一个晚上,也不知道是哪根筋接错了,又想用原先的密钥试试,结果成功了, 以上真是坎坷 附云开发一份在线文档 https://github.com/TencentCloudBase/mp-book/
2020-03-22 - 小程序云开发模糊查询,实现数据库多字段的模糊搜索
最近做小程序云开发时,用到了一个数据库的模糊搜索功能,并且是要求多字段的模糊搜索。 网上也有一大堆资源,但是都是单个字段的搜索。如下图 [图片] 上图只可以实现time字段的模糊搜索。但是我们如果相对数据表里的多个字段做模糊查询呢?该怎么办呢。 多字段模糊搜索 一,如我们的数据表里有以下数据,我们想同时模糊查询name和address字段 [图片] [图片] 如我们搜索“周杰”可以看到我们查询到下面两条数据。 [图片] 二,如我们搜索“编程”,可以搜索到下面数据 [图片] 可以看到我们搜索到的两条数据,一个是name字段为 编程小石头, 一个是address字段里包含“编程“ 字样。 下面把代码贴给大家 [代码] let key = "编程小石头"; console.log("查询的内容", key) const db = wx.cloud.database(); const _ = db.command db.collection('qcl').where(_.or([{ name: db.RegExp({ regexp: '.*' + key, options: 'i', }) }, { address: db.RegExp({ regexp: '.*' + key, options: 'i', }) } ])).get({ success: res => { console.log(res) }, fail: err => { console.log(err) } }) [代码] key就是我们要搜索的关键字。主要是用到了数据库查询的where,or,get方法。 代码都给大家贴出来来,如果对云开发和云数据库还不是很了解的同学可以去翻看下我以前写的文章。
2019-11-06 - [填坑手册]小程序PC版来了,如何做PC端的兼容?!
[图片] 微信宣布小程序将可以在PC端微信打开后,智库君就接到要求,需要兼容PC端小程序,一开始以为官方已经做了完美适配,不需要改什么,但当本人下载内测版开始测试的时候,才发现或许坑还挺多的~~~ 下面分享下本人“搬砖填坑”的全过程: (以下都是PC端小程序特有的问题,手机端正常) 先说下使用流程 [图片] 微信开发者工具菜单栏点击 设置->通用设置,在自动预览部分勾选“启动 PC 端自动预览”。 使用自动预览功能,点击 预览->自动预览->编译并预览,成功的话将在微信 PC 版上自动拉起小程序。 [图片] PC版打开后就横屏问题 [图片] [代码]{ "pages": [], "resizable":false, //在这里设置false,使得小程序默认手机尺寸 "pageOrientation":"portrait", //这里默认设置即可 ... } [代码] PC版微信默认打开小程序是ipad版,这样就会出现各种形变,布局错乱,这个可以在app.json进行配置,静止自动旋转,默认手机竖屏样子打开。 页面找不到问题 [图片] 这个问题本人也找了很久,一直很纳闷IDE工具和手机打开看都没什么问题,用PC打开小程序就出现页面找不到的情况,大致报错是: [代码]page[pages/XXX/XXX] not found.May be caused by :1. Forgot to add page route in app.json.2. Invoking Page() in async task. [代码] 一般这种情况以往是 app.json没配,或者页面里面缺少page(),但这次诡异的地方是只有“PC版小程序”报这个错!后来分析问题发现是:目前PC版小程序不能直接支持ES6,必须转换成ES5,同时由于一些语法转化不够完善,特别是ES7中的await 和 async 导致转化二次报错,这里就需要打开 “增强编译” 配置。 [图片] 打开有CSS报错 [图片] 因为目前PC版小程序估计内核的机制问题,还只支持低版本的选择器,如果你直接写小程序的标签,它无法识别,比如 [代码].popCont navigator{ //navigator 标签是小程序里的,PC端无法支持 width: 560rpx; height: 300rpx; } .popCont image{ //image 标签是小程序里的,PC端无法支持 width: 560rpx; height: 300rpx; } [代码] 但这些写法,其实在手机小程序和IDE工具里是完全正常的,PC版需要做兼容,改成class选择器。 布局结构混乱 如果遇到这种情况,会检查一下是否使用屏幕尺寸(rpx)来计算布局,PC 上屏幕尺寸比窗口尺寸大,应该使用窗口尺寸来计算。 小程序如何判断是 PC 平台? 通过 getSystemInfo 官方接口(platform 是 windows) 通过 UA(PC UA 包含 MiniProgramEnv/Windows) 微信官方PC版小程序内测地址: https://dldir1.qq.com/weixin/Windows/WeChat2.7.0_beta.exe 最新官方IDE调试工具 https://developers.weixin.qq.com/miniprogram/dev/devtools/nightly.html 往期回顾: [打怪升级]小程序评论回复和发帖功能实战(二) [打怪升级]小程序评论回复和发贴组件实战(一) [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二) [填坑手册]小程序目录结构和component组件使用心得
2021-09-13 - 如何写出优雅的深复制
前言 无论在项目开发或者学习中,深拷贝已经是一个老生常谈的话题了,但是在实际中,如何优雅地写出深拷贝是我们值得思考的一个问题 内容 深拷贝 与 浅拷贝 深拷贝 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象 浅拷贝 如果一个对象有着原始对象属性值的一份精确拷贝。如果这个对象属性是基本类型,那么拷贝的就是基本类型的值,如果属性是引用类型,那么拷贝的就是内存地址。 区别 其实深拷贝和浅拷贝的主要区别就是其在内存中的存储类型不同。 堆和栈都是内存中划分出来用来存储的区域。 栈(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 - 请问云开发如何在获取数据库的同时格式化时间戳?
感觉可以通过聚合操作实现,但是API中并没有处理时间戳的 [图片]
2019-11-24 - 如何使用数据库聚合match过滤$.and
我需要在云函数中查询数据库,聚合计算一个人的某月1-31号的消费金额: [代码]let queryFeeSum = await db[代码][代码] [代码][代码].collection([代码][代码]'lunch'[代码][代码])[代码][代码] [代码][代码].aggregate()[代码][代码] [代码][代码].match({[代码][代码] [代码][代码]eno: eno,[代码][代码] [代码][代码]date: $.and([$.gte([[代码][代码]'$date'[代码][代码], dateNumRange.begin]), $.lte([[代码][代码]'$date'[代码][代码], dateNumRange.end])])[代码][代码] [代码][代码]})[代码][代码] [代码][代码].group({[代码][代码] [代码][代码]_id: [代码][代码]null[代码][代码],[代码][代码] [代码][代码]fee: $.sum([代码][代码]'$goodsPrice'[代码][代码])[代码][代码] [代码][代码]})[代码][代码] [代码][代码].end()[代码]date是数值型:它的值是8位整数,如20190701、20190728 上面的 $.and 语句按文档中的写法,聚合这个功能也是官方开发出来不久,找不到相关帮助文档。 运行报错,查看日志提示: {"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"errCode: -502001 database request fail | errMsg: [FailedOperation] (BadValue) unknown operator: $and; "} 请问一下,为什么提示操作失败,无效的值、未知的操作器$and。
2019-07-28 - 传统原生支付用云开发实现(非云支付)
本文的代码已过时,请勿照抄。建议改用云支付。 本文的代码被论坛自动过滤了所有XML的标签,所以照抄是会出错的。需要代码的话,看以前的老版本: https://developers.weixin.qq.com/community/develop/doc/000620ec5acb482103b7bf41d51804?jumpto=comment&commentid=000ea67d7b4da8d6c47acd1e05b8 代码前提:只需要替换两个与自己相关的参数key和mch_id 1、小程序开通微信支付成功,去公众平台(https://mp.weixin.qq.com/); 成功后可以知道自己的mch_id,即商户号。 2、去这里:商户平台(https://pay.weixin.qq.com/),获取key = API密钥,如果是退款的话,还需要下载API证书。 [图片] 以下代码仅包含统一下单,以及小程序端拉起支付的代码。 小程序端: testWxCloudPay: function () { wx.cloud.callFunction({ name: 'getPay', // data: {body:"body",attach:"attach",total_fee:1}, // 可传入相关参数。 success: res => { console.log(res.result) if (!res.result.appId) return wx.requestPayment({ ...res.result, success: res => { console.log(res) } }) } }) }, 云函数getPay: const key = "ABC...XYZ" //换成你的商户key,32位 const mch_id = "1413092000" //换成你的商户号 //以下全部照抄即可 const cloud = require('wx-server-sdk') const rp = require('request-promise') const crypto = require('crypto') cloud.init() function getSign(args) { let sa = [] for (let k in args) sa.push(k + '=' + args[k]) sa.push('key=' + key) return crypto.createHash('md5').update(sa.join('&'), 'utf8').digest('hex').toUpperCase() } function getXml(args) { let sa = [] for (let k in args) sa.push('<' + k + '>' + args[k] + '') sa.push('' + getSign(args) + '') return '' + sa.join('') + '' } exports.main = async(event, context) => { const wxContext = cloud.getWXContext() const appId = appid = wxContext.APPID const openid = wxContext.OPENID const attach = 'attach' const body = 'body' const total_fee = 1 const notify_url = "https://mysite.com/notify" const spbill_create_ip = "118.89.40.200" const nonceStr = nonce_str = Math.random().toString(36).substr(2, 15) const timeStamp = parseInt(Date.now() / 1000) + '' const out_trade_no = "otn" + nonce_str + timeStamp const trade_type = "JSAPI" const xmlArgs = { appid, openid, attach, body, mch_id, nonce_str, notify_url, out_trade_no, spbill_create_ip, total_fee, trade_type } let xml = (await rp({ url: "https://api.mch.weixin.qq.com/pay/unifiedorder", method: 'POST', body: getXml(xmlArgs) })).toString("utf-8") if (xml.indexOf('prepay_id') < 0) return xml let prepay_id = xml.split("")[0] let payArgs = { appId, nonceStr, package: ('prepay_id=' + prepay_id), signType: 'MD5', timeStamp } return { ...payArgs, paySign: getSign(payArgs) } } packge.json: { "name": "getPay", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "zfe", "license": "ISC", "dependencies": { "wx-server-sdk": "latest", "crypto": "^1.0.1", "request-promise": "^4.2.2" } } 附:完整代码片段:如果你觉得在这上面花的时间超过一天了,就去下载代码片段吧。 [图片]
2020-10-20 - 小程序数据如何导出为Excel文件?
如题~想实现在小程序中用户输入数据,点击按钮导出为Excel文件,请问如何实现?
2018-07-12 - 小程序导出数据到excel表,借助云开发后台实现excel数据的保存
我们在做小程序开发的过程中,可能会有这样的需求,就是把我们云数据库里的数据批量导出到excel表里。如果直接在小程序里写是实现不了的,所以我们要借助小程序的云开发功能了。这里需要用到云函数,云存储和云数据库。可以说通过这一个例子,把我们微信小程序云开发相关的知识都用到了。 老规矩,先看效果图 [图片] 上图就是我们保存用户数据到excel生成的excel文件。 实现思路 1,创建云函数 2,在云函数里读取云数据库里的数据 3,安装node-xlsx类库(node类库) 4,把云数据库里读取到的数据存到excel里 5,把excel存到云存储里并返回对应的云文件地址 6,通过云文件地址下载excel文件 一,创建excel云函数 关于云函数的创建,我这里不多说了。如果你连云函数的创建都不知道,建议你去小程序云开发官方文档去看看。或者看下我录制的云开发入门的视频:https://edu.csdn.net/course/detail/9604 创建云函数时有两点需要注意的,给大家说下 1,一定要把app.js里的环境id换成你自己的 [图片] 2,你的云函数目录要选择你对应的云开发环境(通常这里默认选中的) 不过你这里的云开发环境要和你app.js里的保持一致 [图片] 二,读取云数据库里的数据 我们第一步创建好云函数以后,可以先在云函数里读取我们的云数据库里的数据。 1,先看下我们云数据库里的数据 [图片] 2,编写云函数,读取云数据库里的数据(一定要记得部署云函数) [图片] 3,成功读取到数据 [图片] 把读取user数据表的完整代码给大家贴出来。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: "test-vsbkm" }) // 云函数入口函数 exports.main = async(event, context) => { return await cloud.database().collection('users').get(); } [代码] 三,安装生成excel文件的类库 node-xlsx 通过上面第二步可以看到我们已经成功的拿到需要保存到excel的源数据,我们接下来要做的就是把数据保存到excel 1,安装node-xlsx类库 [图片] 这一步需要我们事先安装node,因为我们要用到npm命令,通过命令行 [代码]npm install node-xlsx [代码] [图片] 可以看出我们安装完成以后,多了一个package-lock.json的文件 [图片] 四,编写把数据保存到excel的代码, 下图是我们的核心代码 [图片] 这里的数据是我们查询的users表的数据,然后通过下面代码遍历数组,然后存入excel。这里需要注意我们的id,name,weixin要和users表里的对应。 [代码] for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } [代码] 还有下面这段代码,是把excel保存到云存储用的 [代码] //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) [代码] 下面把完整的excel里的index.js代码贴给大家,记得把云开发环境id换成你自己的。 [代码]const cloud = require('wx-server-sdk') //这里最好也初始化一下你的云开发环境 cloud.init({ env: "test-vsbkm" }) //操作excel用的类库 const xlsx = require('node-xlsx'); // 云函数入口函数 exports.main = async(event, context) => { try { let {userdata} = event //1,定义excel表格名 let dataCVS = 'test.xlsx' //2,定义存储数据的 let alldata = []; let row = ['id', '姓名', '微信号']; //表属性 alldata.push(row); for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } //3,把数据保存到excel里 var buffer = await xlsx.build([{ name: "mySheetName", data: alldata }]); //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) } catch (e) { console.error(e) return e } } [代码] 五,把excel存到云存储里并返回对应的云文件地址 我们上面已经成功的把数据存到excel里,并把excel文件存到云存储里。可以看下效果。 [图片] 我们这个时候,就可以通过上图的下载地址下载excel文件了。 [图片] 我们打开下载的excel [图片] 其实到这里就差不多实现了基本的把数据保存到excel里的功能了,但是我们要下载excel,总不能每次都去云开发后台吧。所以我们接下来要动态的获取这个下载地址。 六,获取云文件地址下载excel文件 [图片] 通过上图我们可以看出,我们获取下载链接需要用到一个fileID,而这个fileID在我们保存excel到云存储时,有返回,如下图。我们把fileID传给我们获取下载链接的方法即可。 [图片] 1,我们获取到了下载链接,接下来就要把下载链接显示到页面 [图片] 2,代码显示到页面以后,我们就要复制这个链接,方便用户粘贴到浏览器或者微信去下载 [图片] 下面把我这个页面的完整代码贴给大家 [代码]Page({ onLoad: function(options) { let that = this; //读取users表数据 wx.cloud.callFunction({ name: "getUsers", success(res) { console.log("读取成功", res.result.data) that.savaExcel(res.result.data) }, fail(res) { console.log("读取失败", res) } }) }, //把数据保存到excel里,并把excel保存到云存储 savaExcel(userdata) { let that = this wx.cloud.callFunction({ name: "excel", data: { userdata: userdata }, success(res) { console.log("保存成功", res) that.getFileUrl(res.result.fileID) }, fail(res) { console.log("保存失败", res) } }) }, //获取云存储文件下载地址,这个地址有效期一天 getFileUrl(fileID) { let that = this; wx.cloud.getTempFileURL({ fileList: [fileID], success: res => { // get temp file URL console.log("文件下载链接", res.fileList[0].tempFileURL) that.setData({ fileUrl: res.fileList[0].tempFileURL }) }, fail: err => { // handle error } }) }, //复制excel文件下载链接 copyFileUrl() { let that=this wx.setClipboardData({ data: that.data.fileUrl, success(res) { wx.getClipboardData({ success(res) { console.log("复制成功",res.data) // data } }) } }) } }) [代码] 给大家说下上面代码的步骤。 1,下通过getUsers云函数去云数据库获取数据 2,把获取到的数据通过excel云函数把数据保存到excel,然后把excel保存的云存储。 3,获取云存储里的文件下载链接 4,复制下载链接,到浏览器里下载excel文件。 到这里我们就完整的实现了把数据保存到excel的功能了。 文章有点长,知识点有点多,但是大家把这个搞会以后,就可以完整的学习小程序云开发的:云函数,云数据库,云存储了。可以说这是一个综合的案例。 有什么不懂的地方,或者有疑问的地方,请在文章底部留言,我看到都会及时解答的。后面我还会出一系列关于云开发的文章,敬请关注。
2019-09-07 - 云开发中的云函数以后能不能支持API网关访问?
目前所有云开发中的云函数的 http 调用都只能通过 access token。 有时候需要一些对外公开访问的 API,云开发就满足不了,比如微信支付的通知回调 notify_url,为了某一个 API 还是要被迫寻找 云开发以外的解决方案。
2019-08-28 - 借助小程序云开发实现小程序支付功能(含源码)
我们在做小程序支付相关的开发时,总会遇到这些难题。小程序调用微信支付时,必须要有自己的服务器,有自己的备案域名,有自己的后台开发。这就导致我们做小程序支付时的成本很大。本节就来教大家如何使用小程序云开发实现小程序支付功能的开发。不用搭建自己的服务器,不用有自己的备案域名。只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 1,云开发的部署和使用 2,支付相关的云函数开发 3,商品列表 4,订单列表 5,微信支付与支付成功回调 支付成功给用户发送推送消息的功能会在后面讲解。 下面就来教大家如何借助云开发使用小程序支付功能。 支付所需要用到的配置信息 1,小程序appid 2,云开发环境id 3,微信商户号 4,商户密匙 一,准备工作 1,已经申请小程序,获取小程序 AppID 和 Secret 在小程序管理后台中,【设置】 →【开发设置】 下可以获取微信小程序 AppID 和 Secret。 [图片] 2,微信支付商户号,获取商户号和商户密钥在微信支付商户管理平台中,【账户中心】→【商户信息】 下可以获取微信支付商户号。 [图片] 在【账户中心】 ‒> 【API安全】 下可以设置商户密钥。 [图片] 这里特殊说明下,个人小程序是没有办法使用微信支付的。所以如果想使用微信支付功能,必须是非个人账号(当然个人可以办个体户工商执照来注册非个人小程序账号) 3,微信开发者 IDE https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 4,开通小程序云开发功能:https://edu.csdn.net/course/play/9604/204526 二,商品列表的实现 效果图如下,由于本节重点是支付的实现,所以这里只简单贴出关键代码。 [图片] wxml布局如下: [代码]<view class="container"> <view class="good-item" wx:for="{{goods}}" wx:key="*this" ontap="getDetail" data-goodid="{{item._id}}"> <view class="good-image"> <image src="{{pic}}"></image> </view> <view class="good-detail"> <view class="title">商品: {{item.name}}</view> <view class="content">价格: {{item.price / 100}} 元 </view> <button class="button" type="primary" bindtap="makeOrder" data-goodid="{{item._id}}" >下单</button> </view> </view> </view> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息,然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看我的历史博客,有写过的) 也有视频讲解: https://edu.csdn.net/course/detail/9604 [图片] 三,支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 config下的index.js是做支付配置用的,主要配置支付相关的账号信息 lib是用的第三方的支付库,这里不做讲解。 重点讲解的是云函数入口 index.js 下面就来教大家如何去配置 1,配置config下的index.js, 这一步所需要做的就是把小程序appid,云开发环境ID,商户id,商户密匙。填进去。 [图片] 2,配置入口云函数 [图片] 详细代码如下,代码里注释很清除了,这里不再做单独讲解: [代码]const cloud = require('wx-server-sdk') cloud.init() const app = require('tcb-admin-node'); const pay = require('./lib/pay'); const { mpAppId, KEY } = require('./config/index'); const { WXPayConstants, WXPayUtil } = require('wx-js-utils'); const Res = require('./lib/res'); const ip = require('ip'); /** * * @param {obj} event * @param {string} event.type 功能类型 * @param {} userInfo.openId 用户的openid */ exports.main = async function(event, context) { const { type, data, userInfo } = event; const wxContext = cloud.getWXContext() const openid = userInfo.openId; app.init(); const db = app.database(); const goodCollection = db.collection('goods'); const orderCollection = db.collection('order'); // 订单文档的status 0 未支付 1 已支付 2 已关闭 switch (type) { // [在此处放置 unifiedorder 的相关代码] case 'unifiedorder': { // 查询该商品 ID 是否存在于数据库中,并将数据提取出来 const goodId = data.goodId let goods = await goodCollection.doc(goodId).get(); if (!goods.data.length) { return new Res({ code: 1, message: '找不到商品' }); } // 在云函数中提取数据,包括名称、价格才更合理安全, // 因为从端里传过来的商品数据都是不可靠的 let good = goods.data[0]; // 拼凑微信支付统一下单的参数 const curTime = Date.now(); const tradeNo = `${goodId}-${curTime}`; const body = good.name; const spbill_create_ip = ip.address() || '127.0.0.1'; // 云函数暂不支付 http 触发器,因此这里回调 notify_url 可以先随便填。 const notify_url = 'http://www.qq.com'; //'127.0.0.1'; const total_fee = good.price; const time_stamp = '' + Math.ceil(Date.now() / 1000); const out_trade_no = `${tradeNo}`; const sign_type = WXPayConstants.SIGN_TYPE_MD5; let orderParam = { body, spbill_create_ip, notify_url, out_trade_no, total_fee, openid, trade_type: 'JSAPI', timeStamp: time_stamp, }; // 调用 wx-js-utils 中的统一下单方法 const { return_code, ...restData } = await pay.unifiedOrder(orderParam); let order_id = null; if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { const { prepay_id, nonce_str } = restData; // 微信小程序支付要单独进地签名,并返回给小程序端 const sign = WXPayUtil.generateSignature({ appId: mpAppId, nonceStr: nonce_str, package: `prepay_id=${prepay_id}`, signType: 'MD5', timeStamp: time_stamp }, KEY); let orderData = { out_trade_no, time_stamp, nonce_str, sign, sign_type, body, total_fee, prepay_id, sign, status: 0, // 订单文档的status 0 未支付 1 已支付 2 已关闭 _openid: openid, }; let order = await orderCollection.add(orderData); order_id = order.id; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { out_trade_no, time_stamp, order_id, ...restData } }); } // [在此处放置 payorder 的相关代码] case 'payorder': { // 从端里出来相关的订单相信 const { out_trade_no, prepay_id, body, total_fee } = data; // 到微信支付侧查询是否存在该订单,并查询订单状态,看看是否已经支付成功了。 const { return_code, ...restData } = await pay.orderQuery({ out_trade_no }); // 若订单存在并支付成功,则开始处理支付 if (restData.trade_state === 'SUCCESS') { let result = await orderCollection .where({ out_trade_no }) .update({ status: 1, trade_state: restData.trade_state, trade_state_desc: restData.trade_state_desc }); let curDate = new Date(); let time = `${curDate.getFullYear()}-${curDate.getMonth() + 1}-${curDate.getDate()} ${curDate.getHours()}:${curDate.getMinutes()}:${curDate.getSeconds()}`; } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } case 'orderquery': { const { transaction_id, out_trade_no } = data; // 查询订单 const { data: dbData } = await orderCollection .where({ out_trade_no }) .get(); const { return_code, ...restData } = await pay.orderQuery({ transaction_id, out_trade_no }); return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: { ...restData, ...dbData[0] } }); } case 'closeorder': { // 关闭订单 const { out_trade_no } = data; const { return_code, ...restData } = await pay.closeOrder({ out_trade_no }); if (return_code === 'SUCCESS' && restData.result_code === 'SUCCESS') { await orderCollection .where({ out_trade_no }) .update({ status: 2, trade_state: 'CLOSED', trade_state_desc: '订单已关闭' }); } return new Res({ code: return_code === 'SUCCESS' ? 0 : 1, data: restData }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了。 [图片] 再来看下,支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表,支付状态,支付成功后的回调。 今天就先讲到这里,后面会继续给大家讲解支付的其他功能。比如支付成功后的消息推送,也是可以借助云开发实现的。 由于源码里涉及到一些私密信息,这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言。单独找我要源码也行(微信2501902696) 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 云开发支付的代码,有需要的进。
真机测试已通过。你照抄就行,保证可通过。 最新完美版本可供参考: https://developers.weixin.qq.com/community/develop/article/doc/0004c4a50a03107eaa79f03cc56c13 小程序端: wx.cloud.callFunction({ name: 'getPay' , data: { total_fee: parseFloat(0.01).toFixed(2) * 100, attach: 'anything', body: 'whatever' } }) .then( res => { wx.requestPayment({ appId: res.result.appid, timeStamp: res.result.timeStamp, nonceStr: res.result.nonce_str, package: 'prepay_id=' + res.result.prepay_id, signType: 'MD5', paySign: res.result.paySign, success: res => { console.log(res) } }) }) 云函数:getPay getPay目录下共两个文件: 1、index.js 2、package.json index.js代码如下: const key = "YOURKEY1234YOURKEY1234YOURKEY123"//这是商户的key,不是小程序的密钥,32位。 const mch_id = "1413090000" //你的商户号 //将以上的两个参数换成你的,然后以下可以不用改一个字照抄 const rp = require('request-promise') const crypto = require('crypto') function paysign({ ...args }) { let sa = [] for (let k in args) sa.push( k + '=' + args[k]) sa.push( 'key=' + key) return crypto.createHash('md5').update(sa.join('&'), 'utf8').digest('hex').toUpperCase() } exports.main = async (event, context) => { const appid = event.userInfo.appId const openid = event.userInfo.openId const attach = event.attach const body = event.body const total_fee = event.total_fee const notify_url = "https://whatever.com/notify" const spbill_create_ip = "118.89.40.200" const nonce_str = Math.random().toString(36).substr(2, 15) const timeStamp = parseInt(Date.now() / 1000) + '' const out_trade_no = "otn" + nonce_str + timeStamp let formData = "<xml>" formData += "<appid>" + appid + "</appid>" formData += "<attach>" + attach + "</attach>" formData += "<body>" + body + "</body>" formData += "<mch_id>" + mch_id + "</mch_id>" formData += "<nonce_str>" + nonce_str + "</nonce_str>" formData += "<notify_url>" + notify_url + "</notify_url>" formData += "<openid>" + openid + "</openid>" formData += "<out_trade_no>" + out_trade_no + "</out_trade_no>" formData += "<spbill_create_ip>" + spbill_create_ip + "</spbill_create_ip>" formData += "<total_fee>" + total_fee + "</total_fee>" formData += "<trade_type>JSAPI</trade_type>" formData += "<sign>" + paysign({ appid, attach, body, mch_id, nonce_str, notify_url, openid, out_trade_no, spbill_create_ip, total_fee, trade_type: 'JSAPI' }) + "</sign>" formData += "</xml>" let res = await rp({ url: "https://api.mch.weixin.qq.com/pay/unifiedorder", method: 'POST',body: formData}) let xml = res.toString("utf-8") if (xml.indexOf('prepay_id')<0) return xml let prepay_id = xml.split("<prepay_id>")[1].split("</prepay_id>")[0].split('[')[2].split(']')[0] let paySign = paysign({ appId: appid, nonceStr: nonce_str, package: ('prepay_id=' + prepay_id), signType: 'MD5', timeStamp: timeStamp }) return { appid, nonce_str, timeStamp, prepay_id, paySign } } package.json 代码如下: { "name": "getPay", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "youself", "license": "ISC", "dependencies": { "crypto": "^1.0.1", "request-promise": "^4.2.2" } } 最后选择:上传和部署:云端安装依赖。
2019-12-14 - 微信小程序前端生成图片用于分享朋友圈最终解决方案
前段时间一直在做微信小程序的,遇到了许多的坑,其中遇到了需要前端合成图片保存到相册用于分享到朋友圈。借简书记录一下最终解决方案,先看一下最终效果 [图片] 该文章的所有演示代码托管与github,代码地址,微信调试工具中访问请[代码]关闭合法域名检查[代码],[代码]开启es6转换[代码],真机调试请打开调试[代码]vconsole[代码] 该文章解决的问题如下: 微信小程序生成图片,并保存到相册 微信小程序生成图片实现响应式 微信小程序canvas原生组件如何给画布添加css动画 保存高清分享图方案 微信小程序生成图片实现单屏适应 微信小程序生成图片,并保存到相册 首先,我们希望能实现如下功能,点击用户头像,从底部弹出一个分享弹窗,可以保存合成图片到相册,可以关闭弹层 我们将该功能封装成一个Component自定义组件 定义wxml基本结构 [代码]<view class="share {{visible ? 'show' : ''}}"> <view class="content"> <canvas class="canvas" canvas-id="share" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close">关闭</view> </view> </view> </view> [代码] 定义wxss样式 [代码].share { position: fixed; top: 0; left: 0; min-height: 100vh; width: 100%; background: rgba(61, 61, 61, 0.5); visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out; z-index: 99999; } .share.show { visibility: visible; opacity: 1; } .share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; } .share .content .footer { width: 562rpx; height: 100rpx; background: #fff; border-top: 2rpx solid #e9e9e9; display: flex; flex-direction: row; justify-content: center; align-items: center; font-size: 28rpx; } .share .content .footer .close { width: 100rpx; height: 100rpx; line-height: 100rpx; flex-grow: 0; flex-shrink: 0; text-align: center; border-left: 2rpx solid #e9e9e9; } .share .content .footer .save { height: 100rpx; line-height: 100rpx; flex-grow: 1; flex-shrink: 1; text-align: center; } .share.show .content .canvas { display: inline-block; } .share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 562rpx; height: 1000rpx; } [代码] 定义json [代码]{ "component": true } [代码] 定义组件构造器 [代码]Component({ properties: { visible: { type: Boolean, value: false }, // 由于需要绘制用户信息,由页面传入 userInfo: { type: Object, value: false } }, methods: { draw() { // 实际绘制函数,后续绘制代码放于此处 } } }) [代码] 基本结构和样式定义完成,接下来开始可一开始我们绘制之旅了,合成图片需要用到微信小程序wx.getImageInfo函数,我们先对它进行Promise化方便后期调用 [代码]function getImageInfo(url) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: url, success: resolve, fail: reject, }) }) } [代码] 前期的准备工作建立完成,我们开始定义绘制方法draw [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = 281 const canvasHeight = 500 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = 60 const avatarHeight = 60 const avatarTop = 40 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(20) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + 50, ) ctx.stroke() // 完成作画 ctx.draw() }) [代码] 接下来,我们需要监测visible属性的变化,决定是否开始绘制 [代码]Component({ properties: { visible: { type: Boolean, value: false, observer(visible) { // 当开始显示分享弹窗时开始绘制 if (visible) { this.draw() } } }, }, ....省略其他代码 }) [代码] 此时,前端的绘制已基本成型,运行小程序变可看见合成图,由于我们的绘制尺寸是基于iphone6s进行绘制的,在iphone6s及部分相同分辨率查看,尺寸完全吻合,没有任何问题,然而当我们用iphone6s plus或者其他不同分辨率的手机打开时却变成了下面这个样子 [图片] 绘制的图像没有完全占满画布了,为什么呢?这个是遇到的第二个问题 微信小程序生成图片实现响应式 其实我们的画布宽高单位都是基于rpx单位,因此在不同分辨率的手机上,实际的尺寸也就不同,然而我们绘制图片的尺寸都是以px为单位,自然无法实现响应式,因此我们需要一个js方法用于转换rpx值为px值 解读微信官方文档我们定义如下一个简单的转换方法 [代码]function createRpx2px() { const { windowWidth } = wx.getSystemInfoSync() return function(rpx) { return windowWidth / 750 * rpx } } const rpx2px = createRpx2px() [代码] 定义好了单位转换函数,我们只需转换相关值即可 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2) const canvasHeight = rpx2px(500 * 2) // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2) const avatarHeight = rpx2px(60 * 2) const avatarTop = rpx2px(40 * 2) // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2)) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2), ) ctx.stroke() // 完成作画 ctx.draw() [代码] 此时不管在什么分辨率下的手机都能正常显示了 微信小程序canvas原生组件如何给画布添加css动画 我们都知道微信小程序的canvas是原生组件,对于原生组件有许多的限制,比如不可以使用css动画,官方文档如下: [图片] 首先我们试着给canvas父层标签View.content标签添加弹出动画,修改样式如下: [代码].share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; // 新增动画控制 transform: translate3d(0, 100%, 0); transition: transform 0.2s ease-in-out; } // 新增动画控制 .share.show .content { transform: translate3d(0, 0, 0); } [代码] 在调试器中使用,一切都很美好,完全按着预期由底部弹出,然后淡隐,不过当你用真机调试,canvas部分的效果变得不那么顺畅,流畅,没有弹出动画,没有淡隐效果,一切都变得那么的僵硬,那我们该怎么办呢? 解决办法的思路如下: 提供一个canvas标签,不可以做隐藏(隐藏会导致绘制失效),通过css tansform属性移除屏幕让其不可见 用image标签代替canvas标签显示给用户查看 当画布绘制完成后,我们保存绘制的图像到临时目录中,并获取图片地址 将地址提供给image标签用于展示 基于以上思路,首先改造我们的文档结构 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> [代码] 新增样式 [代码].share .canvas-hide { position: fixed; top: 0; left: 0; transform: translateX(-100%); width: 562rpx; height: 1000rpx; } [代码] 想要保存canvas绘制的图像到临时目录,我们需要利用微信小程序的一个api接口wx.canvasToTempFilePath,因此首先我们还是对其进行Promise化 [代码]function canvasToTempFilePath(option, context) { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ ...option, success: resolve, fail: reject, }, context) }) } [代码] 在组件的data属性中新增imageFile [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { imageFile: '' } }) [代码] 修改我们的绘制方法 [代码]// 仅列出新增部分,省略之前的代码 // 修改画布的draw函数如下 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) }) [代码] 此时在真机上运行调试,可以看到完美的满足我们的需求(沾沾自喜) 保存高清分享图方案 接下来我们需要实现保存到相册中,用于分享给朋友圈或者其他微博 保存图片到相册需要调用微信小程序api,wx.saveImageToPhotosAlbum,依照惯例进行Promise化 [代码]function saveImageToPhotosAlbum(option) { return new Promise((resolve, reject) => { wx.saveImageToPhotosAlbum({ ...option, success: resolve, fail: reject, }) }) } [代码] 我们为保存相册新增点击事件 [代码]<view class="save" bindtap="handleSave">保存到相册</view> [代码] 最后定义我们的保存方法 [代码]// 仅列出新增部分,省略之前的代码 Component({ methods: { handleSave() { const { imageFile } = this.data if (imageFile) { saveImageToPhotosAlbum({ filePath: imageFile, }).then(() => { wx.showToast({ icon: 'none', title: '分享图片已保存至相册', duration: 2000, }) }) } } } }) [代码] 至此保存到相册功能完成了,但是有点瑕疵,原本我们用于绘制的图片非常的高清,可以绘制后保存的图片变得模糊了,没那么高清,这是过不了UED小姐姐那关的 那如何保证保存的图片不会失真呢,我们可以考虑把canvas大小放大到3倍,绘制3倍的图 修改样式 [代码].share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 1686rpx; // 修改为之前的3倍 height: 3000rpx; // 修改为之前的3倍 } [代码] 修改绘制函数,增长绘制大小为3倍 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2 * 3) // 扩大3倍 const canvasHeight = rpx2px(500 * 2 * 3) // 扩大3倍 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarHeight = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarTop = rpx2px(40 * 2 * 3) // 扩大3倍 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2 * 3)) // 扩大3倍 ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2 * 3), // 扩大3倍 ) ctx.stroke() // 完成作画 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) }) [代码] 我们重新保存图片,发现图片变得高清了,hu~~~ 最后我们可以兴高采烈的把成果交给小测试了,一切看起来都很顺利,可惜终究过不了各种机型分辨率的测试,由于我们的设计基于iphone6s尺寸设计,在部分宽高比不同的机型,高度会超出屏幕高度,变成下面这个样子 [图片] 按钮被挡住了,这下无奈了 微信小程序生成图片实现单屏适应 我们希望分享弹窗内容能在一个屏幕下显示完全,那可以根据当前手机宽高比与设计稿尺寸宽高比求出一个缩放比例对整体内容进行缩放即可 定义缩放比例计算 [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { responsiveScale: 1, // 缩放比例默认为1 }, lifetimes: { ready() { const designWidth = 375 const designHeight = 603 // 这是在顶部位置定义,底部无tabbar情况下的设计稿高度 // 以iphone6为设计稿,计算相应的缩放比例 const { windowWidth, windowHeight } = wx.getSystemInfoSync() const responsiveScale = windowHeight / ((windowWidth / designWidth) * designHeight) if (responsiveScale < 1) { this.setData({ responsiveScale, }) } }, }, }) [代码] 修改wxml文档 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content" style="transform:scale({{responsiveScale}});-webkit-transform:scale({{responsiveScale}});"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save" bindtap="handleSave">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> [代码] 修改wxss样式表 [代码].share .content { // 省略其他定义 // 新增缩放中心控制为顶部中心 transform-origin: 50% 0; } [代码] 整体分享遇到的坑都得到了解决,代码较多,所有的代码都托管到了github,欢迎访问运行代码地址,只有亲力亲为才能真正的掌握知识
2019-01-14