个人案例
- 开发工具基础库3.5.7页面使用openDocument后返回页面报错
[aiad error] unexpected page benchmark path: packageA/pages/chapter/attachments.html(env: Windows,mp,1.06.2407110; lib: 3.5.8) 开发工具调用openDocument成功在外部浏览器打开pdf文档,然后返回工具点击左上角返回上一页必报当前错误,并且无具体错误原因,经排查只有3.5.7和3.5.8会出现这个问题,3.5.6正常无错误 [图片]
09-20 - iOS微信小程序虚拟支付解决方案
众所周知,在IOS微信小程序不支持虚拟支付,一直是困扰IOS开发者、运营最头疼的问题,主要原因是苹果不允许IOS微信上架这类产品。导致微信小程序的开发者在IOS上都不能支付虚拟商品,虚拟商品包含了虚拟课程、会员、虚拟书等。 那么针对这个问题,有什么解决方案呢?今天就这个问题,来分享一下关于个人在这方面的一点经验。 做虚拟开发支付,要弄明白为什么微信小程序不支持iOS进行虚拟支付,所谓的虚拟支付又是指的什么? 第一个问题很简单,如果微信小程序支持iOS虚拟支付,就会绕过苹果商店的应用支付方式,在苹果商店下载的APP进行支付,苹果商店会扣除一部分费用的,这样苹果就减少了一部分收入,这也是iOS系统中一个明确规定的条款。 第二个问题,虚拟支付是指包括银行卡支付、电话卡预付费、信用卡支付、网上支付、手机支付等各种非现金的支付方式。 对于知识付费的博主、知识店铺和企业影响来说,不支持iOS虚拟支付,自然就影响了收入。毕竟微信用户中iOS用户群体占比还是非常大的。 要规避这个问题,很多应用的解决方案是: 1、提供应用官方充值(支付)渠道,例如需要充值时,跳转到官网的充值网站,购买消费时,直接从个人的钱包中进行扣除。 [图片] 2、通过微信小程序客服助手,当用户进行支付时,默认跳转到客服助手聊天界面,系统会默认给用户发送一个支付链接,这种链接是一个公众号,或者是一个H5的链接。用户点击链接就可以正常下单,实则上和第一种方案是一样的逻辑。 [图片] 3、微信扫码支付,当用户进行支付时,直接弹出支付二维码,让用户扫码支付。这种逻辑仍然是一个H5的链接方式。
11-28 - 微信支付商户经营工具-「企业付款到零钱」产品介绍及开通使用过程中的问题说明
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 - wx.authorize授权弹框总是比隐私弹框先弹起来,怎么办?
隐私组件内部已经使用了 lifetimes: { attached: function () { const closePopUp = () => { this.disPopUp() } privacyHandler = resolve => { privacyResolves.add(resolve) this.popUp() // 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗 closeOtherPagePopUp(closePopUp) } closeOtherPagePopUpHooks.add(closePopUp) this.closePopUp = closePopUp }, detached: function () { closeOtherPagePopUpHooks.delete(this.closePopUp) } }, wx.authorize 永远比隐私弹框先弹出来,有遇到问题的吗?
2023-08-24 - 文娱-其他视频和文娱-视频广场指引说明
代码审核环节,将会对小程序运营的内容与所选类目是否相符进行核实,当小程序涉及视频播放、观看等服务,需要补充“文娱-其他视频”或“文娱-视频广场”类目或通过接入视频类目插件合规提审,否则代码审核环节将面临因类目不符被驳回情形。 一、视频类服务,你需要了解: [图片] 二、「其他视频」与「视频广场」案例解析: 1、【文娱-视频广场】:以视频为唯一载体展示的,集合多种类型视频内容 整改建议(2选1): (1)补充选择:【文娱-视频广场】类目 注意事项:该类目补充后首次提审需进入属地主管部门二次审核流程(报备详情),时长为7天,开发者需提前规划好提审时间 (2)移除视频播放、观看功能服务,包括但不限于移除前端、后台代码中所存在视频内容、分类标签等 【具体案例】 [图片] 2、【文娱-其他视频】:除视频外还有其他类型的信息展示形式,如下面案例,除“视频”外还有其他的资讯内容展示 整改建议(2选1): (1)补充【文娱-其他视频】类目或通过接入【文娱-其他视频】插件(插件详情);影视剧类型视频播放暂不支持使用插件视频, 附:插件添加流程:登录MP平台「设置-第三方设置-插件-添加插件-搜索视频插件」 注意事项:文娱-其他视频补充后首次提审需进入属地主管部门二次审核流程(报备详情),时长为7天,开发者需提前规划好提审时间 (2)移除视频功能服务内容,包括但不限于前端展示、后台代码移除视频内容、分类标签等 【具体案例】 [图片] 附:类目所需资质 [图片] 这是一份动态更新的文档,辅助开发者提前了解视频业务形态所需申请的类目,避免开发者因类目不符审核失败或因不了解涉上述类目需二次审核流程时长导致无法按期发布上线,开发者如有其他疑问,可以通过目前开放的咨询渠道反馈: 1、微信开放社区-交流专区-小程序发帖咨询-提出问题-运营相关问题 2、驳回站内信通知-客服咨询入口(MP代码审核客服入口正处于灰度开放中,若未获得灰度测试入口,开发者可前往社区发帖咨询) 我们会根据新出现的问题、相关法律法规更新或产品运营的需要及时对其内容进行修改并更新,制定新的规则,保证微信用户的体验。建议开发者反复查看以便获得最新信息,定期了解更新情况。
2022-05-18 - setData 如何强制刷新 image 的url?
后台服务端对每一个客服头像的保存都是唯一一个名字,如果用户修改头像,同一个客户后端返回的是一样的Url,这个时候怎样强制刷新界面的image显示新的图片?
2022-08-13 - getUserRiskRank 请求参数维护不正确,没法在服务端调用
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/safety-control-capability/riskControl.getUserRiskRank.html [图片][图片] 请求示例的参数 和 请求参数里面维护的字段不一致 ,而且不管使用哪一个都会提示47001 数据格式不正确
2022-05-17 - 安全风控接口机刷用户openid返回61010如何解决?
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/safety-control-capability/riskControl.getUserRiskRank.html 我们小程序投票后台记录的用户信息 很明显是机刷行为,这些机刷用户风控接口都返回61010(其他正常投票用户返回等级是没有问题的,返回的都是0或者1),目前自行测试是微信电脑端直接打开小程序投票会返回61010,如果用手机微信打开小程序后,在用电脑端微信投票会正常返回安全等级0; 接口采用实时调用,并且用户授权后才可以投票,小程序已经开通安全风控接口权限,请问哪里可能出问题? [图片] 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-1f790c0f-68c91d29', ) 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-24e51b45-30b3c8ea', ) 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-116a6c79-67c48d5c', ) 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-59f8ce26-5d15f1ab', ) 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-0dc1fe8d-2bd87ff5', ) 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-73c2a7f0-26bc32cd', ) 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-10096150-67afdb65', ) 【2023-01-13 00:57:46】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-0e338a0b-3fc1a3d5', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0a-7fc83980-6a94c032', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-6f34ad56-1aabe443', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-001fd919-65838f4f', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-216be5ad-4a4d759c', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-144dd6ea-171d810c', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-79b55193-7ad0a0c2', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-34138537-04c27e61', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-51409701-2958429a', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-6c7476ac-52ad97dd', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-701c0b41-046d21a4', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-058e043c-29a9c996', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-3add8f07-151f5a2e', ) 【2023-01-13 00:57:47】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-10f15ad3-4969a029', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-5cf9a9cc-24962ea7', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-3a065cb7-52e3d904', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-2f38ea0a-20978423', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0b-71e2ff05-64b3eec9', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-65b4af7e-5f4ea67c', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-27a5a650-597af626', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-4a93a012-04464b7b', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-7e73d89a-36bba02d', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-4be913b9-07e0bc36', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-3f034b55-15bf47ae', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-3e3fd7a2-7e2a1cd7', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-1006a8ef-25e742a8', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-19529f3a-08fb101b', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-2e422ace-7846aaba', ) 【2023-01-13 00:57:48】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-5d3831b5-66e9b7fe', ) 【2023-01-13 00:57:49】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-099f95e1-464cf4ff', ) 【2023-01-13 00:57:49】: array ( 'errcode' => 61010, 'errmsg' => 'code is expired rid: 63c03c0c-7bb27d9f-4c3920df', )
2023-01-13 - 现金红包与企业付款到零钱有什么区别?
功能概述 现金红包现金红包是一款定向资金发放的营销工具,支持接口发放、上传openid文件发放、配置营销规则“满额送”发放三种方式,用户在客户端领取到红包之后,所得金额进入微信钱包,可用于转账、支付或提取到银行卡。商户可以使用现金红包应用于商品促销、抽奖、企业内部福利、渠道分润等场景。 企业付款到零钱企业付款提供由商户直接付钱至用户微信零钱的能力,支持平台操作及接口调用两种方式。具有免费、快速到账、灵活、安全等优点。商户可以使用企业付款应用于费用报销、员工福利、用户奖励 金额设定规则[图片] [图片] 用户微信端触达区别现金红包触达形式现金红包以“服务通知”中的“模板消息”触达用户,用户需要点击领取,红包才会到达用户的微信零钱包,若24小时内未领取,红包将原路退回,活动名称、商家名称、祝福语字段可自定义修改,模板消息中的logo和名称(品牌推广助手)默认显示商户号关联的公众号昵称和头像。 [图片] [图片] 企业付款到零钱触达形式企业付款到零钱以“微信支付”通知触达用户,资金直接发放到用户的微信零钱包,付款备注可自定义,付款商家默认显示商户号对应的主体名称。 [图片] 欢迎评论区互动哦~ 若对您有帮助,可以给一个赞和关注鼓励么。
2022-11-05 - 【官方教程】利用SourceMap解析JS Error报错信息
一、背景 由于小程序源码是经过编译、打包等工程化转换后运行在微信环境中的,小程序云测服务 在跑测中检测到的JS Error报错信息时,只能给出实际运行时的代码信息,而非小程序开发代码,开发者排查和定位错误原因非常不方便。 以下图为例:堆栈第①行和第②行,出现[代码]app-services.js:行号,列号[代码]这种信息,表示这两行堆栈起源于业务代码错误,但仅根据这两行运行时错误信息无法定位到源码位置的,这时需要用 SourceMap文件 进行反解,才能定位到源码信息。 [图片] 二、获取小程序SourceMap文件 在获取SourceMap文件之前,需要明确以下注意事项: 云测服务提测的开发中版本出现的JSError无法解析。开发中版本实际上使用miniprogram-ci预览能力生成的版本,此时无法获取SourceMap文件,故无法解析代码堆栈若小程序源码没有任何变化,重新编译(上传)小程序后生成的SourceMap文件不会变化。所以在小程序设置体验版,或发布上线的操作,也不会改变SourceMap小程序SourceMap可以从以下途径获取 1、线上版本小程序,可以直接从微信公众平台后台下载SourMap文件 前往 We分析 登录后,进入左侧菜单栏[代码]性能分析/ JS分析[代码] ,点击下载线上SourceMap文件。 [图片] 2、在开发者工具上传代码后下载 在开发者工具上传代码后,会实时生成SourceMap文件,此时点击下载SourceMap文件并保存即可 [图片] 3、使用miniprogram-ci获取 miniprogram-ci工具可以获取最近上传版本的Sourcemap文件,详情可见接口文档:拉取最近上传版本的SourceMap 三、上传SourceMap文件到云测服务 云测服务支持手动上传SourceMap文件和接口自动上传Sourcemap文件 1、手动上传SourceMap文件 用户在提交测试前,先在云测平台[代码]项目管理/SourceMap管理[代码]页面,上传新的SourceMap文件以及对已上传的SourceMap文件进行管理操作。 上传后,后台会从SourceMap文件中提取出小程序版本、代码提交时间、代码提交备注等信息。提交测试后会根据提交小程序的MD5自动匹配对应的SourceMap进行反解。 [图片] 如果测试前未上传,而测试报告中又发现了JS Error,也可以在测试报告页面[代码]JS Error详情[代码]标签中进行上传或选择已有的SourceMap文件实时解析JS Error报错信息。 [图片] 2、使用接口上传SourceMap文件 用户也可以利用第三方接口,在获取到SourceMap后自动上传,具体可参考 第三方接口文档 四、结果展示 以开篇的JS Error为例,在上传对应小程序版本的SourceMap等待页面自动刷新后,JS Error反解后信息如下。可以看到反解后,可以直接定位到用户代码的行数,方便用户定位问题 [图片] 五、最佳实践 【最佳实践】这里我们推荐用户版本更新后,可以用使用 miniprogram-ci上传代码 ,并 获取到sourcemap信息 后,直接调用 第三方API 上传到云测服务,并提交云测任务,获取到结果后,反馈给开发同学。这样可以方便的和现有小程序打包发布的Devops流程相结合。 [图片]
2022-12-07 - 商家转账到零钱
商家转账到零钱 ~~~ 01、商家转账到零钱介绍商家转账到零钱是【企业付款到零钱】的升级版,目前已不支持【企业付款到零钱】产品的申请,但是已经申请的不影响使用,但是本次升级力度很大,所以升级改造不是一件简单的事情。这一点一定要注意。 02、商家转账到零钱如何开通https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter4_3_1.shtml [图片] 具体需要以下二个条件 1、商户号已入驻90日且截止今日回推30天商户号保持连续不间的交易。 2、登录微信支付商户平台-产品中心,开通商家转账到零钱。 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter4_3_2.shtml [图片] 03、商家转账到零钱开发对接需要准备什么1)mchid,商户号id 2)在小程序内,绑定APPID及mchid 3)api证书 4)api v3秘钥 5)商户API证书」的「证书序列号」 每一件事都是必要的,不能少 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter4_3_2.shtml [图片] 04、微信支付平台证书微信支付平台证书的下载是整个对接过程中最难的,所以拿出来单独讲下,其他技术细节有很多开源的项目开源参考,但是这个证书的生成我认为是比较卡人的 我在第一次对接的时候,在社区支付大佬(Memory姐)手把手教授的情况下,没有搞成功,M姐直接帮我下载好了,第二次对接才完整走通了 ~[图片] 商家转账到零钱 ~[图片] 05、参考资料1)官方文档 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter4_3_1.shtml [图片] https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml [图片] 2)开源项目 https://github.com/wechatpay-apiv3/wechatpay-php/blob/main/bin/README.md [图片] 3)贵人相助 06、总结商家转账到零钱功能的完成对于我的答题活动小程序而言无疑是重要的,让整个产品做到了答题-抽奖-发钱闭环,从而使整个小程序更完善了 在这里多谢Memory姐的帮助 额外唠叨两句,小程序红包这个产品是我刚开始首选的,后面各种限制才转到商家转账到零钱 相关截图 [图片] ~ [图片] ~
2022-07-11 - 商家转帐到零钱问题
无法将 JSON 输入源“/body/total_amount”映射到目标字段“转账总金额”中,此字段需要一个合法的 64 位有符号整数 [msg] => Array ( [code] => PARAM_ERROR [detail] => Array ( [location] => body [value] => 970 ) [message] => 无法将 JSON 输入源“/body/total_amount”映射到目标字段“转账总金额”中,此字段需要一个合法的 64 位有符号整数 ) 其它金额目前没有发现问题,就这个970提示这个错误,这个要怎么解决?
2022-10-10 - 关于微信原“企业付款到零钱”接口升级为“商家转账到零钱”的避坑指南,所有坑都在这里了
5月18日,微信发布了一则简短的通知,全文如下: [图片] 媒体界有句话叫:字越少,事越大。 这次升级,也造成了不小的影响。 主要影响简单来说就是:新开通的商户号,不再支持开通【企业付款到零钱】功能。显然,微信官方是想通过【商家转账到零钱】取代原来的【企业付款到零钱】。然而事实是,这两个完全是不同的产品,拥有不同的设计思路和理念,不能算是升级,因此并不能完全代替原有功能。 不仅不能代替原有功能,而且在我看来这次“升级”,完全就是在降级,开倒车。为什么这么说呢? 1、必须开通运营账户。【商家转账到零钱】强制从运营账户出资,所以你必须保证运营账户的余额充足。运营账户的资金从哪里来呢?需要人工充值进去,注意是人工,可以通过银行卡充值,也可以从基本账户转账到运营账户,但必须人工。额,这种设计就很奇葩了,人工智能大行其道的时代,人们在不断的追求自动化的时候,微信居然反其道而行之,变自动为手动,你必须安排专人每天转钱,还不能请假。我们作为用户真的需要这个所谓的运营账户吗?不,是微信需要。 2、需要人工确认支付。这个设计已经不能用奇葩来形容了。【商家转账到零钱】功能,其实只是生成一个订单,支付的时候还需要管理员输入密码+短信验证码才能最终支付,虽然可以设置免密支付的金额,低于这个金额可以不用管理员人工参与,但是这个免密金额最大只支持100元,聊胜于无。好了,自动再次变为手动,为什么?官方说法是:为了确保资金安全。为了安全,所以就加入人工参与,恩,这个脑回路真的清奇。回想一下,我们接入API的初衷是什么?不就是为了实现自动化,解放人力,回家陪老婆孩子吗?好了,现在需要24小时无时无刻地盯着手机,一次又一次的支付密码,无论你在洗澡,在吃饭,在看书,还是在约会。。。照这样下去也就没有什么女朋友了,不用约会了。 3、产品定位混乱。如果是升级就在原有基础上升级,让用户自主选择是否升级。如果是新产品,就完全隔离,互不影响。然而,现在的这个产品定位混乱,看起来像是新加的产品,但是其实只是原功能换了皮肤。很明显的一点是,对于【商家转账到零钱】设置的支付限额,与【企业付款到零钱】的限额根本就是同一个东西,两个会互相影响,这个真是个大大的坑。你本来想设置一个,结果意外的修改了另外一个。这么重要的提示,微信居然只用了一行浅色的小字显示,不仔细看根本发现不了。很容易就踩了大坑。而且更过分的是,如果你之前的额度比较大,开通此功能会重置你的限额,如果想恢复额度,不好意思,需要重新申请提额。有点请君入瓮的意思。[图片] 像这种关键提示,小字显示的问题,还不止这一处。比如,开通运营账户时的“一经开通无法关闭” : [图片] 产品经理这么设计,似乎是很怕你发现这一点,所以放在了最不起眼的地方。你如果去找他理论,说这么重要的提示你为什么不早说,他就拿出这个截图告诉你:我有提示,是你没有看到,这不怪我吧! 4、增加了接入难度。 文档、SDK、demo 聊胜于无。 微信支付V3接口是我见过的最烂的接口文档之一,足以让一个十年经验的老手,卡到怀疑自己这十年是否选错了职业。技术不过关的,还是不要轻易尝试了。 理想的文档应该是有介绍、有接入流程说明、有详细的参数说明、有详细的错误说明、有完善的闭环的SDK、有一看就懂的demo,调用者不需要关心你采用的什么设计思想、什么加密算法,只需要按照demo,参照文档改成自己的参数,运行一下,成功了。看看V3文档,看了一遍又一遍,还是无从下手。根本就是半成品,SDK缺失关键代码、demo只是个片段,跑不通,毫无参考价值。 整个接入过程就是,百度找示例,修改,调试,报错,再百度,再调试,报错,再百度。。。 5、增加了流程的复杂度。 原本一个接口就可以搞定的事情,现在需要两个接口:发起商家转账接口、转账查询接口,增加了流程的复杂度。而且,原先只需要记录一个商户端订单号就可以了,现在需要记录两个单号:批次单号、明细单号,因为后面的查询接口需要用到。 6、没有提前通知。这么巨大的改动,居然没有提前告知。只在升级的时候,发一个不痛不痒的公告。 总之,这个产品让人看不懂。新版本也并没有看出有任何优势。整体来说设计是失败的,不仅无法取代原有功能,而且更加的别扭,不够灵活,虽然【企业付款到零钱】功能也有问题,比如付款信息太少,不容易对账,但是很明显这个升级非但没有解决这些旧问题,反而制造了更多的新问题。希望微信的产品经理在设计产品的时候能够贴近用户,了解用户的真实需求,而不是在办公室里创造需求。否则,只能让广大用户失望,而转投到其他竞品的阵营里。
2022-06-29 - 签名计算有不同版本?
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml 中阐述参与签名的字段为4个 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml 中阐述参与签名的字段为5个 以哪个为准?
2022-12-18 - 腾讯的一小步,开发者的一大步:说一说Cloud.CDN
最近在文档中偶然发现了这么一段,测试完之后,居然情不自禁的想要夸一下腾讯: [图片] 故事是这样的: 很久之前,我们小程序上有图片上传前做安全检测的需求,选择的是云函数的实现方案,图片压缩后直接以Buffer的形式传递。刚上线的几个月,一切运行正常,可是突然某一天(几个月前)开始,陆续有用户反馈上传会失败,当然也不是100%失败,个别幸运儿还是可以成功的。排查发现,是小程序在调用云函数的时候报错了(为了写文章,异常都是最新截图的), 第一种形式的异常(安卓):errCode: -404012 polling exceed max timeout retry. 说“ 超过了超时重试的最大次数”,如果真的以为是暂时的网络不好或者云函数那边临时出了什么状况,等待你的将是现实的沉痛一击,因为它从此再也没有好过: [图片] 第二种形式的异常(iOS):errCode: -1 | errMsg: cloud.callFunction:fail Error: data exceed max size. 说“数据超限了”,意思是嫌弃传的图片太大了,恐怕这也是导致安卓上报超时的原因: [图片] 以前都是好的,突然从某一天开始就不行了,后来,终于在等待中明白,应该是腾讯收紧了cloud.callFunction的数据大小限制,再也没有放宽... 由于这个功能比较次要,用得也少,就一直没改,直到最近才腾出手来。本来已经决定要采取图片先临时传到COS,再把COS URL传给云函数的方案了,但是在做之前想搞明白callFunction的数据大小限制上限到底是多少,居然几十K的图片都传不了,以前的文档是啥也没写的,但出于习惯,我又翻了下文档,于是看到了那段让人惊喜的文字,它的意思是,我们准备要做的这个功能,腾讯用"wx.cloud.CDN"已经提供了!!! 跳到对应的文档链接,说是从2.12.0也就是最新版的基础库开始支持,虽然2.12.0现在占比只有60%多,但会一天天增加。 [图片]https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/utils/Cloud.CDN.html 那就来看看它的效果如何吧,调用超级简单: [图片] 调用结果如下,图片会先传到CDN,然后再拿返回的临时图片URL传给cloud.callFunction: [图片] [图片] 完美!callFunction的传输上限我已经不再关心了~
2020-07-19 - 如何在云开发中优雅地管控 CDN 流量?
在微信开放社区中,有不少使用云开发的小伙伴反馈遇到了“CDN流量消耗如流水”的情况。 有一觉醒来超额的: [图片] 有被高质量图片的加载“吓”到不敢用的: [图片] 遇到以上情况不要慌,很可能是你的使用姿势不对! 那么问题来了,如何在云开发中优雅地管控 CDN 流量消耗呢?本文就来和你详细聊聊! 按量付费和管道付费 为了便于理解,先来看看云开发的流量计费模式。 简单来说,按量计费就是:你有多少个量跑出去了,就给你算多少量;但是它并不限制你同一时间跑出去的量,也就是流量峰值不设限。 如果你在同一时间需要跑出去100M的量,那么峰值就给你开到100M,在同一时间跑完,最后算费用是100M的钱。 而管道付费则是限制给你开多宽的“道路”,按照这个“道路”的宽窄收费,比如你选择1M的网络道路,那就按照1M的价格来收钱;在使用时,你的量只能达到1M的速度,再也高不了了,这个最大速度就被称做带宽。 但是即使低峰没多少量时,你还必须为这个“道路”付钱。还是上述例子,你同一时间需要跑出去100M,但是速度只能给你开到1M,那么这些量在100秒后才可以走完,最后算费用是1M的道路租金*租用时长。 而在现实项目中,总会有业务的高峰和低峰,流量几乎不可能始终保持在一个恒定的速率,这就会造成管道计费有速度的天花板、但闲置时仍在计费的资源浪费问题。 云开发作为 Serverless 云原生一体化后端服务,提供的流量是按量计费的,不限制同一时刻的流量速度,因此,如果开发者使用不合理就会导致流速过快,进而造成“一眨眼的功夫,流量就超额了”的情况。 了解了按量付费和管道付费的区别后,咱们进入正题,说说 CDN。 什么是CDN流量? 我们在使用云开发时,几乎在各处都会看到CDN这个词。 CDN又称内容分发网络,通俗来讲就是将你主存储(源站)中的文件,复制给各地的存储点(CDN节点),当有用户访问这个资源时,直接从就近的存储点(CDN节点)获取即可。 云开发的云存储和静态网站托管天然支持 CDN 加速,所以你的用户通过客户端下载文件跑的流量都是 CDN 流量。 以上 CDN 流量只适用于各种渠道走加速公网下载文件的情况,比如浏览器加载网站,客户端下载资源,外部系统请求文件,通过临时地址的各种访问打开文件都在消耗 CDN 流量。 但是如果是内网访问文件,则不走 CDN 流量消耗,比如在云函数中通过 fileID 访问文件等。 什么是CDN回源流量? 当我们的存储中有文件更新时,存储在 CDN 节点的旧文件又该如何处理呢?在这里引入一个知识点——缓存时间。 这里的缓存时间其实就是文件副本在各地存储点(CDN节点)的有效时间,比如默认是两小时,那么每次文件副本在各地存储点的有效时间就是两小时,超过这个时间之后再收到请求时,存储点(CDN节点)就会丢弃过期的旧文件,向主存储(源站)请求最新的文件,而这一请求所产生的流量就称为 CDN 回源流量。 缓存时间既不能太长也不可太短,如果 CDN 缓存间隔时间过短,那么 CDN 节点上的数据会经常失效,导致频繁回源,增加了源站的负载,进而影响了整体的传输效率;如果缓存间隔时间过长,会带来数据更新不及时等严重的业务问题。 云开发的云存储就提供了非常细微颗粒度的缓存时间设置,你可以针对一个文件、一个路径甚至是文件后缀来进行分别设置。 [图片] 另外,多个缓存规则设置中还有优先级策略,调配变得更加灵活。 云存储是以从后到前的配置模式来做策略计算的,比如一个云存储的域名做了如下缓存配置: [图片] 现在请求此路径下资源/test/abc.jpg,其从后到前匹配方式如下: 匹配第一条所有文件,命中,此时缓存时间为 2 分钟。 匹配第二条,未命中。 匹配第三条,命中,此时缓存时间为300秒。 匹配第四条,命中,此时缓存时间为400秒。 匹配第五条,命中,此时缓存时间为200秒。 如何合理管控CDN? 我们需要把握一个原则: 缩减大小,善用缓存。 缩减大小的意思就是,我们在开发应用时,所需要的多媒体文件,如果没有特殊要求(比如摄像馆的原图发送),需要尽可能的压缩。只有减轻了业务资源的大小,才能够根本的减少流量资源的消耗。 另外,用户在上传多媒体资源时,仍然可以使用平台或框架能力对资源进行压缩后再上传,保证资源都是经过优化后进入存储,这样在请求下载时就会减轻很多负担。 善用缓存意思就是同一个用户、同一份资源尽可能不要请求一次以上,要合理使用客户端的本地缓存能力,将固定资源全部缓存。当用户再此进入时,直接使用缓存的资源。 例如,微信小程序官方就提供了图片缓存配置,开启后所有图片均进行缓存,下次读取相同资源时,直接从缓存中读取。 文档链接:http://mrw.so/6wT3TR 再送给大家一句网络金句:缓存用的好,PV 的效果用的只是 UV 的量。 而在具体使用时,大家还是需要根据自己的业务情况来合理把握。比如你的资源变更非常频繁,就不太适合缓存优化;而你的资源不能压缩,要保持原大小,则就不适合压缩优化。 结语 以上攻略送给各位 hold 不住 CDN 流量的小伙伴们,如果大家觉得 CDN 消耗如流水,用户活跃却没有多少起色,可能就需要好好检查一下自己哪里浪费了。勤俭节约可是中华民族的传统美德,云开发虽好,也需要节约使用哦~ 小程序·云开发「错误监控」功能有奖调研 诚邀各位云开发者参与小程序·云开发「错误监控」功能有奖调研,参与即送小礼品。 [图片] 期待您的宝贵建议,快扫描下图中的二维码参与吧! 产品介绍 云开发(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 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2021-01-21 - 数据库原子操作和事务讲解
使用更新指令(如 inc、mul、addToSet)可以对云数据库的一条记录和记录内的子文档(结合反范式化设计)进行原子操作,但是如果要跨多个记录或跨多个集合的原子操作时,就需要使用云数据库的事务能力。 12.6.1 更新指令的原子操作关系型数据库是很难做到通过一个语句对数据强制一致性的需求来表示的,只能依赖事务。但是云开发数据库由于可以反范式化设计内嵌子文档,以及更新指定可以对单个记录或同一个记录内的子文档进行原子操作,所以通常情况下,云开发数据库不必使用事务。 比如调整某个订单项目的数量之后,应该同时更新该订单的总费用,我们可以设计采用如下方式设计该集合,比如订单的集合为 order: { "_id": "2020030922100983", "userID": "124785", "total":117, "orders": [{ "item":"苹果", "price":15, "number":3 },{ "item":"火龙果", "price":18, "number":4 }] } 客户在下单的时候经常会调整订单内某个商品比如苹果的购买数量,而下单的总价又必须同步更新,不能购买数量减少了,但是总价不变,这两个操作必须同时进行,如果是使用关系型数据库,则需要先通过两次查询,更新完数据之后,再存储进数据库,这个很容易出现有的成功,有的没有成功的情况。但是云开发的数据库则可以借助于更新指令做到一条更新来实现两个数据同时成功或失败: db.collection("order") .doc("2020030922100983") .update({ data: { "orders.0.number": _.inc(1), total: _.inc(15), }, }); 这个操作只是在单个记录里进行,那要实现跨记录要进行原子操作呢?更新指令其实是可以做到事务仿真的,但是比较麻烦,这时就建议用事务了。 12.6.2 事务与 ACID事务就是一段数据库语句的批处理,但是这个批处理是一个 atom(原子),多个增删改的操作是绑定在一起的,不可分割,要么都执行,要么回滚(rollback)都不执行。比如银行转账,需要做到一个账户的钱汇出去了,那另外一个账户就一定会收到钱,不能钱汇出去了,但是钱没有到另外一个的账上;也就是要执行转账这个事务,会对 A 用户的账户数据和 B 用户的账户数据做增删改的处理,这两个处理必须一起成功一起失败。 1、ACID一般来说,事务是必须满足 4 个条件(ACID): Atomicity(原子性)、Consistency(稳定性)、Isolation(隔离性)、Durability(可靠性): 原子性:整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中一部分操作,一致性:事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行前后,数据库都必须处于一致性状态。换句话说,事务的执行结果必须是使数据库从一个一致性状态转变到另一个一致性状态。比如在执行事务前,A 用户账户有 50 元,B 用户账户有 150 元;执行 B 转给 A 50 元事务后,两个用户账户总和还是 200 元。隔离性:事务的隔离性是指在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间事务之间,互不干扰。比如在线银行,同时转账的人虽然很多,但是不会出现影响 A 与 B 之间的转账;可靠性:即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态,已提交事务的更新不会丢失。 2、云函数事务注意事项01不支持批量操作,只支持单记录操作 在事务中不支持批量操作(where 语句),只支持单记录操作(collection.doc, collection.add),这可以避免大量锁冲突、保证运行效率,并且大多数情况下,单记录操作足够满足需求,因为在事务中是可以对多个单个记录进行操作的,也就是可以比如说在一个事务中同时对集合 A 的记录 x 和 y 两个记录操作、又对集合 B 的记录 z 操作。 02云数据库采用的是快照隔离 对于两个并发执行的事务来说,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。 脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据;不可重复读:在一个事务内两次读到的数据是不一样的,受到另一个事务修改后提交的影响,因此称为是不可重复读幻读:第一个事务对表进行读取,当第二个事务对表进行增加或删除操作事务提交后,第一个事务再次读取,会出现增加或减少行数的情况云开发的数据库系统的事务过程采用的是快照隔离(Snapshot isolation),可以避免并发操作带来数据不一致的问题。 事务期间,读操作返回的是对象的快照,而非实际数据事务期间,写操作会:1. 改变快照,保证接下来的读的一致性;2. 给对象加上事务锁事务锁:如果对象上存在事务锁,那么:1. 其它事务的写入会直接失败;2. 普通的更新操作会被阻塞,直到事务锁释放或者超时事务提交后,操作完毕的快照会被原子性地写入数据库中 12.6.3 事务操作的两套 API云开发数据库的事务提供两种操作风格的接口,一个是简易的、带有冲突自动重试的 runTransaction 接口,一个是流程自定义控制的 startTransaction 接口。通过 runTransaction 回调中获得的参数 transaction 或通过 startTransaction 获得的返回值 transaction,我们将其类比为 db 对象,只是在其上进行的操作将在事务内的快照完成,保证原子性。transaction 上提供的接口树形图一览: transaction |-- collection 获取集合引用 | |-- doc 获取记录引用 | | |-- get 获取记录内容 | | |-- update 更新记录内容 | | |-- set 替换记录内容 | | |-- remove 删除记录 | |-- add 新增记录 |-- rollback 终止事务并回滚 |-- commit 提交事务(仅在使用 startTransaction 时需调用) 1、通过 runTransaction 回调获得 transaction以下提供一个使用 runTransaction 接口的,两个账户之间进行转账的简易示例。事务执行函数由开发者传入,函数接收一个参数 transaction,其上提供 collection 方法和 rollback 方法。collection 方法用于取数据库集合记录引用进行操作,rollback 方法用于在不想继续执行事务时终止并回滚事务。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const _ = db.command; exports.main = async (event) => { try { const result = await db.runTransaction(async (transaction) => { const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); console.log(`transaction succeeded`, result); return { aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(-100); } }); return { success: true, aaaAccount: result.aaaAccount, }; } catch (e) { console.error(`事务报错`, e); return { success: false, error: e, }; } }; 事务执行函数必须为 async 异步函数或返回 Promise 的函数,当事务执行函数返回时,SDK 会认为用户逻辑已完成,自动提交(commit)事务,因此务必确保用户事务逻辑完成后才在 async 异步函数中返回或 resolve Promise。 2、通过 startTransaction 获得 transactiondb.startTransaction(),开启一个新的事务,之后即可进行 CRUD 操作;db.startTransaction().transaction.commit(),提交事务保存数据,在提交之前事务中的变更的数据对外是不可见的;db.startTransaction().rollback(),事务终止并回滚事务,例如,一部分数据更新失败,对已修改过的数据也进行回滚。const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const db = cloud.database({ throwOnNotFound: false, }); const _ = db.command; exports.main = async (event) => { try { const transaction = await db.startTransaction(); const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); await transaction.commit(); return { success: true, aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(); return { success: false, error: `rollback`, rollbackCode: -100, }; } } catch (e) { console.error(`事务报错`, e); } }; 也就是说对于多用户同时操作(主要是写)数据库的并发处理问题,我们不仅可以使用原子更新,还可以使用事务。其中原子更新主要用户操作单个记录内的字段或单个记录里内嵌的数组对象里的字段,而事务则主要是用于跨记录和跨集合的处理。
2021-09-10 - cloudPay.unifiedOrder出现签名错误是怎么回事?
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/CloudPay.unifiedOrder.html [图片] 云函数编写截图 [图片] 调用云函数的返回 [图片] catch到的error截图 之前一直都可以直接调用,从昨天开始发现支付调用失败,不知道原因是什么。既然用的是cloudPay的云函数,就是为了不用复杂的方法拼接签名,所以这里错误提示是签名的问题让人费解,希望解答
2020-07-22 - 云开发 数据库中地理:为何云函数查询和客户端查询数据结构不一样?
在云开发数据中添加地理位置的geopoint格式数据,在客户端获取到数据格式如下: {longitude: 1,latitude: 2} 而在云函数中获取的数据格式如下: {coordinates: [1, 2],type: "Point"}
2021-01-23 - wx.getBLEMTU为什么获取的mtu只有20多,原生APP可以获取到240
wx.getBLEMTU为什么获取的mtu只有20多,原生APP可以获取到240,是有什么限制吗,而且文档说MTU值会发生变化,这样岂不是分包的时候要随时调整包大小
2021-10-25 - 小程序管理蓝牙设备开发指北
小程序管理蓝牙设备开发记录 前段时间接到一个管理蓝牙设备的需求,要求能搜索并连接指定设备,并读取设备的信息,然后发送指令给设备,让设备运行起来。 允许连接多台同类型的设备,并对设备做分开管理 期间,遇到不少的坑,在此记录下来,希望能对大家有所帮助,有欠缺的地方,还请大家帮忙指正一下,谢谢 话不多说,接下来就进入开发: 1. 初始化 考虑到需要在不同页面都要使用微信的蓝牙接口,并且还需要一些数据的互通,所以我建了一个单例,用于处理微信接口,和设备状态、信息管理 [代码] constructor(config = {}) { if (!manager.instance) { manager.instance = this; this.connectPool = []; this.cachePool = []; this.discoveryPool = []; this.timeout = 5000; this._timer = null; this.adpterStatus = {open: false} } Object.assign(manager.instance, config); if (!this.adpterStatus.open) { this.initBluetoothAdapter() } return manager.instance; } [代码] [代码]connectPool[代码] 设备连接池,用于存储正在连接的设备 <br> [代码]cachePool[代码] 设备缓存池,用于存储连接过的设备 <br> [代码]discoveryPool[代码] 设备发现池,用于存储扫描到的设备 <br> [代码]timeout[代码] 超时时间 <br> [代码]adpterStatus[代码] 蓝牙适配器状态 [代码]{open: '是否打开', available: '是否可用', discovering: '是否正在搜索设备'}[代码] 若适配器打开状态为[代码]false[代码]那么初始化适配器[代码]initBluetoothAdapter[代码]: [代码] /** * 为了方便处理微信的回调,建了一个公共的callBack方法 */ commonCall(success = ()=>{}, fail = ()=>{}, complete = ()=>{}) { return {success, fail, complete} } initBluetoothAdapter() { const that = this; /** * 监听适配器状态,开启监听之前先关闭监听,防止状态重复 * offBluetoothAdapterStateChange 关闭适配器状态监听 * onBluetoothAdapterStateChange 开启适配器状态监听 */ wx.offBluetoothAdapterStateChange(); wx.onBluetoothAdapterStateChange(res => { // 同步适配器状态,TODO做manager工具内的监听,可以参考下一步搜索状态的监听 3. 发现设备 中的 discoveryPoolDidUpdate 方法 Object.assign(that.adpterStatus, res) // TODO 若适配器重新可获取时,重新开启适配器 // 若适配器open = true,开始 -> 2. 设置监听 that.setListener() }); /** * 开启小程序蓝牙适配器,开启之前先关闭,防止状态重复 * closeBluetoothAdapter 关闭蓝牙适配器 * openBluetoothAdapter 开启蓝牙适配器 */ wx.closeBluetoothAdapter(); wx.openBluetoothAdapter(that.commonCall(success => { // 蓝牙适配器初始化成功 })); } [代码] 2. 设置监听 适配器初始化完成后,设置监听,统一处理数据和状态: [代码]onBluetoothDeviceFound[代码] 蓝牙搜索监听 [代码]onBLEConnectionStateChange[代码] 蓝牙设备连接状态监听,并在连接成功的时候 [代码]onBLECharacteristicValueChange[代码] 蓝牙设备特征值变化监听,用户小程序和蓝牙的交互 [代码] setListener() { wx.offBluetoothDeviceFound() wx.onBluetoothDeviceFound(res => { // 设备搜索监听,更新设备,详情请移步 -> 3. 发现设备 }) wx.offBLEConnectionStateChange() wx.onBLEConnectionStateChange(res => { /** * res = { * errorCode: 0 成功 * errorMsg: 错误信息 * connected: 0 断开连接,1 连接成功 * deviceId:连接设备的deivceId * } * * 设备连接状态更新,若连接成功,则开始针对设备进行数据监听,详情请移步 -> 设备交互 */ }) wx.offBLECharacteristicValueChange() wx.onBLECharacteristicValueChange(res => { // 设备特征值发生变化,更新设备数据,详情请移步 -> 设备交互 }) } [代码] 3. 发现设备 [代码] /** * services: 可以通过设备是否具备特定的服务UUID来筛选自己想要的设备 * sCall: 扫描方法调用成功 * fCall:扫描方法调用失败 */ discoveryBluetoothDevices(services, sCall, fCall) { const that = this; // 扫描前清空discoveryPool that.discoveryPool = []; const discoveryCall = that.commonCall(sCall, fCall); discoveryCall.services = services; wx.stopBluetoothDevicesDiscovery(that.commonCall(__=>__, __=>__, () => { wx.startBluetoothDevicesDiscovery(discoveryCall) })) } [代码] 若设备有新设备,则‘设置监听’中的[代码]onBluetoothDeviceFound[代码]会进行新设备上报 调用[代码]updateDevice[代码]方法进行设备过滤和保存 [代码] updateDevice(device) { if (!device) return; if (!device.name) return; if (!device.advertisData) return; const that = this; // 判断设备是否是新设备 if (that.discoveryPool.map(v => v.deviceId).indexOf(device.deviceId) === -1) { // ab2str 见下方备注 device.advertisData = ab2str(device.advertisData) that.discoveryPool.push(device) // 给单例添加提供给外部监听状态的接口,当设备有更新的时候,触发接口回调 that.discoveryPoolDidUpdate && that.discoveryPoolDidUpdate instanceof Function && that.discoveryPoolDidUpdate(that.discoveryPool) // 私有回调 -> 4.连接设备 that._discoveryPoolDidUpdate && that._discoveryPoolDidUpdate instanceof Function && that._discoveryPoolDidUpdate(device) that._timer && clearTimeout(that._timer) } // 超时时间,超时后若无新设备,则关闭Discovery方法 that._timer = setTimeout(() => { wx.stopBluetoothDevicesDiscovery() that._timer && clearTimeout(that._timer) }, that.timeout) } [代码] [代码]备注:[代码]设备广播数据是[代码]ArrayBuffer[代码]的形式,所以通过[代码]ab2str[代码]的方法进行转换,方法见下: [代码] ab2str(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); } [代码] 4. 连接设备 小程序连接设备的接口[代码]wx.createBLEConnection()[代码]接受的值是[代码]deviceId[代码],作为设备的标识符。 但是这个设备Id在iOS和Android手机上,同一设备的值是不同的,所以如果我们要把deviceId保存给后台服务器,下次再拿到,不一定可以直接使用。 所以为了通用性,我们应该跟设备制造商统一一下,让设备广播出自己的特征码,亦或者直接通过[代码]服务UUID[代码]获取到设备的mac地址,用这些不变且唯一的字符串作为保存到后端的设备标识符。 如果以广播中的特征码为唯一标识符,搜索设备并向后台保存的过程中,无需跟设备进行连接操作,本文以这样的方式进行; 如果通过服务UUID获取到设备的mac,保存给后端,需要扫描到设备之后,连接设备,并通过获取设备mac地址的服务UUID,读取到设备的mac,保存到后台。 [代码] connectBluetoothDevice(indentify, sCall, fCall) { const that = this; const connectCall = that.commonCall(sCall, fCall); if (that.adpterStatus.discovering) { for (let i=0; i<that.discoveryPool.length; i++) { if (that.discoveryPool[i].advertisData == indentify) { connectCall.deviceId = that.discoveryPool[i].deviceId connectCall.timeout = that.timeout wx.createBLEConnection(connectCall) } } } else { that.discoveryPoolDidUpdate = null that.discoveryBluetoothDevices([], s => { // 扫描到设备之后,用广播数据进行比对,若一样,获取该设备的deviceId,并连接 that._discoveryPoolDidUpdate = res => { if (res.advertisData == indentify) { connectCall.deviceId = res.deviceId wx.createBLEConnection(connectCall) wx.stopBluetoothDevicesDiscovery() } else { // 检查是否已经停止扫描 console.log('_discoveryPoolDidUpdate', that.adpterStatus) fCall && fCall instanceof Function && fCall("device not found !") } } }) } } [代码] 5. 设备交互 设备交互有三种形式: [代码]read[代码] 程序读取设备的信息 [代码]write[代码] 向设备发指令 [代码]notify[代码] 订阅设备的上报 蓝牙设备出厂的时候,就设置了一些接口,并定义好访问它的服务ID和特征值ID以及访问方式,通过这些可以跟设备做到交互 用前端跟后端交互的方式理解,跟设备进行交互的时候,服务ID和特征值ID 就相当于我们访问接口的api接口,[代码]read[代码]相当于get接口,获取到数据,[代码]write[代码]相当于post接口,数据发送给后台,后台就对应数据逻辑做相应变更,[代码]notify[代码]相当于与服务器建立websocket连接,实时获取服务器发来的数据(单方向) 由于[代码]服务ID[代码]和[代码]特征值ID[代码]都是这样[代码]00002A23-0000-1000-8000-008BF9B054F3[代码]难以记住的串,所以我们建立一个[代码]服务适配器(services-adpter)[代码],它负责配置我们需要用到的服务,如下 [代码] export const serviceAdapter = [ { // 开始设备 serviceName: 'start', serviceUUID: '服务ID', characterUUID: '特征值ID', inFormatter: '入参格式化方法', outFormatter: '出参格式化方法', type: 'write' } ] [代码] 我们传给设备的数据需要转换[代码]二进制数据[代码]和[代码]异或[代码]操作,所以在这里进行配置入参格式化方法和出参格式化方法 接下来就是交互,在设备连接上之后,处理serviceAdapter中的type = read 和 type = notify的任务 处理serviceAdapter任务的顺序为 :处理read任务,对设备进行属性的初始化 -> 处理notify任务,对设备属性进行监听,并设置callBack -> write任务需要主动触发 [代码] // 处理read任务,对设备进行属性的初始化 const readServices = that.getServicesBy('read') readServices.forEach(rs => { console.log('will start read servce:', rs); const call = that.commonCall(success => { console.log('readBLECharacteristicValue success:', success) }) call.deviceId = device.deviceId call.serviceId = rs.serviceUUID call.characteristicId = rs.characterUUID wx.readBLECharacteristicValue(call) }) // 处理notify任务,对设备属性进行监听,并设置callBack const notifyServices = that.getServicesBy('notify') notifyServices.forEach(ns => { console.log('will start notify servce:', ns); const call = that.commonCall(success => { console.log('notifyBLECharacteristicValueChange success:', success) }) call.deviceId = device.deviceId call.serviceId = ns.serviceUUID call.characteristicId = ns.characterUUID wx.notifyBLECharacteristicValueChange(call) }) [代码] write方式,需要用户主动触发 [代码] beginService(indentify, serviceName, params, sCall, fCall) { const that = this for (let i=0; i<serviceAdapter.length; i++) { let adapter = serviceAdapter[i] if (adapter.serviceName == serviceName) { if (adapter.inFormatter && adapter.inFormatter instanceof Function) { const device = this.deviceBy(indentify, 'connect') const call = that.commonCall(sCall, fCall) call.deviceId = device.deviceId call.serviceId = adapter.serviceUUID call.characteristicId = adapter.characterUUID call.value = adapter.inFormatter(params) wx.writeBLECharacteristicValue(call) } return } } } [代码] indentify 设备的唯一标志符 <br> serviceName 需要访问的服务名称 <br> params 发送给设备的数据 <br> 发送数据给用户,并在监听中获取最新的设备信息和状态 自此我们就初步的完成了设备搜索到连接到交互的过程 6. TODOS 为了让工具更加完善,需要增加错误处理,异常抛出,错误重试等操作,这里就不在此赘述了
2021-07-30 - IOS端 当Promise与异步函数一起使用时 特定情况下永远不会settled
const app = getApp() function wait(delay) { return new Promise(resolve => setTimeout(resolve, delay)) } const requestAuth = async() => { return request(false) } async function request(auth = true) { await Promise.resolve() await wait(300); if (auth) { await requestAuth() } return true } Page({ onLoad() { request().then( () => { //在ios既不会执行到这里 也不会报错 wx.showModal({ title: '在ios上始终无法出现的modal', }) }, console.error ) }, })
2022-01-06 - 小程序里使用async和await变异步为同步,解决回调地狱问题
最近好多同学,学习完石头哥的云开发基础以后,自己实际项目中,总会遇到各种各样的异步问题。 一,异步问题 所谓异步:就是我们请求数据库的数据时,由于网速等各方面原因,数据返回的时间不确定,而我们要使用这些数据,就要等数据返回成功后才可以使用,否则就会报错。 1-1,问题描述 如下: [图片] 好多同学都会认为代码从上往下执行,会先执行请求成功,然后才会执行第11行的代码,商品个数也应该是2. 但是我们的第11行打印却是0.这是为什么呢。 这个错误的原因就是我们使用数据没有写在请求成功里面。正确数据请求返回是异步的,什么时候请求成功不知道,但是我们的第11行代码不会等我们数据请求成功才会执行,所以第11行的打印是0而不是2. 1-2,解决方案 要想解决上面的问题,把你使用数据的地方写到数据请求成功里。 [图片] 这样就能解决异步的问题,但是如果我们有很多地方要使用请求成功的数据,该怎么办呢,总不能把所有的代码都写在数据请求成功里吧。这个时候就要借助async和await来解决这个问题了。 二,使用async和await变异步为同步 所谓的同步,就是我们保持代码正常的从上往下执行。但是呢只要有数据请求,就会有异步问题。所以我们这里要想办法变异步为同步。这就要用到async和await了。 代码如下: [图片] 可以看出,我们不用把使用到数据的代码写到请求成功里就可以了,这样代码读起来是不是常规的从上往下执行的了。 await翻译过来就是等待的意思,其实这里的意思就是,我们等待数据请求完成后,把数据的返回结果赋值给res,然后等数据请求成功以后,就可以正常使用数据请求返回的结果啦。 注意事项 我们在小程序里使用async和await时,一定是成对的。 async放在函数名前面,await放在数据请求前面。 [图片] 并且也要勾选一下:增强编译 [图片] 现在最新版本的小程序开发者工具好像已经支持async和await方法了,好像不勾选增强编译也没事。但是安全起见,还是勾选下增强编译比较好。 三,回调地狱 比如我们有这么一个需求: 用户注册的时候,要先查询是否注册过,没有注册过,才可以新注册。而注册成功后,才可以查看商品列表。 3-1,问题描述 这里给大家分析下需求 [图片] 如果只看流程图,肯定会觉得很简单;但是里面的链路你要认清一个现实。 就是我们如果想最终把商品显示到页面上,必须依赖每个流程都要请求成功。现在是只有3个请求,如果有100个呢,一层套一层的,最后会把你绕晕。这就是回调地狱。 3-2,回调地狱代码 单纯的给你讲,你可能体会不到回调地狱的坏处。那么我用代码实现下我们上面的需求。 假设我们有 用户表:user 商品表:goods 比如我们要注册一个名为”小石头“的用户 第一步:先查询是否注册过 [图片] 可以看出返回的个数为0,代表没有注册过 第二步:注册用户 [图片] 可以看到我们已经可以注册成功了,但是这个时候代码已经嵌套了。 [图片] 第三步:查询商品 由于我们第二步,已经注册’小石头‘成功,所以我们这一步注册一个’大石头‘,注册成功后查询商品。 首先看下代码,这个时候已经嵌套3层了。代码已经变得有点乱了 [图片] 看下结果 [图片] 可以看出我们已经能够成功的查询到商品数据了。 这里只嵌套了三层,看起来还可以接受,如果再继续一层层的嵌套呢。后面代码会变得越来越乱,为了避免回调地狱,我们也可以使用async和await来改造代码。 四,async结合await解决回调地狱 首先看下改造后的代码 [图片] 可以看到代码简洁了很多,逻辑也就是正常的从上往下执行代码 为了更明显的比较。 [图片] 到这里我们就讲完了,是不是感觉使用async和await让你的代码简洁了很多。赶紧跟着石头哥的这篇文章去体验下吧。
2021-05-29 - 教你怎么监听小程序的返回键
更新:2020年7月28日08:51:11 基础库2.12.0起,可以调用wx.enableAlertBeforeUnload监听原生右上角返回、物理返回以及wx.navigateBack时弹框提示 AIP详情请看: https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.enableAlertBeforeUnload.html //======================================== 怎么监听小程序的返回键? 应该有很多人想要监听用户的这个动作吧,但是很遗憾,小程序不会给你这个API的,那是不是就没辙了? 幸好我们还可以自定义导航栏,这样一来我们就可以监听用户的这一动作了。 什么?这你已经知道啦? 那好咱们就不说自定义导航栏的返回监听了,说一下物理返回和左滑?右滑?(不管了,反正是滑)返回上一页怎么监听。 监听物理返回 首先说一下这个监听方法的缺点,虽说是监听,但是还是无法真正意义上的监听并拦截来阻止页面跳转,页面还是会返回上一页,而后重新载入刚刚的页面,如果这不是你想要的,那可以不用往下看了 其次说一下用到什么东西: wx.onAppRoute、wx.showModal 最后是一些主要代码: 重写wx.showModal,主要是加个confirmStay参数和使wx.showModal Promise化 [代码]const { showModal } = wx; Object.defineProperty(wx, 'showModal', { configurable: false, // 是否可以配置 enumerable: false, // 是否可迭代 writable: false, // 是否可重写 value(...param) { return new Promise(function (rs, rj) { let { success, fail, complete, confirmStay } = param[0] param[0].success = (res) => { res.navBack = (res.confirm && !confirmStay) || (res.cancel && confirmStay) wx.setStorageSync('showBackModal', !res.navBack) success && success(res) rs(res) } param[0].fail = (res) => { fail && fail(res) rj(res) } param[0].complete = (res) => { complete && complete(res) (res.confirm || res.cancel) ? rs(res) : rj(res) } return showModal.apply(this, param); // 原样移交函数参数和this }.bind(this)) } }); [代码] 使用wx.onAppRoute实现返回原来的页面 [代码]wx.onAppRoute(function (res) { var a = getApp(), ps = getCurrentPages(), t = ps[ps.length - 1], b = a && a.globalData && a.globalData.pageBeforeBacks || {}, c = a && a.globalData && a.globalData.lastPage || {} if (res.openType == 'navigateBack') { var showBackModal = wx.getStorageSync('showBackModal') if (c.route && showBackModal && typeof b[c.route] == 'function') { wx.navigateTo({ url: '/' + c.route + '?useCache=1', }) b[c.route]().then(res => { if (res.navBack){ a.globalData.pageBeforeBacks = {} wx.navigateBack({ delta: 1 }) } }) } } else if (res.openType == 'navigateTo' || res.openType == 'redirectTo') { if (!a.hasOwnProperty('globalData')) a.globalData = {} if (!a.globalData.hasOwnProperty('lastPage')) a.globalData.lastPage = {} if (!a.globalData.hasOwnProperty('pageBeforeBacks')) a.globalData.pageBeforeBacks = {} if (ps.length >= 2 && t.onBeforeBack && typeof t.onBeforeBack == 'function') { let { onUnload } = t wx.setStorageSync('showBackModal', !0) t.onUnload = function () { a.globalData.lastPage = { route: t.route, data: t.data } onUnload() } } t.onBeforeBack && typeof t.onBeforeBack == 'function' && (a.globalData.pageBeforeBacks[t.route] = t.onBeforeBack) } }) [代码] 改造Page [代码]const myPage = Page Page = function(e){ let { onLoad, onShow, onUnload } = e e.onLoad = (() => { return function (res) { this.app = getApp() this.app.globalData = this.app.globalData || {} let reinit = () => { if (this.app.globalData.lastPage && this.app.globalData.lastPage.route == this.route) { this.app.globalData.lastPage.data && this.setData(this.app.globalData.lastPage.data) Object.assign(this, this.app.globalData.lastPage.syncProps || {}) } } this.useCache = res.useCache res.useCache ? reinit() : (onLoad && onLoad.call(this, res)) } })() e.onShow = (() => { return function (res) { !this.useCache && onShow && onShow.call(this, res) } })() e.onUnload = (() => { return function (res) { this.app.globalData = Object.assign(this.app.globalData || {}, { lastPage: this }) onUnload && onUnload.call(this, res) } })() return myPage.call(this, e) } [代码] 在需要监听的页面加个onBeforeBack方法,方法返回Promise化的wx.showModal [代码]onBeforeBack: function () { return wx.showModal({ title: '提示', content: '信息尚未保存,确定要返回吗?', confirmStay: !1 //结合content意思,点击确定按钮,是否留在原来页面,confirmStay默认false }) } [代码] 运行测试,Oj8K 是不是很简单,马上去试试水吧,效果图就不放了,静态图也看不出效果,动态图懒得弄,想看效果的自己运行代码片段吧 代码片段 https://developers.weixin.qq.com/s/hc2tyrmw79hg
2020-07-28 - 为什么云开发退款API权限待商户平台确认但是商户平台未收到任何确认通知?
[图片]
2020-10-12 - 小程序通过npm安装elliptic包为什么不能正常使用?
安装elliptic包成功后,进行npm构建,示例代码在本地node环境可以正常运行,但是在微信小程序端运行报错如下: VM6216:1 thirdScriptError Cannot read property 'getBytes' of null TypeError: Cannot read property 'getBytes' of null at Rand._rand (http://127.0.0.1:56989/appservice/miniprogram_npm/elliptic/miniprogram_npm/brorand/index.js:99:21) at Rand.generate (http://127.0.0.1:56989/appservice/miniprogram_npm/elliptic/miniprogram_npm/brorand/index.js:94:19) at rand (http://127.0.0.1:56989/appservice/miniprogram_npm/elliptic/miniprogram_npm/brorand/index.js:84:16) at EC.genKeyPair (http://127.0.0.1:56989/appservice/miniprogram_npm/elliptic/index.js:2327:37) at http://127.0.0.1:56989/appservice/pages/index/index.js:18:14 at require (http://127.0.0.1:56989/appservice/__dev__/WAService.js:2:1680701) at <anonymous>:164:7 at HTMLScriptElement.scriptLoaded (http://127.0.0.1:56989/appservice/appservice?t=1581906450710:4521:21) at HTMLScriptElement.script.onload (http://127.0.0.1:56989/appservice/appservice?t=1581906450710:4533:20) 如何解决呢,是因为调用了nodejs内置函数吗,所以这个第三方包不能使用?
2020-02-17 - 为什么不建议用openid作为登录凭证?
在开发小程序的过程中,登录是一个入口场景,基本每个开发者都会遇到, 那么在开发过程中,我们知道 既然openid是唯一的,那我为什么不能用openid作为凭证,还要麻烦的用个第三方session 其实我之前也一直不明白,今天看了下面这个例子,顿时豁然开朗 有可能造成数据越权。 比如今天我通过我的手机登录了微信,打开了小程序。但是明天有个朋友想用我的手机登一下微信。如果用openid作为登录凭证,登录小程序的时候检测到openid已经存在,所以不会再走登录过程,这样我的数据就让我的朋友看到了。所以还是要按照官方推荐的步骤来。 ### 20191224 https://developers.weixin.qq.com/community/develop/doc/0002a028214de86e94079941551800 小明同学很稀罕同桌小花,有天看到小花在某个微信公众号写日记,好巧,猥琐的小明看到并记住了小花的开屏密码。等课间小花同学出去时,将她手机开机并打开了那个公众号,进入了个人中心。 哎呀,时间不够看呀,于是选择了用浏览器打开看到了URL。 你说巧不巧,这个站竟然在URL里有个openid的传值,没有登陆鉴权。 小明用他无比迅捷的手速把url发给了自己的号,还不着痕迹地打扫了战场。 以后的日子里,小明时刻都能通过点击那个url翻看小花的日记,真是爽煞,发起了向女神攻心的神级技能。 ### 20200107 更新:下面这个文章有说明为什么不用openid作为登录态 https://developers.weixin.qq.com/community/develop/doc/000c2424654c40bd9c960e71e5b009 Q2: 既然用户的openId是永远不变的,那么开发者可以使用openId作为用户的登录态么? A:行,这是非常危险的行为。因为openId是不变的,如果有坏人拿着别人的openId来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的第三方session来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。
2020-01-07 - 新能力丨云开发Cloudbase推出登录组件
开发「用户登录模块」是 Web 应用开发者最关心的事项之一,继云开发 CloudBase 原生支持短信验证码登录后,目前云开发已支持短信验证码、邮箱等多种登录鉴权方式,供不同的用户场景使用。 为了进一步优化开发者的使用体验,云开发 CloudBase 全新推出了自带云开发登录能力的 UI 组件——@cloudbase/ui-react,封装了邮箱登录、短信验证码登录、用户名登录、微信授权登录等能力,基本覆盖了云开发已有的用户登录场景。 [图片] 对比之前需要开发者引入 SDK 并使用相关 API 才能实现登录鉴权,现在只需几行核心代码,直接引入组件进行开发即可! [图片] 如何使用 @cloudbase/ui-react UI 组件? 1、前往云开发控制台,在 环境-登录授权 中,开启相应的登录授权开关,如“短信验证码登录”。 [图片] 2、安装依赖 npm install --save @cloudbase/ui-react 目前仅支持了 React + WEUI 组件库UI 组件需结合 @cloudbase/js-sdk@1.5.4-alpha.0 及以上版本使用3、React 示例Demo App.js import { AUTHSTATE, LOGINTYPE, CloudbaseAuthenticator, CloudbaseSignOut, createAuthHooks, } from "@cloudbase/ui-react" import cloudbase from "@cloudbase/js-sdk" const app = cloudbase.init({ env: "your envid" }) const { useAuthData } = createAuthHooks(app) function App() { const { authState, user } = useAuthData() return authState === AUTHSTATE.SIGNEDIN && user ? ( <> Hello, {user.uid} ) : ( ) } export default App 详见文档: https://docs.cloudbase.net/cloudbase-ui/introduce.html 除了本次上线的登录组件外,还有一大波 UI 组件正在筹划,后续会一一和大家见面! 你最期待 CloudBase 上线哪些组件?欢迎大家在评论区提出自己的想法和建议! 产品介绍云开发(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 技术交流群、最新资讯关注微信公众号【腾讯云开发CloudBase】
2021-06-24 - 云数据库在进行数据更新的时候,data中字段匹配的写法是不是没办法写成一个变量?
如下图红线部分,data中'root.object.1.numbers.2'是不是只能写死,不能写成一个变量 [图片]
2021-05-16 - #小商店运营 公众号关联(挂)小商店与小商店商品实操
步骤一、获取小商店的APPID 步骤二、公众号关联小程序 步骤三、公众号菜单配置跳转小程序 步骤四、公众号上体验 步骤五、小程序商品配置 步骤一、获取小商店的APPID 1.微信中打开小商店,依次点击-【右上角三点】-【小商店名称】-【更多资料】-长按复制AppID [图片] 2.http://shop.weixin.qq.com,扫码登录后,依次点击-【店铺管理】-【基础信息】-复制AppID(小程序ID) [图片] 步骤二、公众号关联小程序 1.左边找到小程序管理点击,右上角点击添加,需要管理员扫码 [图片] 2.使用步骤一找到的appid,填入查找 [图片] 3.下一步绑定成功 [图片] 步骤三、公众号菜单配置跳转小程序 1.左边菜单找到【自定义菜单】 [图片] 2.选中需要添加的节点,选择跳转小程序,选择我们刚刚添加的小商店 [图片] 3.填写一个备用地址,一般选历史消息,保存发布 [图片] 步骤四、公众号上体验 [图片] 步骤五、小程序商品配置 小程序appid: wx0d7fb36218e8e006 小程序路径:plugin-private://wx34345ae5855f892d/pages/productDetail/productDetail?productId=297215 这个id需要怎么获取 官方功能未开放暂不说,想其他办法获取商品ID->借助小程序后台的小程码生成功能 [图片] 填入微信号,然后打开小商店小程序 [图片] 效果如下:打开需要复制链接的页面,点右上角三点,会多出一个复制链接功能, 点击复制链接链接如:__plugin__/wx34345ae5855f892d/pages/productDetail/productDetail.html?productId=297215 把productid=后面一截直接拼在上面的连接使用,或者去掉这个链接中的.html直接使用 [图片]
2020-10-29 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15 - 分享到朋友圈onShareTimeline,打开后空白,云函数报错?
[图片] 访问报错![图片] 全局配置打开了! [图片] 云函数配置了匿名 [图片] 数据库配置了可读写 [图片]
2020-07-24 - onShareTimeline的query 参数如何获得?
单页模式如何过去query参数
2020-07-10 - 小程序转发到朋友圈示例及踩坑记录(含云开发环境配置)
偶小程序总算可以转发到朋友圈啦,撒花。。。 下面我们开始一步一步实现这个激动人心的功能,呵呵。 一、代码准备: 在页面js加入代码即可:参考文档(https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onShareTimeline) onShareTimeline:function(){ return { title:"哎呀呀", query:"from=timeline" } } 可能的坑:onShareTimeline生效的前提,需要有onShareAppMessage方法。 二、环境准备: 将本地环境基础库,修改为2.11.3或以上版本 [图片] 做完以上两部,分享到朋友圈功能就已经点亮了(暂时只支持安卓) [图片] 接下来是重头戏,朋友圈用户点击后进入的单页模式的权限处理。这个模式有较多限制,参见:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html 已DEMO为例,用户直接进入小程序,展示如下 [图片] 云开发环境返回的数据都是正常的。 如果是通过朋友圈进入,默认情况下,展示如下: [图片] 这个时候,云函数和数据库均未拿到数据。参见:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/identityless.html 我们就需要开启未登录模式及相关权限: 1.开启云环境的未登录访问权限 [图片] 2.修改对应的数据集合权限: [图片] 3.修改云函数权限(注意,这个地方改了,是所有的云函数均生效,请注意评估风险) [图片] 等待几分钟,权限生效后,从朋友圈进入,也就能获取数据了: [图片] 可能的坑: 权限放开之后的安全问题。
2020-08-07 - onShareTimeline,分享到朋友圈后 不能使用云函数吗?
onShareTimeline,分享到朋友圈后 不能使用云函数吗?
2020-10-29 - JavaScript 内存详解 & 分析指南
前言 JavaScript 诞生于 1995 年,最初被设计用于网页内的表单验证。 这些年来 JavaScript 成长飞速,生态圈日益壮大,成为了最受程序员欢迎的开发语言之一。并且现在的 JavaScript 不再局限于网页端,已经扩展到了桌面端、移动端以及服务端。 随着大前端时代的到来,使用 JavaScript 的开发者越来越多,但是许多开发者都只停留在“会用”这个层面,而对于这门语言并没有更多的了解。 如果想要成为一名更好的 JavaScript 开发者,理解内存是一个不可忽略的关键点。 📖 本文主要包含两大部分: JavaScript 内存详解 JavaScript 内存分析指南 看完这篇文章后,相信你会对 JavaScript 的内存有比较全面的了解,并且能够拥有独自进行内存分析的能力。 🧐 话不多说,我们开始吧! 文章篇幅较长,除去代码也有 12000 字左右,需要一定的时间来阅读,但是我保证你所花费的时间都是值得的。 正文 内存(memory) 什么是内存(What is memory) 相信大家都对内存有一定的了解,我就不从盘古开天辟地开始讲了,稍微提一下。 首先,任何应用程序想要运行都离不开内存。 另外,我们提到的内存在不同的层面上有着不同的含义。 💻 硬件层面(Hardware) 在硬件层面上,内存指的是随机存取存储器。 内存是计算机重要组成部分,用来储存应用运行所需要的各种数据,CPU 能够直接与内存交换数据,保证应用能够流畅运行。 一般来说,在计算机的组成中主要有两种随机存取存储器:高速缓存(Cache)和主存储器(Main memory)。 高速缓存通常直接集成在 CPU 内部,离我们比较远,所以更多时候我们提到的(硬件)内存都是主存储器。 💡 随机存取存储器(Random Access Memory,RAM) 随机存取存储器分为静态随机存取存储器(Static Random Access Memory,SRAM)和动态随机存取存储器(Dynamic Random Access Memory,DRAM)两大类。 在速度上 SRAM 要远快于 DRAM,而 SRAM 的速度仅次于 CPU 内部的寄存器。 在现代计算机中,高速缓存使用的是 SRAM,而主存储器使用的是 DRAM。 💡 主存储器(Main memory,主存) 虽然高速缓存的速度很快,但是其存储容量很小,小到几 KB 最大也才几十 MB,根本不足以储存应用运行的数据。 我们需要一种存储容量与速度适中的存储部件,让我们在保证性能的情况下,能够同时运行几十甚至上百个应用,这也就是主存的作用。 计算机中的主存其实就是我们平时说的内存条(硬件)。 硬件内存不是我们今天的主题,所以就说这么多,想要深入了解的话可以根据上面提到关键词进行搜索。 🧩 软件层面(Software) 在软件层面上,内存通常指的是操作系统从主存中划分(抽象)出来的内存空间。 此时内存又可以分为两类:栈内存和堆内存。 接下来我将围绕 JavaScript 这门语言来对内存进行讲解。 在后面的文章中所提到的内存均指软件层面上的内存。 栈与堆(Stack & Heap) 栈内存(Stack memory) 💡 栈(Stack) 栈是一种常见的数据结构,栈只允许在结构的一端操作数据,所有数据都遵循后进先出(Last-In First-Out,LIFO)的原则。 现实生活中最贴切的的例子就是羽毛球桶,通常我们只通过球桶的一侧来进行存取,最先放进去的羽毛球只能最后被取出,而最后放进去的则会最先被取出。 栈内存之所以叫做栈内存,是因为栈内存使用了栈的结构。 栈内存是一段连续的内存空间,得益于栈结构的简单直接,栈内存的访问和操作速度都非常快。 栈内存的容量较小,主要用于存放函数调用信息和变量等数据,大量的内存分配操作会导致栈溢出(Stack overflow)。 栈内存的数据储存基本都是临时性的,数据会在使用完之后立即被回收(如函数内创建的局部变量在函数返回后就会被回收)。 简单来说:栈内存适合存放生命周期短、占用空间小且固定的数据。 [图片] 💡 栈内存的大小 栈内存由操作系统直接管理,所以栈内存的大小也由操作系统决定。 通常来说,每一条线程(Thread)都会有独立的栈内存空间,Windows 给每条线程分配的栈内存默认大小为 1MB。 堆内存(Heap memory) 💡 堆(Heap) 堆也是一种常见的数据结构,但是不在本文讨论范围内,就不多说了。 堆内存虽然名字里有个“堆”字,但是它和数据结构中的堆没半毛钱关系,就只是撞了名罢了。 堆内存是一大片内存空间,堆内存的分配是动态且不连续的,程序可以按需申请堆内存空间,但是访问速度要比栈内存慢不少。 堆内存里的数据可以长时间存在,无用的数据需要程序主动去回收,如果大量无用数据占用内存就会造成内存泄露(Memory leak)。 简单来说:堆内存适合存放生命周期长,占用空间较大或占用空间不固定的数据。 [图片] 💡 堆内存的上限 在 Node.js 中,堆内存默认上限在 64 位系统中约为 1.4 GB,在 32 位系统中约为 0.7 GB。 而在 Chrome 浏览器中,每个标签页的内存上限约为 4 GB(64 位系统)和 1 GB(32 位系统)。 💡 进程、线程与堆内存 通常来说,一个进程(Process)只会有一个堆内存,同一进程下的多个线程会共享同一个堆内存。 在 Chrome 浏览器中,一般情况下每个标签页都有单独的进程,不过在某些情况下也会出现多个标签页共享一个进程的情况。 函数调用(Function calling) 明白了栈内存与堆内存是什么后,现在让我们看看当一个函数被调用时,栈内存和堆内存会发生什么变化。 当函数被调用时,会将函数推入栈内存中,生成一个栈帧(Stack frame),栈帧可以理解为由函数的返回地址、参数和局部变量组成的一个块;当函数调用另一个函数时,又会将另一个函数也推入栈内存中,周而复始;直到最后一个函数返回,便从栈顶开始将栈内存中的元素逐个弹出,直到栈内存中不再有元素时则此次调用结束。 [图片] 上图中的内容经过了简化,剥离了栈帧和各种指针的概念,主要展示函数调用以及内存分配的大概过程。 在同一线程下(JavaScript 是单线程的),所有被执行的函数以及函数的参数和局部变量都会被推入到同一个栈内存中,这也就是大量递归会导致栈溢出(Stack overflow)的原因。 关于图中涉及到的函数内部变量内存分配的详情请接着往下看。 储存变量(Store variables) 当 JavaScript 程序运行时,在非全局作用域中产生的局部变量均储存在栈内存中。 但是,只有原始类型的变量是真正地把值储存在栈内存中。 而引用类型的变量只在栈内存中储存一个引用(reference),这个引用指向堆内存里的真正的值。 💡 原始类型(Primitive type) 原始类型又称基本类型,包括 [代码]string[代码]、[代码]number[代码]、[代码]bigint[代码]、[代码]boolean[代码]、[代码]undefined[代码]、[代码]null[代码] 和 [代码]symbol[代码](ES6 新增)。 原始类型的值被称为原始值(Primitive value)。 补充:虽然 [代码]typeof null[代码] 返回的是 [代码]'object'[代码],但是 [代码]null[代码] 真的不是对象,会出现这样的结果其实是 JavaScript 的一个 Bug~ 💡 引用类型(Reference type) 除了原始类型外,其余类型都属于引用类型,包括 [代码]Object[代码]、[代码]Array[代码]、[代码]Function[代码]、[代码]Date[代码]、[代码]RegExp[代码]、[代码]String[代码]、[代码]Number[代码]、[代码]Boolean[代码] 等等… 实际上 [代码]Object[代码] 是最基本的引用类型,其他引用类型均继承自 [代码]Object[代码]。也就是说,所有引用类型的值实际上都是对象。 引用类型的值被称为引用值(Reference value)。 🎃 简单来说 在多数情况下,原始类型的数据储存在栈内存,而引用类型的数据(对象)则储存在堆内存。 [图片] 特别注意(Attention) 全局变量以及被闭包引用的变量(即使是原始类型)均储存在堆内存中。 🌐 全局变量(Global variables) 在全局作用域下创建的所有变量都会成为全局对象(如 [代码]window[代码] 对象)的属性,也就是全局变量。 而全局对象储存在堆内存中,所以全局变量必然也会储存在堆内存中。 不要问我为什么全局对象储存在堆内存中,一会我翻脸了啊! 📦 闭包(Closures) 在函数(局部作用域)内创建的变量均为局部变量。 当一个局部变量被当前函数之外的其他函数所引用(也就是发生了逃逸),此时这个局部变量就不能随着当前函数的返回而被回收,那么这个变量就必须储存在堆内存中。 而这里的“其他函数”就是我们说的闭包,就如下面这个例子: [代码]function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一个闭包函数 // 变量 count 发生了逃逸 let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3 [代码] 闭包是一个非常重要且常用的概念,许多编程语言里都有闭包这个概念。这里就不详细介绍了,贴一篇阮一峰大佬的文章。 学习 JavaScript 闭包:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html 💡 逃逸分析(Escape Analysis) 实际上,JavaScript 引擎会通过逃逸分析来决定变量是要储存在栈内存还是堆内存中。 简单来说,逃逸分析是一种用来分析变量的作用域的机制。 不可变与可变(Immutable and Mutable) 栈内存中会储存两种变量数据:原始值和对象引用。 不仅类型不同,它们在栈内存中的具体表现也不太一样。 原始值(Primitive values) 🚫 Primitive values are immutable! 前面有说到:原始类型的数据(原始值)直接储存在栈内存中。 ⑴ 当我们定义一个原始类型变量的时候,JavaScript 会在栈内存中激活一块内存来储存变量的值(原始值)。 ⑵ 当我们更改原始类型变量的值时,实际上会再激活一块新的内存来储存新的值,并将变量指向新的内存空间,而不是改变原来那块内存里的值。 ⑶ 当我们将一个原始类型变量赋值给另一个新的变量(也就是复制变量)时,也是会再激活一块新的内存,并将源变量内存里的值复制一份到新的内存里。 [图片] 🤠 总之就是:栈内存中的原始值一旦确定就不能被更改(不可变的)。 原始值的比较(Comparison) 当我们比较原始类型的变量时,会直接比较栈内存中的值,只要值相等那么它们就相等。 [代码]let a = '123'; let b = '123'; let c = '110'; let d = 123; console.log(a === b); // true console.log(a === c); // false console.log(a === d); // false [代码] 对象引用(Object references) 🧩 Object references are mutable! 前面也有说到:引用类型的变量在栈内存中储存的只是一个指向堆内存的引用。 ⑴ 当我们定义一个引用类型的变量时,JavaScript 会先在堆内存中找到一块合适的地方来储存对象,并激活一块栈内存来储存对象的引用(堆内存地址),最后将变量指向这块栈内存。 💡 所以当我们通过变量访问对象时,实际的访问过程应该是: 变量 -> 栈内存中的引用 -> 堆内存中的值 ⑵ 当我们把引用类型变量赋值给另一个变量时,会将源变量指向的栈内存中的对象引用复制到新变量的栈内存中,所以实际上只是复制了个对象引用,并没有在堆内存中生成一份新的对象。 ⑶ 而当我们给引用类型变量分配为一个新的对象时,则会直接修改变量指向的栈内存中的引用,新的引用指向堆内存中新的对象。 [图片] 🤠 总之就是:栈内存中的对象引用是可以被更改的(可变的)。 对象的比较(Comparison) 所有引用类型的值实际上都是对象。 当我们比较引用类型的变量时,实际上是在比较栈内存中的引用,只有引用相同时变量才相等。 即使是看起来完全一样的两个引用类型变量,只要他们的引用的不是同一个值,那么他们就是不一样。 [代码]// 两个变量指向的是两个不同的引用 // 虽然这两个对象看起来完全一样 // 但它们确确实实是不同的对象实例 let a = { name: 'pp' } let b = { name: 'pp' } console.log(a === b); // false // 直接赋值的方式复制的是对象的引用 let c = a; console.log(a === c); // true [代码] 对象的深拷贝(Deep copy) 当我们搞明白引用类型变量在内存中的表现时,就能清楚地理解为什么浅拷贝对象是不可靠的。 在浅拷贝中,简单的赋值只会复制对象的引用,实际上新变量和源变量引用的都是同一个对象,修改时也是修改的同一个对象,这显然不是我们想要的。 想要真正的复制一个对象,就必须新建一个对象,将源对象的属性复制过去;如果遇到引用类型的属性,那就再新建一个对象,继续复制… 此时我们就需要借助递归来实现多层次对象的复制,这也就是我们说的深拷贝。 对于任何引用类型的变量,都应该使用深拷贝来复制,除非你很确定你的目的就是复制一个引用。 内存生命周期(Memory life cycle) 通常来说,所有应用程序的内存生命周期都是基本一致的: 分配 -> 使用 -> 释放 当我们使用高级语言编写程序时,往往不会涉及到内存的分配与释放操作,因为分配与释放均已经在底层语言中实现了。 对于 JavaScript 程序来说,内存的分配与释放是由 JavaScript 引擎自动完成的(目前的 JavaScript 引擎基本都是使用 C++ 或 C 编写的)。 但是这不意味着我们就不需要在乎内存管理,了解内存的更多细节可以帮助我们写出性能更好,稳定性更高的代码。 垃圾回收(Garbage collection) 垃圾回收即我们常说的 GC(Garbage collection),也就是清除内存中不再需要的数据,释放内存空间。 由于栈内存由操作系统直接管理,所以当我们提到 GC 时指的都是堆内存的垃圾回收。 基本上现在的浏览器的 JavaScript 引擎(如 V8 和 SpiderMonkey)都实现了垃圾回收机制,引擎中的垃圾回收器(Garbage collector)会定期进行垃圾回收。 📢 紧急补课 在我们继续之前,必须先了解“可达性”和“内存泄露”这两个概念: 💡 可达性(Reachability) 在 JavaScript 中,可达性指的是一个变量是否能够直接或间接通过全局对象访问到,如果可以那么该变量就是可达的(Reachable),否则就是不可达的(Unreachable)。 [图片] 上图中的节点 9 和节点 10 均无法通过节点 1(根节点)直接或间接访问,所以它们都是不可达的,可以被安全地回收。 💡 内存泄漏(Memory leak) 内存泄露指的是程序运行时由于某种原因未能释放那些不再使用的内存,造成内存空间的浪费。 轻微的内存泄漏或许不太会对程序造成什么影响,但是一旦泄露变严重,就会开始影响程序的性能,甚至导致程序的崩溃。 垃圾回收算法(Algorithms) 垃圾回收的基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。 实际上,在回收过程中想要确定一个变量是否还有用并不简单。 直到现在也还没有一个真正完美的垃圾回收算法,接下来介绍 3 种最广为人知的垃圾回收算法。 标记-清除(Mark-and-Sweep) 标记清除算法是目前最常用的垃圾收集算法之一。 从该算法的名字上就可以看出,算法的关键就是标记与清除。 标记指的是标记变量的状态的过程,标记变量的具体方法有很多种,但是基本理念是相似的。 对于标记算法我们不需要知道所有细节,只需明白标记的基本原理即可。 需要注意的是,这个算法的效率不算高,同时会引起内存碎片化的问题。 🌰 举个栗子 当一个变量进入执行上下文时,它就会被标记为“处于上下文中”;而当变量离开执行上下文时,则会被标记为“已离开上下文”。 💡 执行上下文(Execution context) 执行上下文是 JavaScript 中非常重要的概念,简单来说的是代码执行的环境。 如果你现在对于执行上下文还不是很了解,我强烈建议你抽空专门去学习下!!! 垃圾回收器将定期扫描内存中的所有变量,将处于上下文中以及被处于上下文中的变量引用的变量的标记去除,将其余变量标记为“待删除”。 随后,垃圾回收器会清除所有带有“待删除”标记的变量,并释放它们所占用的内存。 标记-整理(Mark-Compact) 准确来说,Compact 应译为紧凑、压缩,但是在这里我觉得用“整理”更为贴切。 标记整理算法也是常用的垃圾收集算法之一。 使用标记整理算法可以解决内存碎片化的问题(通过整理),提高内存空间的可用性。 但是,该算法的标记阶段比较耗时,可能会堵塞主线程,导致程序长时间处于无响应状态。 虽然算法的名字上只有标记和整理,但这个算法通常有 3 个阶段,即标记、整理与清除。 🌰 以 V8 的标记整理算法为例 ① 首先,在标记阶段,垃圾回收器会从全局对象(根)开始,一层一层往下查询,直到标记完所有活跃的对象,那么剩下的未被标记的对象就是不可达的了。 [图片] ② 然后是整理阶段(碎片整理),垃圾回收器会将活跃的(被标记了的)对象往内存空间的一端移动,这个过程可能会改变内存中的对象的内存地址。 ③ 最后来到清除阶段,垃圾回收器会将边界后面(也就是最后一个活跃的对象后面)的对象清除,并释放它们占用的内存空间。 [图片] 引用计数(Reference counting) 引用计数算法是基于“引用计数”实现的垃圾回收算法,这是最初级但已经被弃用的垃圾回收算法。 引用计数算法需要 JavaScript 引擎在程序运行时记录每个变量被引用的次数,随后根据引用的次数来判断变量是否能够被回收。 虽然垃圾回收已不再使用引用计数算法,但是引用计数技术仍非常有用! 🌰 举个栗子 注意:垃圾回收不是即使生效的!但是在下面的例子中我们将假设回收是立即生效的,这样会更好理解~ [代码]// 下面我将 name 属性为 ππ 的对象简称为 ππ // 而 name 属性为 pp 的对象则简称为 pp // ππ 的引用:1,pp 的引用:1 let a = { name: 'ππ', z: { name: 'pp' } } // b 和 a 都指向 ππ // ππ 的引用:2,pp 的引用:1 let b = a; // x 和 a.z 都指向 pp // ππ 的引用:2,pp 的引用:2 let x = a.z; // 现在只有 b 还指向 ππ // ππ 的引用:1,pp 的引用:2 a = null; // 现在 ππ 没有任何引用了,可以被回收了 // 在 ππ 被回收后,pp 的引用也会相应减少 // ππ 的引用:0,pp 的引用:1 b = null; // 现在 pp 也可以被回收了 // ππ 的引用:0,pp 的引用:0 x = null; // 哦豁,这下全完了! [代码] 🔄 循环引用(Circular references) 引用计数算法看似很美好,但是它有一个致命的缺点,就是无法处理循环引用的情况。 在下方的例子中,当 [代码]foo()[代码] 函数执行完毕之后,对象 [代码]a[代码] 与 [代码]b[代码] 都已经离开了作用域,理论上它们都应该能够被回收才对。 但是由于它们互相引用了对方,所以垃圾回收器就认为他们都还在被引用着,导致它们哥俩永远都不会被回收,这就造成了内存泄露。 [代码]function foo() { let a = { o: null }; let b = { o: null }; a.o = b; b.o = a; } foo(); // 即使 foo 函数已经执行完毕 // 对象 a 和 b 均已离开函数作用域 // 但是 a 和 b 还在互相引用 // 那么它们这辈子都不会被回收了 // Oops!内存泄露了! [代码] V8 中的垃圾回收(GC in V8) 8️⃣ V8 V8 是一个由 Google 开源的用 C++ 编写的高性能 JavaScript 引擎。 V8 是目前最流行的 JavaScript 引擎之一,我们熟知的 Chrome 浏览器和 Node.js 等软件都在使用 V8。 在 V8 的内存管理机制中,把堆内存(Heap memory)划分成了多个区域。 [图片] 这里我们只关注这两个区域: New Space(新空间):又称 Young generation(新世代),用于储存新生成的对象,由 Minor GC 进行管理。 Old Space(旧空间):又称 Old generation(旧世代),用于储存那些在两次 GC 后仍然存活的对象,由 Major GC 进行管理。 也就是说,只要 New Space 里的对象熬过了两次 GC,就会被转移到 Old Space,变成老油条。 🧹 双管齐下 V8 内部实现了两个垃圾回收器: Minor GC(副 GC):它还有个名字叫做 Scavenger(清道夫),具体使用的是 Cheney’s Algorithm(Cheney 算法)。 Major GC(主 GC):使用的是文章前面提到的 Mark-Compact Algorithm(标记-整理算法)。 储存在 New Space 里的新生对象大多都只是临时使用的,而且 New Space 的容量比较小,为了保持内存的可用率,Minor GC 会频繁地运行。 而 Old Space 里的对象存活时间都比较长,所以 Major GC 没那么勤快,这一定程度地降低了频繁 GC 带来的性能损耗。 💥 加点魔法 我们在上方的“标记整理算法”中有提到这个算法的标记过程非常耗时,所以很容易导致应用长时间无响应。 为了提升用户体验,V8 还实现了一个名为增量标记(Incremental marking)的特性。 增量标记的要点就是把标记工作分成多个小段,夹杂在主线程(Main thread)的 JavaScript 逻辑中,这样就不会长时间阻塞主线程了。 [图片] 当然增量标记也有代价的,在增量标记过程中所有对象的变化都需要通知垃圾回收器,好让垃圾回收器能够正确地标记那些对象,这里的“通知”也是需要成本的。 另外 V8 中还有使用工作线程(Worker thread)实现的平行标记(Parallel marking)和并行标记(Concurrent marking),这里我就不再细说了~ 🤓 总结一下 为了提升性能和用户体验,V8 内部做了非常非常多的“骚操作”,本文提到的都只是冰山一角,但足以让我五体投地佩服连连! 总之就是非常 Amazing 啊~ 内存管理(Memory management) 或者说是:内存优化(Memory optimization)? 虽然我们写代码的时候一般不会直接接触内存管理,但是有一些注意事项可以让我们避免引起内存问题,甚至提升代码的性能。 全局变量(Global variable) 全局变量的访问速度远不及局部变量,应尽量避免定义非必要的全局变量。 在我们实际的项目开发中,难免会需要去定义一些全局变量,但是我们必须谨慎使用全局变量。 因为全局变量永远都是可达的,所以全局变量永远不会被回收。 🌐 还记得“可达性”这个概念吗? 因为全局变量直接挂载在全局对象上,也就是说全局变量永远都可以通过全局对象直接访问。 所以全局变量永远都是可达的,而可达的变量永远都不会被回收。 🤨 应该怎么做? 当一个全局变量不再需要用到时,记得解除其引用(置空),好让垃圾回收器可以释放这部分内存。 [代码]// 全局变量不会被回收 window.me = { name: '吴彦祖', speak: function() { console.log(`我是${this.name}`); } }; window.me.speak(); // 解除引用后才可以被回收 window.me = null; [代码] 隐藏类(HiddenClass) 实际上的隐藏类远比本文所提到的复杂,但是今天的主角不是它,所以我们点到为止。 在 V8 内部有一个叫做“隐藏类”的机制,主要用于提升对象(Object)的性能。 V8 里的每一个 JS 对象(JS Objects)都会关联一个隐藏类,隐藏类里面储存了对象的形状(特征)和属性名称到属性的映射等信息。 隐藏类内记录了每个属性的内存偏移(Memory offset),后续访问属性的时候就可以快速定位到对应属性的内存位置,从而提升对象属性的访问速度。 在我们创建对象时,拥有完全相同的特征(相同属性且相同顺序)的对象可以共享同一个隐藏类。 🤯 再想象一下 我们可以把隐藏类想象成工业生产中使用的模具,有了模具之后,产品的生产效率得到了很大的提升。 但是如果我们更改了产品的形状,那么原来的模具就不能用了,又需要制作新的模具才行。 🌰 举个栗子 在 Chrome 浏览器 Devtools 的 Console 面板中执行以下代码: [代码]// 对象 A let objectA = { id: 'A', name: '吴彦祖' }; // 对象 B let objectB = { id: 'B', name: '彭于晏' }; // 对象 C let objectC = { id: 'C', name: '刘德华', gender: '男' }; // 对象 A 和 B 拥有完全相同的特征 // 所以它们可以使用同一个隐藏类 // good! [代码] 随后在 Memory 面板打一个堆快照,通过堆快照中的 Comparison 视图可以快速找到上面创建的 3 个对象: 注:关于如何查看内存中的对象将会在文章的第二大部分中进行讲解,现在让我们专注于隐藏类。 [图片] 在上图中可以很清楚地看到对象 A 和 B 确实使用了同一个隐藏类。 而对象 C 因为多了一个 [代码]gender[代码] 属性,所以不能和前面两个对象共享隐藏类。 🧀 动态增删对象属性 一般情况下,当我们动态修改对象的特征(增删属性)时,V8 会为该对象分配一个能用的隐藏类或者创建一个新的隐藏类(新的分支)。 例如动态地给对象增加一个新的属性: 注:这种操作被称为“先创建再补充(ready-fire-aim)”。 [代码]// 增加 gender 属性 objectB.gender = '男'; // 对象 B 的特征发生了变化 // 多了一个原本没有的 gender 属性 // 导致对象 B 不能再与 A 共享隐藏类 // bad! [代码] 动态删除([代码]delete[代码])对象的属性也会导致同样的结果: [代码]// 删除 name 属性 delete objectB.name; // A:我们不一样! // bad! [代码] 不过,添加数组索引属性(Array-indexed properties)并不会有影响: 其实就是用整数作为属性名,此时 V8 会另外处理。 [代码]// 增加 1 属性 objectB[1] = '数字组引属性'; // 不影响共享隐藏类 // so far so good! [代码] 🙄 那问题来了 说了这么多,隐藏类看起来确实可以提升性能,那它和内存又有什么关系呢? 实际上,隐藏类也需要占用内存空间,这其实就是一种用空间换时间的机制。 如果由于动态增删对象属性而创建了大量隐藏类和分支,结果就是会浪费不少内存空间。 🌰 举个栗子 创建 1000 个拥有相同属性的对象,内存中只会多出 1 个隐藏类。 而创建 1000 个属性信息完全不同的对象,内存中就会多出 1000 个隐藏类。 🤔 应该怎么做? 所以,我们要尽量避免动态增删对象属性操作,应该在构造函数内就一次性声明所有需要用到的属性。 如果确实不再需要某个属性,我们可以将属性的值设为 [代码]null[代码],如下: [代码]// 将 age 属性置空 objectB.age = null; // still good! [代码] 另外,相同名称的属性尽量按照相同的顺序来声明,可以尽可能地让更多对象共享相同的隐藏类。 即使遇到不能共享隐藏类的情况,也至少可以减少隐藏类分支的产生。 其实动态增删对象属性所引起的性能问题更为关键,但因本文篇幅有限,就不再展开了。 闭包(Closure) 前面有提到:被闭包引用的变量储存在堆内存中。 这里我们再重点关注一下闭包中的内存问题,还是前面的例子: [代码]function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一个闭包函数 let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3 [代码] 现在只要我们一直持有变量(函数) [代码]closure[代码],那么变量 [代码]count[代码] 就不会被释放。 或许你还没有发现风险所在,不如让我们试想变量 [代码]count[代码] 不是一个数字,而是一个巨大的数组,一但这样的闭包多了,那对于内存来说就是灾难。 [代码]// 我将这个作品称为:闭包炸弹 function closureBomb() { const handsomeBoys = []; setInterval(() => { for (let i = 0; i < 100; i++) { handsomeBoys.push( { name: '陈皮皮', rank: 0 }, { name: ' 你 ', rank: 1 }, { name: '吴彦祖', rank: 2 }, { name: '彭于晏', rank: 3 }, { name: '刘德华', rank: 4 }, { name: '郭富城', rank: 5 } ); } }, 100); } closureBomb(); // 即将毁灭世界 // 💣 🌍 💥 💨 [代码] 🤔 应该怎么做? 所以,我们必须避免滥用闭包,并且谨慎使用闭包! 当不再需要时记得解除闭包函数的引用,让闭包函数以及引用的变量能够被回收。 [代码]closure = null; // 变量 count 终于得救了 [代码] 如何分析内存(Analyze) 说了这么多,那我们应该如何查看并分析程序运行时的内存情况呢? “工欲善其事,必先利其器。” 对于 Web 前端项目来说,分析内存的最佳工具非 Memory 莫属! 这里的 Memory 指的是 DevTools 中的一个工具,为了避免混淆,下面我会用“Memory 面板”或”内存面板“代称。 🔧 DevTools(开发者工具) DevTools 是浏览器里内置的一套用于 Web 开发和调试的工具。 使用 Chromuim 内核的浏览器都带有 DevTools,个人推荐使用 Chrome 或者 Edge(新)。 Memory in Devtools(内存面板) 在我们切换到 Memory 面板后,会看到以下界面(注意标注): [图片] 在这个面板中,我们可以通过 3 种方式来记录内存情况: Heap snapshot:堆快照 Allocation instrumentation on timeline:内存分配时间轴 Allocation sampling:内存分配采样 小贴士:点击面板左上角的 Collect garbage 按钮(垃圾桶图标)可以主动触发垃圾回收。 🤓 在正式开始分析内存之前,让我们先学习几个重要的概念: 💡 Shallow Size(浅层大小) 浅层大小指的是当前对象自身占用的内存大小。 浅层大小不包含自身引用的对象。 💡 Retained Size(保留大小) 保留大小指的是当前对象被 GC 回收后总共能够释放的内存大小。 换句话说,也就是当前对象自身大小加上对象直接或间接引用的其他对象的大小总和。 需要注意的是,保留大小不包含那些除了被当前对象引用之外还被全局对象直接或间接引用的对象。 Heap snapshot(堆快照) [图片] 堆快照可以记录页面当前时刻的 JS 对象以及 DOM 节点的内存分配情况。 🚀 如何开始 点击页面底部的 Take snapshot 按钮或者左上角的 ⚫ 按钮即可打一个堆快照,片刻之后就会自动展示结果。 [图片] 在堆快照结果页面中,我们可以使用 4 种不同的视图来观察内存情况: Summary:摘要视图 Comparison:比较视图 Containment:包含视图 Statistics:统计视图 默认显示 Summary 视图。 Summary(摘要视图) 摘要视图根据 Constructor(构造函数)来将对象进行分组,我们可以在 Class filter(类过滤器)中输入构造函数名称来快速筛选对象。 [图片] 页面中的几个关键词: Constructor:构造函数。 Distance:(根)距离,对象与 GC 根之间的最短距离。 Shallow Size:浅层大小,单位:Bytes(字节)。 Retained Size:保留大小,单位:Bytes(字节)。 Retainers:持有者,也就是直接引用目标对象的变量。 📌 Retainers(持有者) Retainers 栏在旧版的 Devtools 里叫做 Object’s retaining tree(对象保留树)。 Retainers 下的对象也展开为树形结构,方便我们进行引用溯源。 在视图中的构造函数列表中,有一些用“()”包裹的条目: (compiled code):已编译的代码。 (closure):闭包函数。 (array, string, number, symbol, regexp):对应类型([代码]Array[代码]、[代码]String[代码]、[代码]Number[代码]、[代码]Symbol[代码]、[代码]RegExp[代码])的数据。 (concatenated string):使用 [代码]concat()[代码] 函数拼接而成的字符串。 (sliced string):使用 [代码]slice()[代码]、[代码]substring()[代码] 等函数进行边缘切割的字符串。 (system):系统(引擎)产生的对象,如 V8 创建的 HiddenClasses(隐藏类)和 DescriptorArrays(描述符数组)等数据。 💡 DescriptorArrays(描述符数组) 描述符数组主要包含对象的属性名信息,是隐藏类的重要组成部分。 不过描述符数组内不会包含整数索引属性。 而其余没有用“()”包裹的则为全局属性和 GC 根。 另外,每个对象后面都会有一串“@”开头的数字,这是对象在内存中的唯一 ID。 小贴士:按下快捷键 Ctrl/Command + F 展示搜索栏,输入名称或 ID 即可快速查找目标对象。 💪 实践一下:实例化一个对象 ① 切换到 Console 面板,执行以下代码来实例化一个对象: [代码]function TestClass() { this.number = 123; this.string = 'abc'; this.boolean = true; this.symbol = Symbol('test'); this.undefined = undefined; this.null = null; this.object = { name: 'pp' }; this.array = [1, 2, 3]; this.getSet = { _value: 0, get value() { return this._value; }, set value(v) { this._value = v; } }; } let testObject = new TestClass(); [代码] [图片] ② 回到 Memory 面板,打一个堆快照,在 Class filter 中输入“TestClass”: 可以看到内存中有一个 [代码]TestClass[代码] 的实例,该实例的浅层大小为 80 字节,保留大小为 876 字节。 [图片] 🤔 注意到了吗? 堆快照中的 [代码]TestClass[代码] 实例的属性中少了一个名为 [代码]number[代码] 属性,这是因为堆快照不会捕捉数字属性。 💪 实践一下:创建一个字符串 ① 切换到 Console 面板,执行以下代码来创建一个字符串: [代码]// 这是一个全局变量 let testString = '我是吴彦祖'; [代码] ② 回到 Memory 面板,打一个堆快照,打开搜索栏(Ctrl/Command + F)并输入“我是吴彦祖”: [图片] Comparison(比较视图) 只有同时存在 2 个或以上的堆快照时才会出现 Comparison 选项。 比较视图用于展示两个堆快照之间的差异。 使用比较视图可以让我们快速得知在执行某个操作后的内存变化情况(如新增或减少对象)。 通过多个快照的对比还可以让我们快速判断并定位内存泄漏。 文章前面提到隐藏类的时候,就是使用了比较视图来快速查找新创建的对象。 💪 实践一下 ① 新建一个无痕(匿名)标签页并切换到 Memory 面板,打一个堆快照 Snapshot 1。 💡 为什么是无痕标签页? 普通标签页会受到浏览器扩展或者其他脚本影响,内存占用不稳定。 使用无痕窗口的标签页可以保证页面的内存相对纯净且稳定,有利于我们进行对比。 另外,建议打开窗口一段之间之后再开始测试,这样内存会比较稳定(控制变量)。 ② 切换到 Console 面板,执行以下代码来实例化一个 [代码]Foo[代码] 对象: [代码]function Foo() { this.name = 'pp'; this.age = 18; } let foo = new Foo(); [代码] ③ 回到 Memory 面板,再打一个堆快照 Snapshot 2,切换到 Comparison 视图,选择 Snapshot 1 作为 Base snapshot(基本快照),在 Class filter 中输入“Foo”: 可以看到内存中新增了一个 [代码]Foo[代码] 对象实例,分配了 52 字节内存空间,该实例的引用持有者为变量 [代码]foo[代码]。 [图片] ④ 再次切换到 Console 面板,执行以下代码来解除变量 [代码]foo[代码] 的引用: [代码]// 解除对象的引用 foo = null; [代码] ⑤ 再回到 Memory 面板,打一个堆快照 Snapshot 3,选择 Snapshot 2 作为 Base snapshot,在 Class filter 中输入“Foo”: 内存中的 [代码]Foo[代码] 对象实例已经被删除,释放了 52 字节的内存空间。 [图片] Containment(包含视图) 包含视图就是程序对象结构的“鸟瞰图(Bird’s eye view)”,允许我们通过全局对象出发,一层一层往下探索,从而了解内存的详细情况。 [图片] 包含视图中有以下几种全局对象: GC roots(GC 根) GC roots 就是 JavaScript 虚拟机的垃圾回收中实际使用的根节点。 GC 根可以由 Built-in object maps(内置对象映射)、Symbol tables(符号表)、VM thread stacks(VM 线程堆栈)、Compilation caches(编译缓存)、Handle scopes(句柄作用域)和 Global handles(全局句柄)等组成。 DOMWindow objects(DOMWindow 对象) DOMWindow objects 指的是由宿主环境(浏览器)提供的顶级对象,也就是 JavaScript 代码中的全局对象 [代码]window[代码],每个标签页都有自己的 [代码]window[代码] 对象(即使是同一窗口)。 Native objects(原生对象) Native objects 指的是那些基于 ECMAScript 标准实现的内置对象,包括 [代码]Object[代码]、[代码]Function[代码]、[代码]Array[代码]、[代码]String[代码]、[代码]Boolean[代码]、[代码]Number[代码]、[代码]Date[代码]、[代码]RegExp[代码]、[代码]Math[代码] 等对象。 💪 实践一下 ① 切换到 Console 面板,执行以下代码来创建一个构造函数 [代码]$ABC[代码]: 构造函数命名前面加个 $ 是因为这样排序的时候可以排在前面,方便找。 [代码]function $ABC() { this.name = 'pp'; } [代码] ② 切换到 Memory 面板,打一个堆快照,切换为 Containment 视图: 在当前标签页的全局对象下就可以找到我们刚刚创建的构造函数 [代码]$ABC[代码]。 [图片] Statistics(统计视图) 统计视图可以很直观地展示内存整体分配情况。 [图片] 在该视图里的空心饼图中共有 6 种颜色,各含义分别为: 红色:Code(代码) 绿色:Strings(字符串) 蓝色:JS arrays(数组) 橙色:Typed arrays(类型化数组) 紫色:System objects(系统对象) 白色:空闲内存 Allocation instrumentation on timeline(分配时间轴) [图片] 在一段时间内持续地记录内存分配(约每 50 毫秒打一张堆快照),记录完成后可以选择查看任意时间段的内存分配详情。 另外还可以勾选同时记录分配堆栈(Allocation stacks),也就是记录调用堆栈,不过这会产生额外的性能消耗。 🚀 如何开始 点击页面底部的 Start 按钮或者左上角的 ⚫ 按钮即可开始记录,记录过程中点击左上角的 🔴 按钮来结束记录,片刻之后就会自动展示结果。 💪 操作一下 ① 打开 Memory 面板,开始记录分配时间轴。 ② 切换到 Console 面板,执行以下代码: 代码效果:每隔 1 秒钟创建 100 个对象,共创建 1000 个对象。 [代码]console.log('测试开始'); let objects = []; let handler = setInterval(() => { // 每秒创建 100 个对象 for (let i = 0; i < 100; i++) { const name = `n${objects.length}`; const value = `v${objects.length}`; objects.push({ [name]: value}); } console.log(`对象数量:${objects.length}`); // 达到 1000 个后停止 if (objects.length >= 1000) { clearInterval(handler); console.log('测试结束'); } }, 1000); [代码] 😈 又是一个细节 不知道你有没有发现,在上面的代码中,我干了一件坏事。 在 for 循环创建对象时,会根据对象数组当前长度生成一个唯一的属性名和属性值。 这样一来 V8 就无法对这些对象进行优化,方便我们进行测试。 另外,如果直接使用对象数组的长度作为属性名会有惊喜~ ③ 静静等待 10 秒钟,控制台会打印出“测试结束”。 ④ 切换回 Memory 面板,停止记录,片刻之后会自动进入结果页面。 [图片] 分配时间轴结果页有 4 种视图: Summary:摘要视图 Containment:包含视图 Allocation:分配视图 Statistics:统计视图 默认显示 Summary 视图。 Summary(摘要视图) 看起来和堆快照的摘要视图很相似,主要是页面上方多了一条横向的时间轴(Timeline)。 [图片] 🧭 时间轴 时间轴中主要的 3 种线: 细横线:内存分配大小刻度线 蓝色竖线:表示内存在对应时刻被分配,最后仍然活跃 灰色竖线:表示内存在对应时刻被分配,但最后被回收 时间轴的几个操作: 鼠标移动到时间轴内任意位置,点击左键或长按左键并拖动即可选择一段时间 鼠标拖动时间段框上方的方块可以对已选择的时间段进行调整 鼠标移到已选择的时间段框内部,滑动滚轮可以调整时间范围 鼠标移到已选择的时间段框两旁,滑动滚轮即可调整时间段 双击鼠标左键即可取消选择 [图片] 在时间轴中选择要查看的时间段,即可得到该段时间的内存分配详情。 [图片] Containment(包含视图) 分配时间轴的包含视图与堆快照的包含视图是一样的,这里就不再重复介绍了。 [图片] Allocation(分配视图) 对不起各位,这玩意儿我也不知道有啥用… 打开就直接报错,我:喵喵喵? [图片] 是不是因为没人用这玩意儿,所以没人发现有问题… Statistics(统计视图) 分配时间轴的统计视图与堆快照的统计视图也是一样的,不再赘述。 [图片] Allocation sampling(分配采样) [图片] Memory 面板上的简介:使用采样方法记录内存分配。这种分析方式的性能开销最小,可以用于长时间的记录。 好家伙,这个简介有够模糊,说了跟没说似的,很有精神! 我在官方文档里没有找到任何关于分配采样的介绍,Google 上也几乎没有与之有关的信息。所以以下内容仅为个人实践得出的结果,如有不对的地方欢迎各位指出! 简单来说,通过分配采样我们可以很直观地看到代码中的每个函数(API)所分配的内存大小。 由于是采样的方式,所以结果并非百分百准确,即使每次执行相同的操作也可能会有不同的结果,但是足以让我们了解内存分配的大体情况。 ✍ 如何开始 点击页面底部的 Start 按钮或者左上角的 ⚫ 按钮即可开始记录,记录过程中点击左上角的 🔴 按钮来结束记录,片刻之后就会自动展示结果。 💪 操作一下 ① 打开 Memory 面板,开始记录分配采样。 ② 切换到 Console 面板,执行以下代码: 代码看起来有点长,其实就是 4 个函数分别以不同的方式往数组里面添加对象。 [代码]// 普通单层调用 let array_a = []; function aoo1() { for (let i = 0; i < 10000; i++) { array_a.push({ a: 'pp' }); } } aoo1(); // 两层嵌套调用 let array_b = []; function boo1() { function boo2() { for (let i = 0; i < 20000; i++) { array_b.push({ b: 'pp' }); } } boo2(); } boo1(); // 三层嵌套调用 let array_c = []; function coo1() { function coo2() { function coo3() { for (let i = 0; i < 30000; i++) { array_c.push({ c: 'pp' }); } } coo3(); } coo2(); } coo1(); // 两层嵌套多个调用 let array_d = []; function doo1() { function doo2_1() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_1(); function doo2_2() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_2(); } doo1(); [代码] ③ 切换回 Memory 面板,停止记录,片刻之后会自动进入结果页面。 [图片] 分配采样结果页有 3 种视图可选: Chart:图表视图 Heavy (Bottom Up):扁平视图(调用层级自下而上) Tree (Top Down):树状视图(调用层级自上而下) 这个 Heavy 我真的不知道该怎么翻译,所以我就按照具体表现来命名了。 默认会显示 Chart 视图。 Chart(图表视图) Chart 视图以图形化的表格形式展现各个函数的内存分配详情,可以选择精确到内存分配的不同阶段(以内存分配的大小为轴)。 [图片] 鼠标左键点击、拖动和双击以操作内存分配阶段轴(和时间轴一样),选择要查看的阶段范围。 [图片] 将鼠标移动到函数方块上会显示函数的内存分配详情。 [图片] 鼠标左键点击函数方块可以跳转到相应代码。 [图片] Heavy(扁平视图) Heavy 视图将函数调用层级压平,函数将以独立的个体形式展现。另外也可以展开调用层级,不过是自下而上的结构,也就是一个反向的函数调用过程。 [图片] 视图中的两种 Size(大小): Self Size:自身大小,指的是在函数内部直接分配的内存空间大小。 Total Size:总大小,指的是函数总共分配的内存空间大小,也就是包括函数内部嵌套调用的其他函数所分配的大小。 Tree(树状视图) Tree 视图以树形结构展现函数调用层级。我们可以从代码执行的源头开始自上而下逐层展开,呈现一个完整的正向的函数调用过程。 [图片] 参考资料 《JavaScript 高级程序设计(第4版)》 Memory Management:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management Visualizing memory management in V8 Engine:https://deepu.tech/memory-management-in-v8/ Trash talk: the Orinoco garbage collector:https://v8.dev/blog/trash-talk Fast properties in V8:https://v8.dev/blog/fast-properties Concurrent marking in V8:https://v8.dev/blog/concurrent-marking Chrome DevTools:https://developers.google.com/web/tools/chrome-devtools 传送门 微信推文版本 个人博客:菜鸟小栈 开源主页:陈皮皮 Eazax-CCC 游戏开发脚手架 更多分享 《为什么选择使用 TypeScript ?》 《高斯模糊 Shader》 《一文看懂 YAML》 《Cocos Creator 性能优化:DrawCall》 《互联网运营术语扫盲》 《在 Cocos Creator 里画个炫酷的雷达图》 《用 Shader 写个完美的波浪》 《在 Cocos Creator 中优雅且高效地管理弹窗》 《Cocos Creator 源码解读:引擎启动与主循环》 公众号 菜鸟小栈 😺我是陈皮皮,一个还在不断学习的游戏开发者,一个热爱分享的 Cocos Star Writer。 🎨这是我的个人公众号,专注但不仅限于游戏开发和前端技术分享。 💖每一篇原创都非常用心,你的关注就是我原创的动力! Input and output. [图片]
2021-01-13 - canvas中使用createImage()创建的image对象onload方法不执行?
微信7.0.20, 使用canvas的createImage()创建的image对象, onload方法不执行, 造成线上使用canvas生成图片的功能异常。 微信7.0.18正常,, 微信的升级到7.0.20全部异常
2020-12-28 - 如何更新云数据库某个字段为数组的某个index的元素的值?
例如数据库中有个字段为: sales:[12,68,18] 如何删除sales[1]的值(按索引删除) 如何更改sales[1]的值(按索引修改)
2020-07-10 - 云开发数据库中数组怎么如何修改?
那位高手能帮帮忙,试了半天没成功,能查到要用的元素,但更改不成功 我的数据结构如下: 比如说我要更改AB数组,第一个元素a的对应值,要怎么在改呀!? 更新AB整个第一个元素也行, 更改第N个息我以改呀! { "A":"123", "B":"名字", "AB":[ {"a":"1","b":"2","c":"3"}, {"d":"4","e":"5","f":"6"}, ...... {"x":"n","x":"n","z":"n"}, ], }
2020-12-12 - 小程序wxacode.getUnlimited下载二维码,AccessToken总是无故失效?
通过auth.getAccessToken获取到一个新的AccessToken,然后调用wxacode.getUnlimited下载二维码,第一次可以下载成功,第二次就返回说token失效,怎么解决?{"errcode":40001,"errmsg":"invalid credential, access_token is invalid or not latest rid: 5f438c3c-4b42b8c3-62792f83"}
2020-08-24 - 一文搞懂微信支付 Api-v3 规则实现(附源码)
v2 与 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-v3 规则 官方文档 ,此规则需要你耐心细品,重复多此细品效果更佳。 以下是我细品后,总结的实现方案,在这里就分享给大家,干货多屁话少直接贴实现。 Talk is cheap. Show me the code. 获取证书序列号 通过代码获取 这里我们使用第三方的库 x509,如你知道其它获取方法欢迎留言 [代码]const cert = x509.parseCert('cert.pem 证书绝对路径') console.log(`证书序列号:${cert.serial}`) [代码] 通过工具获取 openssl x509 -in apiclient_cert.pem -noout -serial 使用证书解析工具 https://myssl.com/cert_decode.html 构建请求头 1、构建请求签名参数 [代码] /** * 构建请求签名参数 * @param method {RequestMethod} Http 请求方式 * @param url 请求接口 /v3/certificates * @param timestamp 获取发起请求时的系统当前时间戳 * @param nonceStr 随机字符串 * @param body 请求报文主体 */ public static buildReqSignMessage(method: RequestMethod, url: string, timestamp: string, nonceStr: string, body: string): string { return method .concat('\n') .concat(url) .concat('\n') .concat(timestamp) .concat('\n') .concat(nonceStr) .concat('\n') .concat(body) .concat('\n') } [代码] 2、使用 SHA256 with RSA 算法生成签名 [代码] /** * SHA256withRSA * @param data 待加密字符 * @param privatekey 私钥key key.pem fs.readFileSync(keyPath) */ public static sha256WithRsa(data: string, privatekey: Buffer): string { return crypto .createSign('RSA-SHA256') .update(data) .sign(privatekey, 'base64') } [代码] 3、根据平台规则生成请求头 authorization [代码] /** * 获取授权认证信息 * * @param mchId 商户号 * @param serialNo 商户API证书序列号 * @param nonceStr 请求随机串 * @param timestamp 时间戳 * @param signature 签名值 * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 */ public static getAuthorization(mchId: string, serialNo: string, nonceStr: string, timestamp: string, signature: string, authType: string): string { let map: Map<string, string> = new Map() map.set('mchid', mchId) map.set('serial_no', serialNo) map.set('nonce_str', nonceStr) map.set('timestamp', timestamp) map.set('signature', signature) return authType.concat(' ').concat(this.createLinkString(map, ',', false, true)) } [代码] 4、Show Time [代码]/** * 构建 v3 接口所需的 Authorization * * @param method {RequestMethod} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param key key.pem 证书 * @param body 接口请求参数 */ public static async buildAuthorization(method: RequestMethod, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, body: string): Promise<string> { let timestamp: string = parseInt((Date.now() / 1000).toString()).toString() let authType: string = 'WECHATPAY2-SHA256-RSA2048' let nonceStr: string = Kits.generateStr() // 构建签名参数 let buildSignMessage: string = this.buildReqSignMessage(method, urlSuffix, timestamp, nonceStr, body) // 生成签名 let signature: string = this.sha256WithRsa(key, buildSignMessage) // 根据平台规则生成请求头 authorization return this.getAuthorization(mchId, serialNo, nonceStr, timestamp, signature, authType) } [代码] 封装网络请求 每个人都有个性,可使用的网络库也比较多(Axios、Fetch、Request 等),为了适配能适配这里做一代理封装。具体实现如下,网络请求库默认是使用的 Axios 1、抽离抽象接口 [代码]/** * @author Javen * @copyright javendev@126.com * @description 封装网络请求工具 */ export class HttpKit { private static delegate: HttpDelegate = new AxiosHttpKit() public static get getHttpDelegate(): HttpDelegate { return this.delegate } public static set setHttpDelegate(delegate: HttpDelegate) { this.delegate = delegate } } export interface HttpDelegate { httpGet(url: string, options?: any): Promise<any> httpGetToResponse(url: string, options?: any): Promise<any> httpPost(url: string, data: string, options?: any): Promise<any> httpPostToResponse(url: string, data: string, options?: any): Promise<any> httpDeleteToResponse(url: string, options?: any): Promise<any> httpPostWithCert(url: string, data: string, certFileContent: Buffer, passphrase: string): Promise<any> upload(url: string, filePath: string, params?: string): Promise<any> } [代码] 2、Axios 具体实现 [代码]/** * @author Javen * @copyright javendev@126.com * @description 使用 Axios 实现网络请求 */ import axios from 'axios' import * as fs from 'fs' import { HttpDelegate } from './HttpKit' import * as FormData from 'form-data' import * as https from 'https' import concat = require('concat-stream') export class AxiosHttpKit implements HttpDelegate { httpGet(url: string, options?: any): Promise<any> { return new Promise((resolve, reject) => { axios .get(url, options) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) } httpGetToResponse(url: string, options?: any): Promise<any> { return new Promise(resolve => { axios .get(url, options) .then(response => { resolve(response) }) .catch(error => { resolve(error.response) }) }) } httpPost(url: string, data: string, options?: any): Promise<any> { return new Promise((resolve, reject) => { axios .post(url, data, options) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) } httpPostToResponse(url: string, data: string, options?: any): Promise<any> { return new Promise(resolve => { axios .post(url, data, options) .then(response => { resolve(response) }) .catch(error => { resolve(error.response) }) }) } httpDeleteToResponse(url: string, options?: any): Promise<any> { return new Promise(resolve => { axios .delete(url, options) .then(response => { resolve(response) }) .catch(error => { resolve(error.response) }) }) } httpPostWithCert(url: string, data: string, certFileContent: Buffer, passphrase: string): Promise<any> { return new Promise((resolve, reject) => { let httpsAgent = new https.Agent({ pfx: certFileContent, passphrase }) axios .post(url, data, { httpsAgent }) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) } upload(url: string, filePath: string, params?: string): Promise<any> { return new Promise((resolve, reject) => { let formData = new FormData() formData.append('media', fs.createReadStream(filePath)) if (params) { formData.append('description', params) } formData.pipe( concat({ encoding: 'buffer' }, async data => { axios .post(url, data, { headers: { 'Content-Type': 'multipart/form-data' } }) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) ) }) } } [代码] 3、使其支持 Api-v3 接口规则 [代码]/** * 微信支付 Api-v3 get 请求 * @param urlPrefix * @param urlSuffix * @param mchId * @param serialNo * @param key * @param params */ public static async exeGet(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, params?: Map<string, string>): Promise<any> { if (params && params.size > 0) { urlSuffix = urlSuffix.concat('?').concat(this.createLinkString(params, '&', true, false)) } let authorization = await this.buildAuthorization(RequestMethod.GET, urlSuffix, mchId, serialNo, key, '') return await this.get(urlPrefix.concat(urlSuffix), authorization, serialNo) } /** * 微信支付 Api-v3 post 请求 * @param urlPrefix * @param urlSuffix * @param mchId * @param serialNo * @param key * @param data */ public static async exePost(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, data: string): Promise<any> { let authorization = await this.buildAuthorization(RequestMethod.POST, urlSuffix, mchId, serialNo, key, data) return await this.post(urlPrefix.concat(urlSuffix), data, authorization, serialNo) } /** * 微信支付 Api-v3 delete 请求 * @param urlPrefix * @param urlSuffix * @param mchId * @param serialNo * @param key */ public static async exeDelete(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer): Promise<any> { let authorization = await this.buildAuthorization(RequestMethod.DELETE, urlSuffix, mchId, serialNo, key, '') return await this.delete(urlPrefix.concat(urlSuffix), authorization, serialNo) } /** * get 方法 * @param url 请求 url * @param authorization 授权信息 * @param serialNumber 证书序列号 */ public static async get(url: string, authorization: string, serialNumber?: string) { return await HttpKit.getHttpDelegate.httpGetToResponse(url, { headers: this.getHeaders(authorization, serialNumber) }) } /** * post 方法 * @param url 请求 url * @param authorization 授权信息 * @param serialNumber 证书序列号 */ public static async post(url: string, data: string, authorization: string, serialNumber?: string) { return await HttpKit.getHttpDelegate.httpPostToResponse(url, data, { headers: this.getHeaders(authorization, serialNumber) }) } /** * delete 方法 * @param url 请求 url * @param authorization 授权信息 * @param serialNumber 证书序列号 */ public static async delete(url: string, authorization: string, serialNumber?: string) { return await HttpKit.getHttpDelegate.httpDeleteToResponse(url, { headers: this.getHeaders(authorization, serialNumber) }) } /** * 获取请求头 * @param authorization 授权信息 * @param serialNumber 证书序列号 */ private static getHeaders(authorization: string, serialNumber: string): Object { let userAgent: string = 'WeChatPay-TNWX-HttpClient/%s (%s) nodejs/%s' userAgent = util.format( userAgent, '2.4.0', os .platform() .concat('/') .concat(os.release()), process.version ) return { Authorization: authorization, Accept: 'application/json', 'Content-type': 'application/json', 'Wechatpay-Serial': serialNumber, 'User-Agent': userAgent } } [代码] 如何使用? 这里以「获取平台证书」为例,来演示上面封装的系列方法如何使用 [代码]try { let result = await PayKit.exeGet( WX_DOMAIN.CHINA, // WX_API_TYPE.GET_CERTIFICATES, config.mchId, x509.parseCert(config.certPath).serial, fs.readFileSync(config.keyPath) ) console.log(`result.data:${result.data}`) // 应答报文主体 let data = JSON.stringify(result.data) // 应答状态码 console.log(`status:${result.status}`) console.log(`data:${data}`) // http 请求头 let headers = result.headers // 证书序列号 let serial = headers['wechatpay-serial'] // 应答时间戳 let timestamp = headers['wechatpay-timestamp'] // 应答随机串 let nonce = headers['wechatpay-nonce'] // 应答签名 let signature = headers['wechatpay-signature'] console.log(`serial:\n${serial}`) console.log(`timestamp:\n${timestamp}`) console.log(`nonce:\n${nonce}`) console.log(`signature:\n${signature}`) ctx.body = data } catch (error) { console.log(error) } [代码] 至此微信支付 Api-v3 规则的接口已经测试通过。 但还有其他细节如要我们继续完善,比如 验证签名、证书和回调报文解密 证书和回调报文解密 AEAD_AES_256_GCM 解密算法实现 [代码] /** * AEAD_AES_256_GCM 解密 * @param key apiKey3 * @param nonce 加密使用的随机串初始化向量 * @param associatedData 附加数据包 * @param ciphertext 密文 */ public static aes256gcmDecrypt(key: string, nonce: string, associatedData: string, ciphertext: string): string { let ciphertextBuffer = Buffer.from(ciphertext, 'base64') let authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16) let data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16) let decipherIv = crypto.createDecipheriv('aes-256-gcm', key, nonce) decipherIv.setAuthTag(Buffer.from(authTag)) decipherIv.setAAD(Buffer.from(associatedData)) let decryptStr = decipherIv.update(data, null, 'utf8') decipherIv.final() return decryptStr } [代码] 保存微信平台证书示例 [代码] // 证书和回调报文解密 let certPath = '/Users/Javen/cert/platform_cert.pem' try { let decrypt = PayKit.aes256gcmDecrypt( config.apiKey3, ctx.app.config.AEAD_AES_256_GCM.nonce, ctx.app.config.AEAD_AES_256_GCM.associated_data, ctx.app.config.AEAD_AES_256_GCM.ciphertext ) // 保存证书 fs.writeFileSync(certPath, decrypt) ctx.body = decrypt } catch (error) { console.log(error) } [代码] 验证签名 示例 [代码]// 根据序列号查证书 验证签名 let verifySignature: boolean = PayKit.verifySignature(signature, data, nonce, timestamp, fs.readFileSync(ctx.app.config.WxPayConfig.wxCertPath)) console.log(`verifySignature:${verifySignature}`) [代码] 构建应答签名参数 [代码]/** * 构建应答签名参数 * @param timestamp 应答时间戳 * @param nonceStr 应答随机串 * @param body 应答报文主体 */ public static buildRepSignMessage(timestamp: string, nonceStr: string, body: string): string { return timestamp .concat('\n') .concat(nonceStr) .concat('\n') .concat(body) .concat('\n') } [代码] 使用平台证书验证 [代码] /** * 验证签名 * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param publicKey 平台公钥 */ public static verifySignature(signature: string, body: string, nonce: string, timestamp: string, publicKey: Buffer): boolean { // 构建响应体中待签名数据 let buildSignMessage: string = this.buildRepSignMessage(timestamp, nonce, body) return Kits.sha256WithRsaVerify(publicKey, signature, buildSignMessage) } [代码] [代码] /** * SHA256withRSA 验证签名 * @param publicKey 公钥key * @param signature 待验证的签名串 * @param data 需要验证的字符串 */ public static sha256WithRsaVerify(publicKey: Buffer, signature: string, data: string) { return crypto .createVerify('RSA-SHA256') .update(data) .verify(publicKey, signature, 'base64') } [代码] 完整示例代码 Egg-TNWX TNWX: TypeScript + Node.js + WeiXin 微信系开发脚手架,支持微信公众号、微信支付、微信小游戏、微信小程序、企业微信/企业号、企业微信开放平台。最最最重要的是能快速的集成至任何 Node.js 框架(Express、Nest、Egg、Koa 等) 微信支付已支持 Api-v3 以及 Api-v2 版本接口,同时支持多商户多应用,国内与境外的普通商户模式和服务商模式,v2 接口同时支持 MD5 以及 HMAC-SHA256 签名算法。 如有疑问欢迎留言或者站内私信。
2020-04-21 - 现金红包 签名错误
按照文档,生成签名和官方的签名工具生成的md5签名是一致的,但是就是报签名错误
2020-08-20 - 小程序添加分账接收方,签名校验总是通不过,求解?
小程序云开发,接入微信支付,添加分账接收方 昨天调试一天,找不出原因的签名校验不通过一直在发生! 微信支付接口签名校验工具 https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=20_1 使用签名校验工具,对一模一样的StringA原字符串进行编译,我生成的原sign值和签名校验工具生成新sign值总是不一样。 数据比对: 原sign值:C714553B1E86EE6D6B190EA204890A4998F50325DD67B42D81B2C30DE9D7F380(我的代码) 新sign值:D8BE851C430B79BD374FC4D406944C8E175642A791A831ED23D453302D634EC9(签名校验工具) 我现在唯一想到的方法:能否给段小程序生成签名的示例代码? 将以下StringA原字符串加密为签名校验工具相同的结果就可以,非常感谢! StringA: appid=wx8c6e071527acdd96&mch_id=1374850001&nonce_str=762ZnhskWpfrysj0&receiver= { "account":"oBGH2RefE5Jj6T3IUP05sc2lgCcx", "name": "阿江", "relation_type": "USER", "type": "PERSONAL_OPENID" } &sign_type=HMAC-SHA256&key=693o12Kl341eZ81n1A5732a58Eb15cGF 我的代码(云函数中调用): const CryptoJS = require('crypto-js') let partnerKey=693o12Kl341eZ81n1A5732a58Eb15cGF let StringA="appid=wx8c6e071527acdd96&mch_id=1374850001&nonce_str=762ZnhskWpfrysj0&receiver= { "account":"oBGH2RefE5Jj6T3IUP05sc2lgCcx", "name": "阿江", "relation_type": "USER", "type": "PERSONAL_OPENID" } &sign_type=HMAC-SHA256&key="+partnerKey StringA = CryptoJS.HmacSHA256(StringA, partnerKey) StringA = CryptoJS.enc.Hex.stringify(StringA) StringA = StringA.toUpperCase() XML测试数据: <xml> <appid>wx8c6e071527acdd96</appid> <mch_id>1374850001</mch_id> <nonce_str>762ZnhskWpfrysj0</nonce_str> <receiver> { "account":"oBGH2RefE5Jj6T3IUP05sc2lgCcx", "name": "阿江", "relation_type": "USER", "type": "PERSONAL_OPENID" } </receiver> <sign_type>HMAC-SHA256</sign_type> </xml> 非常感谢!
2020-07-20 - 2行代码实现小程序直接分享到微信朋友圈功能
期盼已久的小程序直接分享到朋友圈的功能,官方终于支持了。今天就来带大家实现小程序分享到朋友圈的功能。代码很简单。 老规矩,以图为证 新加分享到朋友圈的按钮 [图片] 分享到朋友圈的效果 [图片] 分享成功 [图片] 打开朋友圈分享链接后的效果 [图片] 可以看到底部有个前往小程序,这样我们就可以在朋友圈里直接打开小程序了。 是不是很激动,接下来就教大家如何实现小程序分享到朋友圈的功能吧。 小程序分享到朋友圈的代码编写 默认的分享到朋友圈的按钮是灰色的,如下图 [图片] 或许你都想象不到,小程序分享到朋友圈真是太简单了。只需要下面这几行代码,并且这几行代码是小程序页面的默认配置。 [图片] onShareAppMessage是我们默认就有的配置,也就是onShareTimeline是我们新加的,其实你只要配置好onShareTimeline这段代码,就可以轻松的实现小程序分享到朋友圈功能了。 来看下官方的文档 其实小程序分享到朋友圈只需要满足下图红色框里的两个条件。 [图片] 而这两个条件很好满足,就是我们这两行代码 [图片] 注意点 还有一点要注意的,就是我们要想使用小程序分享到朋友的功能,必须 1,使用最新版的开发者工具 2,使用最新版的调试库 [图片] [图片] 到这里我们就轻松的实现小程序分享到朋友圈的功能了,赶紧给你的小程序添加这个功能吧。
2020-07-12 - 云函数的环境变量有什么用
[图片] 比如这个scale 在函数中如何才能访问到? 是不是我对这个东西的理解有问题 这个环境变量到底是干什么用的?怎么用?
2019-04-28 - 云函数,取环境变量值取不到?
在云函数中配置了环境变量, 在云函数中调用获取时,undefined。 我有一个云函数,testService,配置了环境变量:userId 然后在云函数中获取:process.env.userId,值为undefined 有谁遇到这样的问题吗?
2020-04-05 - 为什么设置了云函数环境变量TZ 为 asia/shanghai仍然是UTC+0?
开发者版本号:1.03.2006090。不管我是否配置TZ变量,是否使用云端安装依赖,都是UTC+0时区,搞了好久没搞好。是跟我没有正式发布有关系吗?
2020-07-14 - 微信支付(企业付款到零钱和企业付款到银行卡条件)
企业付款到零钱 和 企业付款到银行卡条件 使用条件: 1、商户号已入驻90日 2、截止今日回推30天,商户号连续不间断保持有交易 3、 登录微信支付商户平台-产品中心,开通企业付款。 [图片] 温馨提示: 企业付款到零钱地址: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 企业付款到银行卡地址: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_1&index=1 作者:Ams
2020-12-01 - 微信支付APIv3的Nodejs版SDK,让开发变得简单不再繁琐
在向云端推送这个 [代码]wechatpay-axios-plugin[代码] 业务实现时,发现0.1系列还不够好用,还需要进行更多层级的包裹包装,遂再次做了重大更新,让SDK使用起来更简单、飘逸。 先看官方文档,每一个接口,文档都至少标示了[代码]请求URL[代码] [代码]请求方式[代码] [代码]请求参数[代码] [代码]返回参数[代码] 这几个要素,[代码]URL[代码] 可以拆分成 [代码]Base[代码] 及 [代码]URI[代码],按照这种思路,封装SDK其实完全就可以不用动脑,即,对[代码]URI[代码]资源的 [代码]POST[代码] 或 [代码]GET[代码] 请求(条件带上[代码]参数[代码]),取得[代码]返回参数[代码]。 更近一步,我们设想一下,如果把众多接口的[代码]URI[代码]按照斜线([代码]/[代码] [代码]slash[代码])分割,然后组织在一起,是不是就可以构建出一颗树,这颗树的每个节点(实体[代码]Entity[代码])都存在有若干个方法([代码]HTTP METHODs[代码]),这是不是就能把接口[代码]SDK实现[代码]更简单化了?! 例如: /v3/certificates /v3/bill/tradebill /v3/ecommerce/fund/withdraw /v3/ecommerce/profitsharing/orders /v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids/{appid} 树形化即: [代码]v3 ├── certificates ├── bill │ └── tradebill ├── ecommerce │ ├── fund │ │ └── withdraw │ └── profitsharing │ └── orders └── marketing └── busifavor └── users └── {openid} └── coupons └── {coupon_code} └── appids └── {appid} [代码] 按照这种树形构想,我们来看下需要做的[代码]封装实现[代码]工作: 把实体对象,按照实体的排列顺序,映射出请求的URI; 每个对象实体,包含有若干操作方法,其中可选带参数发起RPC请求; 随官方放出更多的接口,SDK需要能够弹性扩容; wechatpay-axios-plugin~0.2.0 版本实现了上述这3个目标,代码包如下截屏: [图片] 我们用伪代码来校验看一下这个[代码]封装实现[代码]: [代码]require('util').inspect.defaultOptions.depth = 10; const { Wechatpay } = require('wechatpay-axios-plugin'); const wxpay = new Wechatpay({mchid: '1', serial: '2', privateKey: '3', certs: {'4': '5'}}); wxpay.v3.certificates; wxpay.v3.bill.tradebill; wxpay.v3.ecommerce.fund.withdraw; wxpay.v3.marketing.busifavor.users['{openid}'].coupons.$coupon_code$.appids['wx233544546545989']; console.info(wxpay); //以下是输出内容 { entities: [], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], v3: { entities: [ 'v3' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], certificates: { entities: [ 'v3', 'certificates' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] }, bill: { entities: [ 'v3', 'bill' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], tradebill: { entities: [ 'v3', 'bill', 'tradebill' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] } }, ecommerce: { entities: [ 'v3', 'ecommerce' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], fund: { entities: [ 'v3', 'ecommerce', 'fund' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], withdraw: { entities: [ 'v3', 'ecommerce', 'fund', 'withdraw' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] } } }, marketing: { entities: [ 'v3', 'marketing' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], busifavor: { entities: [ 'v3', 'marketing', 'busifavor' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], users: { entities: [ 'v3', 'marketing', 'busifavor', 'users' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], '{openid}': { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], coupons: { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], '$coupon_code$': { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons', '{coupon_code}' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], appids: { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons', '{coupon_code}', 'appids' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], wx233544546545989: { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons', '{coupon_code}', 'appids', 'wx233544546545989' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] } } } } } } } } } } [代码] 注: API树实体节点,存储在每个 [代码]entities[代码] 属性上,方便后续的[代码]get[代码], [代码]post[代码] 抑或 [代码]upload[代码] 方法调用调用前,反构成最终请求的[代码]URI[代码];特别地,对于动态树实体节点来说,每个实体节点均提供了 [代码]withEntities[代码] 方法,用来在最终请求前,把动态实体节点替换成实际的值。 正常用法示例如下: [代码]const {Wechatpay} = require('wechatpay-axios-plugin'); const wxpay = new Wechatpay({/*初始化参数,README有*/}, {/*可选调整axios的参数*/}); //拿证书 wxpay.v3.certificates.get(); //带参申请交易账单 wxpay.v3.bill.tradebill.get({params: {bill_date}}); //带参发起账户余额提现 wxpay.v3.ecommerce.fund.withdraw.post({sub_mchid, out_request_no, amount, remark, bank_memo}); //查询用户单张券详情 wxpay.v3.marketing.busifavor.users['{openid}'].coupons.$coupon_code$.appids['wx233544546545989'].withEntities({openid, coupon_code}).get(); [代码] 请求APIv3是不是就“丧心病狂”般的简单了?! 详细功能说明及用法示例,npmjs及github的README均有。 如果喜欢,就给来个 赞 及 Star 吧。
2020-07-17 - 适合云开发的微信支付v2及v3版Nodejs SDK
接续微信支付APIv3的Nodejs版SDK,在2020这个时间节点,之所以再造一遍微信支付v2(相对于APIv3来说)的轮子,实属是无奈之举,线下交易场景常见的付款码支付及退款功能,官方当下还没有开放出来v3版的,只能借助v2接口来处理;[代码]wechatpay-axios-plugin[代码] 从一开始目标就是为云原生而设计,遂再造一遍轮子,也抽出一些共生方法,为v3而用。 设计思路 此类库核心部件是利用了Axios的transform功能,数据在内部流转过程中,会经过 [代码]transformRequest[代码] 及 [代码]transformResponse[代码] 处理,通过构造两个自定义transformer,完整实现v2版的技术规格要求,从而完成 HTTP 请求/响应 处理。 Transformer.request 方法返回值是个数组,含两个方法 [代码][signer, toXml][代码],字面意思即,对输入数据签名,然后转换成xml; Transformer.response 方法返回值是个数组,含两个方法 [代码][toObject, verifier][代码],字面意思即,返回值做数据转换为对象,然后校验签名; 证书设置 凡是涉及资金变动的接口,均需要商户证书,此实现同时支持 [代码]pem[代码] 及 [代码]p12[代码] 格式的证书,使用方法见随包README: [代码]const {Wechatpay, Formatter: fmt} = require('wechatpay-axios-plugin') const client = Wechatpay.xmlBased({ secret: 'your_merchant_secret_key_string', merchant: { cert: '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----', key: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----', // or // passphrase: 'your_merchant_id', // pfx: fs.readFileSync('/your/merchant/cert/apiclient_cert.p12'), }, }) [代码] 实力化一个 [代码]client[代码] 的最小参数为 [代码]secret[代码],即所谓的 密钥,字符串形式,32字节长度。 自定义打印日志 按需,如果需要检测类库的数据情况,在实例化完成后,可以加入如下类似两段代码,即可以打印出日志;当然也可以按需把日志输出至文件等,抛砖引玉而已。 [代码]//在格式转换完后,打印日志 client.defaults.transformRequest.push(data => (console.log(data), data)) //在请求返回,先行打印日志 client.defaults.transformResponse.unshift(data => (console.log(data), data)) [代码] 使用示例 实例化对象 [代码]secret[代码] 所对应的商户类型,可以是服务商、普通商户、特约商户,入参按照官方文档,手捋填入即可,以下几个方法,均测试过,正常运转。 申请退款 [代码]client.post('/secapi/pay/refund', { appid: 'wx8888888888888888', mch_id: '1900000109', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', total_fee: 100, refund_fee: 100, refund_fee_type: 'CNY', nonce_str: fmt.nonce(), }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] [代码]//log输入 { return_code: 'SUCCESS', return_msg: 'OK', appid: 'wx8888888888888888', mch_id: '1365319302', nonce_str: 'X8bpYtUJbPHK0Fyd', sign: '12BDC0390958455875108947AD51D897', result_code: 'SUCCESS', transaction_id: '4200000684202009114087736848', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', refund_id: '50300005642020091102621479983', refund_channel: '', refund_fee: '100', coupon_refund_fee: '0', total_fee: '100', cash_fee: '100', coupon_refund_count: '0', cash_refund_fee: '100' } [代码] 付款码支付 [代码]client.post('/pay/micropay', { appid: 'wx8888888888888888', mch_id: '1900000109', nonce_str: fmt.nonce(), sign_type: 'HMAC-SHA256', body: 'image形象店-深圳腾大-QQ公仔', out_trade_no: '1217752501201407033233368018', total_fee: 888, fee_type: 'CNY', spbill_create_ip: '8.8.8.8', auth_code: '120061098828009406', }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] 现金红包 [代码]client.post('/mmpaymkttransfers/sendredpack', { nonce_str: fmt.nonce(), mch_billno: '10000098201411111234567890', mch_id: '10000098', wxappid: 'wx8888888888888888', send_name: '鹅企支付', re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', total_amount: 1000, total_num: 1, wishing: 'HAPPY BIRTHDAY', client_ip: '192.168.0.1', act_name: '回馈活动', remark: '会员回馈活动', scene_id: 'PRODUCT_4', }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] 企业付款 [代码]client.post('/mmpaymkttransfers/promotion/transfers', { appid: 'wx8888888888888888', mch_id: '1900000109', partner_trade_no: '10000098201411111234567890', openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', check_name: 'FORCE_CHECK', re_user_name: '王小王', amount: 10099, desc: '理赔', spbill_create_ip: '192.168.0.1', nonce_str: fmt.nonce(), }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] 和v3一起用 [代码]const wxpay = new Wechatpay({ mchid: 'your_merchant_id', serial: 'serial_number_of_your_merchant_public_cert', privateKey: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----', certs: { 'serial_number': '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----', } }) [代码] Native下单 [代码]wxpay.v3.pay.transactions.native .post({/*文档参数放这里就好*/}) .then(({data: {code_url}}) => console.info(code_url)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询订单 [代码]wxpay.v3.pay.transactions.id['{transaction_id}'] .withEntities({transaction_id: '1217752501201407033233368018'}) .get({params: {mchid: '1230000109'}}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 关单 [代码]wxpay.v3.pay.transactions.outTradeNo['1217752501201407033233368018'] .post({mchid: '1230000109'}) .then(({status, statusText}) => console.info(status, statusText)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 创建商家券 [代码]wxpay.v3.marketing.busifavor.stocks .post({/*商家券创建条件*/}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询用户单张券详情 [代码];(async () => { try { const {data: detail} = await wxpay.v3.marketing.busifavor.users.$openid$.coupons['{coupon_code}'].appids['wx233544546545989'] .withEntities({openid: '2323dfsdf342342', coupon_code: '123446565767'}) .get() console.info(detail) } catch({response: {status, statusText, data}}) { console.error(status, statusText, data) } } [代码] 上传图片 [代码]const FormData = require('form-data') const {createReadStream} = require('fs') const imageMeta = { filename: 'hellowechatpay.png', // easy calculated by the command `sha256sum hellowechatpay.png` on OSX // or by require('wechatpay-axios-plugin').Hash.sha256(filebuffer) sha256: '1a47b1eb40f501457eaeafb1b1417edaddfbe7a4a8f9decec2d330d1b4477fbe', } const imageData = new FormData() imageData.append('meta', JSON.stringify(imageMeta), {contentType: 'application/json'}) imageData.append('file', createReadStream('./hellowechatpay.png')) Wechatpay.client.post('/v3/marketing/favor/media/image-upload', imageData, { meta: imageMeta, headers: imageData.getHeaders() }).then(res => { console.info(res.data.media_url) }).catch(error => { console.error(error) }) [代码] 下载账单并格式化 [代码]const assert = require('assert') const {Hash: {sha1}} = require('wechatpay-axios-plugin') Wechatpay.client.get('/v3/bill/tradebill', { params: { bill_date: '2020-06-01', bill_type: 'ALL', } }).then(({data: {download_url, hash_value}}) => Wechatpay.client.get(download_url, { signed: hash_value, responseType: 'arraybuffer', })).then(res => { assert(sha1(res.data) === res.config.signed, 'verify the SHA1 digest failed.') console.info(fmt.castCsvBill(res.data)) }).catch(error => { console.error(error) }) [代码] 委托营销 [代码](async () => { try { const res = await Wechatpay.client.post(`/v3/marketing/partnerships/build`, { partner: { type, appid }, authorized_data: { business_type, stock_id } }, { headers: { [`Idempotency-Key`]: 12345 } }) console.info(res.data) } catch (error) { console.error(error) } })() [代码] 查询投诉信息并解密 [代码];(async () => { try { const res = await Wechatpay.client.get('/v3/merchant-service/complaints', {params: { limit: 50, offset: 0, begin_date: (new Date(+new Date - 29*86400*1000)).toJSON().slice(0, 10), end_date: (new Date).toJSON().slice(0, 10), }}) // decrypt the `Sensitive Information` res.data.data.map(row => (row.payer_phone = rsa.decrypt(row.payer_phone, merchantPrivateKey), row)) console.info(res.data) } catch({response: {status, statusText, data, headers}, request, config}) { console.error(status, statusText, data) } })() [代码] TODO v2版的[代码]AES-256-ECB/PKCS7Padding[代码]未做封装,这个不难,npm上也有许多优秀的类库可用,暂且先这样。 写到最后 MIT开放源码@npm, github ,可用于企业商业用途。
2020-10-16 - 微信支付的“现金红包”产品是只能在公众号里面发放吗?
我们准备做一个红包发放小程序,涉及到微信支付给用户发红包。 考虑了“企业付款到零钱”和”现金红包“这两种方式。(小程序红包的场景值决定了它只能通过扫码领取,与业务场景不一致) ”企业付款到零钱“这种实现方式受制于每日10万的商户号限额(可能会超过这个量) ”现金红包“这种实现方式我看文档里面字段有特别写道公众号APPID,想求证一下是只能在公众号里面发吗?
2020-09-11 - 添加分账接收方接口,如果是小程序下的单要分账,接口中的公众账号ID是不是填 小程序的appid?
你好。[图片]
2020-07-28 - 今天学习了下小程序时间戳和日期格式化的相关问题
之前在看别人代码的时候,就看到云函数里面支持 db.serverDate() 云函数的时间使用 db.serverDate(),请求接口时的返回值形如:"2018-09-21T06:41:54.900Z" 的写法,好几次想要继续学习下,都忘记了,今天有空可以好好看看 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.serverDate.html 那么平时我们在往数据库里面存的时候,是倾向于时间戳还是日期呢? 时间戳转日期格式化函数 https://developers.weixin.qq.com/community/develop/doc/000e6adde049607a0c67aa8415b800 日期转时间戳相关问题 https://developers.weixin.qq.com/community/develop/doc/000e6c760b4e384894376dfb751000 其实这两个地方都是可以通过wxs来完成转化的 https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxs/
2020-05-15 - 「笔记」订阅消息-订阅次数维护
前言 距离1月10日模板消息下架只有2天了,在社区里经常能看到有帖子在问关于怎么记录订阅次数的问题,这里在这里介绍一下自己用的简单方案,仅供参考。 误区一 [图片] 上面这个图大家应该都比较熟悉了,很多人总是误以为勾选“总是保持以上选择,不再询问”,就可以无限发送订阅消息,这个是错误的想法,勾选和不勾选唯一的区别就是每次触发订阅的时候会不会弹授权窗口而已。 误区二 订阅消息不能通过bindsubmit的方式触发,必须通过bindtap的方式触发。 误区三 触发订阅窗口后,不管用户点击了允许还是取消,都会进入订阅消息的success回调中,所以通过这个来判断用户是否订阅是错误的。 订阅次数的维护 先看下官方的文档: [图片] 那么我们该如何使用呢? 我们通过 wx.requestSubscribeMessage 接口发送的时候是知道需要让用户订阅哪几个模板的,就是 tmplIds 这个参数填的数组。那么根据官方文档的回调内容,我们就可以直接在success内去获取对应的key所返回的状态。把获取到的状态分别存入自己的数据库里。发送的时候去数据库里查询需要发送的模板并且状态为accept的去发送,如果发送成功则删除一条记录(因为没有过期一说,所以随便删除哪一条记录都不影响)。 参考代码 [图片] 查询模板订阅状态 需要基础库大等于2.10.0才支持。 wx.getSetting({ withSubscriptions: true, success (res) { console.log(res) } }) 官方文档 补充 如果用户选择了不再接收消息会清空之前的订阅次数,但是这个不会主动告诉开发者,所以发送订阅消息失败后,需要根据返回内容自行清空记录,重新计算。 相关文章 「笔记」订阅消息-订阅次数维护(2020年3月更新改动) 「笔记」订阅消息体验踩坑
2020-03-06 - 史上最详细一步一步介绍微信小程序微信支付功能开发
前言微信小程序开发微信支付, 相对于微信的其他功能,实话说相比之下好太多,可能是开发文档是微信支付这边撰写的缘故吧?猜的。所以微信支付在小程序中,虽然参数十分的多,环节特别的细致,但也算不上无从下手。上个项目实施了一次微信小程序支付功能的开发,趁记忆力尚可,赶紧记录一番本次环境平台为 小程序端(uniapp)| 原生一样,只介绍js部分 + egg.js端(node)流程基本都一致,只是语法上有许区别开发准备工欲善其事,必先利其器。开发之前我们需要先准备好哪些必须的开发前提或环境呢?资料:1. 审核并开通微信商户号: •微信支付页链接地址[1][图片] •等待审核, 审核通过后对小程序进行关联[图片] •小程序开启支付[图片] •小程序支付开通后[图片] •打开微信支付页,可以进行绑定查看[图片] •再到微信支付中去记录mch_id,和merchantkey(商户密码),记录merchantkey的方式如图:[图片][图片] •记录下merchantkey ,注意:merchantkey是敏感key,不可泄漏 2. 前面开发好拉取用户的授权,获取用户在当下小程序中的openid(重点必须) 3. 搭建好服务器接口调用, 记录下需要传递给微信服务器使用回调的服务器ip地址以及接口的url地址(提前准备好,可以使用postman做好测试)。(可以为本服务器也可以为另外服务器,主要作为回调) 4. 其他:微信官方文档要求 审核支付功能需要微信小程序已上线,但是当时我申请的时候小程序并未上线也过了,所以这一块我无法做出解释。另外,程序访问商户服务都是通过HTTPS,开发部署的时候需要安装HTTPS服务器 开发流程5. 先来看看官方微信支付给出的流程图[图片] •感觉有点懵逼 •我总结如下:[图片] 开始开发1.根据以上流程图,我们开始进行调用 •第一步 小程序发起支付的代码如下: async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //后面的逻辑为第二阶段 }) } 注意,强烈推荐使用promise函数来实现,可以保证逻辑代码体在实现流程的一致性•第一步 后端node服务器接口获取支付第一部参数的代码如下: /** * 微信统一下单(微信支付)的接口数据(!!!!小程序专用付款方式) * @param {OBject} * 调用微信预支付接口(必填项) * @@排列顺序不可以错! * 1.appid * 2.body: 商品描述 * 3.mch_id: 支付申请配置的商户号 * 4.NonceStr: 随机字符串 * 5.notify_url: 微信付款后的回调地址 //后端egg的接口接收此地址来响应支付成功的回调 * 6.openid: * 7.out_trade_no: 订单号(32位) * 8.spbill_create_ip:后端调用API支付微信的ip地址 (支持32位和64位IP地址) * 9.total_fee: 支付金额 * 10. * /** * //生成微信支付的参数进行ASCII码从小到大排序 * @params: * body: 支付内容 * totalmoney: 支付金额 */ async getPrePayId(obj) { const { config, ctx } = this const { appid, merchantid, merchantkey } = config.wxapp //后台预先设置的appid,merchantid, merchantkey const { ip, notify_url } = config.payaddress const NonceStr = Math.random().toString(36).substr(2, 15) const orderid = uuid.v4().replace(/-/g, '') const { body, totalmoney, openid } = obj //预发起支付第一次签名 const uniorderParams = { appid, body, mch_id: merchantid, nonce_str: NonceStr, notify_url, openid, out_trade_no: orderid, spbill_create_ip: ip, total_fee: totalmoney, trade_type: 'JSAPI' } uniorderParams.sign = ctx.helper.getPreSign(uniorderParams, merchantkey) //根据上面的这个uniorderParams统一下单参数根据ASCii码从小到大排序,加上商户密钥做sign加密 let xml = ' ' + //重点, 必须使用xml格式来发送给微信服务器端 '' + uniorderParams.appid + ' ' + '' + uniorderParams.body + ' ' + '' + uniorderParams.mch_id + ' ' + '' + uniorderParams.nonce_str + ' ' + '' + uniorderParams.notify_url + '' + '' + uniorderParams.openid + ' ' + '' + uniorderParams.out_trade_no + '' + '' + uniorderParams.spbill_create_ip + ' ' + '' + uniorderParams.total_fee + ' ' + '' + uniorderParams.trade_type + ' ' + '' + uniorderParams.sign + ' ' + ''; const temp = await ctx.curl('https://api.mch.weixin.qq.com/pay/unifiedorder', { //统一下单的地址 method: 'POST', data: xml }) let result = {} if (temp.status == 200) { result = await ctx.helper.xmlToJson(temp.data.toString()) } /** * 获取预支付的sign签名 * 带字符串QUERY的URL的&拼接 */ getPreSign: (signParams, merchantkey) => { let keys = Object.keys(signParams).sort() let newArgs = {} keys.forEach( val => { if(signParams[val]) { newArgs[val] = signParams[val] } }) const string = queryString.stringify(newArgs) + '&key=' + merchantkey return crypto.createHash('md5').update(queryString.unescape(string), 'utf8').digest("hex").toUpperCase() } //二次签名... 1. 第一步中,如果 参数没问题,发送给微信服务器中会响应到一个prepay_id,而这个 prepay_id 就是预支付的code 2. 第二步,node服务器向微信服务器发起第二次签名,小程序端无感知 //二次签名 const paysign2 = { appId: result.appid, nonceStr: result.nonce_str, package: `prepay_id=${result.prepay_id}`, timeStamp: parseInt((Date.now() / 1000)).toString(), //注意:时间必须为秒 signType: 'MD5' } paysign2.paySign = ctx.helper.getPreSign(paysign2, merchantkey) const data = { paysign2, orderid } await ctx.model.Ordertable.create({ orderid, openid, money: totalmoney * 100, status: 0 }) //在这里我做了一个支付预处理落地到数据库的操作,当预支付 return data } 在这里我做了一个支付预处理落地到数据库的操作,当预支付通过,支付数据库插入一条status为0的待确认支付状态的数据1.第二步,第二次签名中的回调数据,一起通过接口返回给小程序端 async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //前面的逻辑为第一阶段 //下面的逻辑为第二阶段 //第二次签名 let einfo = temp.data //小程序使用wx.requestPayment拉去支付逻辑 wx.requestPayment({ timeStamp: parseInt(einfo.paysign2.timeStamp).toString(), nonceStr: einfo.paysign2.nonceStr, package: einfo.paysign2.package, signType: 'MD5', paySign: einfo.paysign2.paySign, success: async res => { if (res) { let checkdata = { nonceStr: einfo.paysign2.nonceStr, out_trade_no: einfo.orderid, sign: einfo.paysign2.paySign } //下面是第四步,省略... } }, fail: res => { console.log('@', res) reject(res) } }) } }) } 注意, wx.requestPayment的回调success , 并不一定获取到正确结果,严谨的说。由于发起支付后,后端(node) 发送成功支付后,微信服务器会要求后端进行支付成功数据回调的响应。微信支付的官方说明如下:1.第三步 回调 notify_url填写注意事项•notify_url需要填写商户自己系统的真实地址,不能填写接口文档或demo上的示例地址。•notify_url必须是以https://或http://开头的完整全路径地址,并且确保url中的域名和IP是外网可以访问的,不能填写localhost、127.0.0.1、192.168.x.x等本地或内网IP。•notify_url不能携带参数。[图片]1.node服务器中回调地址代码如图: /** * 确认支付之后的订单 * 回调(微信)再次签名(响应支付成功的结果) * @param {Object} * 1.必须给微信一个响应。支fu的结果, * */ async wxPayNotify(xmldata) { const { ctx } = this let result = await ctx.helper.xmlToJson(xmldata) if (result) { let resxml = ' ' + '' + '' + '' + '' + '' + '' + ' ' if (result.result_code == 'SUCCESS') { await ctx.model.Ordertable.update({ status: 1, transactionid: result.transaction_id, money: result.total_fee }, { where: { orderid: result.out_trade_no, openid: result.openid } }) } return resxml } } > 在这里我上面落地存储数据的支付表,在收到微信的支付成功的回调后,将状态status :0 改为1 表示支付明确 6. 第四步 主动查询 > 由于微信的回调是异步,前端不可能等待微信的回调再来进行下一步逻辑处理,万一网络波动或者其他因素导致微信服务器的回调迟迟没有到我们的数据库中来呢?所以我们需要自己主动发起查询支付结果的API > 此API为:[微信查询支付接口](https://api.mch.weixin.qq.com/pay/orderquery) > 小程序端发起查询请求给后端,后端再向微信服务器调取查询结果: > node服务器的代码如下:(本次逻辑十分重要,关系我们支付的闭环) /** * 微信支付主动调取查询订单状态API */ async checkWxPayResult(obj) { const { ctx, config } = this const { appid, merchantid, merchantkey } = config.wxapp const { nonceStr, out_trade_no } = obj let Params = { appid, mch_id: merchantid, nonce_str: nonceStr, out_trade_no: out_trade_no } Params.sign = ctx.helper.getPreSign(Params, merchantkey) let xml = ''; const temp = await ctx.curl('https://api.mch.weixin.qq.com/pay/orderquery', { method: 'POST', data: xml }) let result = {} if (temp.status == 200) { result = await ctx.helper.xmlToJson(temp.data.toString()) return result } } 7. 将响应结果发送给小程序端 + 小程序端支付的完整逻辑就呈现出来了,代码如下: async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //前面的逻辑为第一阶段 //下面的逻辑为第二阶段 //第二次签名 let einfo = temp.data //小程序使用wx.requestPayment拉去支付逻辑 wx.requestPayment({ timeStamp: parseInt(einfo.paysign2.timeStamp).toString(), nonceStr: einfo.paysign2.nonceStr, package: einfo.paysign2.package, signType: 'MD5', paySign: einfo.paysign2.paySign, success: async res => { if (res) { //虽然是res调取成功,但是我们并不需要这个参数的逻辑回调 let checkdata = { nonceStr: einfo.paysign2.nonceStr, out_trade_no: einfo.orderid, sign: einfo.paysign2.paySign } //下面是第四步,主动查询订单的支付情况 const result = await getWxappPayResult(checkdata) if (result.code == 200) { resolve(result.data) //最后,作为promise进行返回,此刻的支付是100%正确的。还可以参照落地的数据库表进行辅助对照 } } }, fail: res => { console.log('@', res) reject(res) } }) } }) } ``` 至此,小程序的支付就完成了。后续,此文仅仅是使用的node 作为后端, 实际上流程来说,JAVA,PHP等等语言来说,逻辑思路都基本一致的。[图片] 原创不易,喜欢的朋友麻烦点点关注!后续会写java版本的支付流程 ,敬请关注References[代码][1][代码] 微信支付页链接地址: https://pay.weixin.qq.com/
2020-04-29 - 云函数支付,统一下单,functionName:pay_cb 返回支付成功信息,怎么传回小程序 ?
我统一下单,我的functionName:pay_cb函数中返回 retrun{ errcode:0 ,errmsg:success}怎么传回小程序端呢?我现在只能在日志里面能看到这个返回值。 统一下单云函数: const res = await cloud.cloudPay.unifiedOrder({ "body" : "德海生鲜", "outTradeNo" : event.order_number_send, "spbillCreateIp" : "127.0.0.1", "subMchId" : "1591", "totalFee" : 1, "envId": "dehai-alpha-bq", "functionName": "pay_cb", }) console.log("bpay=",res) return res } "functionName": "pay_cb" 云函数: // pay_cb 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env:'dehai-alpha-bk9zq' }) const db = cloud.database() const _ = db.command exports.main = async (event, context) => { await db.collection('orders').doc(event.outTradeNo).set({ data:event }) return { errcode:0, errmsg:'SUCCESS' } }
2020-06-08 - 服务端调用CloudPay.unifiedOrder(),返回"受理关系不存在",请问如何解决?
今天看到云开发支付有新文档,迫不及待的试了一下,支付二维码也出来了,不过一细看,有问题的。 代码用的官方文档下面的示例,不过我按文档的要求把一些必填参考填写了下,还是不行,返回"受理关系不存在"。 errMsg: "cloud.callFunction:ok" requestID: "00223ddd-94e9-11ea-8f79-525400149ac0" result: errCode: 0 errMsg: "cloudPay.unifiedOrder:ok" payment: {timeStamp: "1589353904", nonceStr: "", package: "prepay_id=", signType: "MD5", paySign: "467EBDC447***********3F37"} returnCode: "FAIL" returnMsg: "受理关系不存在"
2020-05-13 - 实战丨如何制作一个完整的外卖小程序(已开源)
最近微信小店开放了,赶着微信全面开放之前,把自己的小程序开源出来给大家使用~ 小程序效果 [图片] [图片] [图片] 开发心得 如何在项目中集成云开发 一开始项目并非基于云开发而开发的,目前考虑用云开发,因此,需要在项目中开启云开发的相关选项。 首先,在小程序文件夹中建立 [代码]cloud[代码] 文件夹,并在package文件中配置,建立用户登录的云函数并上传到微信小程序云中。相关的操作可以参考官方文档。 我在项目目录中添加了 [代码]cloud[代码] 和 [代码]miniprogram[代码] 两个目录,并在 [代码]project.config.json[代码] 文件夹进行配置 [代码]{ "miniprogramRoot": "./miniprogram" "cloudfunctionRoot": "./cloud/" } [代码] 开通云开发 配置完成后,可以点击控制台中的「云开发」来开通云开发。 [图片] 在云开发的界面中配置,并开通云开发。 [图片] 开通数据库集合 云开发不会自动创建数据库集合,因此,你需要手动创建集合。分别创建 店铺表Seller、分类表Category、商品表Food、订单表Order、地址表Address、用户表*_User*。 [图片] 数据操作 有了数据库的表后,就可以在代码中对数据进行操作了。 下方是我进行目录操作的代码。 [代码]const db = wx.cloud.database() const { showModal } = require('../../utils/utils') Page({ onLoad: function(options) { // 管理员认证 getApp().auth() if (options.objectId) { // 缓存数据 this.setData({ isEdit: true, objectId: options.objectId }) // 请求待编辑的分类对象 db.collection('Category') .doc(options.objectId) .get() .then(res => { // 获取分类信息 this.setData({ category: res.data }) }) } }, add: function(e) { var form = e.detail.value if (form.title == '') { wx.showModal({ title: '请填写分类名称', showCancel: false }) return } form.priority = Number.parseInt(form.priority) // 添加或者修改分类 // 修改模式 if (this.data.isEdit) { const category = this.data.category db.collection('Category') .doc(category._id) .update({ data: form }) .then(res => { console.log(res) showModal() }) } else { db.collection('Category') .add({ data: form }) .then(res => { console.log(res) showModal() }) } }, showModal() { // 操作成功提示并返回上一页 wx.showModal({ title: this.data.isEdit ? '修改成功' : '添加成功', showCancel: false, success: () => { wx.navigateBack() } }) }, delete: function() { // 确认删除对话框 wx.showModal({ title: '确认删除', success: res => { if (res.confirm) { const category = this.data.category db.collection('Category') .doc(category._id) .remove() .then(res => { console.log(res) wx.showToast({ title: '删除成功' }) wx.navigateBack() }) } } }) } }) [代码] 联表查询 在使用数据库时,难免要进行联表查询,云开发支持在云函数侧进行联表查询,你可以参考我的代码,来实现联表查询的功能。 [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { const result = await db.collection('Food') .aggregate() .lookup({ from: 'Category', localField: 'category', foreignField: '_id', as: 'categories' }) .end() // .orderBy('priority', 'asc') // .get() console.log(result) return result.list } [代码] 文件上传 在小程序的操作中,难免会遇到需要进行图片上传的场景。在进行图片上传时,云开发提供了方便的云存储供我们查询数据。 在获取到文件的本地路径后,调用 [代码]wx.cloud.uploadFile[代码] 即可上传文件。 [代码]chooseImage() { wx.chooseImage({ count: 1, // 默认9 sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: res => { const tempFilePaths = res.tempFilePaths const file = tempFilePaths[0] const name = utils.random_filename(file) //上传的图片的别名,建议可以用日期命名 console.log(name) wx.cloud.uploadFile({ cloudPath: name, filePath: file, // 文件路径 }).then(res => { console.log(res) const fileId = res.fileID // 将文件id保存到数据库表中 db.collection('Seller').doc(this.data.seller._id) .update({ data: { logo_url: fileId } }).then(() => { wx.showToast({ title: '上传成功' }) // 渲染本地头像 this.setData({ new_logo: fileId }) }, err => { console.log(err) wx.showToast({ title: '上传失败' }) }) }) } }) } [代码] 微信支付逻辑的实现 作为一个商城,难免会有微信支付相关逻辑的实现。在这种情况下,可以借助云开发提供的微信支付云调用功能实现快速的 API 调用和接口的实现。 绑定商户 在使用云开发提供的微信支付时,需要先执行微信支付的绑定,在云开发控制台添加相应的商户号 [图片] 添加后微信会发来通知 [图片] 根据提示,开通账号即可。 [图片] 如果不绑定,将报“受理关系不存在”的错误 [图片] 函数代码调用 配置完成后,只需要在云函数中调用微信支付的接口,就可以实现相关调用的能力 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { console.log('请求中') console.log(cloud.getWXContext().ENV) let { orderId, amount, body } = event const wxContext = cloud.getWXContext() const res = await cloud.cloudPay.unifiedOrder({ body: body, outTradeNo: orderId, spbillCreateIp: '127.0.0.1', subMchId: '1447716902', totalFee: amount, envId: 'dinner-cloud', functionName: 'pay_cb' }) return res.payment } [代码] 这里 [代码]functionName: 'pay_cb'[代码]指的就是支付成功后,微信支付那侧给我的回调信息,后面我们就用它来更新我们的订单状态 小程序端代码调用 调用云函数后,会获得微信支付所需要的各种参数, [图片] 这个时候,就可以在小程序端调用微信支付接口,进行支付,相关代码可以参考 [代码]const { result: payData } = res wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'MD5', paySign: payData.paySign, success: res => { console.log('支付成功', res) wx.showModal({ title: '支付成功', showCancel: false, success: () => { // 跳转订单详情页 wx.navigateTo({ url: '/order/detail/detail?objectId=' + order._id }) } }) }, ... [代码] 微信支付回调处理 微信统一下单里一个pay_cb回调函数,它是一个云函数,后续微信支付的支付信息将会发送在这个函数中,相应的,我们需要编写处理的方法 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { console.log('支付回调') console.log(event) console.log(cloud.getWXContext().ENV) const orderId = event.outTradeNo const resultCode = event.resultCode if (resultCode === 'SUCCESS') { const res = await db .collection('Order') .doc(orderId) .update({ data: { status: 1 } }) console.log(res) return { errcode: 0 } } } [代码] 总结 云开发体验下来,优点自不必多说,微信登录与支付原生支持,调用与调试都很方便,特别是不用启本地服务开发,真的好用; 这个小程序的源码我已经开源了,你可以访问社区官网 获取源码,自行使用~ 作者:黄秀杰,16年开始从事小程序开发与技术布道,同名个人公众号「黄秀杰」。 云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 产品文档:https://cloud.tencent.com/product/tcb 技术文档:https://cloudbase.net 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2020-07-29 - 使用云开发支付,如何开启分账?
统一下单接口CloudPay.unifiedOrder设置 profit_sharing:'Y',无法把该订单设置成分账支付账单,需要如何操作才可以实现分账? 代码段如下: const res = await cloud.cloudPay.unifiedOrder({ "body": event.body,//event.goodName, //商品名称 或 商品描述 "outTradeNo": out_trade_no,//event.outTradeNoTo, //订单号 "spbillCreateIp": "127.0.0.1", //回调地址 "subMchId": mch_id, // 微信支付商户号 "totalFee": event.total_fee,//event.totalFee, //商品支付金额 单位(分) 100代表一块钱 "envId": "fd-bt-jsk",//"yicai-p6gne", //云开发环境ID "functionName": "MyPay_cb", //回调的云函数, "profit_sharing":'Y' //分账 }) 下单和支付是成功的,但就是分不了账
2020-09-25 - 新能力|云调用支持微信支付啦!
导语 小程序·云开发的云调用能力,让用户可以免鉴权快速调用微信的开放能力,极大节约了开发成本。现在,云调用已支持微信支付,用户在云开发控制台可直接绑定微信支付商户,在绑定完成后可在云开发中原生接入微信支付。 使用云开发的云调用来实现相应的支付功能后,开发者无需关心证书、签名、微信支付服务器端文档,使用简单,代码较少,只需要调用相应的函数即可。此外,因为云调用基于微信私有协议实现,官方通过服务商提供支付接口对接支持,不依赖第三方模块,免去泄漏证书、支付情况等其他敏感信息的风险。此外,云开发的云调用还支持云函数作为微信支付进行支付和退款的回调地址,不再需要定时轮询,更加高效。 云调用支付支持接口 云调用支付现已支持如下接口 统一下单接口 查询订单 关闭订单 下载对账单 申请退款 查询退款 如何接入 准备工作 微信开发者工具 Nightly 版 1.02.2005111 及更新的版本 需要已经开通了微信支付,且已绑定了商户号的小程序。 如何开通 在微信开发者工具中,使用绑定的微信小程序账号,打开云开发控制台,在云开发控制台中的 设置 - 全局设置 中添加商户号 [图片] 添加后,需要在绑定的商户号管理员在微信支付提供的【服务商助手】小程序上确认授权。 如果需要 jsapi 和 api 退款权限,需要前往微信支付商户平台我的授权产品中进行确认授权,完成授权后即可调用微信支付相关接口能力。 支付 Demo 代码 在云函数中,调用 cloudPay.unifiedOrder ,即可生成小程序侧调用支付接口所需请求结果 [代码]cloud.cloudPay.unifiedOrder({ "body" : "小秋TIT店-超市", "outTradeNo" : "${Date.now().toString().slice(3)}", "spbillCreateIp" : "127.0.0.1", "subMchId" : "1900009231", "subAppid" : "wxe5f52902cf4de896", "totalFee" : 1, "envId": "test-f0b102", "functionName": "pay_cb" }) [代码] 关键开发流程 小程序调用云函数,在云函数中调用统一下单接口,参数中带上接收异步支付结果的云函数名和其所在云环境 ID 统一下单接口返回的成功结果对象中有 payment 字段,该字段即是小程序端发起支付的接口(wx.requestPayment)所需的所有信息 小程序端拿到云函数结果,调用 wx.requestPayemnt 发起支付 支付完成后,在统一下单接口中配置的云函数将收到支付结果通知 支付回调 微信支付云调用在调用时,需要传递 envId 和 functionName 这两个参数,这两个参数将会在微信支付成功后,发送相应的消息通知,来告知开发者用户的支付状态。 相关文档 云调用微信支付能力说明:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/wechatpay.html API 文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/Cloud.CloudPay.html 视频教程 为了帮助你掌握云调用微信支付,这里为你准备了快速上手视频~快去试试吧~ https://www.bilibili.com/video/BV1Tz4y1d7CX 总结 云开发的微信支付云调用能力,可以让更多的开发者安全、快捷的实现支付,让企业的资金更加的安全。 小调研 云调用现在已经支持了微信支付,除了微信支付,你还有什么特别想要的功能么?不妨在下方评论区中留言告诉我们。
2020-09-14 - 云支付,pay_cb/functionName的正确打开姿势
纯代码:第一步就秒过: // pay_cb 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _ = db.command exports.main = async (event, context) => { //回调信息备案 await db.collection('cloudPay').doc(event.outTradeNo).set({ data:event }) //其他业务逻辑 return { errcode:0, errmsg:'SUCCESS' } } 更多内容: [图片]
2020-10-25 - 首测微信小商店开放组件
目前微信小店开放组件已经申请成功,且第一时间去体验了一把。 想介入 微信小店开放组件的开发者可以点击下方链接提交申请 https://developers.weixin.qq.com/doc/ministore/minishopopencomponent/Introduction.html 官方在审核完成后会推送申请成功的模板消息,且拉入官方组件问题反馈群 你的微信小程序后台也会增加小商店店开放组件 [图片] 点击去管理会打开对应小程序的小商店后台,此后台是对应你小程序申请的插件版小店后台 与 单独申请的小店后台不互通,需要单独提交资质审核签约 官方提供了几个组件开发者可通过路由跳转进入对应的组件页面 例如下方商品详情 const productId = [商品id] // 填写具体的商品Id wx.navigateTo({ url: plugin-private://wx34345ae5855f892d/pages/productDetail/productDetail?productId=${productId}, }); 文档的接入方式我不多赘述,可自行看文档,这里说一下文档没得 鄙人使用的是uni-app框架开发小程序所以说一下uniapp介入方式 找到manifest.json文件源码视图找到微信小程序配置 "mp-weixin": { /* 小程序特有相关 */ "appid": "wx2afea6afe2d23263", "setting": { "urlCheck": false, "es6": true, "postcss": true, "minified": true }, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于定位您是否位于图书馆范围内" } }, "requiredBackgroundModes": [ "audio" ], "usingComponents": true, "plugins": { "mini-shop-plugin": { "version": "1.0.63", // 必须是小程序购物组件最新版本号,微信开发者工具调试时可获取最新版本号(复制时请去掉注释) "provider": "wx34345ae5855f892d" // 必须填小程序购物组件appid,不要修改(复制时请去掉注释) } } }, 官方文档里提到的组件版本是1.1.0,介入后控制台报错提示找不到1.1.0,最终找到小程序插件信息最新版本才更新到1.0.63,不得不说这文档有点坑 附上插件的信息链接 https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx34345ae5855f892d&token=1836153220&lang=zh_CN 此时已经可以在小程序内使用插件了 去尝试一下 先在后台提交上架商品拿到SpuId [图片] /** * 商品详情 * @Author: wkiwi * @function: productDetail */ productDetail(productId){ uni.navigateTo({ url:`plugin-private://wx34345ae5855f892d/pages/productDetail/productDetail?productId=${productId}` }) }, 调用方法进入商品详情 [图片] 体验完美,可以说几分钟时间就集成了一个官方商城出来。 但是别高兴的太早,总结了本次介入的问题 1.文档问题,还在内测阶段可能不完善 存在版本问题 以及跳转说明问题 下方官方说默认不传tabId会进入全部分类 [图片] 可是并不如此,会toast提示tabId必须为 all/pendingPay/pendingRecevied/afterSale 中的一项 2.产品层面问题 在小程序内介入小店,大多应该就是充当官方商城的吧,但是商品列表组件竟然未提供,这是让开发者徒手再撸一个商品列表首页吗???? 目前用户想进入到商品列表必须通过商品详情左下方的店铺按钮才可以进入商品列表,造成入口较深,体验不佳 此时点击 店铺按钮 又造成另外一个问题,跳转进入商品列表也就是微信小商店的首页,此时头部没有返回按钮,点击手机的返回按钮会直接退出小程序 [图片] 猜测 店铺首页为小商店的tab页面,导致无法返回 此问题导致小程序退出率大大增加,运营辛辛苦苦留存的用户这么轻而易举让用户退出了?????? 此时小程序想浏览小程序本身的首页是必须通过清除小程序后台,重新点击进入才能回到小程序层级,否则你只能在插件版本的小商店内跳转!!! 本次体验整体感觉微信小商店 给小程序一个快速集成官方商城的一个解决方案,但是在产品上还有些问题,本阶段上述产品问题未解决我是不可能接入的,希望官方尽快完善上述问题 ----------------------------------接更新---------------------------------- 详情页点击首页按钮无法返回小程序首页解决方案 const miniShopPlugin = requirePlugin('mini-shop-plugin') miniShopPlugin.initHomePath('/pages/index/index') // /pages/index/index为自己小程序首页路径 进入小商店组件首页路径 wx.navigateTo({ url: `plugin-private://wx34345ae5855f892d/pages/home/home`, });
2020-10-30 - 请问大神们,为什么canvas不能动态设置大小
您好!大神!!! 我想通过canvas生成缩略图,但是老是只生了图片的一部份 (客户端 IPHONE7有此问题 开发者工具不会出现这个问题) WXML <canvas canvas-id="shareCanvas" style=" top:-{{cavHeight}}px;left:-{{cavWidth}}px; width:{{cavWidth}}px;height:{{cavHeight}}px;background:#ccc;"></canvas> JS 1.设置默认大小 data: { PicUrl: "", cavWidth:100, cavHeight:100, pixelRatio: device.pixelRatio }, 2.选择图片后自动调整画布大小 wx.getImageInfo({ src: PicUrl, success: function(res) { console.log(res); const ctx = wx.createCanvasContext('shareCanvas') console.log("画布信息"); console.log(ctx); var cavWidth = res.width;// / device.pixelRatio; var cavHeight = res.height;// / device.pixelRatio; console.log("画布宽度:"+ctx.width+"px 高度:"+ctx.height); console.log("图片宽度:" + res.width + "px 高度:" + res.height); console.log(ctx); page.setData({ cavWidth: cavWidth, cavHeight: cavHeight }) 最终客户端只显示 了图片的一部份 就是canvas的默认值 cavWidth:100, cavHeight:100, 而代码 page.setData({ cavWidth: cavWidth, cavHeight: cavHeight }) 并没有生效 抓狂了,谢谢大神们指引一下
2019-03-11 - this.init.bind(this)这种写法有没有api?
wx.createSelectorQuery() .select('#canvas') .fields({ node: true, size: true, }) .exec(this.init.bind(this))
2020-04-14 - Canvas2d wx.canvasToTempFilePath 在什么时候 调用才是最合适的?
1首先 我是在整个海报全部绘制完成后 在最后调用了wx.canvasToTempFilePath 发现只有背景 没有图片 2改进方法 定义一个变量 count = 0 在所有绘制图片的地方加上1 循环定时器判断绘制完成的图片的数量和想要绘制的图片数量是否相等,相等则调用,最后发现在模拟器每次都是正常的 但是在真机上 ios13 每次绘制头像的时候count都不会自增 导致数量不相等 不能绘制海报 下面是绘制图片的代码 // 绘制图片 drawImg = (ctx, url, x, y, w, h) => { // 创建图片 const img = this.canvas.createImage() // 设置图片路径 img.src = url img.onload = () => { // 图片加载完成 绘制图片 ctx.drawImage(img, x, y, w, h) this.imgCount++ // 绘制成功一张 把图片的个数+1 console.log(`绘制图片的count` , this.imgCount) } }
2020-03-12 - canvas 2D 真机不显示,开发工具上无任何问题?
<div class="poster-canvas" :style="{height:canvasHeight+'px'}"> <canvas type="2d" id="myCanvas" style="width:100%;height:100%;"></canvas> 开发工具上无任何问题,但真机调试/预览/发布至体验版都不显示canvas,控制台无报错,保存图片为空白 canvas用到的网络图片域名无问题,download,getImageInfo也都用了,都不行
2020-04-28 - TypeError: r.Canvas is not a constructor ?
[图片] canvas画布, 开发工具不报错,真机调试就出现错误: Unhandled promise rejection TypeError: r.Canvas is not a constructor
2019-10-31 - canvas 类型为2d时,使用CanvasContext.drawImage与预期的结果不符?
原本使用 var context = wx.createCanvasContext('firstCanvas') 这种方式来使用canvas,并且制作成分享图,得到的图片没有问题。后来优化成使用 2d 的方式来制作图片,ctx.drawImage 画出的图片却有问题 期望的结果: [图片] 得到的结果: [图片] 参数都是一样的,只是原先用的是图片本地路径,2d 用的是一个image, canvas的宽高为375*667: ctx.drawImage(path, 0, 0, 375, 375)
2020-02-19 - [开盖即食]小程序Canvas官方新版API实战
[图片] [图片] 最近本人在开发一个新项目的时候,注意到官方在2.9.0开始支持了一个canvas 2D的新API,同时对webGL上支持也有了很大的改进,相信很多人用canvas的组件做一些分享海报,战绩和新闻帖功能。 [图片] 这里是新的引入方式。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html 那么新的canvas2D API有啥好处呢? 原本的API微信有做一定的修改,现在全面支持源生H5 JS的写法,迁移H5的老代码变成更加容易,学习成本更低 修复了一些诡异的BUG,例如原本在IOS早期版本写法顺序会导致clip()图片裁切失效等~ 性能上的优化和提升,复杂动画上帧数明显 举例写法上的一些改变: 1、设置font的写法: [代码]//原本(传值的写法) ctx.setFontSize(20); ctx.fillText('MINA', 100, 100) ctx.draw() //现在(和源生H5写法一致,赋值) ctx.font = "16px"; ctx.fillStyle = 'blue'; //可以直接写颜色,原本的不支持 //不需要 ctx.draw() [代码] 2、获取并添加图片写法: [代码]//原本 //使用的是 wx.getImageInfo的方法 wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { console.log(res); ctx.drawImage(res.path, 0, 0); ctx.draw(true); }, fail: function (res) { //失败回调 } }); //现在 //可以直接img.onload调用 const headerImg = canvas.createImage(); headerImg.src = headImage;//微信请求返回头像 headerImg.onload = () => { ctx.save(); ctx.beginPath()//开始创建一个路径 ctx.arc(38, 288, 18, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.drawImage(headerImg,0,0); ctx.closePath(); ctx.restore(); } [代码] 3、将canvas生成虚拟地址便于下载(重点): [图片] 由于官方文档没有写清楚,误导了挺多人的。这里canvas对象必须通过选择器获取,并获得对应的node节点。 [代码]async saveImg() { let self = this; //这里是重点 新版本的type 2d 获取方法 const query = wx.createSelectorQuery(); const canvasObj = await new Promise((resolve, reject) => { query.select('#posterCanvas') .fields({ node: true, size: true }) .exec(async (res) => { resolve(res[0].node); }) }); console.log(canvasObj); wx.canvasToTempFilePath({ //fileType: 'jpg', //canvasId: 'posterCanvas', //之前的写法 canvas: canvasObj, //现在的写法 success: (res) => { console.log(res); self.setData({ canClose: true }); //保存图片 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 }) // setTimeout(() => { // self.setData({show: false}) // }, 6000); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") } else { util.showToast("请截屏保存分享"); } }, complete(res) { wx.hideLoading(); console.log(res); } }) }, fail(res) { console.log(res); } }, this) }, [代码] 分享个canvas海报的代码片段: [图片] 片段名: PoCf4emw7TgE 片段link: https://developers.weixin.qq.com/s/PoCf4emw7TgE [图片] [图片] 总结,相对之前还要看官方文档的canvas自定义API,现在写起来更加的方便,老代码迁移起来得心应手,只要你之前会canvas,那么各种效果和动画,拿来就怼,没什么大问题~ 一些奇怪的问题(注意!!!) canvas 2d 目前(2020年4月3日)还不支持真机调试,会报错!!! IDE工具 1.02.2003190 直接保存新版本canvas的API图片是打不开的,但是直接用手机保存在相册是没问题的,请更新到1.02.2003250 最新版即可解决~ 一些老款手机用新的API保存图片会有报错问题,如华为NOTE10,请更新系统到能支持的最新,且微信也是,即可解决~ 部分Android设备诡异的闪退和报错 [图片] 这种有可能是代码写法的问题,比如: [代码]//缺省写法 会导致部分Android机器 闪退 ctx.font = "bold 16px"; ctx.fillStyle = "#000" //在canvas 2D的写法中,所以写法必须规范且完整 ctx.font = "normal bold 12px sans-serif"; ctx.fillStyle = '#707070'; [代码] 所以在canvas 2D 的环境,所以写法必须原始且规范,不能用缺省写法,不然就会有诡异的闪退/报错。 后续:官方在7.0.13的Android版本已修复。 https://developers.weixin.qq.com/community/develop/doc/00088c13e1437890692afd8d85ec00 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-05-09 - 只有三行代码的神奇云函数的功能之三:100%成功获取unionid
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之三:100%成功获取unionid: 保证100%成功获取unionid,需要用户信息授权。 强调一下:这个100%是指必须绑定了开放平台,那么不管用户是什么情况,不管有没有关注公众号,一定100%能获取到unionid。 依然需要符合unionid机制:第1条。 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html js: getUserInfo: function (e) { app.globalData.userInfo = e.detail.userInfo if (!app.globalData.unionid ) { wx.cloud.callFunction({ name: 'login', data: { weRunData: wx.cloud.CloudID(e.detail.cloudID) } }).then(res => { app.globalData.unionid = res.result.weRunData.data.unionId }) } }, 其他功能: 神奇功能之四:获取电话号码: 还是这三行代码,获取用户的电话号码。 https://developers.weixin.qq.com/community/develop/article/doc/0006a8ec7ac860c94bf90a34f5d813 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, https://developers.weixin.qq.com/community/develop/article/doc/000ea802c00f70894cf9fe72556013 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 [图片]
2020-10-25 - 绑定了开放平台,通过wx.getUserInfo和wx.login,解密后没有unionid呢?
开放平台已经绑定了小程序,但是通过uinionid获取机制的第一条途径,获取不到用户的unionid,请问是怎么回事呢? [图片] 同一个开放平台下的网页应用就能获取到unionid;可是小程序获取不到,只有openid,请问是怎么回事呢?
2020-03-17 - canvas 快速生成心得 1
前言 由于接到需求需要使用 canvas 生成一些图片,然后每次改动的都不是特别多,但是真的很烦。网上也没有现成的demo,我决定自己new一个。(本期完成图片大小 位置 圆角的设置,字体大小 颜色 加粗 换行 省略 居中的功能) 代码 index.wxml 代码 [代码]<canvas class="canvas" canvas-id="shareCanvas"></canvas> [代码] index.wxss [代码].canvas { position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: calc(750px / 2 ); height: calc(1050px / 2 ); /* transform: translateY(-200%); */ z-index: 1; background: #98c379; } [代码] index.js [代码]const app = getApp() const ctx = wx.createCanvasContext('shareCanvas'); const keyValue = { 'image': 0, 'text': 1 } Page({ data: { canvasList:[ { type:keyValue.image, width:750, height:1050, x:0, y:0, url:'https://mg-shop-test.oss-cn-hangzhou.aliyuncs.com/public/xcx_images/index-share-list-001.png', borderRadius:0, }, { type: keyValue.image, width: 124, height: 124, x: 313, y: 48, url:'https://wx.qlogo.cn/mmopen/vi_32/UFBlX5BwfmflsuyEhGFSNCksLX33yicawcSx4rYRB25uTC7HkWgSiclKjWPkJor2BPPdXSG3FQuI0WDt5EAHQmsg/132', borderRadius: 62, }, { type:keyValue.text, text:'Hello World 的简介', width:550, lineHeight:40, fontSize: 32, fontWeight: 'bold', color: '#FFFFFF', lineNumber: '1', x: 100, y: 200, textAlign: 'center' }, { type:keyValue.text, text:' Hello World 中文意思是『你好,世界』。因为《The C Programming Language》中使用它做为第一个演示程序,非常著名,所以后来的程序员在学习编程或进行设备调试时延续了这一习惯。', width:550, lineHeight:40, fontSize:28, fontWeight:'normal', color:'#87CEEB', lineNumber:'max', x:100, y:300, textAlign:'right' },{ type: keyValue.text, text: '“Hello, world"程序是指在计算机屏幕上输出“Hello,world”这行字符串的计算机程序,“hello, world”的中文意思是“你好,世界。”。这个例程在Brian Kernighan 和Dennis M. Ritchie合著的《The C Programme Language》使用而广泛流行', width: 550, lineHeight: 36, fontSize: 26, fontWeight: 'normal', color: '#b3e98f', lineNumber: 3, x: 100, y: 550, textAlign: 'center' } ] }, onLoad: function () { this.downFile(); }, // 下载所需所需文件 downFile(index = 0){ let item = this.data.canvasList[index]; if(item.type == keyValue.image){ // 验证它的类型是否为 image wx.downloadFile({ url:item.url, complete:res=>{ if (res.errMsg == 'downloadFile:ok'){ item.tempFilePath = res.tempFilePath; item.x = item.x / 2; item.y = item.y / 2; item.width = item.width / 2; item.height = item.height / 2; item.borderRadius = item.borderRadius / 2; if (++index < this.data.canvasList.length) { // 验证是否为canvasList的最后一个 this.downFile(index); } else{ this.drawCanvas(); } } else{ console.error(res.errMsg); } } }) } else if (item.type == keyValue.text) { // 验证它的类型是否为 string item.x = item.x / 2; item.y = item.y / 2; item.width = item.width / 2; item.lineHeight = item.lineHeight / 2; item.fontSize = item.fontSize / 2; if (++index < this.data.canvasList.length) { this.downFile(index); // 验证是否为canvasList的最后一个 } else { this.drawCanvas(); } } }, // 绘制图片 drawCanvas(index = 0){ let item = this.data.canvasList[index]; if (item.type == keyValue.image){ if(item.borderRadius == 0){ // 验证是否需要绘制圆角 ctx.drawImage(item.tempFilePath, item.x, item.y, item.width,item.height); } else{ // 怎么使用官方文档说的明明白白,我这不过多结束 // https://developers.weixin.qq.com/miniprogram/dev/api/CanvasContext.clip.html ctx.beginPath() let borderRadius = Math.sqrt(Math.pow((item.height / 2), 2) + Math.pow((item.width / 2) - item.borderRadius, 2)); // 计算圆角的半径(原理下图1) ctx.arc(item.width / 2 + item.x, item.height / 2 + item.y, borderRadius , 0, 2 * Math.PI) ctx.clip() console.log(item.tempFilePath, item.x, item.y, item.width, item.height); ctx.drawImage(item.tempFilePath, item.x, item.y,item.width,item.height); ctx.restore() } ctx.save(); } else if(item.type == keyValue.text){ ctx.setFillStyle(item.color); // 设置字体颜色 ctx.setFontSize(item.fontSize); // 设置字体大小 let tempTextList = []; // 存储分割字符串使用 let tempIndexNumber = 0; for (let i = 1; i < item.text.length ; i++){ if (ctx.measureText(item.text.substring(tempIndexNumber, i)).width > item.width){ i--; tempTextList.push(item.text.substring(tempIndexNumber, i)); tempIndexNumber = i; } else if(i == item.text.length - 1){ tempTextList.push(item.text.substring(tempIndexNumber)); } } let tempLength = item.lineNumber == 'max' ? tempTextList.length : item.lineNumber; for(let i = 0 ; i < tempLength ; i++){ if(item.fontWeight == 'bold'){// 是否加粗字体 if (i == tempLength - 1 && tempLength < tempTextList.length){ // 验证是否超出了所需的行数限制 ctx.fillText(tempTextList[i].substring(0, tempTextList[i].length - 2) + '...', item.x, item.lineHeight * i + item.y + item.fontSize + 0.5); ctx.fillText(tempTextList[i].substring(0, tempTextList[i].length - 2) + '...', item.x + 0.5, item.lineHeight * i + item.y + item.fontSize); } else if (tempLength == tempTextList.length){ if(item.textAlign == 'center'){// 文字居中 let tempWidth = (item.width - ctx.measureText(tempTextList[i]).width) / 2; ctx.fillText(tempTextList[i], tempWidth + item.x, item.lineHeight * i + item.y + item.fontSize + 0.5); ctx.fillText(tempTextList[i], tempWidth + item.x + 0.5, item.lineHeight * i + item.y + item.fontSize); } else if(item.textAlign == 'right'){// 文字居右 let tempWidth = item.width - ctx.measureText(tempTextList[i]).width; ctx.fillText(tempTextList[i], tempWidth + item.x, item.lineHeight * i + item.y + item.fontSize + 0.5); ctx.fillText(tempTextList[i], tempWidth + item.x + 0.5, item.lineHeight * i + item.y + item.fontSize); } else { // 文字默认居左 不作处理 ctx.fillText(tempTextList[i], item.x, item.lineHeight * i + item.y + item.fontSize + 0.5); ctx.fillText(tempTextList[i], item.x + 0.5, item.lineHeight * i + item.y + item.fontSize); } } else{ ctx.fillText(tempTextList[i], item.x, item.lineHeight * i + item.y + item.fontSize + 0.5); ctx.fillText(tempTextList[i], item.x + 0.5, item.lineHeight * i + item.y + item.fontSize); } } else{ if (i == tempLength - 1 && tempLength < tempTextList.length){ ctx.fillText(tempTextList[i].substring(0, tempTextList[i].length - 2) + '...', item.x, item.lineHeight * i + item.y + item.fontSize); } else if (tempLength == tempTextList.length){ if (item.textAlign == 'center') {// 文字居中 let tempWidth = (item.width - ctx.measureText(tempTextList[i]).width) / 2; ctx.fillText(tempTextList[i], tempWidth + item.x, item.lineHeight * i + item.y + item.fontSize); } else if (item.textAlign == 'right') {// 文字居右 let tempWidth = item.width - ctx.measureText(tempTextList[i]).width; ctx.fillText(tempTextList[i], tempWidth + item.x, item.lineHeight * i + item.y + item.fontSize); } else { // 文字默认居左 不作处理 ctx.fillText(tempTextList[i], item.x, item.lineHeight * i + item.y + item.fontSize); } } else { // 文字默认居左 不作处理 ctx.fillText(tempTextList[i], item.x, item.lineHeight * i + item.y + item.fontSize); } } } } if (++index < this.data.canvasList.length) {// 验证是否绘制完成 this.drawCanvas(index); } else { // 渲染图片 ctx.draw(true,()=>{ this.savePhoto() }); } }, // 保存图片到本地 savePhoto(){ wx.canvasToTempFilePath({ destWidth: 750 / 2, destHeight: 1050 / 2, quality: 1, fileType: 'jpg', canvasId: 'shareCanvas', complete: res => { console.log(res); if (res.errMsg == 'canvasToTempFilePath:ok'){ wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, complete: res => { if (res.errMsg == 'saveImageToPhotosAlbum:ok') { wx.showToast('保存成功'); } else { wx.showToast('用户取消了保存'); } } }) } } }) } }) [代码] [图片] [图片] 效果图 小程序代码片段分享 结尾 头一次编写分享文章,如有编写错误或者写不好的地方希望大家多担待,如有不明白的地方请留言,我将对其解答。后期将会再为 canvas 迭代几个版本尽量能将所有样式能再canvas 中实现出来。 By:Axs
2019-03-25 - 云服务器调用security.imgSecCheck完成代码分享
云服务器代码: // 云函数入口文件 const cloud = require(‘wx-server-sdk’) cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const {value} = event; try { const res = await cloud.openapi.security.imgSecCheck({ media: { header: {‘Content-Type’: ‘application/octet-stream’}, contentType: ‘image/png’, value:Buffer.from(value) } }) return res; } catch (err) { return err; } } 本地函数: wx.chooseImage({count: 1}).then((res) => { if(!res.tempFilePaths[0]){ return; } console.log(JSON.stringify(res)) if (res.tempFiles[0] && res.tempFiles[0].size > 1024 * 1024) { wx.showToast({ title: ‘图片不能大于1M’, icon: ‘none’ }) return; } wx.getFileSystemManager().readFile({ filePath: res.tempFilePaths[0], success: buffer => { console.log(buffer.data) wx.cloud.callFunction({ name: ‘checkImg’, data: { value: buffer.data } }).then( imgRes => { console.log(JSON.stringify(imgRes)) if(imgRes.result.errorCode == ‘87014’){ wx.showToast({ title:‘图片含有违法违规内容’, icon:‘none’ }) return }else{ //图片正常 } [代码] } ) }, fail: err => { console.log(err) } } ) 我相信做出来的人很多,但是没有分享出来,我今天分享出来就是为了避免更多程序员不要在这种简单的问题上,浪费太多的时间,我就浪费了很多时间,兼职太坑爹了[代码]
2019-07-26 - 内容安全检测图片API:openapi.security.imgSecCheck完美解决方案。
背景需求: 我个人做了一款小程序的小游戏,本质是小程序。里面有个自定义图片的功能。用户从本地相册选一张图片进行裁剪,之后保存到缓存中或者上传到服务器。然后用户再用这张图片作为素材进行其它操作。这里就涉及到内容安全了,提交审核没有通过也是因为这个没有做内容安全。防止一些色情低俗的事情发生。 正文: 思路:相册选图片 --> 裁剪小的图片 --> 内容安全检测 --> 通过 --> 裁剪大的图片 --> 保存。 失败的原因:绝大多数是因为检测图片不能大于1M,而导致超时,或者是errCode:-1,又或者是其它问题。 [图片] [图片] 核心代码图片: [代码]默认裁剪小尺寸图片 (我的业务需求是正方形图片,也可动态计算宽高比例) [代码] [图片] 检测图片 部分iOS不兼容encoding: ‘ucs2’。注释掉就好了 [图片] [图片] 云函数 [图片] 测试情况: 正常图片不含违法违规,测试20次,全部通过。小程序上线后暂无发现检测失败情况。百度搜索的“人体油画”等等均可通过。 PS:第一次写经验分享哈,看不懂可以问我。体验一下我的小程序想问我这个小程序其它的功能点也可以喔! 技术会迭代更新,用到的技术会有时效性,看编辑时间,可能当时的技术现在不适用了
2020-10-22 - 小程序云开发攻略,最棘手的问题
背景 最近小程序非常的火,应公司业务发展要求,开发维护了几款小程序,公司开发的小程序都是由后端提供的接口,开发繁琐而复杂,直到小程序出现了云开发,仔细研读了文档之后,欣喜不已,于是我着手开发了本人的第一款小程序 小程序云开发教程地址 点我查看>> 分析 云开发为开发者提供完整的原生云端支持和微信服务支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,同时这一能力,同开发者已经使用的云服务相互兼容,并不互斥。 优势 无需自建服务器,数据库,无需自建存储和CDN 数据库模型很简单,就是一个json形式的对象格式 调用服务端云函数自动获取openid,再也没有繁琐的授权登陆流程了,只要进入小程序就是登陆状态,体验真的好 开发迅速,只需要前端就能搞定所有开发工作 需要解决的问题 数据库切换问题 使用过云开发的人都发现云开发切换数据库环境是最头疼的,如果手动去切换容易搞错,不小心在当前环境修改了线上数据库数据 直到官方出了这个函数问题也就迎刃而解 [代码]cloud.updateConfig({ env: ENV === 'local' ? 'dev-aqijb' : ENV }); [代码] 我使用的是服务端云开发功能,为什么要这样判断,因为在开发工具中ENV = ‘local’,所以这么判断一下,保证开发工具中使用的是测试环境数据库 使用taro多端开发框架,借助于webpack,还可以通过process.env.NODE_ENV值区分当前代码开发环境 [代码]await Taro.cloud.init({ env: `${process.env.NODE_ENV === 'development' ? 'dev-aqijb' : 'pro-hljv7'}` /* env: 'pro-hljv7' */ }); [代码] 这样可以保证开发环境和线上环境可以使用对应环境的数据库 数据库字段定义问题 因为JS是弱类型语言,不能像typescript那样静态定义变量类型,这样添加到数据库的字段数量和字段类型都无法控制 我不想用typescript,能不能实现这样的功能呢,可以用superstruct库来实现这个功能 superstruct git地址 点我查看>> 详细使用案例见下方代码 函数文件太多的问题 官方和他人教程的例子都是一个文件对应一个云函数,通过开发体验我发现这样做并不好,当项目有多个表的时候,找个函数文件真的太难了 我们可以将一个表的增删改查函数全部写入一个文件中 教程: 首先每个云函数文件中package.json引入superstruct [代码]{ "dependencies": { "wx-server-sdk": "latest", "superstruct": "latest" } } [代码] 以下代码是一个完整的云函数例子 [代码]const cloud = require('wx-server-sdk'); const { struct, superstruct } = require('superstruct'); cloud.init(); //小区信息 const Model = () => { const db = cloud.database(); const _ = db.command; const collection = db.collection('address'); return { async add(data) { try { data = struct({ name: 'string', //名字 phone: 'string', unit: 'number', //楼单元号 doorNumber: 'string', //门号 communityId: 'string', //小区id _openid: 'string' //用户的id //isDefault: 'boolean' //是否默认地址 })(data); } catch (e) { const { path, value, type } = e; const key = path[0]; if (value === undefined) { const error = new Error(`${key}_required`); error.attribute = key; throw error; } if (type === undefined) { const error = new Error(`attribute_${key}_unknown`); error.attribute = key; throw error; } const error = new Error(`${key}_invalid`); error.attribute = key; error.value = value; throw error; } let res = await this.getList({ _openid: data._openid }); if (res.data.length >= 1) { return { msg: '当前只支持保存一个地址' }; } res = await collection.add({ data, createTime: db.serverDate(), updateTime: db.serverDate() }); return res; }, async getAdressById({ _openid, _id }) { const user = await collection .where({ _openid, _id: _.eq(_id) }) .get(); return user; }, //更新指定的id 先判断手机号修改没,没修改直接就改数据,修改过判断一下库中有没有这条数据 async update(data) { //更新表的操作 }, //删除指定id的shop async remove({ _id, _openid }) { //删除表的操作 }, /** * 获取商列表 * @param {*} option {category 类别, pagenum 页码} */ async getList({ _openid }) { const shopList = await collection .where({ _openid }) .get(); return shopList; } }; }; exports.main = async (event, context) => { const { func, data } = event; const { ENV, OPENID } = cloud.getWXContext(); // 更新默认配置,将默认访问环境设为当前云函数所在环境 console.log('ENV', ENV); cloud.updateConfig({ env: ENV === 'local' ? 'dev-aqijb' : ENV }); let res = await Model()[func]({ ...data, _openid: OPENID }); return { ENV, data: res }; }; [代码] 函数使用方式 [代码]wx.cloud.callFunction({ 'address', //云函数文件名 data: { func: 'add', //云函数中定义的方法 data: {} //需要上传的数据 } }); [代码] 图片 视频等文件 直接打开云开发控制台选择存储直接上传文件,复制url地址就可以放到代码中使用了 扫码体验我的小程序: [图片]
2019-09-29 - 教大家用20行js代码,开发好小程序订阅消息
微信小程序官方决定在2020-1-10全面线下小程序模板消息,要去替换为订阅消息。那对开发者而言,又要一个一个地方去修改代码兼容.... 所以我替大家写了段代码,来快速解决问题。复制下面这段代码到app.js文件最上面即可解决问题。代码的主要功能是在每一个tap类型的点击事件中触发订阅弹窗,这样用户点几次界面,你就可以发几次消息。这也是让发送次数最大化,不可能比这个次数还多了。 预期结果是:用户点几次弹窗,就会注意到有一个不再提醒按钮,一旦选了它,那你就可以随便发订阅消息了! // 记录原Page方法 const originPage = Page; // 重写Page方法 Page = (page) => { Object.keys(page).forEach(function(key){ if(key !== 'data'){ let originMethod = page[key]; page[key] = function () { let e = arguments[0]; //给所有的点击事件增加订阅消息弹窗 if(!!e && !!e.type && e.type === 'tap'){ wx.requestSubscribeMessage({ tmplIds: ['3E66jPXafsnikZoQR5uk0OUzIUVASZE5scyAu5YCHPI'], ////////这里替换为自己的模板ID///// success (res) { // console.log(res) }, fail (res) { // console.log('订阅消息失败',res) } }) } return originMethod.call(this,...arguments) } } }); return originPage(page); };
2020-01-09 - 小程序初级指南--图片及其优化
图片格式 开发中常见的图片格式有 GIF、PNG8、PNG24、JPEG、WEBP。 我们需要根据图片格式的特性和场景需要选取适合的图片格式,而不是设计给什么用什么。 PNG PNG 的目的是替代GIF和TIFF文件格式,同时增加一些GIF文件格式所不具备的特性。流式网络图形格式(Portable Network Graphic Format,PNG)名称来源于非官方的“PNG’s Not GIF”,是一种位图文件(bitmap file)存储格式,读成“ping”。PNG用来存储灰度图像时,灰度图像的深度可多到16位,存储彩色图像时,彩色图像的深度可多到48位,并且还可存储多到16位的α通道数据。PNG使用从LZ77派生的无损数据压缩算法。 特性 支持256色调色板技术,文件体积小。无损压缩最高支持48位真彩色图像以及16位灰度图像。支持Alpha通道的透明/半透明特性。支持图像亮度的Gamma校准信息。支持存储附加文本信息,以保留图像名称、作者、版权、创作时间、注释等信息。渐近显示和流式读写,适合在网络传输中快速显示预览效果后再展示全貌。使用CRC防止文件出错。最新的PNG标准允许在一个文件内存储多幅图像。 更多 PNG官方站 - PNG General Information PNG格式 维基百科 - PNG JPEG JPEG是一种针对照片视频而广泛使用的一种有损压缩标准方法.特性 适用于储存24位元全采影像采取的压缩方式通常为有损压缩不支持透明或动画压缩比越高影像耗损越大,失真越严重压缩比在10左右肉眼无法辨出压缩图与原图的差别更多 维基百科 - JPEG WEBP WebP,是一种同时提供了有损压缩与无损压缩的图片文件格式,WebP支持无损压缩和透明色的功能。特性 同时提供有损压缩和无损压缩两种图片文件格式文件体积小,无损压缩后,比 PNG 文件少了 45% 的文件大小;有损压缩后,比 JPEG 文件少了 25% - 34% 文件大小浏览器兼容差,目前只支持客户端 Chrome 和 Opera 浏览器以及安卓原生浏览器(Andriod 4.0+),WebP兼容性更多 更多关于WebP: 维基百科 - WEBP WEBP探寻之路 GIF GIF图象是基于颜色列表的(存储的数据是该点的颜色对应于颜色列表的索引值),最多只支持8位(256色)。GIF文件内部分成许多存储块,用来存储多幅图象或者是决定图象表现行为的控制块,用以实现动画和交互式应用。特性 优秀的压缩算法使其在一定程度上保证图像质量的同时将体积变得很小。可插入多帧,从而实现动画效果。可设置透明色以产生对象浮现于背景之上的效果。由于采用了8位压缩,最多只能处理256种颜色,故不宜应用于真彩色图片更多 维基百科 - GIF GIF文档 团队约定 内容图 内容图多以商品图等照片类图片形式存在,颜色较为丰富,文件体积较大。优先考虑 JPEG 格式,条件允许的话优先考虑 WebP 格式尽量不使用PNG格式,PNG8 色位太低,PNG24 压缩率低,文件体积大 背景图 背景图多为图标等颜色比较简单、文件体积不大、起修饰作用的图片。PNG 与 GIF 格式,优先考虑使用 PNG 格式,PNG格式允许更多的颜色并提供更好的压缩率图像颜色比较简单的,如纯色块线条图标,优先考虑使用 PNG8 格式,避免不使用 JPEG 格式图像颜色丰富而且图片文件不太大的(40KB 以下)或有半透明效果的优先考虑 PNG24 格式图像颜色丰富而且文件比较大的(40KB - 200KB)优先考虑 JPEG 格式条件允许的,优先考虑 WebP 代替 PNG 和 JPEG 格式 优化 图片是页面显示中很重要的部分,图片加载关系到用户体验、应用性能常见处理方式 减少文件体积大小 上线的图片都应该经过压缩处理,压缩后的图片不应该出现肉眼可感知的失真区域。压缩优化图片大小 采用合适的图片格式 减少图片资源请求数 合成雪碧图使用建议 适合使用频率高更新频率低的小图标尽量不留太多的空白体积较大的图片不合并确保要合并的小图坐标数值和合并后的 Sprites 图尺寸均为偶数预加载 图片预加载可以提高用户体验,对于图片长列表和图片占比很大的背景图尤其重要。 css 预加载 利用css的background属性可以预先加载图片。加载后隐藏。在其他地方在请求一样的地址时会优先去加载缓存内的图片进行显示,达到一个预加载的效果。不好的地方就是会影响影响页面渲染速度 显性预加载 显性预加载指的则是处于预加载过程时页面有明确的加载提示,比如进度条或者是Loading图标,让用户有个心理预期,减少等待的烦躁感。 隐形预加载(基于用户行为的资源预加载 通过触屏页面进度加载对应的资源。常见tabs切换,通常的处理是当用户去点击选项卡按钮的时候,在对应面板呈现的时候,我们再去加载图片内容。于是,就存在这样一个不好的体验——由于内容呈现瞬时,而图片呈现是异步的,就很容易出现选项卡主体内容切换过来了,结果是个空白,过了会儿图片才出现。 预加载组件 先加载一张缩略图,该缩略图通过样式设置为和原图一样的宽高,这样用户首先能很快速地看到一张模糊的图片,此时再去对原图做预加载,加载完成之后对缩略图进行替换,因为此时图片已经下载过了,所以界面上能无缝地切换为原图显示 链接:https://aotu.io/notes/2017/01/06/wxapp-img-loader/index.html 懒加载 指的是图片在页面渲染的时候先不加载,页面渲染完成后在指定动作触发后再加载图片。这种方式通常比较合适于篇幅较长的页面,并且图片内容的重要性低于页面信息内容,能够快速地先将重要的页面信息呈现给用户。 lazy-load image 自带属性。 图片懒加载,在即将进入一定范围(上下三屏)时才开始加载。lazy-loadbooleanfalse图片懒加载,在即将进入一定范围(上下三屏)时才开始加载 官方推荐优化方式--关于图片资源的优化 目前图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面。建议开发者尽量减少使用大图片资源 控制代码包内的图片资源 小程序代码包经过编译后,会放在微信的 CDN 上供用户下载,CDN 开启了 GZIP 压缩,所以用户下载的是压缩后的 GZIP 包,其大小比代码包原体积会更小。 但我们分析数据发现,不同小程序之间的代码包压缩比差异也挺大的,部分可以达到 30%,而部分只有 80%,而造成这部分差异的一个原因,就是图片资源的使用。GZIP 对基于文本资源的压缩效果最好,在压缩较大文件时往往可高达 70%-80% 的压缩率,而如果对已经压缩的资源(例如大多数的图片格式)则效果甚微。 写在最后 凡事都是实践出真知。围绕着业务,切合实际的进行优化处理。 不要为了优化而优化。 参考链接: https://guide.aotu.io/index.html https://aotu.io/notes/2017/01/06/wxapp-img-loader/index.html https://developers.weixin.qq.com/miniprogram/dev/framework/
2019-12-30 - 小程序红包配置及开发小结
配置: 1、进入商户平台 在产品中心找到小程序红包 开通小程序红包功能 2、开通后在左边的APPID授权管理中关联该小程序APPID 3、进入小程序后台 在功能==》微信支付中确认关联并授权 4、回到商户平台APPID授权管理中确认关联 5、这是最容易忽略的一点 在商户平台 产品中心 小程序红包的产品设置中 拉到最下面 小程序红包权限中开通该小程序的红包功能 到此小程序红包配置完成 开发: 发送红包 var mdhbhe = Convert.ToInt32(fee * 100); string mch_billno = mdminihb.Mch_id + DateTime.Now.ToString("yyyyMMdd") + GenerateNonceStr(); WxPayData hb = new WxPayData(); hb.SetValue("act_name", mdminihb.Act_name);//活动名称 hb.SetValue("mch_billno", mch_billno);//单号 hb.SetValue("mch_id", mdminihb.Mch_id);//发送红包的商户号 hb.SetValue("nonce_str", GenerateNonceStr()); hb.SetValue("notify_way", "MINI_PROGRAM_JSAPI"); hb.SetValue("re_openid", openid); hb.SetValue("remark", mdminihb.Remark); hb.SetValue("send_name", mdminihb.Send_name);//商户名称 hb.SetValue("total_amount", mdhbhe);//红包金额 单位分 hb.SetValue("total_num", 1);//红包数量 hb.SetValue("wishing", mdminihb.Wishing);//祝福语 hb.SetValue("wxappid", mdminihb.Wxappid);//绑定在商户的小程序的appid 不是公众号的 hb.SetValue("scene_id", mdminihb.Scene_id); var sign = hb.MakeSign2(mdminihb.Mch_key);//商户秘钥 hb.SetValue("sign", sign); string xml = hb.ToXml(); string response = HttpService.HbPost(xml, url, true, 6, mdminihb.Mch_path, mdminihb.Mch_certkey); WxPayData result = new WxPayData(); result.FromXml(response);//将xml格式的结果转换为对象以返回 var package = ""; if (result.GetValue("return_code").ToString() == "SUCCESS" && result.GetValue("result_code").ToString() == "SUCCESS") { //这边是成功后返回的代码 具体逻辑判断自己处理 package = result.GetValue("package").ToString();//成功后返回的 package = HttpUtility.UrlEncode(package); //这是用于领取红包的代码 WxPayData inputObj = new WxPayData(); inputObj.SetValue("appId", mdminihb.Wxappid);//这边是小程序的appId 这个appId 一定要记住 I要大写 inputObj.SetValue("timeStamp", timeStamp); inputObj.SetValue("nonceStr", nonceStr); inputObj.SetValue("package", package); var paySign = inputObj.HBMakeSign(mdminihb.Mch_key);//商户秘钥 } 签名方法: public string MakeSign2(string key) { //转url格式 string str = ToUrl(); //在string后加入API KEY str += "&key=" + key + ""; var rd = Md5.md5(str, 32); // 所有字符转为大写 return rd.ToUpper(); } 还有记得带证书 写的比较笼统 有不清楚的再补充 补充说明1:目前小程序红包仅支持用户微信扫码打开小程序,进行红包领取。(场景值1011,1025,1047,1124,小程序场景值详情参见文档 这个条件一定要注意 所以特别注意一定要通过wx.getLaunchOptionsSync()先看下场景值对不对 特别说明 体验版的二维码是无法领取红包的(第三方的要注意) 补充说明2:第二次领取红包的签名不需要大写
2020-01-02 - Ai onnx 推理实践课程
AI ONNX推理实践课程旨在帮助学员掌握ONNX模型推理的基本应用和实践技巧,从而能够在小程序中使用人工智能模型。本课程适合对模型部署感兴趣的初学者、开发者及研究人员。
07-22 - 云开发,获取群ID——调试出来真的很简单。
1 app.js中 onLaunch: function (options) { if (!wx.cloud) console.error(‘请使用 2.2.3 或以上的基础库以使用云能力’) else wx.cloud.init({ traceUser: true, }) [代码]if (options.shareTicket) wx.getShareInfo({ shareTicket: options.shareTicket, success: function (res) { console.log('getShareTiket---shareTicket-->res', res) //获取cloudID let cID=res.cloudID //调用云函数mytest wx.cloud.callFunction({ name: 'mytest', // 这个 CloudID 值到云函数端会被替换 data: { weRunData: wx.cloud.CloudID(cID) }, success: function (res) { console.log('wx cloud mytest fun res', res); } }) } }) [代码] }, 2 云函数mytest const cloud = require(‘wx-server-sdk’) cloud.init() exports.main = (event, context) => { return { event } } /console.log(‘wx cloud mytest fun res’, res);查看打印出来的res, 真是一个惊喜。 不用npm,不用加密解密,不用传数据到自己开发服务器上。哎,一个群ID花了我好多时间啊,最后到底是迎来柳暗花明了。/
2019-09-24 - 小程序码二进制流如何生成二维码图片?
- 需求的场景描述(希望解决的问题) getWXACodeUnlimit接口从微信后台获取到了小程序码的二进制流。 如何用 获得的小程序码二进制流 生成 可见的小程序图片? 试过file.writeFile() 生成的文件,不能用图片编辑器查看 - 希望提供的能力 ++++++++++++++++++++++++++++++ 分割线 +++++++++++++++++++++++++++++++ 最后还是没有解决,我是用了第三方的后台,直接生成的
2019-01-08 - wxacode.getUnlimited,python后台调用,如何处理二进制流图片?
请教,后台post请求获取的数据resp类型是response obj,resp.text得到string类型。str.encode(resp.text)得到byte(二进制),写入jpg或者png格式的图片中打开图片显示错误,无法打开,求解; 另外,是不是开发版的小程序是无法生成正确的二维码的,导致图片错误?那我如何才能测试我是生成了正确的二维码呢,谢谢! [图片] [图片]
2019-09-23 - 小程序如何生成海报分享朋友圈
摘要: 小程序开发必备技能啊… 原文:小程序如何生成海报分享朋友圈 作者:小白 Fundebug经授权转载,版权归原作者所有。 项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。 需求 利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。 生成的海报如下: [图片] 需求分析 1、利用小程序官方提供的api可以直接分享转发到微信群打开小程序 2、利用小程序生成海报保存图片到相册分享到朋友圈,用户长按识别二维码关注公众号或者打开小程序来达到裂变的目的 实现方案 一、分析如何实现 相信大家应该都会有类似的迷惑,就是如何按照产品设计的那样绘制成海报,其实当时我也是不知道如何下手,认真想了下得通过canvas绘制成图片,这样用户保存这个图片到相册,就可以分享到朋友圈了。但是要绘制的图片上面不仅有文字还有数字、图片、二维码等且都是活的,这个要怎么动态生成呢。认真想了下,需要一点一点的将文字和数字,背景图绘制到画布上去,这样通过api最终合成一个图片导出到手机相册中。 二、需要解决的问题 二维码的动态获取和绘制(包括如何生成小程序二维码、公众号二维码、打开网页二维码) 背景图如何绘制,获取图片信息 将绘制完成的图片保存到本地相册 处理用户是否取消授权保存到相册 三、实现步骤 这里我具体写下围绕上面所提出的问题,描述大概实现的过程 ①首先创建canvas画布,我把画布定位设成负的,是为了不让它显示在页面上,是因为我尝试把canvas通过判断条件动态的显示和隐藏,在绘制的时候会出现问题,所以采用了这种方法,这里还有一定要设置画布的大小。 [代码]<canvas canvas-id="myCanvas" style="width: 690px;height:1085px;position: fixed;top: -10000px;"></canvas> [代码] ②创建好画布之后,先绘制背景图,因为背景图我是放在本地,所以获取 <canvas> 组件 canvas-id 属性,通过createCanvasContext创建canvas的绘图上下文 CanvasContext 对象。使用drawImage绘制图像到画布,第一个参数是图片的本地地址,后面两个参数是图像相对画布左上角位置的x轴和y轴,最后两个参数是设置图像的宽高。 [代码]const ctx = wx.createCanvasContext('myCanvas') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) [代码] ③创建好背景图后,在背景图上绘制头像,文字和数字。通过getImageInfo获取头像的信息,这里需要注意下在获取的网络图片要先配置download域名才能生效,具体在小程序后台设置里配置。 获取头像地址,首先量取头像在画布中的大小,和x轴Y轴的坐标,这里的result[0]是我用promise封装返回的一个图片地址 [代码]let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36; //绘制的头像在画布上的位置 ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 [代码] 这里举个例子说下如何绘制文字,比如我要绘制如下这个“字”,需要动态获取前面字数的总宽度,这样才能设置“字”的x轴坐标,这里我本来是想通过measureText来测量字体的宽度,但是在iOS端第一次获取的宽度值不对,关于这个问题,我还在微信开发者社区提了bug,所以我想用另一个方法来实现,就是先获取正常情况下一个字的宽度值,然后乘以总字数就获得了总宽度,亲试是可以的。 [图片] [代码]let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); [代码] ④绘制公众号二维码,和获取头像是一样的,也是先通过接口返回图片网络地址,然后再通过getImageInfo获取公众号二维码图片信息 ⑤如何绘制小程序码,具体官网文档也给出生成无限小程序码接口,通过生成的小程序可以打开任意一个小程序页面,并且二维码永久有效,具体调用哪个小程序二维码接口有不同的应用场景,具体可以看下官方文档怎么说的,也就是说前端通过传递参数调取后端接口返回的小程序码,然后绘制在画布上(和上面写的绘制头像和公众号二维码一样的) [代码]ctx.drawImage('小程序码的本地地址', x轴, Y轴, 宽, 高) [代码] ⑥最终绘制完把canvas画布转成图片并返回图片地址 [代码] wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath // 返回的图片地址保存到一个全局变量里 that.setData({ showShareImg: true }) wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) [代码] ⑦保存到系统相册;先判断用户是否开启用户授权相册,处理不同情况下的结果。比如用户如果按照正常逻辑授权是没问题的,但是有的用户如果点击了取消授权该如何处理,如果不处理会出现一定的问题。所以当用户点击取消授权之后,来个弹框提示,当它再次点击的时候,主动跳到设置引导用户去开启授权,从而达到保存到相册分享朋友圈的目的。 [代码]// 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } [代码] 总结 至此所有的步骤都已实现,在绘制的时候会遇到一些异步请求后台返回的数据,所以我用promise和async和await进行了封装,确保导出的图片信息是完整的。在绘制的过程确实遇到一些坑的地方。比如初开始导出的图片比例大小不对,还有用measureText测量文字宽度不对,多次绘制(可能受网络原因)有时导出的图片上的文字颜色会有误差等。如果你也遇到一些比较坑的地方可以一起探讨下做个记录,下面附下完整的代码 [代码]import regeneratorRuntime from '../../utils/runtime.js' // 引入模块 const app = getApp(), api = require('../../service/http.js'); var ctx = null, // 创建canvas对象 canvasToTempFilePath = null, // 保存最终生成的导出的图片地址 openStatus = true; // 声明一个全局变量判断是否授权保存到相册 // 获取微信公众号二维码 getCode: function () { return new Promise(function (resolve, reject) { api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => { console.log(res, '获取微信公众号二维码') if (res.code == 200) { console.log(res.content, 'codeUrl') resolve(res.content) } }).catch(err => { console.log(err) }) }) }, // 生成海报 async createCanvasImage() { let that = this; // 点击生成海报数据埋点 that.setData({ generateId: '点击生成海报' }) if (!ctx) { let codeUrl = await that.getCode() wx.showLoading({ title: '绘制中...' }) let code = new Promise(function (resolve) { wx.getImageInfo({ src: codeUrl, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) Promise.all([headImg, code]).then(function (result) { const ctx = wx.createCanvasContext('myCanvas') console.log(ctx, app.globalData.ratio, 'ctx') let canvasWidthPx = 690 * app.globalData.ratio, canvasHeightPx = 1085 * app.globalData.ratio, avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36, //绘制的头像在画布上的位置 codeurl_width = 80, //绘制的二维码宽度 codeurl_heigth = 80, //绘制的二维码高度 codeurl_x = 588, //绘制的二维码在画布上的位置 codeurl_y = 984, //绘制的二维码在画布上的位置 wordNumber = that.data.wordNumber, // 获取总阅读字数 // nameWidth = ctx.measureText(that.data.wordNumber).width, // 获取总阅读字数的宽度 // allReading = ((nameWidth + 375) - 325) * 2 + 380; // allReading = nameWidth / app.globalData.ratio + 325; allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; console.log(wordNumber, wordNumber.toString().length, allReading, '获取总阅读字数的宽度') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制 ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.setFontSize(28); // 文字字号 ctx.fillText(that.data.currentChildren.name, 103, 78); // 绘制文字 ctx.font = 'normal bold 44px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(wordNumber, 325, 153); // 绘制文字 ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('打败了全国', 26, 190); // 绘制文字 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#faed15'); // 文字颜色 ctx.fillText(that.data.percent, 154, 190); // 绘制孩子百分比 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('的小朋友', 205, 190); // 绘制孩子百分比 ctx.font = 'normal bold 32px sans-serif'; ctx.setFillStyle('#333333'); // 文字颜色 ctx.fillText(that.data.singIn, 50, 290); // 签到天数 ctx.fillText(that.data.reading, 280, 290); // 阅读时长 ctx.fillText(that.data.reading, 508, 290); // 听书时长 // 书籍阅读结构 ctx.font = 'normal normal 28px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].count, 260, 510); ctx.fillText(that.data.bookInfo[1].count, 420, 532); ctx.fillText(that.data.bookInfo[2].count, 520, 594); ctx.fillText(that.data.bookInfo[3].count, 515, 710); ctx.fillText(that.data.bookInfo[4].count, 492, 828); ctx.fillText(that.data.bookInfo[5].count, 348, 858); ctx.fillText(that.data.bookInfo[6].count, 212, 828); ctx.fillText(that.data.bookInfo[7].count, 148, 726); ctx.fillText(that.data.bookInfo[8].count, 158, 600); ctx.font = 'normal normal 18px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].name, 232, 530); ctx.fillText(that.data.bookInfo[1].name, 394, 552); ctx.fillText(that.data.bookInfo[2].name, 496, 614); ctx.fillText(that.data.bookInfo[3].name, 490, 730); ctx.fillText(that.data.bookInfo[4].name, 466, 850); ctx.fillText(that.data.bookInfo[5].name, 323, 878); ctx.fillText(that.data.bookInfo[6].name, 184, 850); ctx.fillText(that.data.bookInfo[7].name, 117, 746); ctx.fillText(that.data.bookInfo[8].name, 130, 621); ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像 ctx.draw(false, function () { // canvas画布转成图片并返回图片地址 wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath that.setData({ showShareImg: true }) console.log(res.tempFilePath, 'canvasToTempFilePath') wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) }) }) } }, // 保存到系统相册 saveShareImg: function () { let that = this; // 数据埋点点击保存学情海报 that.setData({ saveId: '保存学情海报' }) // 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } }, [代码]
2019-06-15 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - 通过 SelectorQuery 获取 Canvas 节点,绘图问题?
[代码]<[代码][代码]canvas[代码][代码] [代码][代码]type[代码][代码]=[代码][代码]"2d"[代码][代码] [代码][代码]id[代码][代码]=[代码][代码]"canvas"[代码][代码] [代码][代码]style[代码][代码]=[代码][代码]"width: 100%; height: 500px;"[代码][代码]></[代码][代码]canvas[代码][代码]>[代码][代码]Page({[代码][代码] [代码][代码]data: {[代码] [代码] [代码][代码]},[代码][代码] [代码][代码]onLoad: [代码][代码]function[代码][代码]() { [代码][代码] [代码][代码]// 通过 SelectorQuery 获取 Canvas 节点[代码][代码] [代码][代码]wx.createSelectorQuery()[代码][代码] [代码][代码].select([代码][代码]'#canvas'[代码][代码])[代码][代码] [代码][代码].fields({[代码][代码] [代码][代码]node: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]})[代码][代码] [代码][代码].exec([代码][代码]this[代码][代码].init.bind([代码][代码]this[代码][代码]))[代码][代码] [代码][代码]},[代码][代码] [代码][代码]init(res) {[代码][代码] [代码][代码]const canvas = res[0].node[代码][代码] [代码][代码]const ctx = canvas.getContext([代码][代码]'2d'[代码][代码])[代码][代码] [代码][代码]this[代码][代码].drawPath(ctx)[代码][代码] [代码][代码]},[代码][代码] [代码][代码]drawPath(ctx){[代码][代码] [代码][代码]var[代码] [代码]dwidth = 800 / 80.0;[代码][代码] [代码][代码]var[代码] [代码]pos = Math.ceil(Math.random() * 75) + 30;[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 0; i < 80; i++) {[代码][代码] [代码][代码]ctx.beginPath();[代码][代码] [代码][代码]ctx.moveTo((i * dwidth), pos);[代码][代码] [代码][代码]var[代码] [代码]pos2 = Math.ceil(Math.random() * 75) + 30;[代码][代码] [代码][代码]ctx.lineTo(((i + 1) * dwidth), pos2);[代码][代码] [代码][代码]pos = pos2;[代码][代码] [代码][代码]ctx.stroke();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]ctx.strokeStyle = [代码][代码]"green"[代码][代码];[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 10; i < 80; i++) { [代码][代码]//清除后半部份继续绘制[代码][代码] [代码][代码]ctx.clearRect((i * dwidth), 0, ((i + 1) * dwidth), 150);[代码][代码] [代码][代码]ctx.beginPath();[代码][代码] [代码][代码]ctx.moveTo((i * dwidth), pos);[代码][代码] [代码][代码]var[代码] [代码]pos2 = Math.ceil(Math.random() * 75) + 30;[代码][代码] [代码][代码]ctx.lineTo(((i + 1) * dwidth), pos2);[代码][代码] [代码][代码]pos = pos2;[代码][代码] [代码][代码]ctx.stroke();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码] [代码]})[代码][图片][图片] 开发工具中显示是清晰的,在真机预览是模糊的,麻烦帮忙看一下是哪里的问题?
2019-10-26 - 小程序订阅消息开发指南
2019年10月12日微信开放了小程序订阅消息的功能。按官方的说法,目前的模板消息在实现小程序服务闭环上存在缺陷: 1. 部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰;2. 模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求模板消息确实存在上述的硬伤,不利于小程序的用户留存和用户体验。为了解决这些问题,微信官方推出了用户订阅消息功能。我在微慕专业版上加了订阅消息的功能,并验证了这个功能。这个功能是否能都达到官方的预期,这个我感觉不那么乐观。这里我先说我的感受:目前的订阅消息还不完善,后续还有很大的优化空间。 目前,官方只开放了“一次性订阅消息”,尚未开放“长期性订阅消息”,因此我只尝试了“一次性订阅消息”。 一次性订阅消息:用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 订阅消息推送位置:服务通知 订阅消息下发条件:用户自主订阅 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面 以下我简单说明订阅消息的开发过程和使用体验。 一.订阅消息的开发1.获取订阅消息的模板ID 在微信小程序的管理后台,在左侧“功能”菜单,选择“订阅消息”,然后点击“添加” [图片] 然后选择你需要的消息模板,并配置关键词。 [图片] 配置完成后,如下图所示。 [图片] 值得关注的是,在配置好的模板详情页面里的“详细内容”很重要,这个就是开发订阅消息时需要遵循的消息格式,这个格式和模板消息有细微的差别 根据微慕小程序的需要,我选用了“新的评论提醒”和“内容更新提醒”这两个消息模版。前者用于提醒发表话题或文章的作者,有新的话题或文章评论,增强作者与读者之间的交流互动;后者是提醒订阅用户,小程序有新的文章发布,引导用户回归小程序。 订阅消息申请模板的时候,需要选择所属类目,只能选择当前小程序相关的类目模板,对于模板消息不需要选择对应类目。如果删除小程序类目,就会把订阅消息模板一起删除。因此删除类目要小心谨慎。 [图片] 2.触发用户订阅,获取下发的权限 触发用户订阅,微信小程序提供的api是: [代码]wx.requestSubscribeMessage[代码],用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 注意:微信小程序开发工具尚不支持此功能,在开发工具触发订阅的api,会提示: requestSubscribeMessage:fail 开发者工具暂时不支持此 API 调试,请使用真机进行开发 调用api的代码示例如下: [代码]wx.requestSubscribeMessage({[代码] [代码]tmplIds: ["模板A","模板B"],[代码] [代码]success: function (res) {[代码] [代码]//成功[代码] [代码]},[代码] [代码]fail(err) {[代码] [代码]//失败[代码] [代码]console.error(err);[代码] [代码]}[代码] [代码]})[代码] wx.requestSubscribeMessage(Object object) 的回调函数[代码]object.success [代码]参数有两个:errMsg和TEMPLATE_ID; 接口调用成功时errMsg值为’requestSubscribeMessage:ok’。TEMPLATE_ID是动态的键,即模板id,值包括’accept’、’reject’、’ban’。’accept’表示用户同意订阅该条id对应的模板消息,’reject’表示用户拒绝订阅该条id对应的模板消息,’ban’表示已被后台封禁。例如 { errMsg: “requestSubscribeMessage:ok”, zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: “accept”} 表示用户同意订阅zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE这条消息。 个人觉得这个动态键不是特别合理,代码处理起来有些麻烦,如果改成静态键的json格式比较方便处理,例如: [代码]{[代码] [代码] errMsg:"requestSubscribeMessage:ok",[代码] [代码] result: [[代码] [代码] { templateId:"zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE",[代码] [代码]status:"accept"[代码] [代码]}[代码] [代码] ][代码] [代码]}[代码] 在手机上调用此api方法会调出订阅消息的界面,如下图所示: [图片] 关于这个订阅消息的授权有几点要注意: 1) 在确认提示框里,如果用户选择“取消”表示拒绝(取消)订阅消息,选择“允许”表示用户订阅一次消息。 2) 如果用户不勾选“总是保持以上选择,不再询问”,那么每次用户触发都会弹出提示框。 3) 如果用户勾选“总是保持以上选择,不再询问”,那么将再也不会唤起这个对话框。同时,如果选择“取消”,那么以后每次调用这个api的时候,都会自动拒绝;如果选择“允许”,那么以后每次调用此api,都会自动允许授权。 目前小程序没有提供获取用户是否授权订阅消息的方法。通过wx.openSetting 方法无法获取用户是否授权消息订阅的信息,scope 列表没有订阅消息的内容。 如果想从自动拒绝转换到自动自动运行,需要打开小程序的设置去配置。设置方法:点击小程序右上角的三个点,打开如下对话框 [图片] 然后选择“设置”,在设置项里选择“订阅消息” [图片] [图片] 4)对于同一种消息,用户可以订阅多次,订阅多少次,就会收到多少次订阅消息,这个订阅次数是否有上限,官方没有说明,初步判断是不限的。但是,微信不会提供订阅的次数,因此需要在小程序的后端服务里存储用户订阅的次数。因此,我在微慕小程序专业版里,提供了一个给用户多次订阅的设置,并记录用户订阅的次数。 [图片] 如果用户需要某个消息服务,可以订阅多次,当然也可以在点击“订阅”的对话框里选择“取消”,“取消”一次也就减少一次订阅。 5)对于支付的场景,也需要用户确认是否订阅,这个我觉得不合理,支付后给用户一个订单推送消息应该是刚性需求,不需要再询问一遍用户是否订阅。 2.调用接口下发订阅消息 订阅消息下发的接口是小程序后台服务端调用:subscribeMessage.send,此方法类似下发模板消息的方法,详细调用说明见参考官方的链接: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html 订阅消息的下发接口方法和模板消息稍有不同, 模板消息的json格式如下 [代码]"data": {[代码] [代码]"keyword1": {[代码] [代码]"value": "内容1",[代码] [代码]"color": "#000"[代码] [代码]},[代码] [代码]"keyword2": {[代码] [代码]"value": "内容2",[代码] [代码]"color": "#000"[代码] [代码]}[代码] [代码]}[代码] 而订阅消息的json格式如下: [代码]"data": {[代码] [代码]"thing1": {[代码] [代码]"value": "内容"[代码] [代码]},[代码] [代码]"number2": {[代码] [代码]"value": 20[代码] [代码]}[代码] 订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,如果不按类型提交,会导致发送失败。同时如果是文本型的内容,字数也有限制,超过限制也会发送失败,但具体字数是多少,官方没有给出,同时中英文混合计算的长度也有差异,据我目前测试25个中文字符是可以的。希望官方能给出具体的字符长度限制的明确数字。 如果调用下发的次数大于用户的订阅次数,调用接口下发订阅消息会返回失败。报如下错误 [图片] 二.订阅消息使用心得1.订阅消息虽然把订阅的授权的交给了用户,但是也增加了用户使用难度,同时,一次性订阅只能收到一次,操作起来比较繁琐,如果不是刚需用户可能会首次就拒绝了这个服务,要想重新获取授权,需要用户自己打开小程序设置里去配置,颇为麻烦,小程序没有提供更简便的方法去唤起。 2.小程序的服务商为了获得更多给用户发送订阅消息的次数,肯定会想方设法去埋点引诱用户去点击订阅,这种诱导估计也是违规。 3.用户使用门槛和学习比较高,比如某个预约的服务,原来的场景是用户只要有提交表单,小程序就可以推送消息给用户,但是现在需要用户主动去订阅,无形中多了一步,如果用户不熟悉订阅消息或者直接点了“取消”,小程序就没法通知到用户了,用户可能因此错失服务,对商家和用户都是损失。 4.微信小程序将采用订阅消息,并逐步取消模板消息,虽然微信官方试图在方便用户和不打扰用户这两种选择里去寻求平衡,但订阅消息目前的模式恐怕无法达到这个期望,至少在我看来,无论对小程序的服务商,还是小程序的用户,都感到不方便。 update:2020年5月18日,日前订阅消息已经支持微信小程序开发工具。
2020-05-18 - 微信小程序生成图片和图片保存
生成图片这个功能需要使用 Canvas ,先将要保存的图片使用 Canvas 画出来,然后调用相关方法保存到手机上。文中使用或是未使用的有关小程序中 Canvas 的 api 可以在 小程序Canvas相关Api 这里查看。 为了使用方便,我将这个需求的实现做成了一个组件,便于项目的其他地方复用,也遇到了一些因为使用自定义组件带来的问题 <!–more–> 要实现的效果图 [图片] 实现思路 相关的代码比较长,而且大部分为调用 Canvas 的 api 代码,整体贴出无必要。本文中只描述思路,具体代码看 小程序生成图片的微信代码片段 。 代码中的几个方法 [代码]drawRoundedRect[代码] :用来绘制圆角矩形,此处不详述它的画图原理 [代码]point[代码] :为方法 1 服务的,一看就懂 [代码]downFile[代码] :对微信的下载方法进行了一层简单封装,传入 url ,返回一个 Promise [代码]save[代码] : 保存图片的相关逻辑 [代码]doAuth[代码] :当调用 [代码]wx.saveImageToPhotosAlbum[代码] 方法保存图片时,如果没有保存图片的权限会保存失败,此时需要让用户重新授权 [代码]computedPercent[代码] :一个快捷的计算比例的方法,传入从设计图上量出来的像素数即可, [代码]oldWidth[代码] 是设计图上的 Canvas 区域宽度 [代码]initData[代码] :数据初始化,获取设备相关信息,将网络图下载到本地 [代码]writeCanvas[代码] : 主要画图逻辑,调用此方法时保证所需数据已处理完毕,开始画图 数据初始化 需求中需要显示用户头像和小程序码,小程序码后面是要挂参数的,可以简单理解为要挂个用户参数在后面,类似这样 [代码]?uid=2233[代码] ,这就是组件中要传入的 scene 的值,然后根据这个参数,在开始画图之前,先调用接口从服务端那里获取图片的链接,再利用微信的 [代码]wx.downloadFile[代码] 方法将图片下载到本地,在 Canvas 中使用本地路径,用户头像和小程序码都下载好了之后就可以开始画图了,否则的话,提示网络错误。 为了演示方便,本文中的网络图都换成了本地图片 画图 由于是在组件中,所以获取 Canvas 上下文的时候要传入 [代码]this[代码] ,根据设计图,从里向外依次往 Canvas 上叠加就是了,从设计图上量出的像素调用方法来获取比例,画完之后调用 Canvas 的 [代码]draw[代码] 方法技术绘画,并且将 loading 状态取消 保存图片 保存图片的时候,要注意保存的图片的宽高都乘一下设备的像素比,防止出来的图片太小了,保存图片需要相关权限,若用户为授权,要弹窗让他授权之后再进行保存操作 遇到的问题 以下部分问题有时效性,请大家理性看待。 canvas 结束绘画要调用 [代码]ctx.draw()[代码] 方法,不调用的话是什么都不会显示的 [代码]wx.createCanvasContext(string canvasId, Object this)[代码] ,在自定义组件下,当前组件实例的 [代码]this[代码] ,表示在这个自定义组件下查找拥有 [代码]canvas-id[代码] 的 [代码]<canvas>[代码] ,如果省略则不在任何自定义组件内查找。简而言之就是在页面级调用这个方法的时候,第二个参数可以不传,会默认传入 this ;但是在自定义组件中调用这个方法的话,要传入 this 部分手机上保存的图片分辨率太小,导致图片上的字看不清。最后使用了一个百分比的计算,所有的的坐标或者大小都是根据屏幕宽高和设计图宽高比例计算出来了,这样可以保证即使在不同手机上或者在不同的容器中,都是有恰当比例的;并且保存图片的时候,要注意保存的图片的宽高都乘一下设备的像素比 Canvas 中不能放网络图,所以网络图需要先下载到本地之后在使用 如果用到了下载网络图片的话,别忘记设置微信的 [代码]downloadFile[代码] 合法域名 结语 对于大部分同学来说,自己项目的需求和我这里实现的可能是有些出入的,所以本文只是起到一个例子的作用,可以帮助你快速上手这个模块的开发
2023-08-14 - 小程序海报生成工具,可视化编辑直接生成代码使用,你的海报你自己做主
开门见山 工具地址 点我直达>>painter-custom-poster 由于挂载在github page上,打开速度会慢一些,请耐心等待或自行解决git网速问题 背景 在做小程序时候,我们经常会有一个需求,需要将小程序分享到朋友圈,但是朋友圈是不允许直接分享小程序,那我们还有其他的办法解决吗?答案肯定是有的,即 canvas 生成个性化海报分享图片到朋友圈 分析 小程序中有大量的生成图片需求,但是使用过 canvas 的人,都会发现一些难以预料的问题>>有关小程序的坑 直接在 canvas 上绘制图形,对于普通开发者来说代码会特别凌乱并且难以维护,经常会花费很久的时间去优化代码 不同的环境渲染问题,例如在开发者工具看起来好好的,一到 Android 真机,就出现图片不显示,位置不对应等等问题 解决 那可不可以开发一款生成海报的插件库呢? 首先,只需要提供一份简单的参数配置文件即可 解决掉小程序Canvas遇到的一些大大小小的坑 有严苛的测试环节,解决各种环境和各种机型遇到的问题,并提供稳定的线上版本 长期维护,并有专人更新迭代更新颖的功能 以上的要求当然是可以的,曾经的我也想尝试开发一款出来,但是后来尝试了几款现成的工具之后就放弃了,毕竟轮子这个东西,是需要不断维护更新的,另外已经有这么多优秀现成的插件了,我为何还要费力去写呢,贡献代码岂不更美哉,以下是我收集的几款 小程序生成图片库,轻松通过 json 方式绘制一张可以发到朋友圈的图片>>Painter 小程序组件-小程序海报组件>>wxa-plugin-canvas 微信小程序:一个 json 帮你完成分享朋友圈图片>>mp_canvas_drawer 我想干什么 唠了这么多,好像提供给大家插件就没我什么事情了…想走是不可能的 为了能够制作出更酷炫的海报,我思考了许久 虽然有了插件后,只需要提供配置代码就能够制作出一款海报来,但是我发现还是有些许问题 制作海报效率还是不够高,微调一个元素的大小和位置,就需要不断的修改保存代码,等待片刻,查看效果,真的烦 一个小小的位置调整可能就需要来回调整无数次,这种最简单的机械化劳动,这辈子是不可能的 拿着完美的稿子,递给设计师看,这个位置不对,这个线太粗,这个颜色太重…你信不信我打死你 对于一些精美复杂的海报,实现起来真的不太现实 那我需要怎么做呢,请点击这个链接体验>>painter-custom-poster 点击左侧例子展示中的任意一个例子,然后导入代码就能看到效果图,这下你应该能猜到了我的想法了 如何实现 刚开始我想用简单的html和css加拖动功能实现,通过简单尝试之后就放弃了,因为这个功能真的太复杂了,简单的工具肯定是不行的 中间这个计划停滞了很长时间,一度已经放弃 直到发现了这个库fabric.js,真的太太优秀了,赞美之词无以言表,唯一的缺点就是中文教程太少,必须生啃英文加谷歌翻译 fabric介绍,你可以很容易地创建任何一个简单的形状,复杂的形状,图像;将它们添加到画布中,并以任何你想要的方式进行修改:位置、尺寸、角度、颜色、笔画、不透明度等 How To Use 目前工具一共分成4部分 例子展示 用来将一些用户设计的精美海报显示出来,通过点击对应的例子并将代码导入画布中 画布区 显示真实的海报效果,画布里添加的元素,都可以直接用鼠标进行拖动,旋转,缩放操作 操作区 第一排四个按钮 复制代码 将画布的展示效果转化成小程序海报插件库所需要的json配置代码,目前我使用的是Painter库,默认会转化成这个插件的配置代码,将代码直接复制到card.js即可 查看代码 这个功能用不用无所谓,可以直观的看到生成的代码 导出json 将画布转化成fabric所需要的json代码,方便将自己设计的海报代码保存下来 导入json 将第3步导出的json代码导入,会在画布上显示已设计的海报样式 第二排五个按钮 画布 画布的属性参数 详解见下方 文字 添加文字的属性参数 详解见下方 矩形 添加矩形的属性参数 详解见下方 图片 添加图片的属性参数 详解见下方 二维码 添加二维码的属性参数 详解见下方 第三排 各种元素的详细设置参数 激活区 激活对象是指鼠标点击画布上的元素,该对象会被蓝色的边框覆盖,此时该对象被激活,可以执行拖动 旋转 缩放等操作 激活区只有对象被激活才会出来,用来设置激活对象的各种配置参数,修改value值后,实时更新当前激活对象的对应状态,点击其他区域,此模块将隐藏 快捷键 ‘←’ 左移一像素 ‘→’ 右移一像素 ‘↑’ 上移一像素 ‘↓’ 下移一像素 ‘ctrl + z’ 撤销 ‘ctrl + y’ 恢复 ‘delete’ 删除 ‘[’ 提高元素的层级 ‘]’ 降低元素的层级 布局属性 通用布局属性 属性 说明 默认 rotate 旋转,按照顺时针旋转的度数 0 width、height view 的宽度和高度 top、left 如 css 中为 absolute 布局时的作用 0 background 背景颜色 rgba(0,0,0,0) borderRadius 边框圆角 0 borderWidth 边框宽 0 borderColor 边框颜色 #000000 shadow 阴影 ‘’ shadow 可以同时修饰 image、rect、text 等 。在修饰 text 时则相当于 text-shadow;修饰 image 和 rect 时相当于 box-shadow 使用方法: [代码]shadow: 'h-shadow v-shadow blur color'; h-shadow: 必需。水平阴影的位置。允许负值。 v-shadow: 必需。垂直阴影的位置。允许负值。 blur: 必需。模糊的距离。 color: 必需。阴影的颜色。 举例: shadow:10 10 5 #888888 [代码] 渐变色支持 你可以在画布的 background 属性中使用以下方式实现 css 3 的渐变色,其中 radial-gradient 渐变的圆心为 中点,半径为最长边,目前不支持自己设置。 [代码]linear-gradient(-135deg, blue 0%, rgba(18, 52, 86, 1) 20%, #987 80%) radial-gradient(rgba(0, 0, 0, 0) 5%, #0ff 15%, #f0f 60%) [代码] !!!注意:颜色后面的百分比一定得写。 画布属性 属性 说明 默认 times 控制生成插件代码的宽度大小,比如画布宽100,times为2,生成的值为200 1 文字属性 属性名称 说明 默认值 text 字体内容 别跟我谈感情,谈感情伤钱 maxLines 最大行数 不限,根据 width 来 lineHeight 行高(上下两行文字baseline的距离) 1.3 fontSize 字体大小 30 color 字体颜色 #000000 fontWeight 字体粗细。仅支持 normal, bold normal textDecoration 文本修饰,支持none underline、 overline、 linethrough none textStyle fill: 填充样式,stroke:镂空样式 fill fontFamily 字体 sans-serif textAlign 文字的对齐方式,分为 left, center, right left 备注: fontFamily,工具中的第一个例子支持文字字体,但是导入小程序为什么看不到呢,小程序官网加载网络字体方法>> 加载字体教程>> 文字高度 是maxLines lineHeight2个字段一起计算出来的 图片属性 属性 说明 默认 url 图片路径 mode 图片裁剪、缩放的模式 aspectFill mode参数详解 scaleToFill 缩放图片到固定的宽高 aspectFill 图片裁剪显示对应的宽高 auto 自动填充 宽度全显示 高度自适应居中显示 Tips(一定要看哦~) 本工具不考虑兼容性,如发现不兼容请使用google浏览器 painter现在只支持这几种图形,所以暂不支持圆,线等 如果编辑过程,一个元素被挡住了,无法操作,请选择对象并通过[ ]快捷键提高降低元素的层级 文字暂不支持直接缩放操作,因为文字大小和元素高度不容易计算,可以通过修改激活栏目maxLines lineHeight fontSize值来动态改变元素 如发现导出的代码一个元素被另一个元素挡住了,请手动调整元素的位置,json数组中元素越往后层级显示就越高,由于painter没有提供层级参数,所以目前只能这样做 本工具导出代码全是以px为单位,为什么不支持rpx, 因为painter在rpx单位下,阴影和边框宽会出现大小计算问题,由于原例子没有提供px生成图片方案,可以下载我这里修改过的demo>>Painter即可解决 文本宽度随着字数不同而动态变化,想在文本后面加个图标根据文本区域长度布局, 请参考Painter文档这块教程直接修改源码 由于本工具开发有些许难度,如出现bug,建议或者使用上的问题,请提issue,源码地址>>painter-custom-poster 海报贡献 如果你设计的海报很好看,并且愿意开源贡献,可以贡献你的海报代码和缩略图,例子代码文件在example中,按顺序排列,例如现在库里例子是example2.js,那你添加example3.js和example3.jpg图片,事例可以参考一下文件夹中源码,然后在index.js中导出一下 导出代码 代码不要格式化,会报错,请原模原样复制到json字段里 生成缩略图 刚开始我想在此工具中直接生成图片,但是由于浏览器图片跨域问题导致报错失败 所以请去小程序中生成保存图片,图片质量设置0.2,并去tinypng压缩一下图片 找到painter.js,替换下边这个方法,可以生成0.2质量的图片,代码如下 [代码] saveImgToLocal() { const that = this; setTimeout(() => { wx.canvasToTempFilePath( { canvasId: 'k-canvas', fileType: 'jpg', quality: 0.2, success: function(res) { that.getImageInfo(res.tempFilePath); }, fail: function(error) { console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`); that.triggerEvent('imgErr', { error: error }); } }, this ); }, 300); } [代码] TODO 颜色值选择支持调色板工具 文字padding支持 缩放位置弹跳问题优化 假如需求大的话,支持其他几款插件库代码的生成 ~ 创作不易,如果对你有帮助,请给个星星 star✨✨ 谢谢 ~
2019-09-27 - 纯云开发二手书商城的全开源demo
这是为母校写的一个纯粹的公益小程序,原生+云开发,写文章太累了,所以所有代码我都写了注释,还是很适合入门学习的,特别是云开发 [图片] [图片] [图片] 程序本身来说,我认为没啥多大的亮点,只不过把很多单个案例综合起来了,云开发方面,比如:支付、提现、获取用户手机号、发短信、发邮箱。。。。。。。界面上,清一色的flex布局。 和完整版得商城小程序,还差了一丢丢–购物车,因为思考了一下,这个小程序着实用不着,用来学习还是可以了滴 源码和使用教程发在Github: https://github.com/xuhuai66/used-book-pro
2019-09-18 - 小程序请求数据双向混合加密和防篡改+防重放攻击的实现
前言 大家好,借着中秋放假明日又要上班的这个晚上,平常又没空,趁这个时间点就决定来一篇。 [图片] 我们都知道微信小程序的服务端API上,官方可谓是做足了心思,对用户的数据进行加密,虽然对我们开发者来说似乎是一种麻烦,但是从长远角度来看,是十分有必要的,用户隐私高于一切。 那么在小程序的开发过程中与后端对接接口时是否有想过这样的问题呢? HTTPS真的百分百安全吗? 数据被成功抓包后安全吗? 数据被篡改后还有效吗? 请求被重放后安全吗? 世界上没有绝对安全的系统,但我们可以让它被破解的成本变高。 本篇文章专业性并不高,如果存在错误请大家为我指出来,以免误导别人,谢谢! 以下我们将小程序端称为C端,服务端称为S端,服务端代码是Node.js,仅供参考,但原理都一样,后端可以是其它语言。 思考 围绕着以上的问题,探究一下问题的答案? 本部分只对问题做思考,具体实现请参考下面的实现部分。 HTTPS真的百分百安全吗?只能说是相对安全,当然,在微信小程序的沙箱环境里,HTTPS通信会更加安全,否则官方可能会要求我们对请求加密了对吧。但百密一疏,C端是否存在漏洞,假设C端安全了难道S端就安全嘛?细思恐极,退一万步讲,百分百安全是不存在的,由于篇幅问题相关漏洞大家可以搜索探究。 假设数据被中间人成功抓包,如果数据是明文传输,那么将导致数据泄露,因此对数据进行加密是必要的,但应该如何加密呢?如何做到密钥安全?C端和S端如何进行数据的加解密?RSA和AES加密应该使用哪种加密明文?如何充分发挥RSA和AES两个加密算法的特长? 假设数据被篡改,如果因为请求数据被篡改而导致严重后果,那么很大程度上其实是代码设计有问题,正常设计中安全应被摆在重要的位置,至少不应该出现购买某样商品时是通过C端发送商品价格给S端调起支付那样(只需篡改商品价格即可支付极少的钱购买商品),如果代码设计并不存在严重问题,那么数据被篡改也是不可忽视的,我们需要进行数据签名,让不同的数据拥有唯一的MD5哈希值,如果数据被篡改,通过哈希值即可判断数据是否被篡改,当然也可能会有人问,既然数据被篡改,那攻击者会不会重新生成一个哈希值代替?是的。但是我们有接下来会说到的key,key会作为签名的数据变量之一,由于攻击者并不知道key值因此无法重新签名,key值建议不是固定值而是周期性更换的随机值,例如随着用户的登录态而产生并随之抹除。 假设请求被重放,大部分时候由于数据被加密和防篡改处理过,攻击者并无法直接获得数据或篡改,但如果通过劫持到的登录认证请求的原始数据并重新发起该请求,则攻击者将可能获得重要的认证数据,使得系统将攻击者作为正常用户处理,后续的请求攻击者仍能伪装成正常的用户进行后续攻击。 期望效果 在开始实现之前先看一下完成后的效果,以下是截取了Network中其中一个GET请求发送的数据: [图片] 实际上发送的数据是如下图所示: [图片] 然后是该请求返回的数据: [图片] 实际上返回的数据是如下图所示: [图片] 整个流程: [图片] 实现 由于整个流程在C端的实现上顺序反过来的,因此下面的步骤也将是反向而行。 工具库下载 工欲善其事,必先利其器!这三个库是经过修改压缩的,支持在小程序上使用并且体积可观(总共69.1KB,如果不涉及密码哈希处理只需要前两个库,体积只有65.3KB),接下来的实现操作将会使用到,建议大家可以根据实际情况对功能进行二次封装: CryptoJS.js:点击下载 RSA.js:点击下载 SHA256.js(可选):点击下载 在线的各类加解密工具(可选,可以收藏起来,平时测试挺有用):点击访问 请求数据防重放 要防范请求重放攻击,首先需要了解Unix时间戳 timestamp概念,和时间戳不一样的是它的单位是秒,事实上这个需求也只需要秒级即可。除此之外还将用到另一个值:nonce,它是一个随机产生并只能被使用一次的值,长度自定,请求越频繁长度需要越长(降低同一时间产生相同nonce的几率),C端发送请求时需要将timestamp和生成的nonce加入发送的参数中。那么,如何将两者结合呢?我这为防重放的目标下了一个简单的定义。 同样的请求只能发生一次,且请求必须在规定时间内发出。 何为同样的请求?不是指两次发送的参数一致就是一样的,而是连timestamp和nonce也一样才算是同样的请求。 那么S端如何确认其是同样的请求呢? S端每次接收到一个请求,都会将该请求的nonce存入缓存并保持60秒(这个阈值不一定是60秒,可以根据实际需要定义),时间过后该值将被移除,建议S端采用Redis存储nonce,这样可省去检测和移除nonce的代码。如果S端发现当前请求的nonce存在于已存储的nonce之中,则此请求发生重复,那么timestamp有何用? 如果只使用nonce我们只能保证该请求60秒内不会重复,但60秒后依然任人宰割,这不是要的结果。所以timestamp将用来限制时间,S端时间戳减去C端发送请求的时间戳,得到的差值为N秒,如果N秒大于60秒则此请求过期,那么则可以保证,60秒内因为nonce相同而被判为请求重放,60秒后因为时间差超过而被判为请求已过期,因此确保了请求不会被重放。。 以下展示三种情况: [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:5rKbMs2Fm3 C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:false 【重放校验通过】 [代码] [代码]C端时间戳:1568487722 //2019/9/15 03:05:22 C端NONCE:IzFEs52bAC C端发送请求 -> S端接收请求 S端时间戳:1568487922 //2019/9/15 03:02:00 CS端时间差:1568487922 - 1568487722 = 200 C端NONCE是否存在于缓存:false 【重放校验不通过,请求已过期,因为时间差超过60秒】 [代码] [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:IxwPHQU0nA C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:true 【重放校验不通过,此请求为重放请求,因为nonce已经存在,此请求已经完成,不可重复】 [代码] timestamp和nonce将作为参数参与下面部分的签名。 请求数据防篡改 C端数据签名 首先通过对参数按照参数名进行字典排序(调过一些第三方API的朋友应该明白),假设当前需要传输的参数如下: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3" } [代码] 进行字典排序,参数名顺序应为: [代码]const keys = Object.keys(data); //获得参数名数组 keys.sort(); //字典排序 console.log(key); //["a", "b", "c", "nonce", "timestamp"]; [代码] 参数字典排序后应和参数一起拼接为字符串,至于使用什么拼接符就要与S端商量了,如果参数值是一个数组或一个对象(如c为[1,2,3])那么可以将数据值转为JSON字符串再拼接。以上参数拼接后字符串如下: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 [代码] 下一步是计算拼接字符串的MD5哈希值了嘛? 不是。因为这样拼接的字符串很容易被攻击者伪造签名并篡改数据,这样就失去了签名的意义了。也就是说缺了一个key值。key值又从何而来?上面思考部分有提到建议从登录后发放,并且这个key与该用户的登录态绑定,登录态有效期间,将使用这个key进行请求的签名与验签,至于如何鉴别用户,我们会在最终发送给S端的参数加入一个sessionId作为登录态唯一标识,这里不加是因为这部分数据是需要参与后续的加密的,而sessionId不参与加密。 但是可能又会有一个问题,登录前没有key怎么实现的登录请求?事实上,登录请求并不怕篡改,因为攻击者自己也不知道账号密码,所以无需提供key用于登录请求的签名。 提到登录密码这个需要注意一点,密码不能明文传输,请计算哈希值后传输,S端比对账户密码哈希值即可确认是否正确,同样S端非特殊情况也不能明文存储密码,建议SHA-1或更高级的SHA-256计算后的值,MD5值可能被使用彩虹表(一种为各种常见密码建立的MD5映射表)破解。SHA256计算的库已在上面工具库下载提供。 拼接上我们登录时随机生成的key: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB [代码] 接下来计算32位MD5哈希值 为什么上面说MD5会被破解而这里却用MD5计算?因为此处计算MD5的目的并不是为了隐藏明文数据,而只是用于数据校验 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const signStr = "a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB"; const sign = CryptoJS.MD5(signStr).toString(); //a42af0962de99e698d27030c5c9d3b0e [代码] 这么一来我们的数据签名阶段就完成了,然后需要把签名加入参数之中,将和参数一起传输,需要注意的是传输参数无需在意参数名排序。以下是当前的参数处理结果: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } [代码] 其实从上面这两部分内容来看,会发现防重放和防篡改是相辅相成的,就像两兄弟一样,少了谁都干不好这件事。 S端验证签名 既然有签名那必定也有验签,验签流程其实就是重复C端的前面流程并比对CS两端得出的签名值是否一致。S端取得请求数据后(假设数据未加密,暂时不讨论解密),将除了sign之外的参数名进行字典排序,sign用一个临时变量存下,然后排好序的参数和C端一样拼接得到字符串。 接下来根据上面部分提到的未加密参数sessionId获得用户登录态并获取到该状态的临时key拼接到字符串末尾,接下来进行MD5计算即可得到S端方获得的签名,此时与请求中携带的sign比较是否一致则可确定签名是否有效,如果不一致返回签名错误。具体流程请参考以下: [代码]C端发送请求 -> S端接收请求 //请求参数为data const _sign = data.sign; //排序并拼接除sign外的参数 let signStr = a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 const sessionId = ...; //用户的sessionId const sessionData = getUserSessionData(sessionId);//根据sessionId查询用户登录态sessionData并取出key,并拼接到字符串尾部 const key = sessionData.key signStr += key; //MD5计算并比对,代码仅供参考 const sign = crypto.md5(signStr); if(sign !== _sign) { //签名错误! } [代码] 请求数据加解密 有了上面部分的参数处理铺垫后,接下来就该开始本文章最核心的加解密,在这之前我们先了解AES和RSA两种加密算法。了解概念后我们再来思考一下两者的一些特性。 AES加解密需要密钥,除某些模式外还需要提供初始化向量,可使用密钥解出明文,是对称加密算法。 RSA加解密需要一对密钥,分别为公钥和私钥,公钥加密,私钥解密,是非对称加密算法。 两者在加密长文本性能上AES占优势。 根据这些让我们发现,他们可以形成互补关系,RSA加解密安全性高但长文本处理性能不及AES。AES加解密长文本性能优于RSA但需要明文密钥和向量加解密,密钥的安全性成问题。 那么在C端生成随机的AES密钥和向量,使用密钥和向量使用aes-128-cbc加密模式(也可以根据实际需要采用其它的模式)加密真正需要传输的参数(参数则是经过防重放+防篡改处理的参数)得到encryptedData,然后将该密钥使用RSA公钥加密得到encryptedKey,下面我顺便把AES加密的向量也一起加密了得到encryptedIV,这样就完美的互补了对方的缺点,既能够较快的完成数据加密又能保证密钥安全性,两全其美。 整个个加解密流程如下图所示: [图片] 下面的四个小章节将逐一描述流程实现: C端加密数据 C端加密后的数据应如下(并非固定格式,根据自己需要定制): [代码]{ "sessionId": "xxxxxxxx", "encryptedData": "xxxxxx", "encryptedKey": "xxxxxx", "encryptedIV": "xxxxxxx" } [代码] 但RSA公钥又是怎么发放到C端的呢?答案是在登录认证的时候服务器下发的,登录成功时服务器会创建RSA密钥对并把公钥发放给C端,私钥存在服务器上该用户的登录态数据中。流程如下所示: [代码]C端发起登录请求 -> S端接收登录请求 S端登录认证是否通过 true S端生成RSA密钥对 - publicKey , privateKey S端查询相关用户信息 S端生成登录态信息 -> 向登录态信息存入privateKey私钥,登录态信息类似如下 "xxxxxx": { id: "xxxxx", authData: { key: "xxxxxx", privateKey: "xxxxxx" } } S端返回登录态唯一标识sessionId和publicKey公钥以及相关用户信息 -> C端 C端存储登录态信息于本地,后续请求将使用服务器提供的公钥进行加密 [代码] 其中privateKey就是该用户当前登录态所使用的解密私钥,C端通过公钥加密后的AES密钥数据只能用该私钥解密。 如果希望登录阶段的请求也加密,那么可以手动生成一个RSA密钥对,然后客户端放置一个固定的公钥,服务器也使用一个固定的私钥进行登录阶段的加解密。 具体实现如下: 此处引入了CryptoJS.js和RSA.js [代码]const CryptoJS = require('./CryptoJS'); const RSA = require('./RSA.js'); //假设当前已登录成功并获得S端下发的RSA公钥且已存入本地存储 //createRandomStr为生成随机大小写英文和数字的字符串 const aesKey = createRandomStr(16); //生成AES128位密钥 16字节=128位 const aesIV = createRandomStr(16); //生成初始化向量IV const raw = JSON.stringfly({ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" }); const encryptedData = CryptoJS.AES.encrypt(data: raw, key: aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //使用CBC模式和Pkcs7填充加密 const authData = wx.getStorageSync("authData"); //读取本地存的RSA公钥 RSA.setPublicKey(authData.publicKey); //设置RSA公钥 const encryptedKey = RSA.encrypt(aesKey); //RSA加密AES加密密钥 const encryptedIV = RSA.encrypt(aesIV); //RSA加密AES加密初始化向量,是否加密向量可由自己决定 //最后的处理结果 const result = { sessionId: authData.sessionId, encryptedData, encryptedKey, encryptedIV }; [代码] S端解密数据 [代码]const crypto = require('crypto'); const cryptojs = require('crypto-js'); const { sessionId, encryptedData, encryptedKey, encryptedIV } = requestData; const authData = getUserSessionData(sessionId); //获取用户登录态数据 const privateKey = authData.privateKey; const aesKey = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedKey); //使用私钥解密得到AES密钥 const aesIV = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedIV); //使用私钥解密得到AES iv向量 const key = cryptojs.enc.Base64.parse(aesKey); const iv = cryptojs.enc.Utf8.parse(aesIV); const decryptedData = cryptojs.AES.decrypt(encryptedData, key, { iv, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //采用与加密统一的模式和填充进行解密 const { "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } = decryptedData; //至此解密得到C端传来的数据 [代码] S端加密数据 当S端处理完C端的请求后应加密响应数据,那么加密响应数据应该使用什么密钥呢?既然C端已经将加密的密钥发送过来了,那么干脆将C端使用的AES密钥拿来加密响应数据就可以了。加密的数据传回C端后,C端只需使用该请求所使用的AES加密密钥进行解密即可。响应的加密数据如下: [代码]const cryptojs = require('crypto-js'); const responseData = JSON.stringify(...); //此为S端需要返回给C端的数据 const aesKey = ...; //此为之前C端用来加密数据的AES密钥 const aesIV = createRandomStr(16); //生成初始化向量IV const encryptedData = cryptojs.AES.encrypt(data, aesKey, { iv: aesIV, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //加密响应数据 const encryptedResponse = { "encryptedData": encryptedData, "iv": aesIV }; //得到加密后的响应数据并返回给C端 [代码] C端解密数据 C端接收到S端的响应数据后应对加密的数据进行解密,此次解密就是单纯的AES解密了,使用发起请求时用于加密数据的AES密钥配合响应数据的iv向量对encryptedData进行解密,得到解密后的数据即为S端真正的响应数据。实现过程如下: 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const key = ...; //之前用于加密请求参数的AES加密密钥 const { encryptedData, iv } = responseData; const aesIV = CryptoJS.enc.Utf8.parse(iv); const aesKey = CryptoJS.enc.Utf8.parse(key); const decryptedData = CryptoJS.AES.decrypt(encryptedData, aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //AES解密响应数据 const { ... } = decryptedData; //得到解密后的响应数据 [代码] 结语 第一次写这么长的文章,可能存在大量纰漏,如果大佬发现问题欢迎指出(`・ω・´)我会马上修改。 也许小程序的运行环境会比我想象中的更安全 也许HTTPS也会比我想象中的更安全 也许Web服务器引擎也比我想象中的更安全 但,安全不总是先行一步的吗?
2019-10-24 - 腾讯云 微信小程序 即时通信IM demo
产品简介 即时通信(Instant Messaging,IM)基于QQ 底层 IM 能力开发,仅需植入 SDK 即可轻松集成聊天、会话、群组、资料管理能力,帮助您实现文字、图片、短语音、短视频等富媒体消息收发,全面满足通信需要。 应用场景 客服咨询 即时通信 IM 可满足商家与用户多场景沟通的需要,为客户提供专属客服服务,提升服务效率,通过与智能机器人结合,可有效降低人力成本,沉淀客户价值。 [图片] 直播弹幕 即时通信 IM 可支持弹幕、 送礼和点赞等多消息类型,轻松打造良好的直播聊天互动体验;提供弹幕内容审核能力,保证您的直播免受不雅信息干扰。 [图片] 网红带货 即时通信 IM 与商业直播相结合,通过提供点赞、询价、购物券等特定消息类型,帮助直播客户实现流量变现。 [图片] 教学白板 即时通信 IM 为可提供在线课堂,文本消息,画笔轨迹等能力,轻松实现教师学生沟通、画笔轨迹保存、大班课与小班课教学等教学场景。 [图片] 社交沟通 即时通信 IM 可实现单聊、群聊、弹幕等多种聊天模式,支持文字、图片、语音、短视频等多种消息类型,有效提升用户粘性与活跃度。 [图片] 企业办公 即时通信 IM 为企业客户提供覆盖桌面与移动端的完整解决方案,满足设备无缝切换的需求,提高企业内外沟通效率。 [图片] 智能设备 即时通信 IM 提供人与物、物与物协同通信,携手共进引领 5G 通信时代潮流。 [图片] 快速体验,IMSDK小程序demo运行 本 IM 小程序 demo 是基于 MpVue 框架进行开发的。[代码]一分钟跑通 demo[代码] 小节只是用于引导您打开编译后的文件进行快速预览,如果您想要进行二次开发,请看[代码]开发运行[代码]小节。 一分钟跑通demo 克隆仓库到本地 [代码]# 命令行执行 git clone https://github.com/tencentyun/TIMSDK.git # 进入小程序 Demo 项目 cd TIMSDK/WXMini [代码] 安装微信小程序 开发者工具。 使用微信开发者工具导入项目,请注意目录为 [代码]/dist/wx[代码],然后填入自己的小程序 AppID。 [图片] 配置 [代码]SDKAPPID[代码] 和 [代码]SECRETKEY[代码],获取方式参考:密钥获取方法 打开 [代码]/debug/GeneraterUserSig.js[代码] 文件 按图示填写相应配置后,保存文件 [图片] 本地配置如下图所示 勾选ES6转ES5选项 勾选不检验合法域名选项 基础库版本 > 2.1.1 [图片] 点击编译即可运行 [图片] 注意事项 合法域名 如果您要发布小程序,请将以下域名在【微信公众平台】>【开发】>【开发设置】>【服务器域名】中进行配置 进入微信公众平台,在小程序开发的服务器域名配置相关域名信息 添加到 request 合法域名: 域名 说明 是否必须 [代码]https://webim.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://yun.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://pingtas.qq.com[代码] Web IM 统计域名 必须 添加到 uploadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件上传域名 必须 添加到 downloadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件下载域名 必须 [图片] 开发运行 项目目录 [代码]├───sdk/ - 存放tim-wx.js,demo 中未使用,仅供自行集成 ├───build/ ├───config/ ├───dist/ │ └───wx/ - MpVue 项目编译后文件目录,使用小程序开发工具导入此文件夹 ├───src/ │ ├───components/ - 组件 │ ├───pages/ - 页面 │ ├───store/ - Vuex 目录 │ ├───stylus/ - 全局主题色样式,可以修改全局颜色 │ ├───utils/ - 方法 │ ├───app.json │ ├───App.vue │ └───main.js ├───static/ - 静态依赖资源 │ ├───debug/ - 包含 userSig 验证登录方法 │ ├───images/ - 图片 │ └───iview/ - 使用的 iview 组件 ├───_doc/ ├───.babelrc ├───.editorconfig ├───.eslintignore ├───.eslintrc.js ├───.postcssrc.js ├───index.html ├───package-lock.json ├───package.json ├───project.config.json └───README.md [代码] 准备工作 获取到您应用的 [代码]SDKAPPID[代码] 和 [代码]SECRETKEY[代码],方式参考:密钥获取方法 安装微信小程序 开发者工具 安装 nodejs 环境 ( Version > 8 ) ,选择合适您安装环境的安装包 安装后,在命令行输入[代码]node --version[代码] ,如果 > 8 即可 启动流程 克隆仓库到本地 [代码]# 命令行执行 git clone https://github.com/tencentyun/TIMSDK.git # 进入 Demo 项目 cd TIMSDK/WXMini [代码] 将[代码]project.config.json[代码]文件中的[代码]appid[代码]修改为自己微信小程序的[代码]appid[代码] [图片] 配置 [代码]SDKAPPID[代码] 和 [代码]SECRETKEY[代码],获取方式参考:密钥获取方法 打开 [代码]/static/debug/GeneraterUserSig.js[代码] 文件 按图示填写相应配置后,保存文件 [图片] 安装依赖并启动 [代码]# 安装demo构建和运行所需依赖 npm install # 构建并生成最终可在小程序开发工具内使用的代码 npm run start [代码] 使用 [代码]npm install[代码] 命令,如果有些依赖包无法成功安装 您可以试着切换源, 例如: [代码]npm config set registry http://r.cnpmjs.org/[代码] 然后再执行 [代码]npm install[代码] 使用微信开发者工具导入项目,目录为[代码]/dist/wx[代码] [图片] 本地配置如下图所示 勾选ES6转ES5选项 勾选不检验合法域名选项 基础库版本 > 2.1.1 [图片] 点击开发工具的编译即可预览该项目 [图片] 注意事项 合法域名 如果您要发布小程序,请将以下域名在【微信公众平台】>【开发】>【开发设置】>【服务器域名】中进行配置 进入微信公众平台,在小程序开发的服务器域名配置相关域名信息 添加到 request 合法域名: 域名 说明 是否必须 [代码]https://webim.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://yun.tim.qq.com[代码] Web IM 业务域名 必须 [代码]https://pingtas.qq.com[代码] Web IM 统计域名 必须 添加到 uploadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件上传域名 必须 添加到 downloadFile 合法域名: 域名 说明 是否必须 [代码]https://cos.ap-shanghai.myqcloud.com[代码] 文件下载域名 必须 [图片] 项目截图 [图片] 备注 页面结构 目录 /src/pages 页面 简介 login/ 登录页 index/ 首页,对话列表 chat/ 聊天对话页 & 群信息/用户信息 contact/ 通讯录 own/ 个人信息 create/ 创建群聊 members/ 群成员 profile/ 修改个人信息 groups/ 群列表 groupDetail/ 群详细页 system/ 系统通知页 blacklist/ 黑名单页 detail/ 个人信息&群信息 friend/ 发起会话 mention/ @选择页 注意事项 1. 避免在前端进行签名计算 本 Demo 为了用户体验的便利,将 [代码]userSig[代码] 签发放到前端执行。若直接部署上线,会面临 [代码]SECRETKEY[代码] 泄露的风险。正确的 [代码]userSig[代码] 签发方式是将 [代码]userSig[代码] 的计算代码集成到您的服务端,并提供相应接口。在需要 [代码]userSig[代码] 时,发起请求获取动态 [代码]userSig[代码]。更多详情请参见 服务端生成 UserSig。 2. 如果无法访问github或者访问速度过慢 下载zip包 解压后,进入 TIMSDK/WXMini目录,即可查看demo代码。
2019-09-16 - canvas生成海报图、分享
首先话不多说元素样式走起来 canvas宽高由js变量动态定义 html <canvas class=‘canvas’ canvas-id=“shareCanvas” style=“width:{{canvasWidth}}px;height:{{canvasHeight}}px”></canvas> css .canvas{ margin: 0 auto; letter-spacing: 2rpx; //画布上文字间距 我实在不知道canvas api怎么控制字间距 margin-top: 10%; } 然后绘制canvas代码 canvasImg:function(){ [代码]var that = this; wx.showLoading({ title: '图片正在生成' }); //拿到用户名称和用户头像 name ,img var name = app.header.userInfo.nickName; var img = app.header.userInfo.avatarUrl.replace("http:", "https:"); //请求后台接口拿到小程序码临时url //这里是我们后台根据我传递的页面路径和标识获取对应小程序的小程序码 返回一个图片临时缓存的url wx.request({ url: app.data.proxy + '/miniprogram/getUnlimited', data: { type: app.data.pdd, page:'pages/index/index' }, success(res) { var image = res.data.result; //拿到临时url 使用getImageInfo缓存到本地 wx.getImageInfo({ src:image, success(res){ //liload 小程序码本地缓存地址 var liload = res; //获取用户设备宽高 wx.getImageInfo({ src: img, success(res) { var width,height; wx.getSystemInfo({ success(res) { width = res.screenWidth * 0.6; height = Math.round(width * 1066 / 600); that.setData({ canvasWidth: width, canvasHeight: height }); } }); var x = width/750; //设置相对canvas自适应根元素大小 const ctx = wx.createCanvasContext('shareCanvas'); ctx.drawImage('../img/pinduoduo.jpg', 0, 0, 500*x, 812*x); ctx.save(); ctx.setTextAlign('center'); // 文字居中 ctx.setFillStyle('#fff'); // 文字颜色:黑色 ctx.setFontSize(16); // 文字字号 ctx.fillText(name, 250*x, 250*x); //名字 ctx.setFontSize(14); // 文字字号 ctx.fillText('邀请你使用【拼拼宝盒】', 250*x, 300*x); ctx.save(); //圆形头像框 ctx.setStrokeStyle('rgba(0,0,0,.2)'); ctx.arc(250 * x, 140 * x, 60 * x, 0, 2 * Math.PI); ctx.setStrokeStyle('rgba(0,0,0,.2)'); ctx.arc(250 * x, 460 * x, 120 * x, 0, 2 * Math.PI); ctx.fill('#fff'); //小程序码 ctx.clip(); ctx.drawImage(liload.path, 150*x, 360*x, 200*x, 200*x); //头像 ctx.clip(); ctx.drawImage(res.path, 190*x, 80*x, 120*x, 120*x); ctx.save(); ctx.stroke(); ctx.draw(); wx.hideLoading(); } }); } }) } }); [代码] } 绘制完成 [图片] 保存事件 这里首先用getSetting检测用户有没有开启相册权限 有的话直接保存 没有的话弹到权限页面让用户授权 btnTap:function () { [代码]var that = this; wx.showLoading({ title: '正在保存', mask: true, }); wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { that.saveImg(); } else if (res.authSetting['scope.writePhotosAlbum'] === undefined) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { that.saveImg(); }, fail() { wx.showToast({ title: '您没有授权,无法保存到相册', icon: 'none' }) } }) } else { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { that.saveImg(); } else { wx.showToast({ title: '您没有授权,无法保存到相册', icon: 'none' }) } } }) } } }) [代码] } 用户有授权调用保存图片API 保存图片到手机 saveImg:function(){ [代码] wx.canvasToTempFilePath({ canvasId: 'shareCanvas', success: function (res) { wx.hideLoading(); var tempFilePath = res.tempFilePath; wx.saveImageToPhotosAlbum({ filePath: tempFilePath, success(res) { wx.showModal({ content: '图片已保存到相册,赶紧晒一下吧~', showCancel: false, confirmText: '好的', confirmColor: '#333', success: function (res) { if (res.confirm) { var arr = []; arr.push(tempFilePath); //保存后全屏显示 wx.previewImage({ urls: arr, current: arr }); } }, fail: function (res) { } }) }, fail: function (res) { wx.showToast({ title: '没有相册权限', icon: 'none', duration: 2000 }); } }) } }); [代码] } 好了 就这么多了 第一次发帖 有不足之处望各路大佬见谅、指出不胜感激 附代码片段:https://developers.weixin.qq.com/s/8Z8oXbm17ojh
2020-07-28 - BookChat v2.3 发布,通用书籍阅读小程序,增加分享海报和广告功能
BookChat 介绍 BookChat - 面向程序员的开源书籍和文档阅读学习小程序,同时也是一款基于 Apache 2.0 开源协议进行开源的通用书籍阅读微信小程序。 它非常轻量,200KB 左右的大小,麻雀虽小五脏俱全,该有的功能一个没少;同时参照了腾讯官方的 微信小程序设计指南 进行设计,拥有简洁美观的页面和良好的用户体验。 升级日志 抽屉兼容优化:部分安卓手机机型,阅读页面抽屉间距不合理,以致书籍目录和阅读偏好设置部分被遮挡 阅读体验优化:打开书籍阅读,自动跳转到上次阅读的位置,再也不用担心忘记上次阅读到了哪里 增加广告功能:微信小程序如需添加微信小程序广告,只需修改[代码]config.js[代码]的配置项 [代码]// 横幅广告id,如果申请了腾讯小程序的广告,则创建一个横幅广告,把广告的AdUnitId粘贴进来即可,不投放广告,则把该值设置为空 const bannerAdUnitId = '' [代码] 增加分享海报:分享到朋友圈,也没有那么困难重重了 使用了海报生成组件库: https://github.com/kuckboy1994/mp_canvas_drawer [图片] 其它若干小细节优化 相关地址 BookChat 开源地址 Gitee(码云): https://gitee.com/truthhun/BookChat Github: https://github.com/truthhun/BookChat BookChat 后端程序 BookStack 开源地址 Gitee(码云): https://gitee.com/truthhun/BookStack Github: https://github.com/truthhun/BookStack BookChat 体验码 [图片]
2019-08-14 - 小程序云开发之数据库自动备份
数据是无价的,我们通常会把重要的业务数据存放在数据库中,并需要对数据库做定时的自动备份工作,防止数据异常丢失,造成无法挽回的损失。 小程序云开发提供了方便的云数据库供我们直接使用,云开发使用了腾讯云提供的云数据库,拥有完善的数据保障机制,无需担心数据丢失。但是,我们还是不可避免的会担心数据库中数据的安全,比如不小心删除了数据集合,写入了脏数据等。 还好,云开发控制台提供了数据集合的导出,导入功能,我们可以手动备份数据库。不过,总是手动备份数据库也太麻烦了点,所有重复的事情都应该让代码去解决,下面我们就说说怎么搞定云开发数据库自动备份。 通过查阅微信的文档,可以发现云开发提供了数据导出接口databaseMigrateExport [代码]POST https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=ACCESS_TOKEN [代码] 通过这个接口,结合云函数的定时触发功能,我们就可以做数据库定时自动备份了。梳理一下大致的流程: 创建一个定时触发的云函数 云函数调用接口,导出数据库备份文件 将备份文件上传到云存储中以供使用 1. 获取 access_token 调用微信的接口需要 access_token,所以我们首先要获取 access_token。通过文档了解到使用 auth.getAccessToken 接口可以用小程序的 appid 和 secret 获取 access_token。 [代码]// 获取 access_token request.get( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`, (err, res, body) => { if (err) { // 处理错误 return; } const data = JSON.parse(body); // data.access_token } ); [代码] 2. 创建数据库导出任务 获取 access_token 后,就可以使用 [代码]databaseMigrateExport[代码] 接口导出数据进行备份。 [代码]databaseMigrateExport[代码] 接口会创建一个数据库导出任务,并返回一个 job_id,这个 job_id 怎么用我们下面再说。显然数据库的数据导出并不是同步的,而是需要一定时间的,数据量越大导出所要花费的时间就越多,个人实测,2W 条记录,2M 大小,导出大概需要 3~5 S。 调用 [代码]databaseMigrateExport[代码] 接口需要传入环境 Id,存储文件路径,导出文件类型(1 为 JSON,2 为 CSV),以及一个 query 查询语句。 因为我们是做数据库备份,所以这里就导出 JSON 类型的数据,兼容性更好。需要备份的数据可以用 query 来约束,这里还是很灵活的,既可以是整个集合的数据,也可以是指定的部分数据,这里我们就使用 [代码]db.collection('data').get()[代码] 备份 data 集合的全部数据。同时我们使用当前时间作为文件名,方便以后使用时查找。 [代码]request.post( `https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=${accessToken}`, { body: JSON.stringify({ env, file_path: `${date}.json`, file_type: '1', query: 'db.collection("data").get()' }) }, (err, res, body) => { if (err) { // 处理错误 return; } const data = JSON.parse(body); // data.job_id } ); [代码] 3. 查询任务状态,获取文件地址 在创建号数据库导出任务后,我们会得到一个 job_id,如果导出集合比较大,就会花费较长时间,这时我们可以使用 databaseMigrateQueryInfo 接口查询数据库导出的进度。 当导出完成后,会返回一个 [代码]file_url[代码],即可以下载数据库导出文件的临时链接。 [代码]request.post( `https://api.weixin.qq.com/tcb/databasemigratequeryinfo?access_token=${accessToken}`, { body: JSON.stringify({ env, job_id: jobId }) }, (err, res, body) => { if (err) { reject(err); } const data = JSON.parse(body); // data.file_url } ); [代码] 获取到文件下载链接之后,我们可以将文件下载下来,存入到自己的云存储中,做备份使用。如果不需要长时间的保留备份,就可以不用下载文件,只需要将 job_id 存储起来,当需要恢复备份的时候,通过 job_id 查询到新的链接,下载数据恢复即可。 至于 job_id 存在哪,就看个人想法了,这里就选择存放在数据库里。 [代码]await db.collection('db_back_info').add({ data: { date: new Date(), jobId: job_id } }); [代码] 4. 函数定时触发器 云函数支持定时触发器,可以按照设定的时间自动执行。云开发的定时触发器采用的 [代码]Cron[代码] 表达式语法,最大精度可以做的秒级,详细的使用方法可以参考官方文档:定时触发器 | 微信开放文档 这里我们配置函数每天凌晨 2 点触发,这样就可以每天都对数据库进行备份。在云函数目录下新建 [代码]config.json[代码]文件,写入如下内容: [代码]{ "triggers": [ { "name": "dbTrigger", "type": "timer", "config": "0 0 2 * * * *" } ] } [代码] 完整代码 最后,贴出可以在云函数中使用的完整代码,只需要创建一个定时触发的云函数,并设置好相关的环境变量即可使用 appid secret backupColl:需要备份的集合名称,如 ‘data’ backupInfoColl:存储备份信息的集合名称,如 ‘db_back_info’ 注意,云函数的默认超时时间是 3 秒,创建备份函数时,建议将超时时间设定到最大值 20S,留有足够的时间查询任务结果。 [代码]/* eslint-disable */ const request = require('request'); const cloud = require('wx-server-sdk'); // 环境变量 const env = 'xxxx'; cloud.init({ env }); // 换取 access_token async function getAccessToken(appid, secret) { return new Promise((resolve, reject) => { request.get( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`, (err, res, body) => { if (err) { reject(err); return; } resolve(JSON.parse(body)); } ); }); } // 创建导出任务 async function createExportJob(accessToken, collection) { const date = new Date().toISOString(); return new Promise((resolve, reject) => { request.post( `https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=${accessToken}`, { body: JSON.stringify({ env, file_path: `${date}.json`, file_type: '1', query: `db.collection("${collection}").get()` }) }, (err, res, body) => { if (err) { reject(err); } resolve(JSON.parse(body)); } ); }); } // 查询导出任务状态 async function waitJobFinished(accessToken, jobId) { return new Promise((resolve, reject) => { // 轮训任务状态 const timer = setInterval(() => { request.post( `https://api.weixin.qq.com/tcb/databasemigratequeryinfo?access_token=${accessToken}`, { body: JSON.stringify({ env, job_id: jobId }) }, (err, res, body) => { if (err) { reject(err); } const { status, file_url } = JSON.parse(body); console.log('查询'); if (status === 'success') { clearInterval(timer); resolve(file_url); } } ); }, 500); }); } exports.main = async (event, context) => { // 从云函数环境变量中读取 appid 和 secret 以及数据集合 const { appid, secret, backupColl, backupInfoColl } = process.env; const db = cloud.database(); try { // 获取 access_token const { errmsg, access_token } = await getAccessToken(appid, secret); if (errmsg && errcode !== 0) { throw new Error(`获取 access_token 失败:${errmsg}` || '获取 access_token 为空'); } // 导出数据库 const { errmsg: jobErrMsg, errcode: jobErrCode, job_id } = await createExportJob(access_token, backupColl); // 打印到日志中 console.log(job_id); if (jobErrCode !== 0) { throw new Error(`创建数据库备份任务失败:${jobErrMsg}`); } // 将任务数据存入数据库 const res = await db.collection('db_back_info').add({ data: { date: new Date(), jobId: job_id } }); // 等待任务完成 const fileUrl = await waitJobFinished(access_token, job_id); console.log('导出成功', fileUrl); // 存储到数据库 await db .collection(backupInfoColl) .doc(res._id) .update({ data: { fileUrl } }); } catch (e) { throw new Error(`导出数据库异常:${e.message}`); } }; [代码]
2019-08-12 - 一键完成小程序国际化
随着小程序使用的人数增长,小程序管理后台陆续收到非中文母语的用户要求支持英文的请求。小程序一开始是直接在程序里使用中文字符串的方式,要做国际化只能把这些硬编码中文的地方全部替换为i18n调用。写了个脚本跑了下,发现小程序里涉及到需要转换硬编码的地方有2000+处。。。手动修改不太科学,于是写了个名为mina-i18n的工具,用于小程序的国际化转换,有兴趣的同学可以用自己的小程序项目实验一下: npm install -g mina-i18n mina-i18n /path/to/origin/mina/project /path/to/i18n/mina/project工具主要包含对JS和WXML两种文件的处理,JS使用babel处理,WXML使用htmlparser库处理,json和css由于没法使用函数调用,这个需要用户自行处理了。 JS处理: JS文件的处理思路:找到中文字符串,将其改为以中文字符串为参数的i18n函数调用。我们的目标是一键转换后可以直接运行,而替换的行为发生在任何一个文件里,所以我们需要一个全局可访问的函数。在小程序里,有两个可以全局访问的变量,一个是getApp(),一个是wx,为了代码的简洁性,我们决定在wx上挂载一个L函数作为全局i18n函数。即: "中文" 转换为 wx._t("中文")直接使用正则表达式匹配是无法做到的符合语法的替换,这里使用babel完成保留程序上下文的替换,替换逻辑写在babel 的插件里的 StringLiteral 的回调里(所有JS程序里解析到的字符串都会进入这个回调),代码如下: const visitor = { StringLiteral(path) { const parentPath = path.parent // 每个中文字符串只处理一次,识别到已处理过就退出,不然会死循环 if (t.isCallExpression(parentPath)) { const callee = parentPath.callee if (t.isMemberExpression(callee)) { const { object, property } = callee if ( t.isIdentifier(object) && object.name === MINA_I18N_JS_FUNCTION_CALLEE && property.name === MINA_I18N_FUNCTION_NAME ) { return } } } const reg = /\s*[\u4E00-\u9FA5]+\s*/g const stringValue = path.node.value if (reg.test(stringValue)) { path.replaceWith(t.CallExpression( t.MemberExpression( t.identifier(MINA_I18N_JS_FUNCTION_CALLEE), t.identifier(MINA_I18N_FUNCTION_NAME) ), [t.stringLiteral(stringValue)] )) } } } WXML处理: 对WXML的处理思路与JS类似,就是找出WXML文件里的中文文本,替换为以文本为参数的i18n函数调用。在小程序里使用函数调用需要用到 WXS语言 ,这个语言无法使用小程序API,只有极少数的类库,反正你当成是个纯运算逻辑的WXML语法助手就行。因为WXS语言没法从外部获取数据,或者WXML里的i18n函数调用只能是类似 i18nFunc("中文","en") 这种形式,就是说具体要转为哪种语言,必须明确地告知WXS的函数。一般用户的语言都是从系统信息中获取,或者用户手动选择后存在服务端,这个语言的变量信息我们在JS里可以获取,而且这个变量每个页面都会用到,而WXML里是没法访问到getApp()和wx这些全局变量的,我们只能在每个页面的Page函数的data变量里加上这个语言变量Lang,才能在每个WXML里使用类似 i18nFunc("中文", Lang) 的形式来做文本替换。处理代码如下: const visitor = { ObjectProperty(path, state) { if (path.node.key.name === 'data') { const parentPath = path.parentPath || {} const parentParentPath = parentPath.parentPath || {} const node = parentParentPath.node if ( t.isObjectExpression(parentPath) && t.isCallExpression(parentParentPath) && node && node.callee && t.isIdentifier(node.callee) && node.callee.name === 'Page' ) { // 变量插入只处理一次,识别到已处理过就退出,不然会死循环 if ( t.isCallExpression(path.node.value) && t.isObjectExpression(path.node.value.arguments[0]) && path.node.value.arguments[0].properties[0].key.name === MINA_I18N_LANG_NAME ) { return } path.replaceWith( t.objectProperty( path.node.key, t.CallExpression( t.MemberExpression( t.identifier('Object'), t.identifier('assign') ), [ t.objectExpression([ t.objectProperty( t.identifier(MINA_I18N_LANG_NAME), t.CallExpression( t.MemberExpression( t.identifier(MINA_I18N_JS_FUNCTION_CALLEE), t.identifier(MINA_I18N_GETLANG_FUNCTION_NAME) ), [] ) ) ]), path.node.value ] ) ) ) } } } } 有了可在WXML里运行的 i18n函数,接下来就是对WXML源文件里的中文文本进行替换。WXML的转换处理要比JS复杂一些,处理JS的时候babel帮我们做好了语法树的解析、遍历和代码生成过程,我们只要实现语法树访问的回调就行,WXML就没有这么好用的工具了,所以需要自己做多点工作。WXML的解析用了 htmlparser2 ,这个库会返回给你一棵DOM树,按着根节点就能遍历整棵树。不过在实际解析生成WXML的时候,发现这个库有个地方没法满足,就是它解析出来的节点属性无法区分开是为空还是 boolean attributes ,也就是说<view hidden></view>和<view hidden=""></view>解析出来的结果都是一样的{hidden: ""},这其实有很大的歧义,因为在小程序里<view hidden></view>会表示这个view的hidden=true,而<view hidden=""></view>这会被解释为hidden=false,因此按照这个解析结果生成的话,就发现原来小程序里应该隐藏的view都被显示了。。。搜了下其他有其他人给作者提过这个问题的 issue ,我看作者的意思大概是这个库不是用来处理序列化或者生成代码这类事的,在github搜了下好像这个库又确实是JS里HTML解析库里排名第一的,于是就自己fork出来 一份,加了个选项来做boolean attributes 的区分。 在做WXML处理的时候,我犯了个错误,就是把处理WXML跟处理HTML这两件事等同起来了,可能之前做过一些HTML相关的处理工作,所以写着写着就很顺地按着原来用正则表达式处理的方式去做了,结果发现在处理节点属性为{{xxx}}的动态属性时总是有edge case处理不到。中间隔了两天去做其他需求后再回来一想,这{{xxx}}里的xxx就是JS代码啊,直接用babel不就完事了吗?困在原来处理HTML的思路里调试了大半天浪费时间。正确的处理WXML的方式就是用htmlparser解析静态文本,解析到的{{xxx}}语法交给babel处理,最后再重新合成WXML代码。这里面还有属性的单双引号处理、uincode与汉字的一些处理等细节,具体就不展开了,代码里都有: function buildWXML(root) { if (Array.isArray(root)) { let xmlString = '' root.forEach(node => { xmlString += buildWXML(node) }) return xmlString } else if (root.type === 'text') { const text = root.data return processMinaTemplateText(text) } else if (root.type === 'tag') { const tagName = root.name.replace('wx-', '') let tagString = `<${tagName}` const attr = root.attribs || {} Object.keys(attr).forEach(key => { if (attr[key] === null) { tagString += ` ${key}` } else { const attrValue = processMinaTemplateText(attr[key], { isAttrValue: true }) tagString += ` ${key}="${attrValue}"` } }) const children = root.children || [] if (isSelfCloseTag(tagName) && children.length === 0) { tagString += '/>' } else { tagString += '>' children.forEach(node => { tagString += buildWXML(node) }) tagString += `</${tagName}>` } return tagString } else if (root.type === 'comment') { return `<!--${root.data}-->` } else { return '' } } function processMinaTemplateText(text, options = {}) { const textArray = text.split(/({{[^}]*}})/g) let returnText = '' textArray.forEach(item => { if (item.startsWith('{{') && item.endsWith('}}')) { let expression = item.substring(2, item.length - 2) returnText += '{{' + processMinaTemplateExpression(expression) + '}}' } else { returnText += processPlainText(item) } }) return returnText } function processMinaTemplateExpression(code) { const visitor = { StringLiteral(path) { const parentPath = path.parent if (t.isCallExpression(parentPath)) { const callee = parentPath.callee if (t.isIdentifier(callee) && callee.name === MINA_I18N_FUNCTION_NAME) { return } } const reg = /\s*[\u4E00-\u9FA5]+\s*/g const stringValue = path.node.value if (reg.test(stringValue)) { path.replaceWith(t.CallExpression( t.identifier(MINA_I18N_FUNCTION_NAME), [t.stringLiteral(stringValue), t.identifier(MINA_I18N_LANG_NAME)] )) createI18NData(stringValue) } } } try { const result = babel.transform(code, { plugins: [ { visitor } ], generatorOpts: { quotes: 'single', compact: false } }) const i18nScriptContent = transformText(toSingleQuotes(result.code)) return i18nScriptContent.replace(/[\r\n]+$/g, '').replace(/^;/g, '').replace(/;$/g, '') } catch (e) { console.log(`parser expression : ${code}, error: ${JSON.stringify(e)}`) return code } } 翻译:一开始做这个工具的目的,就是要做到一键转换后就可以在微信开发者工具里跑起来。经过上面两步的处理,我们已经把项目里出现过的所有中文字符串都提取并替换,接下来我们就要实现 i18n 翻译函数。 最简单的是简体和繁体的转换,因为已经有 opencc 这个优秀的开源库可以处理 。 const OpenCC = require('opencc') const opencc = new OpenCC() const hant_text = opencc.convertSync(text) 中英文的翻译没法离线,只能找线上服务。在找了google,有道,金山,必应,百度等翻译服务后,发现微软的必应是最方便的,不需要申请token或者去对抗防爬虫,翻译效果也不错,非常适合用在一个可以无条件使用的工具里,接口调用非常简洁: request.post({ url: 'https://cn.bing.com/ttranslatev3', form: { fromLang: 'zh-Hans', to: 'en', text: text }, json: true, timeout: 2000 }).then(res => { return res[0].translations[0].text }) 绝大多数中文项目的i18n做到繁体+英文就够了,外贸项目等特殊情况的话,改一下调用bing API的语言参数就行,bing支持多种语言翻译。 至此,工具也算基本完成了,使用了自己小程序和github上找的几个开源小程序项目做测试,基本都是一键转换后就能直接在开发者工具上跑的,真机预览也没出问题。有兴趣的同学可以在自己项目的小程序上跑跑看。我看了下日常使用的各个小程序,可以说几乎100%都是没有提供切换语言选项的,万一以后有需求的话,希望这个工具可以方便到大家。 对源码有兴趣的同学可以到 github 看一下,如果转换的小程序跑起来有错误的话也欢迎提issue,我会尽快解决的。
2019-08-13 - 10行代码实现微信小程序支付功能,使用小程序云开发实现小程序支付功能(含源码)
前面给大家讲过一个借助小程序云开发实现微信支付的,但是那个操作稍微有点繁琐,并且还会经常出现问题,今天就给大家讲一个简单的,并且借助官方支付api实现小程序支付功能。 传送门 借助小程序云开发实现小程序支付功能 老规矩,先看本节效果图 [图片] 我们实现这个支付功能完全是借助小程序云开发实现的,不用搭建自己的服务器,不用买域名,不用备案域名,不用支持https。只需要一个简单的云函数,就可以轻松的实现微信小程序支付功能。 核心代码就下面这些 [图片] 一,创建一个云开发小程序 关于如何创建云开发小程序,这里我就不再做具体讲解。不知道怎么创建云开发小程序的同学,可以去翻看我之前的文章,或者看下我录制的视频:https://edu.csdn.net/course/play/9604/204528 创建云开发小程序有几点注意的 1,一定不要忘记在app.js里初始化云开发环境。 [图片] 2,创建完云函数后,一定要记得上传 二, 创建支付的云函数 1,创建云函数pay [图片] [图片] 三,引入三方依赖tenpay 我们这里引入三方依赖的目的,是创建我们支付时需要的一些参数。我们安装依赖是使用里npm 而npm必须安装node,关于如何安装node,我这里不做讲解,百度一下,网上一大堆。 1,首先右键pay,然后选择在终端中打开 [图片] 2,我们使用npm来安装这个依赖。 在命令行里执行 npm i tenpay [图片] 安装完成后,我们的pay云函数会多出一个package.json 文件 [图片] 到这里我们的tenpay依赖就安装好了。 四,编写云函数pay [图片] 完整代码如下 [代码]//云开发实现支付 const cloud = require('wx-server-sdk') cloud.init() //1,引入支付的三方依赖 const tenpay = require('tenpay'); //2,配置支付信息 const config = { appid: '你的小程序appid', mchid: '你的微信商户号', partnerKey: '微信支付安全密钥', notify_url: '支付回调网址,这里可以先随意填一个网址', spbill_create_ip: '127.0.0.1' //这里填这个就可以 }; exports.main = async(event, context) => { const wxContext = cloud.getWXContext() let { orderid, money } = event; //3,初始化支付 const api = tenpay.init(config); let result = await api.getPayParams({ out_trade_no: orderid, body: '商品简单描述', total_fee: money, //订单金额(分), openid: wxContext.OPENID //付款用户的openid }); return result; } [代码] 一定要注意把appid,mchid,partnerKey换成你自己的。 到这里我们获取小程序支付所需参数的云函数代码就编写完成了。 不要忘记上传这个云函数。 [图片] 出现下图就代表上传成功 [图片] 五,写一个简单的页面,用来提交订单,调用pay云函数。 [图片] 这个页面很简单, 1,自己随便编写一个订单号(这个订单号要大于6位) 2,自己随便填写一个订单价(单位是分) 3,点击按钮,调用pay云函数。获取支付所需参数。 下图是官方支付api所需要的一些必须参数。 [图片] 下图是我们调用pay云函数获取的参数,和上图所需要的是不是一样。 [图片] 六,调用wx.requestPayment实现支付 下图是官方的示例代码 [图片] 这里不在做具体讲解了,把完整代码给大家贴出来 [代码]// pages/pay/pay.js Page({ //提交订单 formSubmit: function(e) { let that = this; let formData = e.detail.value console.log('form发生了submit事件,携带数据为:', formData) wx.cloud.callFunction({ name: "pay", data: { orderid: "" + formData.orderid, money: formData.money }, success(res) { console.log("提交成功", res.result) that.pay(res.result) }, fail(res) { console.log("提交失败", res) } }) }, //实现小程序支付 pay(payData) { //官方标准的支付方法 wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, //统一下单接口返回的 prepay_id 格式如:prepay_id=*** signType: 'MD5', paySign: payData.paySign, //签名 success(res) { console.log("支付成功", res) }, fail(res) { console.log("支付失败", res) }, complete(res) { console.log("支付完成", res) } }) } }) [代码] 到这里,云开发实现小程序支付的功能就完整实现了。 实现效果 1,调起支付键盘 [图片] 2,支付完成 [图片] 3,log日志,可以看出不同支付状态的回调 [图片] 上图是支付成功的回调,我们可以在支付成功回调时,改变订单支付状态。 下图是支付失败的回调, [图片] 下图是支付完成的状态。 [图片] 到这里我们就轻松的实现了微信小程序的支付功能了。是不是很简单啊。 如果感觉图文不是很好理解,我后面会录制视频讲解。 视频讲解 https://edu.csdn.net/course/detail/25701 源码地址: https://github.com/qiushi123/xiaochengxu_demos [图片] 014云开发实现小程序支付,就是我们的源码,如果你导入源码或者学习过程中有任何问题,都可以加我微信2501902696(备注小程序)
2019-08-14 - BookChat v2.1 发布,开源的书籍阅读微信小程序
BookChat 介绍 微信叫[代码]WeChat[代码],所以我们叫[代码]BookChat[代码]. [代码]BookChat[代码] - 面向程序员的开源书籍和文档阅读学习小程序,同时也是一款基于 Apache 2.0 开源协议进行开源的通用书籍阅读微信小程序。 升级日志 实现微信登录和绑定 移除小程序的普通注册(微信一键登录更便捷) 调整内容显示样式,优化内容阅读体验 列表页增加搜索入口 优化登录提示和登录跳转 小程序体验码 体验一下,相信你会喜欢,没有理由 [图片] 开源地址 Gitee: https://gitee.com/truthhun/BookChat GitHub: https://github.com/TruthHun/BookChat
2019-07-18 - 干货:如何借助小程序云开发实现小程序支付功能(含源码)
正文共:5081 字 13 图 预计阅读时间:13 分钟 我们在做小程序支付相关的开发时总会遇到这些难题 1.小程序调用微信支付时必须要有自己的服务器 2.有自己的备案域名 3.有自己的后台开发 这就导致我们做小程序支付时的成本很大 本节就来教大家如何使用小程序云开发实现小程序支付功能的开发,不用搭建自己的服务器,不用有自己的备案域名,只需要简简单单的使用小程序云开发。 老规矩先看效果图: [图片] 本节知识点 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> [代码] 我们所需要做的就是借助云开发获取云数据库里的商品信息然后展示到商品列表,关于云开发获取商品列表并展示本节不做讲解(感兴趣的同学可以翻看作者历史博客,有写过的) [图片] 三、支付云函数的创建 首先看下我们支付云函数都包含那些内容 [图片] 简单先讲解下每个的用处 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; onst 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, spill_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 }); } } } [代码] 其实我们支付的关键功能都在上面这些代码里面了 [图片] 再来看下支付的相关流程截图 [图片] 上图就涉及到了我们的订单列表、支付状态、支付成功后的回调 今天就先讲到这里后面会继续给大家讲解支付的其他功能——比如支付成功后的消息推送也是可以借助云开发实现的 [图片] 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心!
2019-09-19 - wxs下的时间格式化
1、首先,创建一个wxs的文件,内容如下 var formatTime = function(date) { var date = getDate(date); //返回当前时间对象 var year = date.getFullYear() var month = fixz(date.getMonth() + 1) var day = fixz(date.getDate()) var hour = fixz(date.getHours()) var minute = fixz(date.getMinutes()) var second = fixz(date.getSeconds()) return [year, month, day].join(’-’) + ’ ’ + [hour, minute, second].join(’:’) } var fixz = function(num) { if (num < 10) { return ‘0’ + num } return num } module.exports = { formatTime: formatTime } 2、在wxml中引用文件 <wxs module=‘tools’ src=‘tools.wxs’></wxs> 3、在wxml中转格式 {{tools.formatTime(item.createTime)}} 写在最后,为什么不直接用外部的js?因为所有的所有的数据需要提前在page下的js中处理好才能输出到wxml中,比较麻烦。用wxs可以直接在页面中转化,而且可以直接复用。
2019-05-13 - 99%的程序都没有考虑的网络异常?使用Fundebug.notify()主动上报
近日看到一篇文章99%的程序都没有考虑的网络异常,开篇提到: 绝大多数程序只考虑了接口正常工作的场景,而用户在使用我们的产品时遇到的各类异常,全都丢在看似 ok 的 try catch 中。如果没有做好异常的兼容和兜底处理,会极大的影响用户体验,严重的还会带来安全和资损风险。 于是,笔者分析了 GitHub 上的一些开源微信小程序,发现大多数的代码异常处理确实是不够的。 登录接口只考虑成功的情况,没考虑失败的情况 [代码]//调用登录接口 wx.login({ success: function() { wx.getUserInfo({ success: function(res) { that.globalData.userInfo = res.userInfo; typeof cb == "function" && cb(that.globalData.userInfo); } }); } }); [代码] 网络请求只考虑[代码]then[代码]不考虑[代码]catch[代码] [代码]util.getData(index_api).then(function(data) { //this.setData({ // //}); console.log(data); }); [代码] 考虑了异常情况但是没有做妥善的处理 [代码]db.collection("config") .where({}) .get() .then(res => { console.log(res); if (res.data.length > 0) { Taro.setStorage({ key: "config_gitter", data: res.data[0] }); } }) .catch(err => { console.error(err); }); [代码] 也许 99%的情况下接口都是正常返回的,只有 1%的情况会失败。看起来好像不是一件严重的事情,但是考虑到用户的量级,这个事情就不那么简单了。假设有 100 万用户,那么就有 1 万用户遇到异常情况,而且如果用户的使用频次很高,影响的何止 1 万用户。并且,如今产品都是体验至上,如果遇到这样的问题,用户极大可能就弃你而去,流失了客户就等于流失了收入。 如何妥善地处理接口异常的情况是一件严肃的事情,应当被重视起来。 妥善处理请求异常 那么,应当如何做呢?首先要定义请求异常的处理代码,比如微信开放接口的参数中有[代码]fail[代码](“接口调用失败的回调函数”)、Promise 的[代码]catch[代码]部分;其次,根据异常可能导致的后果,在函数中做相应的处理。如果会导致后续操作失败、或则界面无反馈,那么应当在 fail 回调中正确处理;如果你真的认为基本不可能出问题,那么至少写个异常上报。即使出错了,也知道具体的情况。 下图是微信支付接口的参数列表,其中包含了接口调用失败的回调函数([代码]fail[代码])。 [图片] 而且官方也给出了示例: [代码]wx.requestPayment({ timeStamp: "", nonceStr: "", package: "", signType: "MD5", paySign: "", success(res) {}, fail(res) {} }); [代码] 在回调函数[代码]fail[代码]中上报异常 为了确保完全掌握小程序的运行状况,我们将异常上报。Fundebug 的微信小程序插件除了可以自动捕获异常外,还支持通过API 接口主动上报异常。 根据其官方文档: 使用 fundebug.notify(),可以将自定义的错误信息发送到 Fundebug name: 错误名称,参数类型为字符串 message: 错误信息,参数类型为字符串 option: 可选对象,参数类型为对象,用于发送一些额外信息 示例: [代码]fundebug.notify("Test", "Hello, Fundebug!", { metaData: { company: "云麒", location: "厦门" } }); [代码] 首先在 Fundebug 创建一个小程序监控项目,并按照指示接入插件,然后在[代码]app.js[代码]的[代码]onLaunch[代码]函数下面调用[代码]wx.requestPayment[代码]来进行测试。 [图片] Fundebug 的微信小程序插件捕获并上报了异常: [图片] 在[代码]metaData[代码]标签还可以看到我们配置的 metaData,也就是[代码]fail[代码]回调函数的[代码]res[代码]参数。 [图片] 因此,我们可以知道失败的原因是订单过期。 另外,如果在二维码页面停留时间过久,也会触发报错: [图片] 通过简单的加入几行代码,就可以将小程序的异常情况了如指掌。而且 Fundebug 的微信小程序插件还可以监控线上 JavaScript 执行异常、自动捕获[代码]wx.request[代码]请求错误、监控慢 HTTP 请求,推荐大家接入试用! 关于Fundebug Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用! [图片] 版权声明 转载时请注明作者 Fundebug以及本文地址: https://blog.fundebug.com/2019/07/08/report-http-error-by-fundebug-notify/
2019-07-08 - json2canvas:使用JSON生成小程序海报
作者:诗人的咸鱼 原文:小程序生成分享海报,一个json就够了。同时支持web Fundebug经授权转载,版权归原作者所有。 需求 在项目里写过几个canvas生成分享海报页面后,觉得这是个重复且冗余的工作.于是就想有没有能通过类似json直接生成海报的库. 然后就在github找到到两个项目: wxa-plugin-canvas,不太喜欢配置文件的写法.就没多去了解 mp_canvas_drawer,使用方式就比较符合直觉,不过可惜功能有点少. 然后就想着能不能自己再造个轮子.于是就有了这个项目 json2canvas,你可以简单的理解为是mp_canvas_drawer的增强版吧. json2canvas canvas绘制海报,写个json就够了. 项目的canvas绘制是基于cax实现的.所以天然的带来一个好处,json2canvas同时支持小程序和web 功能 支持缩放. 如果设计稿是750,而画布只有375时.你不需要任何换算,只需要将scale设置为0.5即可. 支持文本(长文本自动换行,感谢 coolzjy@v2ex 提供的正则 https://regexr.com/4f12l ,优化了换行的计算方式(不会粗暴的折断单词)) 支持图片(圆角) 支持圆型,矩形,矩形圆角 支持分组(cax里很好用的一个功能) 同时支持小程序和web 示例 web-demo 界面左边的json,可以进行编辑,直接看效果哟~ 小程序-demo [代码]git clone https://github.com/willnewii/json2canvas.git 微信开发者工具导入项目 example/weapp/ [代码] 小程序安装 [代码]npm i json2canvas 微信开发者工具->工具->构建npm [代码] 在需要使用的界面引入Component [代码]{ "usingComponents": { "json2canvas":"/miniprogram_npm/json2canvas/index" } } [代码] 效果图 想要生成一个这样的海报,需要怎么做?(红框是图片元素,蓝框是文字元素,其余的是一张背景图。) [图片] 一个json就搞定.具体支持的元素和参数,请查看项目readme [代码]{ "width": 750, "height": 1334, "scale": 0.5, "children": [ { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/bg_concerts_1.jpg", "width": 750, "height": 1334 }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 100, "x": 48, "y": 44, "isCircular": true, }, { "type": "circle", "r": 50, "lineWidth": 5, "strokeStyle": "#CCCCCC", "x": 48, "y": 44, }, { "type": "text", "text": "歌词本", "font": "30px Arial", "color": "#FFFFFF", "x": 168, "y": 75, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/medal_concerts_1.png", "width": 300, "x": "center", "y": 361 }, { "type": "text", "text": "一生活一场 五月天", "font": "38px Arial", "color": "#FFFFFF", "x": "center", "y": 838, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "text", "text": "北京6场,郑州2场,登船,上班,听到你想听的歌了吗?", "font": "24px Arial", "color": "#FFFFFF", "x": "center", "y": 888, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "rect", "width": 750, "height": 193, "fillStyle": "#FFFFFF", "x": 0, "y": "bottom" }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 117, "height": 117, "x": 47, "y": 1180 }, { "type": "text", "text": "长按识别小程序二维码", "font": "26px Arial", "color": "#858687", "x": 192, "y": 1202 }, { "type": "text", "text": "加入五月天 永远不会太迟", "font": "18px Arial", "color": "#A4A5A6", "x": 192, "y": 1249 }] } [代码] 问题反馈 有什么问题可以直接提issue
2019-06-29 - 小程序setData
setData setData 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 Object 以 key: value 的形式表示,将 this.data 中的 key 对应的值改变成 value。 具体的介绍请看微信开发文档:https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html 注意: 直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。 this.setData 两种情况赋值 触发渲染 页面 xxx.js 中 data 的数据 [代码]lunList: [[代码][代码] [代码][代码]{ id: 0, [代码][代码]choose: [代码][代码]false[代码][代码], [代码][代码]},[代码][代码] [代码][代码]{ id: 1, [代码][代码]choose: [代码][代码]false[代码][代码], [代码][代码]},[代码][代码] [代码][代码]{ id: 2, [代码][代码]choose: [代码][代码]false[代码][代码], [代码][代码]}[代码][代码] [代码][代码]][代码][代码]showlun: [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]var[代码] [代码]_this = [代码][代码]this[代码][代码], index = e.currentTarget.dataset.id;[代码][代码] [代码][代码]console.log(e, index) [代码][代码]//index,表示点击的第几个索引[代码][代码] [代码][代码]if[代码] [代码](_this.data.lunList[index].choose == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]_this.data.lunList[index].choose=[代码][代码]true[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]_this.data.lunList[index].choose=[代码][代码]false[代码][代码] [代码][代码]}[代码][代码] [代码][代码]_this.setData({[代码][代码] [代码][代码]lunList:_this.data.lunList[代码][代码] [代码][代码]})[代码][代码] [代码][代码]console.log(_this.data.lunList)[代码][代码] [代码][代码]}[代码]ps:数据一旦过大,再js中操作整个数据渲染页面 [代码]showlun: [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]var[代码] [代码]_this = [代码][代码]this[代码][代码], index = e.currentTarget.dataset.id;[代码][代码] [代码][代码]console.log(e, index)[代码][代码] [代码][代码]//重点 字符串组合的形式, this.setData 自动识别为 ["lunList[2].choose"] 这样直接操作 数据中的某一项直接赋值[代码][代码] [代码][代码]var[代码] [代码]ss = [代码][代码]'lunList['[代码] [代码]+ index + [代码][代码]'].choose'[代码] [代码] [代码][代码]if[代码] [代码](_this.data.lunList[index].choose == [代码][代码]false[代码][代码]) {[代码][代码] [代码][代码]_this.setData({[代码][代码] [代码][代码][ss]: [代码][代码]true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]_this.setData({[代码][代码] [代码][代码][ss]: [代码][代码]false[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]console.log(_this.data.lunList)[代码][代码] [代码][代码]}[代码]ps:对于对象或数组字段,可以直接修改一个其下的子字段,这样做通常比修改整个对象或数组更好
2019-06-21 - 云开发·云调用生成小程序码
云开发·云调用生成小程序码 小程序云开发已经支持云调用,开放了很多接口,一直想要的获取小程序码也支持了。这下轻量的小程序也可以有自定义小程序码的功能。 1. 需求 获得一个带参数的小程序码,传播出去以后,用户扫码进入指定页面,根据参数做不同的处理。本文只讲小程序码生成、存储、展示部分。参数处理不多介绍,可以查看 项目代码 了解更多。 2. 开通云开发 新建小程序可以从开发工具的云开发模板初始化项目,根据云开发操作指引新建项目即可。 但是这里有个问题,已发布小程序的页面才能生成小程序码。如果现有的小程序没有开通云开发,需要做以下几步: 开发工具开通云开发,设定云开发的环境; 将原来的代码(除了[代码]project.config.json[代码]以外的所有文件)放到新建的 [代码]miniprogram[代码] 目录; 新增 [代码]cloudfunctions[代码] 目录; [代码]app.json[代码] 新增配置 [代码]"cloud": true[代码]; [代码]project.config.json[代码] 配置 [代码]"miniprogramRoot":"miniprogram/"[代码] 和 [代码]"cloudfunctionRoot":"cloudfunctions/"[代码]; 修改小程序基础库版本,最低要 2.3.0 [代码]"libVersion": "2.3.0"[代码]。 3. 生成小程序码 下面可以开始写代码开发了,开始之前,建议先看完官方教程。特别是开发工具的使用步骤,开发和调试时如果遇到奇怪的问题,可以尝试重启开发工具、重装开发工具,也可以去微信开放社区发帖。(重启和重装都是我在社区中发现的答案,能解决各种不应该存在的问题)。 3.1 准备文件 在 [代码]cloudfunctions[代码]目录右键新建Node.js云函数 [代码]getqr[代码]。 生成小程序码需要单独指定权限。在 [代码]getqr[代码] 目录新建 [代码]config.json[代码] ,里面写以下内容: [代码]{ "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } [代码] 小程序码的获取方式有三种,这里只用到了接口 getUnlimited,选择这个接口的原因是漂亮的圆形小程序码,数量无限制。具体区别可以去 获取小程序码官方文档查看详情。 正常情况下,这个时候云函数可以部署测试了。如果遇到部署不成功、各种权限问题,可以尝试本地部署上传所有文件、重启试试。 3.2 生成小程序码 生成小程序码的代码如下,可以指定页面和页面参数 [代码]scene[代码],还有小程序码的尺寸。 注意这里的 [代码]scene[代码] 有限制: 最大32个可见字符; 只支持数字,大小写英文以及部分特殊字符:[代码]!#$&'()*+,/:;=?@-._~[代码]; 注意参数格式:下面实例代码生成小程序码后,扫码获得 [代码]pages/demo/demo?scene=id%3D6[代码] 。 [代码]try { const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/demo/demo', scene: 'id=6', width: 240, }) console.log(result) return result } catch (err) { console.log(err) return err } [代码] 直接调用,比服务端调用少了 access_token 参数。 3.3 上传到云存储 返回值中的 buffer 就是图片内容,直接上传到云存储: [代码]const uploadResult = await cloud.uploadFile({ cloudPath: 'shareqr/' + qr_name_hash + '.jpg', fileContent: result.buffer, }); [代码] 我在云存储新建了 [代码]shareqr[代码] 目录保存小程序码; 图片名根据参数取md5摘要; [代码]getUnlimited[代码] 返回的图像是 [代码]jpeg[代码] 格式,后缀硬编码写 [代码].jpg[代码]。 3.4 获取图片临时路径 直接上代码 [代码]getURLReault = await cloud.getTempFileURL({ fileList: [uploadResult.fileID] }); fileObj = getURLReault.fileList[0] return fileObj [代码] 3.5 直接从存云存储获取 生成过以后图片已经保存在云存储,用同样的参数第二次调用没必要再生成一次,去掉一次网络请求,可以节省不少时间。 前面说到文件名使用请求参数摘要,知道了目录和文件名,再加上文件bucket前缀就可以拼出来 [代码]fileID[代码],用[代码]fileID[代码] 可以查询云存储的文件。 比如我刚刚生成的 fileID 是 [代码]cloud://dev-xxxx.8888-dev-xxxx/qr/44ea42f05091c3bec771123e6e8cd4c2.jpg[代码], 前缀就是 [代码]cloud://dev-xxxx.8888-dev-xxxx/[代码]。再拼上目录、文件名、后缀就是 [代码]fileID[代码]。 注:此处的 [代码]fileID[代码]拼接方法并不是来自官方文档,只是在使用中发现这个前缀不会变。还需要官方解释说明[代码]fileID[代码]规则。 如果会改变,就需要再用云数据库存储[代码]fileID[代码],更麻烦一些。 3.6 云函数完整代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk'); const crypto = require('crypto'); const bucketPrefix = 'cloud://dev-xxxx.8888-idc-4d11a4-1257831628/qr/'; // env: 'dev-xxxx' // 云函数入口函数 exports.main = async (event, context) => { const full_path = event.page + '?' + event.scene; const qr_name_hash = crypto.createHash('md5').update(full_path).digest('hex'); const temp_id = bucketPrefix + qr_name_hash + '.jpg'; // return { // full_path, // qr_name_hash, // temp_id // } try { // 先尝试获取文件,存在就直接返回临时路径 let getURLReault = await cloud.getTempFileURL({ fileList: [temp_id] }); // return getURLReault; let fileObj = getURLReault.fileList[0]; if (fileObj.tempFileURL != '') { fileObj.fromCache = true; return fileObj; } // 生成小程序码 const wxacodeResult = await cloud.openapi.wxacode.getUnlimited({ scene: event.scene, page: event.page, width: 280 //二维码的宽度,单位 px,最小 280px,最大 1280px }) // return wxacodeResult; if (wxacodeResult.errCode != 0) { // 生成二维码失败,返回错误信息 return wxacodeResult; } // 上传到云存储 const uploadResult = await cloud.uploadFile({ cloudPath: 'qr/' + qr_name_hash + '.jpg', fileContent: wxacodeResult.buffer, }); // return uploadResult; if (!uploadResult.fileID) { //上传失败,返回错误信息 return uploadResult; } // 获取图片临时路径 getURLReault = await cloud.getTempFileURL({ fileList: [uploadResult.fileID] }); fileObj = getURLReault.fileList[0]; fileObj.fromCache = false; // 上传成功,获取文件临时url,返回临时路径的查询结果 return fileObj; } catch (err) { return err } } [代码] 4. 小程序页面调用 调用页面就比较简单了,在小程序新建一个 [代码]pages/share/share[代码] 在 [代码]onLoad[代码] 函数调用云函数。 [代码]// 使用前记得先初始化云函数,一版放到 app.js onLaunch() 中 // wx.cloud.init({env: 'dev-8888'}) wx.cloud.callFunction({ name: 'getqr', data: { page: 'pages/demo/demo', scene: 'id=6', } }).then(res => { console.log(res.result); if (res.result.status == 0) { _this.setData({ qr_url: res.result.tempFileURL }) }else{ wx.showToast({ icon: 'none', title: '调用失败', }) } }).catch(err => { console.error(err); wx.showToast({ icon: 'none', title: '调用失败', }) }) [代码] 至此完整的调用过程已经全部完成,详细代码可以到 项目代码 查看。 代码中还对入口页面和share页面的参数做了包装,云函数可以直接使用,小程序可以稍做修改适应自己业务。 写在最后 小程序云开发已经开放了很多功能,除了这次提到的生成小程序码,云调用还可以发送模板消息。有需要的开发者又一个理由可以快速上线新功能了。 云开发还开放了[代码]HTTP API[代码],也就是用自己的服务器调用云函数。以前看完云开发介绍文章最大的疑问就是,你说的都很好,可是后台数据怎么管理呢?不能跟自己的服务器结合,只能放一些轻量的小程序。有了 [代码]HTTP API[代码] 以后就可以用自己的服务器做管理后台了。这时候你要问,都用上服务器了,还需要云开发做什么。首先,云开发免费;其次,免费功能已经够强,就差不能做Web管理后台了;最后,获取access_token(小程序及小游戏调用不要求IP地址在白名单内。)
2020-07-10 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - 随手写的一个css头像轮播,代码较菜,不喜勿喷.哈哈哈
[图片] 大概就长这样样子,可以向左自动轮播的那种,主要就是用到了css的延迟transition-delay这个属性,然后配合定时器做的一个css的轮播.哈哈哈,然后没了,欢迎各位改造,指出里面的错误.觉得想用的朋友就拿去用吧 代码链接:https://developers.weixin.qq.com/s/ox93u6m17W9X
2019-06-13 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 前端XSS攻击
一、前言 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点。在移动互联网时代,前端人员除了传统的 XSS、CSRF 等安全问题之外,又时常遭遇网络劫持、非法调用 Hybrid API 等新型安全问题。当然,浏览器自身也在不断在进化和发展,不断引入 CSP、Same-Site Cookies 等新技术来增强安全性,但是仍存在很多潜在的威胁,这需要前端技术人员不断进行“查漏补缺”。 二、XSS定义 在给大家介绍xss前,大家先来看一个例子: [代码]<html> <title>Welcome!</title> Hi <script> var pos=document.URL.indexOf("name=")+5; document.write(document.URL.substring(pos,document.URL.length)); </script> Welcome to you </html> [代码] 这个例子是个欢迎页面,name是截取URL中get过来的name参数 正常操作: http://域名A/welcome.html?name=Joe 非正常操作: http://域名A/welcome.html?name=<script>alert(document.cookie)</script> 当执行非正常操作时: [图片] 完了,我们的cookie数据被截取了,XSS攻击出现了。 为什么会这样呢?我们来看一下: 1、受害者的浏览器接收到这个非正常操作的链接,发送HTTP请求到域名A并且接受到上面的HTML页; 2、受害者的浏览器开始解析这个HTML为DOM,DOM包含一个对象叫document,document里面有个URL属性,这个属性里填充着当前页面的URL; 3、当解析器到达javascript代码,它会执行它并且修改你的HTML页面。倘若代码中引用了document.URL,那么,这部分字符串将会在解析时嵌入到HTML中,然后立即解析,同时,javascript代码会找到(alert(…))并且在同一个页面执行它,这就产生了xss。 那么什么是XSS呢? xss跨站脚本攻击(Cross Site Scripting),指攻击者在网页中嵌入脚本代码(例如js代码), 当用户浏览此网页时,脚本就会在用户的浏览器上执行,从而达到攻击者的目的。比如获取用户的Cookie,导航到恶意网站,携带木马等。 大部分的xss漏洞都是由于没有处理好用户的输入,导致攻击脚本在浏览器中执行,这就是跨站脚本漏洞的根源。 三、XSS攻击类型 XSS攻击分为存储型、反射型和 DOM 型三种。 1、存储型 XSS a: 攻击者将恶意代码提交到目标网站的数据库中。 b: 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返给浏览器。 c: 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。 d: 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。 简单例子: 表单中填写数据: [代码]<input type=“text” name=“content” value=“这里是用户填写的数据”> [代码] 正常操作: 1、用户是提交相应留言信息; 2、将数据存储到数据库; 3、其他模块要显示保存的数据,从数据库查询出来并显示。 非正常操作: 1、攻击者在value填写<script>alert(‘foolish!’)</script>【或者html其他标签(破坏样式。。。)、一段攻击型代码】; 2、将数据存储到数据库中; 3、其他用户取出数据显示的时候,将会执行这些攻击性代码 这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。 2、反射型 XSS a: 攻击者构造出特殊的 URL,其中包含恶意代码。 b: 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。 c: 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。 d: 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。 简单例子: 正常发送消息: http://域名A/message.html?message=Hello,World! 接收者将会接收信息并显示Hello,Word 非正常发送消息: http://域名A/message.html?message=<script>alert(document.cookie)</script> 接收者接收消息显示的时候将会弹出警cookie信息 反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。 反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。 3、DOM 型 XSS a: 攻击者构造出特殊的 URL,其中包含恶意代码。 b: 用户打开带有恶意代码的 URL。 c: 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。 d: 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网 站接口执行攻击者指定的操作。 简单例子: [代码]<script> document.body.innerHTML="<div style=visibility:visible;><h1>This is DOM XSS</h1></div>"; </script> [代码] 攻击者可以利用innerHTML来篡改页面 DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。 四、如何预防XSS攻击 1、输入过滤,对用户提交的数据进行有效性验证,仅接受指定长度范围内并符合我们期望格式的的内容提交,阻止或者忽略除此外的其他任何数据。比如:电话号码必须是数字和中划线组成,而且要设定长度上限。过滤一些些常见的敏感字符,例如:< > ‘ “ & # \ javascript expression “onclick=” “onfocus”;过滤或移除特殊的Html标签, 例如: <script>, <iframe> , < for <, > for >, " for;过滤JavaScript 事件的标签,例如 “onclick=”, “onfocus” 等等。 例子1: [代码]<input v-model="value" type="text" /> [代码] Input输入值带有html标签时,如value=”<b>aaaaaaaa</b>”,调接口提交到后台的没有做HtmlEncode转码,那么面页输出的将会是: aaaaaaaa粗体。原理是因为提交的内容里带有html标签<b></b>。浏览器解析页面源码时把用户提交的内容当成了html代码。所以才会输出粗体的 aaaaaaaa。 解决方案: A:前端提交前转码:encodeHtml(value); B:后端java转码:Encoder.encodeHtml(value);如果是Param类,直接使用.getHtmlString(key)。 例子2: 提交的数据url编码,数据提交时一定要对传参数的数进行URLencode处理,假如提交内容 aaaa&344353中文啊&&&&aaa=bbbb&&&fsdfsdsdf,如果没有通过 URLencode处理的话,那么提交的链接自然是http://www.baidu.com/?val=aaaa&344353中文啊&&&&aaa=bbbb&&&fsdfsdsdf 这样后台取得的val的值会是aaaa。并且还会有一个aaa=bbbb。主要造成这现象的原因是提交的内容包含了"&“和”=",没有对这些特殊的字符做转义处理,所以造成了后台取参错误。 解决方案: A:前端提交参数前:encodeUrl(url); B:后端拿到数据进行编码:Encoder.encodeURL(url); 其他例子解决方案: A:对输入的数据限制长度范围; [代码]<input v-model="value" maxlength="20" type="text" /> [代码] B:对输入的数据进行校验(正则表达式) [代码]var re = /^[a-zA-z]\w{3,15}$/; If(re.test(value)){ return true; }else{ return false; } [代码] 2、输出编码,当需要将一个字符串输出到Web网页时,同时又不确定这个字符串中是否包括XSS特殊字符(如< > &‘”等),为了确保输出内容的完整性和正确性,可以使用编码(HTMLEncode)进行处理。 例子1: 输出js数据时,要先编码,否则有可能因为"等造成截断。如var a = ‘<%=aString %>’;假如aString的值是"aa’;alert();’"。没有encodeJs的页面就会执行alert了。而有encodeJs的页面则会把输出内容里的单引号都替换成:\x22,双引号替换成\x27。这样就不会造成js里的引号截断了。 解决方案: A:前端JS函数:$.toJSON(html); B:后端java函数:Encoder.encodeJson(html);如果是Param类,直接使用.getJsonString(key)。 例子2: <a href=”url”>跳转链接</a> java处理代码时获取url参数并且不做任何处理,直接在html标签的事件中植入攻击代码。 当用户访问的链接是xxx/sigup.jsp?url=");alert(document.cookie)😭"; 点击跳转链接按钮的时候,就会弹出cookie的信息 解决方案: A:前端把href换成onclick方法: Html: [代码]<a onclick=”openUrl(url)”>跳转链接</a> [代码] Js: [代码]Function openUrl(url) { url = encodeUrl(url); Window.location.href = url; } [代码] B:后台调用方法: Encoder.encodeURL(value2); 如果是Param类,直接使用.getUrlString(key)。 其他案例: A:把字符串转换为写在html标签中属性值: 字符串是输出在html标签中的属性里,所以首先要防止字符串里的引号造成属性的引号截断。所以至少字符串里的引号就要转义了。当然,还有额外的一些特殊字符也是要转义的。 解决方案: js函数:encodeHtmlAttr(html); java函数 :Encoder.encodeHtmlAttr(html);如果是Param类,直接使用getHtmlAttrString; 如果是输出在js里初始化的话,尽量使用jquery的attr/val等函数来赋值( encode json 数据 ),降低复杂度。 B: input-text用例 :<s>a&aa</s>'b"c 在页面中直接初始化时,要使用:<input value="<%=Encoder.encodeHtml(str)%>"/> 在JS中初始化时,要使用:$("#xxx").val("<%=Encoder.encode.Json(str)%>"); C: textarea用例 : <textarea><s>a&aa</s>'b"c</textarea>bbb,不在页面中直接初始化textarea。 在JS中初始化:$("#xxx").val("<%=Encoder.encodeJson(str)%>") 3、DOM型的XSS攻击防御,把变量输出到页面时要做好相关的编码转义工作,如要输出到 <script>中,可以进行JS编码;要输出到HTML内容或属性,则进行HTML编码处理。根据不同的语境采用不同的编码处理方式。 例子: 当用户访问路径为xxx/pr.jsp?keywordCond=aaa");alert(document.cookie)😭"时候,直接scripts.append输出:scripts.append(keywordCond),就会弹出cookie的信息。 解决方案: A:在append前,将keywordCond进行编码 [代码]keywordCond = encodeUrlComponent(keywordCond); scripts.append(keywordCond) [代码] B:后台调用方法: [代码]String keywordCond = Encoder.encodeHtmlJs(keywordCond); [代码] 1、将重要的cookie标记为http only, 这样的话当浏览器向Web服务器发起请求的时就会带上cookie字段,但是在脚本中却不能访问这个cookie,这样就避免了XSS攻击利用JavaScript的document.cookie获取cookie。 例子: A:Tomcat服务器,在Tomcat下的conf的web.xml设置如下信息。 [代码]<session-config> <cookie-config> <http-only>true</http-only> </cookie-config> <session-config> [代码] B:resin服务器,在resin.conf下设置session信息。 [代码]<session-config> <enable-cookies>true</enable-cookies> <cookie-config> <http-only>true</http-only> </cookie-config> </session-config> [代码] 五、总结 XSS的攻击五花八门,毕竟那么多情况场景,开发人员无法一一照顾过来,我们前端和后端尽可能对提交数据做好过滤。开发人员要注意在正确的地方使用正确的编码方式,有时为了防御XSS,在一个地方我们需要联合HTMLEncode、JavaScriptEncode、URLEncode进行编码,甚至是叠加,并不是固定一种方式编码,具体情况具体分析。 针对XSS攻击类型,我们日常开发需要做好以下部分: 1.在HTML标签、属性中输出时,用HTMLEncode。 2.在script标签中输出时,用JavaScriptEncode。 3.在地址中输出一般如果变量是整个URL,则用URLEncode。 4.在提交数据前,做一些正则校验,或者在输入框中做一些限制。 六、参考资料 1、http://it.faisco.cn/page/forum/articleDetail.jsp?articleId=1656 2、http://web.jobbole.com/95312
2019-06-10 - CryptoJS 加解密使用示例
[代码]// SHA1 加密[代码] [代码]var[代码] [代码]value = [代码][代码]"123456"[代码][代码];[代码] [代码]var[代码] [代码]wordArray = CryptoJS.SHA1(value);[代码] [代码]var[代码] [代码]str = wordArray.toString(CryptoJS.enc.Hex);[代码] [代码]// HmacSHA1加密[代码] [代码]var[代码] [代码]message = [代码][代码]"message"[代码][代码];[代码] [代码]var[代码] [代码]key = [代码][代码]"key"[代码][代码];[代码] [代码]var[代码] [代码]wordArray = CryptoJS.HmacSHA1(message, key);[代码] [代码]var[代码] [代码]str = wordArray.toString(CryptoJS.enc.Hex);[代码] [代码]// md5 加密[代码] [代码]var[代码] [代码]md5 = CryptoJS.MD5([代码][代码]"md5"[代码][代码]).toString();[代码] [代码]// AES 加解密 开始[代码] [代码]/**[代码] [代码] [代码][代码]* //AES 解密方法[代码] [代码] [代码][代码]* word 字符串[代码] [代码] [代码][代码]*/[代码] [代码]const AES_JIA = [代码][代码]function[代码] [代码](word, key, iv) {[代码] [代码] [代码][代码]let encryptedHexStr = CryptoJS.enc.Hex.parse(word);[代码] [代码] [代码][代码]let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);[代码] [代码] [代码][代码]let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });[代码] [代码] [代码][代码]let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);[代码] [代码] [代码][代码]return[代码] [代码]decryptedStr.toString();[代码] [代码]}[代码] [代码]/**[代码] [代码] [代码][代码]* //AES 加密方法[代码] [代码] [代码][代码]* word 字符串[代码] [代码] [代码][代码]*/[代码] [代码]const AES_JIE = [代码][代码]function[代码] [代码](word, key, iv) {[代码] [代码] [代码][代码]let srcs = CryptoJS.enc.Utf8.parse(word);[代码] [代码] [代码][代码]let encrypted = CryptoJS.AES.encrypt(srcs, key, {[代码] [代码] [代码][代码]iv: iv,[代码] [代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码] [代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]return[代码] [代码]encrypted.ciphertext.toString().toUpperCase();[代码] [代码]}[代码] [代码]const word = [代码][代码]"字符串格式"[代码][代码]; [代码][代码]// 字符串格式[代码] [代码]const key = CryptoJS.enc.Utf8.parse([代码][代码]"1234567890123456"[代码][代码]); [代码][代码]//十六位十六进制数作为密钥 ,十六位,十六位,不要 误以为 1234567890123456 == 123 是行得通的 字符长度16不等于 3,除非 key = 123[代码] [代码]const iv = CryptoJS.enc.Utf8.parse([代码][代码]''[代码][代码]); [代码][代码]//十六位十六进制数作为密钥偏移量[代码] [代码]var[代码] [代码]ctext = AES_JIA(word, key, iv);[代码] [代码]console.log([代码][代码]"ctext=>"[代码][代码], ctext); [代码][代码]// AES 加密[代码] [代码]var[代码] [代码]ptext = AES_JIE(ctext, key, iv);[代码] [代码]console.log([代码][代码]"ptext=>"[代码][代码], ptext); [代码][代码]// AES 解密[代码] [代码]// AES 加解密 结束[代码] [代码]//DES 加密[代码][代码]function[代码] DES_JIA[代码](message, key, iv) {[代码][代码] [代码][代码]var[代码] [代码]keyHex = CryptoJS.enc.Utf8.parse(key);[代码][代码] [代码][代码]var[代码] [代码]encrypted = CryptoJS.DES.encrypt(message, keyHex, {[代码][代码] [代码][代码]iv: iv,[代码][代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码][代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码][代码] [代码][代码]});[代码][代码] [代码][代码]return[代码] [代码]encrypted.toString();[代码][代码]}[代码] [代码]//DES 解密[代码][代码]function[代码] [代码]DES_JIE(ciphertext, key, iv) {[代码][代码] [代码][代码]var[代码] [代码]keyHex = CryptoJS.enc.Utf8.parse(key);[代码][代码] [代码][代码]// direct decrypt ciphertext[代码][代码] [代码][代码]var[代码] [代码]decrypted = CryptoJS.DES.decrypt({[代码][代码] [代码][代码]ciphertext: CryptoJS.enc.Base64.parse(ciphertext)[代码][代码] [代码][代码]}, keyHex, {[代码][代码] [代码][代码]iv: iv,[代码][代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码][代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码][代码] [代码][代码]});[代码][代码] [代码][代码]return[代码] [代码]decrypted.toString(CryptoJS.enc.Utf8);[代码][代码]}[代码] [代码]var[代码] [代码]des_text = DES_JIA(word, key, iv);[代码][代码]console.log([代码][代码]"des_text=>"[代码][代码], des_text); [代码][代码]// des 加密[代码] [代码]var[代码] [代码]ntext = DES_JIE(des_text, key, iv);[代码][代码]console.log([代码][代码]"ntext=>"[代码][代码], ntext); [代码][代码]// des 解密[代码] 调试(SHA1 加密)图片示例: [图片] 参考资料: https://cryptojs.gitbook.io/docs/ https://www.bootcdn.cn/crypto-js/
2019-05-29 - 干货--02 余小浪
哈喽 我又来了 这是我第二次分享文章了 希望能够帮助大家 也希望大家喜欢~ 第一个 image组件中的 mode=“aspectFill” 属性 这个属性是等比例缩放 如果你的图片是这个属性的需要注意注意注意 图片渲染完成后 再等比例缩放 及 先渲染 再等比缩放 例子: 当你要获取这个图片距离顶部的距离是 需要使用 wx.createSelectorQuery来来找到这个标签并获取到这个标签的参数 一般会写在 onReady() 生命周期钩子函数里 但是 问题就在这个时候出 现了 我获取的标签数据 不是 实际的数据 而是 图片没有缩放的数据 解决这个问题的时候 我使用了 setTimeout 函数 把时间设置为500 即 半秒后 再获取图片的标签的 参数 这时候 获取到的数据就是正确的数据了 暂时没有测试不写等待时间 有兴趣大家可以试一下 第二个 前端绘制海报性能优化 绘制海报我们用到了canvas 绘制海报的前 提是 绘制的素材要下载到本地 如果我们在绘制的时候下载素材 这个时 候 绘制的进度就会变慢 优化的思想如下 B页面是绘制海报的 A页面 点击某个按钮 进入到 B页面 那么我们就在 渲染A页面的时候 就下载素材呢 等到了B页面 素材都已经有了 直接使用,绘制效果会非常好 甚至是 秒绘制完成 在B页面onUnload函数内 清除下载文件的缓存 避免缓存太多 第三 字符串10 减去 数字0 最后 变成了 数字 10 let string = “10” string - 0 此时 string 就是 数字 10 类型是number // JS的隐式转换 很常用的一种改变数据类型的方式 0 的 布尔值 是 false 第四 防止数据抖动的方法 数据抖动 说白了 就是 一个按钮有一个事件 然后用户在很短的事件内重复点击 类似的有 购买物品 提交完成按钮 这些 解决方法 先声明一个变量 值为true 当做锁 当执行函数的时候 把这个锁变成 false 那么这个函数就被锁死了 只有这个函数完成所有操作的时候 再把锁变成true 此刻用户才可以再次真正的点击 代码如下: [图片] [图片] 今天的分享就到这里了 如果喜欢请大家动动小手指 点个赞吧 欢迎各位大佬亲临指导 如果有问题请及时指出 我会第一时间修改的 嘻嘻
2019-05-21 - 生成分享朋友圈微海报
[图片] 描码体验 文未有小程序源码地址 (另有偿提供后台数据接口服务) [图片] 模板代码参考 [代码] <view class='padding text-center'> <image src="{{shareImage}}" mode="widthFix"></image> <canvasdrawer :painting.sync="painting" @getImage.user="eventGetImage"></canvasdrawer> </view> <view class="padding"> <button wx:if="{{shareImage}}" class='cu-btn block bg-red margin-tb-sm lg' @tap="eventSave">保存分享图</button> </view> [代码] 部分js代码 [代码]event = { getImage (e) { wepy.showToast({ title: e, icon: "success", duration: 2000 }); } } async buildPoster(){ if(this.shareImage == ''){ wepy.showLoading({ title: "生成中", mask: true }); let poster = await commApi.GetArticlePoster(this.id) this.painting = poster } this.showposter = true; this.$apply(); } methods = { async eventSave() { // 保存图片至本地 const res = await wepy.saveImageToPhotosAlbum({ filePath: this.shareImage }); if (res.errMsg === "saveImageToPhotosAlbum:ok") { wepy.showToast({ title: "分享图已保存到相册", icon: "success", duration: 2000 }); } }, eventGetImage(event) { wepy.hideLoading(); const { tempFilePath, errMsg } = event; if (errMsg === "canvasdrawer:ok"){ this.shareImage = tempFilePath; }else{ wepy.showToast({ title: errMsg, icon: "success", duration: 2000 }); } }, } [代码] 后端php代码 [代码]public function defalutArticlePoster($app, $user, $article){ // $title_length = Str::length($article->title,'UTF-8'); // dd($title_length); $poster = [ 'width'=>460, 'height'=> 500, 'clear'=> true, 'views'=>[ [ 'type'=> 'rect', 'background'=> '#666', 'top'=> 0, 'width'=> 460, 'height'=> 500, 'left'=> 0 ], [ 'type'=> 'rect', 'background'=> '#ffffff', 'top'=> 2, 'width'=> 456, 'height'=> 496, 'left'=> 2 ], [ 'type'=> 'image', 'url'=> $article->cover? $article->cover:'https://wx1.wechatrank.com/base64img/20190402233111.jpeg', 'top'=> 70, 'left'=> 28, 'width'=> 400, 'height'=> 320 ], [ 'type'=> 'text', 'content'=> $article->title, 'fontSize'=> 18, 'lineHeight'=> 24, 'color'=> '#333', 'textAlign'=> 'left', 'top'=> $title_length>22?16:26, 'left'=> 28, 'width'=> 387, 'MaxLineNumber'=> 2, 'breakWord'=> true, 'bolder'=> true ], [ 'type'=> 'text', 'content'=> str_replace(array(" ", " ", "\t", "\n", "\r", "\r\n", PHP_EOL), '', $article->intro), 'fontSize'=> 18, 'lineHeight'=> 24, 'color'=> '#666', 'textAlign'=> 'left', 'top'=> 406, 'left'=> 28, 'width'=> 310, 'MaxLineNumber'=> 3, 'breakWord'=> true, 'bolder'=> true ], [ 'type'=> 'image', 'url'=> url('/qrcode/article/'.$article->qrcode), 'top'=> 406, 'left'=> 360, 'width'=> 68, 'height'=> 68 ] ], ]; return $poster; } [代码] 示例代码来源: https://github.com/yizenghui/wxcms/blob/master/src/pages/poster/index.wpy 项目地址: https://github.com/yizenghui/wxcms https://github.com/simmzl/wepy_canvas_drawer 觉得对您有帮助请点个赞,谢谢
2019-05-27 - 借助云开发实现小程序模版消息推送(不用搭建服务器就可以实现微信消息推送)
上一节给大家将了借助云开发实现小程序支付功能,那么我们就要想了,能不能借助云开发实现小程序消息推送功能呢? 还别说,云开发还真能实现推送的功能。 一直关注我的同学肯定知道老师之前也写过借助java后台实现小程序消息推送的文章。 我们借助java后台虽然也能轻松的实现消息推送。但是呢?用java开发后台推送,必须要搭建服务器,学习java代码,部署java代码当然你就是做java开发的,或者学习过java,这没什么。 但是作为小程序开发人员来说,用java显得太重了。 传送门: 《借助小程序云开发实现小程序支付功能(含源码)》 《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 下面就来教大家如何借助云开发实现小程序模版消息的推送功能。 老规矩,先看效果图 [图片] 下面来讲实现步骤 一,定义推送的云函数 由于我们的云推送功能只能在云函数里调用,所以我们这里必须要在云函数里实现推送功能。 1,首先我们定义一个云函数push0524。 如果你还不知道如何使用云开发,如何定义云函数,去翻下老师之前的文章。有写的。 [图片] 把完整的代码贴给大家 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { console.log(event) return sendTemplateMessage(event) } //小程序模版消息推送 async function sendTemplateMessage(event) { const { OPENID } = cloud.getWXContext() // 接下来将新增模板、发送模板消息、然后删除模板 // 注意:新增模板然后再删除并不是建议的做法,此处只是为了演示,模板 ID 应在添加后保存起来后续使用 const addResult = await cloud.openapi.templateMessage.addTemplate({ id: 'AT0002', keywordIdList: [3, 4, 5] }) const templateId = addResult.templateId //新增的模版id const sendResult = await cloud.openapi.templateMessage.send({ touser: OPENID, templateId, formId: event.formId, page: 'pages/index/index', data: { keyword1: { value: '云开发实现推送', }, keyword2: { value: '2019 年 5 月 24 日', }, keyword3: { value: '编程小石头', }, } }) //删除模版id await cloud.openapi.templateMessage.deleteTemplate({ templateId, }) return sendResult } [代码] 上面代码所实现的就是 1,创建模版,拿到模版id 2,使用模版ID,填充模版消息,发送模版 3,删除模版。 我们正常开发时,模版都是在小程序后台获取到的。这里是为例演示方便。所以正常开发时,只需要实现第二步就行了。 推送的关键代码就是这个方法: cloud.openapi.templateMessage.send 通常我们定义完push0524云函数以后,如果直接调用的话,会报错误的。 [图片] 来看下这个错误,看到红色框里的permission就知道,肯定是权限的问题。所以我们在定义完云函数以后,要在push0524云函数下面添加权限配置页面。如下图 [图片] 重要的就是这个: “templateMessage.send”, 推送权限。因为推送是云开发给我们提供的,我们这里调用时,必须配置相关权限,才能使用的。 到这里我们的推送功能就实现了。下面我们来验证下。 二,验证云开发推送 验证其实很简单,和我们之前的《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 类似。只不过一个是在java后台推送,一个是在小城里推送。下面我们简单写个小程序里验证推送的demo。 功能很简单 1,获取formid,因为推送必须有formid的 2,点击调用push0524实现推送 [图片] 简单的贴下代码 [图片] [图片] 需要注意的一点:我们测试时,必须要真机测试。因为模拟器没法获取到formid的。 [图片] 我们在推送成功的success回调中打印下log。如果log中出现,send:ok字样,就代表我们推送成功了。来看下推送成功的效果。 微信聊天列表接收到了消息提醒 [图片] 消息内容 [图片] 到这里我们就用云开发实现完整的消息推送功能了。是不是很简单。 有任何关于编程的问题都可以加老师微信 2501902696(备注小程序)也可以找老师索要完整源码。 编程小石头码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 【U计划】弹幕biubiu小程序开发经验分享
弹幕biubiu小程序开发经验分享 Hello,大家好~我们是来自清华大学软件学院大三的弹幕弹幕团队,我是团队的Leader&Developer。我们团队开发的小程序叫作“弹幕biubiu”,主要应用场景是各类晚会现场。你可能已经发现了,我们的团队名和小程序名是不一样的,这是因为我们在确定了团队名称之后,发现这个名字已经被其他的小程序占用了,所以我们只能将小程序换成另一个名字。这也提醒了大家,在开发小程序的时候,一定要先确认自己起的名字没有被使用哦~ 我们的小程序是从去年10月开始开发的,直至今年3月基本完成。之后在今年4月的清华大学软件学院学生节上,我们的小程序作为观众弹幕互动平台被使用,并取得了广泛好评。这一方面说明我们小程序的实用性,另一方面也说明了弹幕互动及其衍生方向依然有很大的发展空间。 [图片] 我们团队的开发选用了敏捷开发的方式,并采用Scrum框架(下图源自清华大学软件学院刘强老师的软件工程课件)。本文将介绍我们团队在开发过程中所做的一些主要工作,希望能够给大家一些启发与帮助。[图片] 1.立项 万事开头难,开发过程中最困难的地方,往往就是在最开始的地方。一个团队中不缺技术人员,而缺少设计人员,也就是“有思想的人”。而且好的想法一定是来源于生活的,如果不仔细观察生活,只是天马行空地构想,是无法获得好的项目主题的。 我们团队在计划开发一个小程序之后,就开始讨论主题。首先我们确定了我们小程序的大致方向。我们发现在每次举办院系学生节时,都需要科协同学用一天的时间去部署弹幕墙,这样效率较低,而且也常常出现弹幕墙宕机的情况,所以我们决定开发一个学生节小程序。之后我们团队通过头脑风暴,将自己设想为学生节举办方与学生节观众,讨论我们可能需要哪些功能,不需要哪些功能等等,从而将项目目标进一步细化。 在这里我们并不能只单纯地讨论,我们需要一个记录者,将所有人的想法记录下来并进行归整,之后再由每个成员进行修改完善,得到我们开发的第一份文档——产品规划文档(弹幕biubiu的产品规划文档)。如果大家对产品规划文档形式不太清楚的话,可以参照上述我们的文档。在文档中我们对于产品的定位、产品的特性以及产品的路线都有了一个明确的描述。 当我们有了产品规划文档之后,立项的过程还没有结束。因为这个文档只是根据自己团队的想法写成的,但是真正的用户会有成千甚至上万人,并不一定每个人都和团队内的成员想法相同。除此之外,市场上可能已经有一些类似的产品。所以我们必须要进行一个调查,明确其他可能的用户的需求以及现有市场上的产品提供的功能。这样的调查有助于我们跳出团队内固有的思维模式,催生出一些新的点子,同时也能避免无用功。在调查结束之后,我们获得了更多的用户需求。我们需要对需求进一步整理细化,写出用户故事(弹幕biubiu的需求获取与用户故事文档),并画出用户故事地图。 [图片] 到此为止,我们基本完成了所有立项工作。这时整个团队应当对自己的项目开发目标有了明确清晰的认知。 2.设计 当明确需求之后,我们就要开始设计工作,也就是进入到敏捷开发的迭代周期中了。设计主要分为两部分: 系统设计 原型设计 系统设计中最重要的工具是UML,即统一建模语言。你可能会用到其中的类图、活动图、用例图等。 2.1 系统设计 系统设计目的是确立技术开发过程中的总纲,这也是开发过程中极为重要的一步,整个开发过程都是围绕系统设计文档展开的。 我们团队开发的小程序使用的是MVC模式,即模型(model)-视图(view)-控制器(controller)。这种模式满足了高内聚、低耦合的程序结构,便于团队开发与管理。 系统设计要求我们首先对于整体系统结构有一个清晰的构想。我们的弹幕小程序的系统架构图如下所示,可以看到这里我们将系统清晰地分为了数层。 [图片] 之后我们就需要对系统的结构进行细化,主要包括两部分内容: 数据存储结构。也就是数据库的设计。我们用一个数据库来管理数据,那么我们需要构造哪些表,每个表中需要保存那些数据等等,都是我们需要考虑的问题。我们团队采用了Mysql+Redis两种数据库相结合的存储模式,保证了数据的读写效率。 前后端接口设计。接口设计可以方便团队前后端开发的分离,提高开发效率。我们团队采用的是Restful的API接口规范,大家可以自行查阅了解该规范的内容。 因为不同的小程序会有不同的系统,所以我这里的设计思路也仅供大家参考,主要的还是要开发团队自己思索并设计,得到最适合自己的系统结构。当然合适的系统架构不意味着在项目的最初就要将所有细节想得十分透彻。一个好的系统架构主要有两方面的特征:稳定的框架与可扩展性。稳定的框架保证了开发过程中无需对代码进行很大程度的调整重构;可扩展性保证了开发人员可以很轻松地将后续的内容添加进系统,而不会影响系统整体的特性。 2.2 原型设计 原型设计,也就是UI设计。我们团队使用的工具是墨刀。墨刀有着丰富的素材库,并且可以设计控件行为,方便团队成员理解交互过程。 [图片] 原型设计往往会是团队中讨论最激烈的环节,因为每个人的审美是不同的,更何况团队中基本都是理工生(sigh…)。我们在原型设计时也进行了多次讨论与修改,才最终确定其样式。 3.开发 3.1 团队管理 自组织团队是敏捷开发的基础,团队被授权自己管理工作进程,并决定如何完成工作。团队成员在开发的过程中需要各司其职,扮演好自己的角色。但是根据著名的“20%定律”,每个团队中总会有20%的成员是free rider,所以这就需要团队的领导者对团队进行良好管理。 在开发的初期,一个团队需要制定自己团队的开发章程,包括每周的开会时间、开会地点、每个成员负责开发的模块、团建安排等,并在之后的开发过程中严格按照章程的规定管理团队。 其次,团队在每次例会时,需要每名成员汇报之前任务的开发进度,对开发过程中遇到的问题进行讨论思考,并确定下一阶段的开发任务。每次的例会都需要指定一名成员记录会议内容,方便团队日后查看。 团队管理中,最主要的就是任务安排的部分。任务安排主要包括两点:明确分工与时间规划。 明确分工是要让大家清楚自己具体是要做什么,其重点在于“细化”。举个例子,如果你和我说,“你去做一个主办方管理网站”,我肯定一头雾水无从下手;但是如果你和我说:“你去实现一个主办方登录的功能,主办方输入用户名和密码就可以跳转到活动列表页面”,那我就可以很快地完成这一个任务。所以明确分工是团队成员,尤其是组织者,需要重点注意的。 时间规划则是让大家有一个紧迫感。做时间规划最好的方法是,组织者先定一个大概的时间,然后所有团队成员进行协商,定出每个人都满意或至少不反对的时间安排。因为团队成员都会有惰性,就算最好的团队也不例外,所以一个明确的时间规划可以让每个人有计划有安排地完成任务。从这个意义上来讲,时间规划也是调动成员热情的不错的方案。 3.2 代码管理与持续集成 我们团队使用git进行代码版本的控制管理,在开发过程中维护了三个代码库,分别对应于系统后端、微信小程序以及弹幕主墙应用程序。我们也利用Github上的Issues、Projects、Wiki等功能辅助我们进行开发。由于我们团队尚未开源,所以这里也不方便向大家展示代码库的具体细节。 此外,我们团队使用Travis CI辅助我们进行代码的持续集成与自动部署,感兴趣的话大家可以自主学习一下它的使用方法。 3.3 文档管理 通过上文我们也发现了,在开发过程中我们会写很多的文档,所以合理的文档管理也是开发中的重点问题。我们团队使用的在线文档工具是石墨文档。石墨文档有三个优点: 有清晰简洁的界面与丰富的功能 支持多成员在线编辑 可以很方便地导出为word文档与pdf文档 我们团队还维护了一个产品文档目录,这样每次要修改或查阅文档时,都有一个很便捷的入口。 [图片] 4.测试 测试主要有三部分:单元测试、功能测试与性能测试。 单元测试。单元测试,就是对软件中最小可测试单元进行检查和验证。不同软件的单元测试是不同的,比如我们在开发后端时,使用的是Python中的Django框架,这一框架是自带单元测试模块的,所以我们只需在test.py中实现所有测试样例即可。单元测试保证了软件最基本的正确性,最佳的模式是开发者在开发的过程中就将单元测试样例写好。(现在微信小程序还没有单元测试的模块,希望之后小程序团队可以在这方面给出接口。) 功能测试。功能测试就是要测试软件系统的各个功能能否正常执行。功能测试的辅助软件有很多,但最简单也最方便的就是人工手动测试,也就是开发者模拟用户的使用场景测试一遍自己的软件系统。 性能测试。软件性能也是评判一个软件好坏的重要依据。就以我们的弹幕小程序为例,虽然现在的学生节晚会只有三百多人,但是如果要拓展到所有晚会时,就不得不考虑高并发的情况。所以我们团队使用Jmeter对于发送弹幕的功能进行了性能测试,并在测试之后通过图片压缩等方式提高了我们小程序的性能。 除此之外,测试环节还包括安全性测试、易用性测试、兼容性测试等。测试过程中大家需要尤其注意的地方是:一定要将测试场景与测试样例想全面。越是严密的测试,软件系统也就相应越理想。 5.分析与维护 在开发与测试结束之后,小程序也就被正式投入使用了。因为用户行为是多种多样的,所以这个时候不出意外会出现一些奇奇怪怪的bug。作为开发者一定要给出一个用户反馈的途径,并且根据用户反馈的问题,制定下一个迭代周期的任务。这样循环往复,直至软件达到预期。下图为我们小程序为用户提供的反馈接口: [图片] 6.总结 以上就是我在开发过程中的一些经验与体会,希望能够给大家一些帮助与启示。弹幕biubiu小程序的开发,对于我来说是一个特别宝贵的经历。在这个过程中我学到了很多新的知识,接触到了很多新的事物,也发现了其他同学很多的优点。同时也很感谢刘强与刘璘两位老师对我们团队的支持与指导,在这里也推荐一下两位老师在学堂在线上的软件工程课程,如果大家感兴趣的话可以去了解学习一下,相信会给你们很大的帮助。 附言 如果大家对我们的小程序感兴趣的话,也可以使用一下呀~ 使用说明 主办方管理网站 应用程序下载链接(也可在主办方管理网站中下载) 小程序二维码 [图片] 大家有什么问题或者建议的话,也欢迎随时与我交流~ 我的Github地址是:https://github.com/JL-Cheng 我的邮箱是:chengjl16@163.com
2019-05-23 - Python 实现小程序云存储文件上传
一、 自己定期有听BBC六分钟英语的习惯。但BBC六分钟官网由于大家都了解的缘故,不能直接访问。也不想装别的APP,就想着何不自己做个小程序? 二、 想到就做。做为一个方便自己的小程序,不需要很复杂的架构。 闲置的DigitalOcean服务器下载音频和对话脚本,传回国内COS。最后用小程序展示就好了。 从写抓取脚本和小程序制作上线花了大概一天的时间。上线后发了几个微信群,引来了100多个用户。也反馈了一些问题,最多的就是加载慢。。 虽然只为自己兴趣制作的,那既然引来了用户。咱也不是乔布斯,不能不听用户的反馈。没什么说的上CDN。 三、 上完CDN,速度有了。也就没再去管了,之后就一直在佛系运营。 直到17号开发者社区推送了一条消息:可以外网上传文件到云存储了! https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/index.html 说实话,之前也考虑过小程序里的云存储,但好像只能通过开发者工具人工上传。。 这很不极客。 所以收到这条推送之后第一时间就去看了文档,理了一遍小程序云存储上传逻辑是: step1: 用小程序Appid 和Appsecret 拿 Access_token step2: 用拿到的Access_token拿文件上传URL和相关参数 step3: 用拿到的URL和相关参数拼接完整的POST请求来上传文件 四、 理顺了逻辑,接下来就是码代码了。 Python用来http请求的,选用requests,相信没人挑错的。 首先:拿access_token [代码]def get_token(): token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + ID + "&secret=" + SECRET try: token = requests.get(token_url) token = token.json() return token["access_token"] except Exception as e: logging.error(e) [代码] 然后,用access_token获取文件上传相关参数 [代码] def get_upload_url(token, env, path): post_url = "https://api.weixin.qq.com/tcb/uploadfile?access_token=" + token playload = json.dumps({"env":env, "path":path}) try: upload = requests.post(post_url, data=playload) return upload.json() except Exception as e: logging.error(e) [代码] 拿到参数后需要解析重新拼接来完成上传: [代码]def parse_form(res): form = {} form["key"] = res["url"].split("/")[-1] form["Signature"] = res["authorization"] form["x-cos-security-token"] = res["token"] form["x-cos-meta-fileid"] = res["cos_file_id"] return (form, res["url"]) [代码] 最后,就是上传了: [代码]def upload(res, file): form = res[0] upload_url = res[1] with open(file, "rb") as f: form["file"] = f.read() try: success = requests.post(upload_url, files=form) except Exception as e: logging.error(e) [代码] 完整源码可以添加个人微信 iKeepLearn 获取 最后放上自己做的小程序,6minute 同步更新BBC Learning English。 [图片]
2019-05-22 - 5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)
由于小程序2020年1月10日以后改模板消息为订阅消息,所以我写了一篇新的文章来更新这个知识点 《小程序订阅消息推送(含源码)java实现小程序推送,springboot实现微信消息推送》 我们在做小程序开发时,消息推送是不可避免的。今天就来教大家如何实现小程序消息推送的后台和前台开发。源码会在文章末尾贴出来。 其实我之前有写过一篇:《springboot实现微信消息推送,java实现小程序推送,含小程序端实现代码》 但是有同学反应这篇文章里的代码太繁琐,接入也比较麻烦。今天就来给大家写个精简版的,基本上只需要几行代码,就能实现小程序模版消息推送功能。 老规矩先看效果图 [图片] 这是我们最终推送给用户的模版消息。这是用户手机微信上显示的推送消息截图。 本节知识点 1,java开发推送后台 2,springboot实现推送功能 3,小程序获取用户openid 4,小程序获取fromid用来推送 先来看后台推送功能的实现 只有下面一个简单的PushController类,就可以实现小程序消息的推送 [图片] 再来看下PushController类,你没看错,实现小程序消息推送,就需要下面这几行代码就可以实现了。 [图片] 由于本推送代码是用springboot来实现的,下面就来简单的讲下。我我们需要注意的几点内容。 1,需要在pom.xml引入一个三方类库(推送的三方类库) [图片] pom.xml的完整代码如下 [代码]<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.qcl</groupId> <artifactId>wxapppush</artifactId> <version>0.0.1-SNAPSHOT</version> <name>wxapppush</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--微信小程序模版推送--> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>3.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> [代码] 其实到这里我们java后台的推送功能,就已经实现了。我们只需要运行springboot项目,就可以实现推送了。 下面贴出完整的PushController.java类。里面注释很详细了。 [代码]package com.qcl.wxapppush; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.bean.WxMaTemplateData; import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage; import cn.binarywang.wx.miniapp.config.WxMaInMemoryConfig; import me.chanjar.weixin.common.error.WxErrorException; /** * Created by qcl on 2019-05-20 * 微信:2501902696 * desc: 微信小程序模版推送实现 */ @RestController public class PushController { @GetMapping("/push") public String push(@RequestParam String openid, @RequestParam String formid) { //1,配置小程序信息 WxMaInMemoryConfig wxConfig = new WxMaInMemoryConfig(); wxConfig.setAppid("XXX");//小程序appid wxConfig.setSecret("xxx");//小程序AppSecret WxMaService wxMaService = new WxMaServiceImpl(); wxMaService.setWxMaConfig(wxConfig); //2,设置模版信息(keyword1:类型,keyword2:内容) List<WxMaTemplateData> templateDataList = new ArrayList<>(2); WxMaTemplateData data1 = new WxMaTemplateData("keyword1", "获取老师微信"); WxMaTemplateData data2 = new WxMaTemplateData("keyword2", "2501902696"); templateDataList.add(data1); templateDataList.add(data2); //3,设置推送消息 WxMaTemplateMessage templateMessage = WxMaTemplateMessage.builder() .toUser(openid)//要推送的用户openid .formId(formid)//收集到的formid .templateId("eDZCu__qIz64Xx19dAoKg0Taf5AAoDmhUHprF6CAd4A")//推送的模版id(在小程序后台设置) .data(templateDataList)//模版信息 .page("pages/index/index")//要跳转到小程序那个页面 .build(); //4,发起推送 try { wxMaService.getMsgService().sendTemplateMsg(templateMessage); } catch (WxErrorException e) { System.out.println("推送失败:" + e.getMessage()); return e.getMessage(); } return "推送成功"; } } [代码] 看代码我们可以知道,我们需要做一些配置,需要下面信息 1,小程序appid 2,小程序AppSecret(密匙) 3,小程序推送模版id 4,用户的openid 5,用户的formid(一个formid只能用一次) 下面就是小程序部分,来教大家如何获取上面所需的5个信息。 1,appid和AppSecret的获取(登录小程序管理后台) [图片] 2,推送模版id [图片] 3,用户openid的获取,可以看下面的这篇文章,也可以看源码,这里不做具体讲解 小程序开发如何获取用户openid 4,获取formid [图片] 看官方文档,可以知道我们的formid有效期是7天,并且一个form_id只能使用一次,所以我们小程序端所需要做的就是尽可能的多拿些formid,然后传个后台,让后台存到数据库中,这样7天有效期内,想怎么用就怎么用了。 所以接下来要讲的就是小程序开发怎么尽可能多的拿到formid了 [图片] 看下官方提供的,只有在表单提交时把report-submit设为true时才能拿到formid,比如这样 [代码] <form report-submit='true' > <button form-type='submit'>获取formid</button> </form> [代码] 所以我们就要在这里下功夫了,既然只能在form组件获取,我们能不能把我们小程序里用到最多的地方用form来伪装呢。 下面简单写个获取formid和openid的完整示例,方便大家学习 效果图 [图片] 我们要做的就是点击获取formid按钮,可以获取到用户的formid和openid,正常我们开发时,是需要把openid和formid传给后台的,这里简单起见,我们直接用获取到的formid和openid实现推送功能 下面来看小程序端的实现代码 1,index.wxml [图片] 2,index.js [图片] 到这里我们小程序端的代码也实现了,接下来测试下推送。 [代码]formid: 6ee9ce80c1ed4a2f887fccddf87686eb openid o3DoL0Uusu1URBJK0NJ4jD1LrRe0 [代码] [图片] 可以看到我们用了上面获取到的openid和formid做了一次推送,显示推送成功 [图片] [图片] 到这里我们小程序消息推送的后台和小程序端都讲完了。 这里有两点需要大家注意 1,推送的openid和formid必须对应。 2,一个formid只能用一次,多次使用会报一下错误。 [代码]{"errcode":41029,"errmsg":"form id used count reach limit hint: [ssun8a09984113]"} [代码] 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言,我会把源码下载链接贴在留言区。 单独找我要源码也行(微信2501902696) 视频讲解:https://edu.csdn.net/course/detail/23750 源码链接:https://github.com/qiushi123/wxapppush
2020-01-08 - 1个开发如何撑起一个用户过亿的小程序
作者 LeeHey 2018年12月,腾讯相册累计用户量突破 [代码]1亿[代码],月活 [代码]1200万[代码],阿拉丁指数排行 [代码]Top30[代码],已经成为小程序生态的重量级玩家。 三个多月来,腾讯相册围绕 [代码]在微信分享相册照片[代码]这一核心场景,快速优化和新增一系列社交化功能,配合适当的运营,实现累计用户量突破 [代码]1亿[代码],大大超过预期。 [图片] (9个月,腾讯相册用户量破亿) 可是,谁曾想到,这样一个亿级体量的小程序,竟然是一个开发做出来的?他又是有哪般“绝技”,可以一个人撑起一个用户过亿的小程序? 后台人力紧缺,怎么办? 当我第一次见到腾讯相册小程序的开发David(化名)时,他显得忧心忡忡。 “年底的目标是要过千万的用户,但现在只有几位前端和后台开发。不仅如此,我们的后台开发还不是百分百能够投入到这个项目,大部分时间要抽身支援其它项目,人力非常紧缺。此外,原有后台系统有不少历史包袱,在原有架构上做新的社交化功能开发是不现实的。怎么办? “要不试试 [代码]小程序·云开发[代码]吧,只需要前端就可以把小程序搞起,正好解决我们缺后台的难题。” 于是,David作为腾讯相册前端开发团队的骨干,担当起用 [代码]小程序·云开发[代码]实现腾讯相册小程序社交化功能的重任。 “第一次接触到 [代码]小程序·云开发[代码]时,觉得这个东西(小程序·云开发)理念挺新颖的——— [代码]小程序无服务开发模式[代码]。在一般的小程序开发中,有三大功能小程序开无法绕开后台的帮助,它门分别是数据读取、文件管理以及敏感逻辑的处理(如权限)。因此,传统的开发模式,在小程序端都必须发送请求到后台进行鉴权,并且处理相关的文件或者数据。即使使用 Node 来搭建后端服务,也需要耗费不少的搭基础架构、后期运维的工作量。” [图片] “而 [代码]小程序·云开发[代码]则释放了小程序开发者的手脚,赋予了开发者安全、稳定读取数据、上传文件和控制权限的能力,其它的负载、容灾、监控等,我们小程序开发者只需要关注业务逻辑,专注写好业务逻辑即可,其他的事情完全可以不用操心了!本来我还一筹莫展,了解完 [代码]小程序·云开发[代码]的产品原理以后,我瞬间心里有谱了。” 二维码扫不出来了 [图片] 道路总是不平坦的 ,在腾讯相册小程序通往用户破亿的道路上,困难重重。 由于腾讯相册的二维码需要带上的信息量过大,因此它的二维码显得密密麻麻。这种密集的二维码在某些Android机型下,容易出现无法识别小程序的问题。 这严重制约了腾讯相册小程序分享获客的能力。 [图片] (需要存储name, ownerid, page等大量信息) 这个事情的解决并不难,只需后台开发把数据先存储到数据库中,然后把数据id放到分享链接上,这样,链接便可以转化成32个字符的短链接,让二维码看起来没有那么密集了。 但由于后台人力不足,于是前端开发David利用小程序· 云开发的数据库存储能力,通过调用 [代码]db.collection('qr').add[代码]接口,快速实现数据在数据库中的存储。 [图片] (云开发数据库,格式类似MongoDB) [图片] (云开发数据库索引,可加快数据读取) [图片] 此外,腾讯相册还借住小程序·云开发的云函数能力,生成辨识度更高的小程序码(小程序码文档https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html),用以在朋友圈里传播分享。 [图片] (生成小程序码的云函数逻辑) [图片] (优化后的分享图片和小程序码) 2天上线评论点赞功能 [图片] (评论与点赞功能) 腾讯相册在微信端的核心应用场景是“在微信做分享相册照片”,为了增强腾讯相册用户在微信里的互动,提升用户粘性和留存,腾讯相册决定新增评论与点赞功能,并且把聊天评论就直接在微信聊天窗口里面实现。 在这里,腾讯相册的David面临了两个选择,一是按原开发模式(前台开发-后台开发-前后台联调)做这个功能,面临的问题便是开发周期长、缺后台、迭代速度慢;另一个就是借助云开发的能力,自己上。 为了加快产品迭代速度,David决定采取云开发的开发方式。评论、点赞通过云开发的数据库插入和查询接口,如 [代码]db.collection('comment').add[代码],很快就实现了。 但遇到棘手的问题是,对于一些敏感的操作比如删除和编辑评论、点赞这些敏感操作,还需要到用户的鉴权操作,而这些鉴权信息,都在原有的后台。此时,云函数的路由功能便发挥出作用了。 [图片] (评论点赞逻辑) 用户进行评论点赞的时候,会在小程序端发起请求调用云函数并带上 [代码]openid[代码],云函数用 [代码]openid[代码] 查询原有的后台服务看看该用户是否有权限进行操作,如果用户具有权限,则把评论和点赞的数据都写入云开发的数据库中。 就这样,借住小程序·云开发的能力,腾讯相册仅用2天时间,完成了在传统开发模式下需要1周多工作量的开发工作。 [图片]
2019-02-15