个人案例
- 日语助手
日语初学者的小工具
日语助手扫码体验
- 猜个动画
看图猜动画
猜个动画扫码体验
- 网站域名(www.lianjiepay.com)已经备案,域名访问还是提示 无法确认该网页的安全性?
[图片]域名已经备案,也已经提交申请恢复,还是这样怎么回事? 这个域名也已经备案 [图片]
07-17 - PHP开发调用msgSecCheck,违规内容也返回无问题?
一开始使用的EasyWechat调用的,errcode=0,后面使用第三方工具直接调用msg_sec_check还是这样提示,参考下图,不知道哪里写错了。 [图片]
07-09 - AI智能体应用发布篇(公众号/小程序)
前言 上一篇《教你 3 分钟搭建 AI 助手(无需编码)》让大家快速搭建了微信云开发的AI智能体Web版和H5版。 如果想在微信生态中快速获取用户,那么公众号和小程序是必须要做的载体,所以这篇主要分享以下 3 点: AI智能体发布到公众号 小程序中集成AI智能体 AI深度合成类目如何申请 步骤 AI智能体发布到公众号 进入云模板 首先我们先进入云模板控制台,在这里再教大家一种快速进入云模版的方式,除了在《教你 3 分钟搭建 AI 助手(无需编码)》中提到的云开发控制台进入的方式之外。 还可以直接在微信开发者工具的代码目录区域右键呼出菜单然后选择「通过云模板或AI配置页面」菜单项。 注:非正式AppId,小游戏,游客态、代开发小程序,不是小程序开发者,看不到该菜单项。 [图片] 授权公众号 从「我的应用」列表进入「AI智能体应用」详情页点击「添加至多个平台」 [图片] 可以选择一个AI智能体进行「配置」支持 3 个平台 微信小程序客服 微信公众号(服务号) 微信公众号(订阅号) [图片] 配置方式非常方便只需要填写AppId即可 [图片] 前往微信公众平台“设置与开发” - “基本配置” - “公众号开发信息”,复制”开发者ID(AppID)”信息 [图片] 获取到AppID填写后点击「下一步」扫码授权即可,授权成功后 未授权 会变为 已授权 状态 [图片] 接下来来看下效果: [图片] 没有认证的公众号需要回复“继续”,已认证公众号无需回复“继续”可直接输出文案 小程序中集成AI智能体 集成应用 回到「添加至多个平台」面板选择「添加至小程序」 [图片] 根据操作指南下载好代码包解压 [图片] 复制到 miniprogram/ 目录下 [图片] 再将下载的 project.config.json 进行替换 [图片] 当以上两步都完成之后可以以下两种方式进行跳转: JS跳转代码 [代码]wx.navigateTo({url: "/$weda_root/packages/mIOXHS1t/pages/chat/index"}); [代码] WXML布局代码 [代码]<navigator url="/$weda_root/packages/mIOXHS1t/pages/chat/index">跳转至智能体</navigator> [代码] 在这里就相当于把整个AI智能体应用集成到小程序中了 [图片] 在这里需要注意,如果要发布小程序上线还需要在小程序管理后台更新域名配置 [图片] 集成API 如果想自定义界面,可以直接集成API即可 回到「AI智能体应用」详情页面切换到「接口展示」 [图片] 可直接复制代码在小程序端进行调用,以查看AI智能体列表接口为例 [代码] wx.cloud.callFunction({ name: 'cloudbase_module', data: { name: 'ai_bot_get_bot_list', data: { filter: { where: { }, }, select: { $master: true, // 常见的配置,返回主表字段 }, }, }, success: (res) => { console.log(res) }, }); [代码] [图片] 每个接口除了有示例代码,还有详细的参数说明: [图片] [图片] AI深度合成类目如何申请 想要上线AI相关的小程序,必须申请深度合成类目,所以这一步至关重要,回到「AI智能体应用」详情页切换到「AI算法备案资料」- 「获取AI算法合作协议」输入小程序主题即可 [图片] 截图证明,在这里需要注意截图一定要露出云模板字样,这样便于类目审核人员区分截图证明来源 [图片] 然后到小程序管理后台添加类目选择【深度合成 - AI问答】选择 2.2 使用第三方技术,上传截图证明即可 [图片] 通过以上方式类目已审核通过 [图片] 总结 本篇主要讲解了AI智能体应用的多平台发布,整体而言从创建到发布非常方便,不管你是公众号运营者还是小程序开发都可以拥有自己AI智能体应用,赶紧去试试吧~
06-28 - 微信小程序分享到朋友圈单页模式适配
微信小程序支持分享到朋友圈,但能力有所限制,为此专门搞了一个单页模式,主要目的是防止滥用于营销、诱导等。 常见问题:1、小程序从朋友圈打开,一片空白 2、没有触发 HTTP 请求 3、页面顶部渲染异常,被挡住 解决方案:问题 1、2,大部分都是配置出了问题。 排查顺序: 1、分享参数正确与否, 2、是否使用了禁用能力相关接口,查看控制台报错 3、调用HTTP接口不能包含用户登陆状态(如 token),兼容访客访问 问题 3:通常这是因为该页面设置自定义[代码]navigationStyle: custom[代码] , 只需要把页面配置调整为 { "singlePage": { "navigationBarFit": "squeezed" } }
06-11 - 很久前注册的微信公众号目前已经完全忘记了, 如何找回或者直接注销?
很久之前注册的微信公众号, 但是现在已经从名称到邮件, 还有ID全都忘记了, 请问如何直接注销? 我已经不准备再用了, 但是只能找到找回公众号的接口. 想从新注册一个公众号却提示个人名下公众号已经满了. 这成死局了. 注册很久的公众号是不是微信能够提供一个直接注销的接口啊? 为什么还非要找回才能注销呢? 目前已经崩溃!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
01-30 - Donut 上传应用商店遇到的问题及解决方案
分享我自己碰到的一些上架问题,大家也可以留言补充! 1.安卓端单独微信登入就可以 2.尽量所有《点击事件》都有反馈 3.要提供账号注销的功能 4.软件页面要提供展示入口《隐私协议》/《用户协议》 5.应用宝- icp备案域名 打开后需要有app宣传页面等,页面最好不要带登入入口 6.聊天、社区等需要提供安全报告下面有案例 7.ios端如果不使用苹果支付,ios登入等,在标识符里面不要勾选,描述文件里面也不要有。会被驳回! 8.有wx.openLocation功能的,这个功能暂时不能用,用wx.getLocation被华为驳回了 9.如果APP中未使用微信支付,可在开发者工具project.miniapp.json中勾选不代支付能力的opensdk。 [图片] 一、软件版权 软件版权 1.《计算机软件著作权登记证书》当前最快申请速度为20-30个工作日,不包含材料整理时间; 2.《电子版权认证证书》和《软件著作权认证证书》最快申请速度为3个工作日,最慢申请时间10-15个工作日,在时效方面具有非常大的优势。 [流泪/]花了6个月注册了软著,忘记写简称了,又花了15天办了软件著作权认证证书 其实来说《软件著作权认证证书》就可以上架绝大部分应用商店了,白交了一次学费,并浪费了时间! 软著找第三方申请tb pdd等,不要自己傻傻的申请,要不然很多地方改正驳回 二、ICP备案 备案查询后截图,打印盖公章!!! 感觉是随缘审核,有的时候需要盖公章,有的时候不需要。 三、UI问题 尽量做到UI自适应,UI设计尽量自己设计, 如果UI与其他软件页面一致会有驳回的风险。 四、安全评估报告 登录全国互联网安全服务管理平台(http://www.beian.gov.cn ),请选择主页评估报告登录 按照我这个填即可 服务名称 功能名称 ..... 评估情形:具有舆论属性或社会动员能力的信息服务上线,或者信息服务增设相关功能; 评估方法:自评估 1、安全管理负责人、信息审核人员及安全管理机构设立情况。 安全管理负责人:XXX 审核人员:XXX 2、用户真实身份核验及注册信息留存措施。 建立台账、专人记录留存。 3、对用户账号、操作时间、操作类型、网络源地址和目标地址、网络源端口、客户端硬件特征等日志信息,以及用户发布信息记录的留存措施。 建立台账,专人记录留存备案。 4、对用户账号和通讯群组名称、昵称、简介、备注、标识,信息发布、转发、评论和通讯群组等服务功能中违法有害信息的防范处置和有关记录保存措施。 设专人进行扫描、查找,及时处理,并进行纪律备案。 5、个人信息保护以及防范违法有害信息传播扩散、社会动员功能失控风险的技术措施。 定期组织人员培训,宣传个人信息保护知识,严防社会动员功能失控。 6、建立投诉、举报制度,公布投诉、举报方式等信息,及时受理并处理有关投诉和举报的情况。 在网络的显著位置设置投诉电话,并安排专人接听电话,受理并及时处理各类投诉和举报情况。 7、建立为监管部门和执法部门依法履职提供技术、数据支持和协助的工作机制的情况。 安排专人专责,为监管部门和执法部门依法履职提供技术、数据支持和协助 填完后,会让你打印一份PDF上传,然后等待wang警上门核查,等待通过即可! 上传后,没人审核怎么办?联系当地网络部门催审, 我催审后,第二天就上门解决了。 五、隐私协议 网上复制的只能找那些通用型模板, 软件权限说明,尽量在隐私协议上补充清楚。 开启了推送消息需要在《隐私协议说明》并在软件设置里面设置一个《个性化推荐》开关 有获取用户信息,并推荐给其他人 需要在《隐私协议说明》并在软件设置里面设置一个《把我推荐给可能认识的人》开关 三方SDK列表 为保障相关功能的实现与应用安全稳定的运行,我们可能会接入由第三方提供的软件开发包(SDK)实现相关目的。 我们会对合作方获取信息的软件工具开发包(SDK)进行严格的安全监测,以保护数据安全。 我们对接入的相关第三方SDK在目录中列明。 请注意,第三方SDK可能因为其版本升级、策略调整等原因导致数据处理类型存在一定变化,请以其公示的官方说明为准。 综合运行类 第三方SDK名称:微信 SDK 链接:https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/Android.html/ 收集信息类型:设备标识信息、软件安装列表 使用目的:接入微信开放平台,让你的移动应用支持微信分享、微信收藏和微信支付。 第三方SDK名称:wmpf_demo_external 链接:https://github.com/wmpf/wmpf_demo_external 收集信息类型:设备标识信息、软件安装列表 使用目的:该运行环境能让硬件在脱离微信客户端的情况下运行微信小程序 祝大家上架顺利!!! 先写这么多吧,想到了在补充。
2023-07-11 - Donut ios企业证申请流程及打包过程
企业注册申请苹果证书及打包过程流程 一、注册账号 1.申请ios企业证书首先是需要注册苹果账号,邮箱注册 2.苹果注册地址:https://appleid.apple.com/account 二、苹果开发者账户申请 1.苹果开发者地址:https://developer.apple.com 2.手机或者mac下载应用:Apple Developer App 3.查询是否存在邓白氏D-U-N-S编码 查询地址:https://developer.apple.com/enroll/duns-lookup/#!/search 4.输入你的企业详细信息全部以英文字符 5.查询不存在后,下一步提交注册D-U-N-S编码,全部以英文字符 6.申请D-U-N-S编码大概需要3-7天左右 [图片] 7.Apple Developer App登入-立即注册填写的信息要与你申请的信息一致,最后填入D-U-N-Sb编码,最后付费 [图片][图片] 三、苹果开发者证书申请 主要是证书、标识符、描述文件 [图片] 1.证书申请 ios开发者证书生成(我用的是 Appuploader)去管理https://www.applicationloader.net [图片] 2.标识符 [图片] 下一步======= [图片] 下一步======= [图片] 下一步==== [图片] 开启以下权限: Access WiFi Information Associated Domains Hotspot Wireless Accessory Configuration 可以另外配置苹果登入、苹果支付、消息通知等其他选项 保存后就完成了标识符的申请 [图片] 3.描述文件 [图片] 下一步==== [图片] 下一步==== [图片] 下一步==== [图片] 下一步==== [图片] 现在拥有了p12证书,还有描述文件,就可以去苹果电脑打包ipa了 无苹果电脑 使用VMware虚拟机模拟苹果 mac OS系统最好是13版本以上,具体怎么实现不演示 [图片] 环境安装教程:https://dev.weixin.qq.com/docs/framework/dev/framework/env/mac.html 安装sudo gem install cocoapods 然后输入密码自动隐藏 回车即可 sudo gem install cocoapods 不显示安装流程 sudo gem install cocoapods -V 显示安装流程 双击导入p12证书 可能出现证书无效的情况 删除掉所有证书 [图片] 前往https://developer.apple.com/certificationauthority/AppleWWDRCA.cer 去从新下载一个中级证书. 开发工具去打包ipa 复制证书名称 导入描述文件 [图片] [图片] [图片] 打包完成 有什么地方缺失的地方指正后修改
2023-08-18 - 小程序录音实时波形图实现
[图片] 实现思路:主要使用canvas绘制柱状图,音量大小由微信recorderManager.onFrameRecorded回调的数据通过快速傅里叶转换算法实现转换 <view style="display: flex; position: relative; justify-content: center; width: 100%; background-color: black;"> <canvas id="myCanvas" type="2d" catch:touchstart="onTouchStart" catch:touchmove="onTouchMove" catch:touchend="onTouchEnd" style="width: 100%;height: 220px;"></canvas> <view style="position: absolute; width: 0.5px; height: 220px; background-color: blue;" /> </view>
2023-04-26 - 个人公众号流量主最新的扣税标准是什么?
一个月几百块的流量主,都达不到起征点,怎么能扣一百多的税出去,【累计税前金额】这一项不应该按照单月周期来计算吗,为什么现在时间跨度扩大到2个月了 [图片]
2023-05-05 - 怎么在小程序里面实现点按翻译
首发于公众号 iKeepLearn [图片] #0 English Podcast (原 BBC English Podcast)一直没有怎么维护。最近刚好有时间,就登录后台看看用户反馈。有没有什么值得更新的。其中呼声最高的就是希望加入翻译功能。所以就先实现这一点吧。 #1 确定需求后,就先搜一下其他小程序,看看他们实现的效果。中国日报社和微信研究院联合推出的每日英文电台就刚好有这个功能。试了一下效果不错。所以这个需求点是可实现的。 那怎么实现呢?毕竟小程序里面不像网页和 APP 那样支持长按选择文字并弹出自定义菜单项,官方支持选择文字的就 text 组件。但没有相关的 api 去获取选定的内容? 在搜索了一圈后也没有找到很好的实现,那就只好自己来实现。 #2 思考了一下,只有用常见的支持绑定点击事件的 view 了。思路是先把英文句子按单词分割,然后每一个单词绑定相应的事件。这样就能获取点按的单词了,再调用接口去获取翻译内容。 所以数据结构按这样设计 [图片] 思路确定后,相关编码就很简单了。 wxml文件和wxss文件 [图片] #3 在找翻译接口的时候,有谷歌,必应、百度和有道作为备选。最后思考了一下还是选了微信官方的接口。这样可以避免关键字检测? 本来在微信服务市场找到的英中翻译接口是最适合需求的,因为不需要服务器调用可以直接在小程序客户端里使用。只是按文档接入后提示出错了。只好选择多语言翻译接口。 [图片] #4 最后可以扫码下方的小程序体验点按翻译 [图片]
2022-12-19 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - wx.getUserProfile不能和wx.login一起使用?
mac 模拟器1.05.2102010 基础库2.16.0 调用wx.login获取code后,再调用wx.getUserProfile,可能会失败,触发fail函数,error msg: ''getUserProfile:fail can only be invoked by user TAP gesture"。 如果不能同时使用,那如何校验用户信息的准确性或者解密encryptedData呢?
2021-04-08 - 小程序获取某项权限的通用写法
温馨提示: [代码]errMsg: "openSetting:fail can only be invoked by user TAP gesture." [代码] wx.openSetting 不能直接在程序中调用,但是可以通过 wx.showModal 拉起。 【相关链接】 1、 wx.getSetting(Object object) 2、 wx.authorize(Object object) 3、 wx.showModal(Object object) 4、 wx.openSetting(Object object) update at 2022.08.02 再次查看这篇文章发现代码并不好懂,决定修改下 ; 逻辑还是一样的(代码中有注释),只是改变了写法 (1) uni.getSetting -> (2) uni.authorize -> (3) uni.showModal -> (4) uni.openSetting 以获取录音权限为例: 正常情况下,第一次,会提示用户授权,截图如下: [图片] 如果用户选择“允许”,那么一切皆顺 如果用户选择“拒绝”,就比较麻烦了,需要在 [代码]openSetting[代码] 打开,截图如下: [图片] 因为[代码]openSetting[代码]不能直接由程序调用,所以需要由[代码]showModal[代码]拉起,截图如下: [图片] 下面是代码截图: [图片] [图片] 下面是获取某项授权的完整代码 [代码]// 获取微信录音权限 // scopeStr = "scope.record" // modalMsg = "需要您授权获取录音权限" async getWxRecordAuth(scopeStr, modalMsg) { try { let [flag1, flag2] = [true, false] // (1) wx.getSetting 检查用户之前是否已经授权 flag1 = await new Promise((resolve)=>{ wx.getSetting({ success(res) { if (res.authSetting[scopeStr]) { resolve(true) } else { resolve(false) } } }) }) if (!flag1) { // (2) wx.authorize 弹窗请用户授权 flag1 = await new Promise((resolve)=>{ wx.authorize({ scope: scopeStr, success(res) { resolve(true) }, fail() { resolve(false) } }) }) } if (!flag1) { // (3) wx.showModal ,新规范下, // 必须通过按钮或者`wx.showModal`调起`wx.openSetting` flag2 = await new Promise((resolve)=>{ wx.showModal({ title: '授权', showCancel: false, content: modalMsg, success(res) { if (res.confirm) { resolve(true) } else if (res.cancel) { resolve(false) } else { resolve(false) } } }) }) } if (flag2) { // (4) wx.openSetting 请用户打开授权 flag1 = await new Promise((resolve)=>{ wx.openSetting({ success(res) { if (res.authSetting[scopeStr]) { resolve(true) } else { resolve(false) } } }) }) } return flag1 } catch (err) { console.log(err) return false } }, [代码] update at 2022.08.02 下面是之前的内容 小程序获取某项权限 【相关链接】微信小程序:开放能力/用户信息/授权 【相关链接】表单组件/button , [代码]open-type[代码] : [代码]openSetting[代码] uni -> wx , 有些地方写的是 [代码]uni[代码] 直接替换为 [代码]wx[代码] 可以把 [代码]scope.userLocation[代码] 替换为其他值,或者封装一下,变成通用的获取权限方式 封装时,别忘记,把 [代码]"需要您授权获取地理位置权限"[代码] 也封装一下,这个是弹窗的提示信息 提示: 这里的几个方法 promise 返回的是数组,[代码]arr[0][代码] 有值,就是出错了;[代码]arr[1][代码]才是success时返回的结果值,想要获取的结果值都存放在 [代码]arr[1][代码] 中。 [代码]// (1) uni.getSetting -> (2) uni.authorize -> (3) uni.showModal -> (4) uni.openSetting //授权获取地理位置权限 async getAuth_userLocation(){ try { //(1)getSetting const authSetting = await uni.getSetting(); if (authSetting['scope.userLocation']) { return true; } else { //(2)authorize const resAuthorize = await uni.authorize({ scope: 'scope.userLocation' }) //resAuthorize[0]:拒绝,resAuthorize[1]:接受; if (resAuthorize[1]) { return true; } else { //(3)showModal const resShowModal = await uni.showModal({ title: '授权', content: '需要您授权获取地理位置权限' }); //resShowModal[0]: null, resShowModal[1]: `confirm`,`cancel`都定义在"resShowModal[1]"中 if (resShowModal[1]) { if (resShowModal[1].confirm) { //(4)openSetting const resOpenSetting = await uni.openSetting(); //resOpenSetting[0]: null, resOpenSetting[1]: `authSetting`定义在"resOpenSetting[1]"中 if (resOpenSetting[1]) { const authSetting = resOpenSetting[1].authSetting; if (authSetting && authSetting['scope.userLocation']) { return true; } else { return false; } } else { return false; } } if (resShowModal[1].cancel) { return false; } } else { return false; } } } } catch (err) { console.log(err); return false; } }, [代码] 原来的写法 [代码] getCameraAuth(){ const that = this; // 获取摄像头权限 // (1) uni.getSetting -> (2) uni.authorize -> (3) uni.showModal -> (4) uni.openSetting uni.getSetting({ success(res) { const authSetting = res.authSetting; if (authSetting['scope.camera']) { // 已经授权 that.is_camera_auth = true; } else { // 未授权 uni.authorize({ scope: 'scope.camera', success(resSuccess) { // 同意授权 that.is_camera_auth = true; },fail(resFail) { console.log("resFail: ", resFail); // 引导用户授权 uni.showModal({ title: '授权', content: '需要您授权获取摄像头权限', success: function (res) { if (res.confirm) { uni.openSetting({ success(resOpenSetting) { // resOpenSetting: {errMsg: "openSetting:ok", authSetting: {scope.camera: false}} //console.log("resOpenSetting: ", resOpenSetting) const authSetting = resOpenSetting.authSetting if (authSetting && authSetting['scope.camera']) { that.is_camera_auth = true; } else { uni.showToast({icon:'none', title: '您拒绝授权小程序获取摄像头权限', duration: 1500}); } } }); } else if (res.cancel) { uni.showToast({icon:'none', title: '您拒绝授权小程序获取摄像头权限', duration: 1500}); } } }); } }); } } }); }, [代码] 这是原来的嵌套写法,没有上面的 Promise 版本方便;放在这里作为参照。
2022-08-02 - 推荐一个小程序的GraphQL框架
项目需要所完成的,目前已用于生产环境~
2018-07-17 - 小程序云开发支持graphql
先看效果 query-schema: [代码] query GetAll( $dice: Int! $sides: Int $firstName: String $lastName: String ) { rollDice(numDice: $dice, numSides: $sides) getName(firstName: $firstName, lastName: $lastName) } [代码] request: [图片] response: [图片] log: 配合apollo [代码]const {data} = useQuery(GQL,{ variables: { dice: 1, sides: 3, firstName: 'Yang', lastName: 'Robot' } }) console.log('data', data) [代码] [图片] 为什么要使用graphql?RESTFUL的方式不好吗? 1. 免去API文档的维护 直接根据schema查看接口名和参数 [图片] 2. 可以合并请求,减少客户端等待时间 多个并行请求合并在一个graphql请求里 依赖关系的请求直接通过ResolveProperty处理成一个请求 如:friend.friend.friend,在server实现friend的friend属性解析后,可以只通过一个请求获取结果。而REASTFUL需要异步请求3次 3. 声明式数据获取 返回数据格式符合预期 数据字段类型符合预期 小程序云开发服务端实现graphql-server的思路 client将query-string等通过小程序云开发请求传到server server通过graphql将query-string转换成AST 将AST对比schema和rootValue,运行对应的resolver函数,获取数据 根据query格式化数据 最后将数据传给client 小程序云开发怎么使用graphql server端 1. 新建graphql云函数 2. 安装wx-server-graphql依赖 [代码]npm install --save wx-server-graphql [代码] 3. 创建graphql服务 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const { graphqlWXServer } = require('wx-server-graphql') var { buildSchema } = require('graphql') // 使用 GraphQL Schema Language 创建一个 schema var schema = buildSchema(` type Query { rollDice(numDice: Int!, numSides: Int): [Int] getName(firstName: String, lastName: String): String } `) // root 提供所有 API 入口端点相应的解析器函数 var root = { getName: (args, context) => { const { firstName, lastName } = args return `${firstName} ${lastName}` }, rollDice: (args, context) => { const { numDice, numSides } = args return [numDice, numSides] } } // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return await graphqlWXServer({ wxParams: event, context: wxContext, schema: schema, rootValue: root }) } [代码] client端 1. 配合apollo实现client [代码]import React from 'react' import { ApolloClient } from 'apollo-client' import { ApolloProvider } from '@apollo/react-hooks' import { InMemoryCache } from 'apollo-cache-inmemory' import { ApolloLink, Observable } from 'apollo-link' import { print } from 'graphql/language/printer' // 利用link重置apolloClient请求grapql的方式为wx.cloud.callFunction // 参考文档 https://www.apollographql.com/blog/apollo-link-creating-your-custom-graphql-client-c865be0ce059/ class WXLink extends ApolloLink { constructor(options = {}) { super() this.options = options } request(operation) { return new Observable(observer => { wx.cloud.callFunction({ name: this.options.name || 'graphql', data: { ...operation, query: print(operation.query) }, success: function(res) { observer.next(res.result) observer.complete() }, fail: observer.error }) }) } } wx.cloud.init({ env: 'env-id', }) const client = new ApolloClient({ link: new WXLink({ name: 'graphql', }), cache: new InMemoryCache(), }) export const Provider = ({ children, ...props }) => ( <ApolloProvider client={client}>{children}</ApolloProvider> ) [代码] 2. 编写query,发起请求 [代码]import { useQuery } from '@apollo/react-hooks' import gql from 'graphql-tag' const GET_NAME = gql` query GetAll( $dice: Int! $sides: Int $firstName: String $lastName: String ) { rollDice(numDice: $dice, numSides: $sides) getName(firstName: $firstName, lastName: $lastName) } ` const result = useQuery(GET_NAME, { variables: { dice: 1, sides: 3, firstName: 'Yang', lastName: 'Robot' } }) console.log('data', result.data) [代码] NPM https://www.npmjs.com/package/wx-server-graphql Github https://github.com/Yrobot/wx-server-graphql 如果喜欢的话就去github点个赞吧 希望 wx-server-graphql 可以节约大家的开发时间
2020-07-14 - 利用云开发实现个性海报制作
#0 小程序海报对用户拉新、留存、回流都有着非常重要的作用。 个性海报的制作也就成了小程序开发者的必要功课。 结合 BBC English Podcast 小程序,今天我分享一下怎么利用云开发完成个性化海报的制作。 #1 个性海报可以放在云函数里用图片处理 npm 包来制作,也可以放在小程序端制作。 为了节省云函数资源,我们放在小程序端来制作。 在开始之前,我们理一下个性化海报生成的流程: 步骤一、确定海报内容 步骤二、确定海报样式 步骤三、获取小程序页面的小程序码或者二维码 步骤四、合成海报 步骤五、让用户下载保存 步骤六、上传生成的海报并添加记录到云数据库给下一个用户分享里直接下载使用 这里我手写了一个简单的流程图。 [图片] #2 海报内容和海报样式都是个性化比较强的,就不多作介绍了。 从生成小程序或者二维码开始。 开始之前,先看一下我已经实现的效果。 [图片] [图片] [图片] 在用户点击生成时,需要先判断之前是否有用户已经生成过。 如果已生成,则直接展示生成好的图片。 [图片] 如果没有已生成的海报时,需要生成海报,在生成之前也需要先判断是否已有生成好的小程序码, 没有则先生成小程序码 [图片] 获取小程序码需要在云函数里面操作,这里需要注意给云函数配置调用生成小程序码的权限 [图片] 获取到小程序码的 fileID 之后需要转换成 url。 给海报生成函数调用。 [图片] 生成海报我们用开源库https://github.com/Kujiale-Mobile/Painter 海报样式也可以用这个在线工具拖拽生成https://lingxiaoyi.github.io/painter-custom-poster/ 通过上面的工具按自己需求生成对应的代码后, 我们可以精简一些空属性和把动态内容改成对应的参数传递进去, [图片] 模版定义好后就可以生成海报。 [图片] 生成海报之后,我们需要上传到云存储并添加记录到对应的数据库里面方便下一个用户分享时直接下载使用 [图片] 最后,就是给用户提供保存到相册的功能了。 [图片] #3 首发在 ikeeplearn 公众号 到这里,利用云开发实现个性海报就结束了。 如果你有其他疑问或者需求欢迎加我微信一起交流 [图片]
2020-01-01 - 小程序scroll-view翻转后 scroll-into-view的替代方案
背景 腾讯云医小程序有医患聊天会话的场景,由于会话场景存在查询历史消息的场景,小程序中按照常规思路加载历史消息时会出现跳动的问题;跳动的原因是由于在’顶部’插入dom,会使得后面的dom被往后面推,然后重新设置scroll-top或者scrol-into-view从而导页面出现跳动;我们尝试采用【 前端开发中聊天场景的体验优化】文章中的方案处理跳动的场景。该文章的核心观点将scroll-view元素通过设置css样式 transform: rotateX(180deg); 进行翻转,这样将历史消对应的dom结构放在尾部,当添加更多的历史消息(dom)时,由于dom是添加在尾部很优雅的绕过了插入历史消息跳动的场景。但是当我们按照这种方式实现后,发现scroll-view元素提供的scroll-into-view属性不好使了。因此有了本文通过计算scrollTop值设置scrollTop来达到相同目的。 复现该问题的小程序代码片段:代码片段 目前已经反馈给官方(官方已确认是内部组件实现暂不支持翻转的场景 基础知识介绍 计算scrollTop涉及到一些web和小程序的基础知识,后面针对这些基础点进行简单介绍 .scroll-into-view 微信小程序提供的scroll-view元素提供了属性 scroll-into-view,该属性的作用是可以将指定dom滚动到scroll-view可见区域内 [图片] 关于boundingClientRect 下图是MDN解释该属性时提供的,从下图中可以看到top/bottom/left/right的值是元素的左上角和右下角相对于视口左上角的水/垂直距离 [图片] 为了更深入理解这些值。给出了一个简易的demo(代码片段),获取实例元素的的boundingClientRect的值后,可以看到这些值是根据元素的border边界进行计算的 [图片] [图片] [图片] 值得注意的是,当元素处于一个滚动区域内部,left/top值是考虑滚动操作的即包含滚动距离的(参考MDN 另外,当我们把容器元素又或者元素自身设置 transform: rotateX/Y(180deg):不会导致top和bottom的值互换(left与right的值互换); 总会有这样的结论,当dom元素的宽度和高度不为0时,top值一定小于bottom值,left值一定小于right值 关于scrollTop 当一个容器的内容的高度大于其容器高度时,overflow不为visible/hidden时,则会出现滚动条。出现滚动条后,内容区域则可以滚动,此时scrollTop的值是容器可视区域的顶部到内容区域顶部的距离,见下面示意图。 [图片] 值得注意的是,滚动条出现在盒模型中的content区域,见下图滚动条不会覆盖padding/border部分。因此上面说到内容区域高度超过容器高度并不严谨,严谨的说法应该是超过容器的content区域的高度。 [图片] 如果此时给容器设置css样式: transform: rotateX(180deg); 即沿垂直方向翻转180度,scrollTop的值会发生变化吗。 下面我们看下实际的对比效果,为了方便查看滚动条的效果,给滚动条轨道(红色部分)以及滑块(黑色部分)添加了背景色,发现整个元素包括滚动条在内一并进行了翻转。 正常情况(左侧),应用翻转css样式(右侧) [图片] [图片] 翻转后的scrollTop值示意图 [图片] 通过计算scrollTop值来模拟scroll-into-view效果(针对scroll-view翻转的场景j) 由于boundingClinetRect的值是包含border边界的,因此当数据项包含padding,border等区域不会影响这里的计算过程,可以认为下面示意图中的数据项部分的边界是border边界; 由于滚动条是出现在content区域,因此容器元素的的border-top/padding-top不为0时,会影响计算流程,因此这里分为两种情况进行介绍: 2.1 假设scroll-view元素的的border-top/padding-top为0 2.2 假设scroll-view元素的的border-top/padding-top不为0 border-top/padding-top为0的情况 为了方便说明计算过程,我定义三种状态,初始态、中间态、最终态 示意图中的区域说明 白色背景的为视口, 绿色背景的是容器(scroll-view)的可视区域, 灰色区域是内容区域,并且内容区域的高度超过了容器的高度, 红色区域是一个数据项 [图片] 现在的目标是将数据项从初始态滚动到最终态即scroll-into-view的效果:border的上边界与可视区域上边界对齐 第一步:从初始态达到中间态 根据上面关于scrollTop的描述,这里如果scrollTop的值是targetDistance即数据项的底部到内容区域的底部的距离,就可以达到中间态,因此现在的目标是求targetDistance 初始状态的已知变量 初始状态下的的scrollTop值:currentScrollTop (由于容器发生翻转,所以scrollTop视觉上指向容器下方) 数据项的boundingClientRect.bottom为 itemBottom 容器的boundingClientRect.bottom为 contianerBottom 通过示意图很容易得出 [代码]targetDistance = currentScrollTop + (containerBottom - itemBottom) [代码] 第二步:从中间到达最终态 已知变量:容器高度:containerHeight、数据项高度:itemHeight 最终态是数据项的顶部距离容器顶部,从示意图中看到中间态到最终态的scrollTop是减少了的,减少的值其实就是cotainerHeight - itemHeight 经过第一步和第二步我们就可以得到scrollTop的计算公式 [代码]let itemScrollTop = currentScrollTop + containerBottom - itemBottom itemScrollTop -= (containerHeight - itemHeight) => itemScrollTop = currentScrollTop + containerBottom - itemBottom - (containerHeight - itemHeight) [代码] border-top/padding-top不为0的情况 [图片] 根据上面第一种情况的介绍的思路,很容易得到下面结果,不再赘述(X 就是容器padding-top + border-top的值) [代码]let itemScrollTop = currentScrollTop + containerBottom - itemBottom - X itemScrollTop -= (containerHeight - itemHeight - X) => itemScrollTop = currentScrollTop + containerBottom - itemBottom - X - (containerHeight - itemHeight - X) => itemScrollTop = currentScrollTop + containerBottom - itemBottom - (containerHeight - itemHeight) [代码] 【结论】两种情况最终的计算过程是一样的,因此在实现的过程中不需要进行区分 代码实现 代码片段见:https://developers.weixin.qq.com/s/y1X11dmr7AqC 视图层代码 [代码]{{item.content}} #scroll-view { position: absolute; top: 50px; bottom: 50px; width: 100%; background-color: rgba(0, 0, 0, 0.1); // 关键case transform: rotateX(180deg); } [代码] 逻辑层核心代码 [代码]scrollTo () { const itemId = '#item_id_50' const containerId = '#scroll-view' Promise.all([this._queryBoundingClient(itemId), this._getScrollInfo(containerId)]) .then((res = [[[{}]], {}]) => { const [[[ { bottom: itemBottom, height: itemHeight }]], { bottom: containerBottom, scrollTop, height: containerHeight }] = res let itemScrollTop = containerBottom - itemBottom + scrollTop itemScrollTop -= (containerHeight - itemHeight) this.setData({ scrollTop: itemScrollTop }) }) }, _queryBoundingClient (selector) { // 获取目标dom的相关位置/尺寸信息 return new Promise(resolve => { const query = this.createSelectorQuery(); query.selectAll(selector).boundingClientRect(); query.exec(resolve); }) }, _getScrollInfo (idSelector) { // 用来获取容器层相关位置/尺寸信息 return new Promise(resolve => { const query = this.createSelectorQuery() query.select(idSelector).boundingClientRect() query.select(idSelector).scrollOffset() query.exec((res = [{}, {}]) => { const [{ top, bottom, height }, { scrollHeight, scrollTop }] = res const scrollInfo = { scrollTop, scrollHeight, top, bottom, height } resolve(scrollInfo) }) }) } [代码]
2023-03-23 - 如何批量下载云开发存储文件到本地
有人说打开云开发,存储点击文件有下载地址,这对于少量资源是可以的,下面的方法是下载上万文件的方法: 通过访问官方:https://docs.cloudbase.net/cli-v1/install [图片] [图片] 第四步、云存储文件路径:/qrcode/20220311 ==》需要下载到本地:如 E:\qrcode 1、首先在本地: E:\qrcode\文件内创建一个名为:cloudbaserc.json 的json文件。 { "envId":"你的云开发环境的id" } 2、通过命令访问E盘: e: 3、访问需要下载到本地文件路径 cd qrcode 4、开始下载云端文件到本地,执行命令,对于云端文件路径有子目录可以通过 / 进行访问。 tcb storage download qrcode/20220311 . --dir 5、等等文件下载中,直至文件全部下载。
2022-03-11 - 考研刷题小程序云开发实战-页面设计与制作(题库首页、排名页、我的)
前言为啥你的UI界面感觉乱?对于小程序开发者来说,特别是对于初阶开发者或者初学者,排版的好坏是这个阶段核心要考虑的问题,也就是细节。但是不少同学总是在这个上面很不注重,总想着能用就行。做出来的界面异常难看,谈何用户体验! 产品好不好看,真的重要吗?你自己仔细思考思考一下。 唯有美观无法构建一款产品,但是打造一个毫无美感可言的产品往往会让情况变得更加糟糕。所以,我设计并制作了这款精美优质的考研题库小程序。 1、创建并配置页面1.1、在pages文件夹中,分别创建三个文件夹home、rank、my,分别对应三个页面题库首页、排行榜页、我的页; 1.2、每个文件夹分别包含这四个文件.js、.json、.wxml、.wxss [图片] 1.3在app.json中配置pages和tabBar,代码如下: { "pages": [ "pages/home/home", "pages/rank/rank", "pages/my/my" ], "tabBar": { "color": "#aaa", "selectedColor": "#ffa517", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/home/home", "iconPath": "/image/sy2.png", "selectedIconPath": "/image/sy2-a.png", "text": "题库" }, { "pagePath": "pages/rank/rank", "iconPath": "/image/zxly2.png", "selectedIconPath": "/image/zxly2-a.png", "text": "排名" }, { "pagePath": "pages/my/my", "iconPath": "/image/wd2.png", "selectedIconPath": "/image/wd2-a.png", "text": "我的" } ] }, "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "考研刷题小博士", "navigationBarTextStyle": "black" } } [图片] 2、题库首页2.1、场景是往往会有考研相关资讯或者答题优惠兑换活动,需要有丰富的轮播广告作为展示。使用了swiper组件实现轮播图效果,里面使用了image标签展示图片,并设置mode='aspectFill'达到宽度100%满屏效果,代码如下: <swiper class="screen-swiper" autoplay="true" interval="5000" duration="500"> <swiper-item> <image src="/image/b2-y.jpg" mode='aspectFill'></image> </swiper-item> </swiper> 2.2、专项考试或者专项练习,按四个科目分类,我这里使用了样式grid实现基本布局,col-2实现两个一行的排版效果,靓丽抢眼的颜色搭配,外加一些样式进行修饰,整体页面效果呈现为简洁美观,符合个人极简主义的风格。 <view class='grid col-2 padding-left-sm padding-right-sm'> <view class='padding-sm' bindtap="goToAnswer" data-category="马克思原理"> <view class='bg-yellow padding radius shadow-warp'> <view class="text-lg">马克思原理</view> <view class='margin-top-sm text-Abc'>共390题</view> </view> </view> <view class='padding-sm' bindtap="goToAnswer" data-category="毛中特"> <view class='bg-orange padding radius shadow-warp'> <view class="text-lg">毛中特</view> <view class='margin-top-sm text-Abc'>共210题</view> </view> </view> <view class='padding-sm' bindtap="goToAnswer" data-category="思修"> <view class='bg-olive padding radius shadow-warp'> <view class="text-lg">思修</view> <view class='margin-top-sm text-Abc'>共260题</view> </view> </view> <view class='padding-sm' bindtap="goToAnswer" data-category="近代史"> <view class='bg-orange padding radius shadow-warp'> <view class="text-lg">近代史</view> <view class='margin-top-sm text-Abc'>共230题</view> </view> </view> </view> 3、排名页和我的3.1、排名页排版布局演示(源码见仓库) [图片] 3.2、我的页排版布局演示(源码见仓库) [图片] 总结这期的考研刷题小程序云开发实战,主要演示了整个页面的开发流程与配置,并对关键的地方进行了讲解,细枝末节可以详细看看代码即可,其实并不难。难就难在创意、设计与实现等一系列。价值区别在于,能看与好看、能用与好用。
2022-02-18 - 新版开发者工具设置文件忽略无依赖白名单无效?
开发者工具版本: Stable 1.05.2201240 开发环境:uni-app 电脑环境:window 10 64位 问题描述: 新版中默认启用了 “无依赖文件过滤能力” [图片] 由于使用到了 “导出到插件”的功能,该JS文件并没有在页面中引用,会被忽略掉 [图片] 将“上传时过滤无依赖文件”去掉勾选 [图片] [图片] 开发工具中,没有问题,但在真机中,提示文件找不到,Android和iOS都是如此 即使设置忽略白名单也是无效 [图片] [图片] 更坑的是,开发工具回退到不能再回退,依然是如此 最后实在没办法,使用了半年前的旧版本 Stable 1.05.2107090 才正常
2022-02-15 - 实现一个高亮的代码编辑框
实现效果 [图片] 实现思路 说到富文本编辑,首先想到的自然是 [代码]editor[代码] 组件了,然而 [代码]editor[代码] 组件设置 [代码]html[代码] 的方法只有 [代码]setContents[代码],但是这个方法是用来初始化内容的,每次设置都会使得光标变到开头,如果用这个方法,每输一个字符光标都会跳到开头,无法使用。 因此设想了一种新的方案,将编辑和显示分开;底层放置一个 [代码]rich-text[代码] 用于显示高亮后的代码,上层放置一个编辑器,将颜色设置为透明,字体和大小与底层一致;当编辑器输入字符时,通过高亮处理后显示在底层的 [代码]rich-text[代码] 上;这样就实现了一个高亮的代码编辑框 编辑器选择 小程序中一共有 3 种输入框,[代码]input[代码]、[代码]textarea[代码] 和 [代码]editor[代码],其中 [代码]input[代码] 只能输入单行文本,并不适合此场景;[代码]textarea[代码] 可以编辑多行文本,本是个不错的方案,然而一方面 [代码]textarea[代码] 是原生组件,会受到一些限制,另一方面,似乎在真机上给 [代码]textarea[代码] 设置字体无法生效,用默认的字体又有点丑;因此最终还是选用了 [代码]editor[代码] 高亮方案 这里选择了轻量且强大的 prismjs 代码实现 [代码]rich-text[代码] 通过 [代码]absolute[代码] 布局固定在 [代码]editor[代码] 下方,[代码]editor[代码] 被设置成透明颜色(除光标外) [代码]<view class="editor-view"> <rich-text class="highlight" nodes="{{code}}" /> <editor id="editor" class="editor" placeholder="请输入 html" bindinput="input" /> </view> [代码] 每输入一个字符,在 [代码]js[代码] 中进行高亮处理后在 [代码]rich-text[代码] 中显示 [代码]const Prism = require("./prism.js"); Page({ input(e) { // markdown 则改为 Prism.highlight(e.detail.text, Prism.languages.markdown, "markdown") this.setData({ code: "<pre style=\"color:#ccc;font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\">" + Prism.highlight(e.detail.text, Prism.languages.html, "html") + "</pre>" }) } }) [代码] 应用场景 markdown 编辑器 [代码]markdown[代码] 编辑器作为一种简单的富文本编辑器,通过这样的方式实现就可以比较美观 富文本编辑完成后进行微调 在 [代码]editor[代码] 中编辑完成后,可以通过编辑 [代码]html[代码] 进行样式的微调 性能 在模拟器中是非常流畅的,在真机上稍有延缓,个人觉得是可以接受的(可以适当限制内容长度,过长的内容不进行高亮处理) 立即体验 代码片段
2020-02-21 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - 公众平台/小程序服务端API的access_token的内部设计
一、背景 对于使用过公众平台的API功能的开发者来说,access_token绝对不会陌生,它就像一个打开家门的钥匙,只要拿着它,就能使用公众平台绝大部分的API功能。因此,对于开发者而言,access_token的使用方式就变得尤其的重要。在日常API接口的运营中,经常遇到各种的疑问:为什么我的access_token突然非法了?为什么刚刚拿到的access_token,用了10min就过期了?对于这些疑问,我们提供出access_token的设计方案,便于开发者对access_token使用方式上的理解。 对于access_token的获取,可以参考公众平台的官方文档:auth.getAccessToken、获取Access token 二、access_token的内部设计 2.1 access_token的时效性 众所周知,access_token是通过appid和appsecret来生成的。内部设计的步骤如下: (1)开发者通过https请求方式: GET https://API.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,传入appid及apppsecret的参数 (2)公众平台后台会校验appid和哈希(appsecret)是否与存储匹配,若匹配,结合当前时间戳,生成新的access_token。 (3)生成新的access_token的同时,会对老的access_token的过期时间戳更新为当前时间戳。 (4)返回新的access_token给开发者。 这里以图示的方式说明一下,新旧token交替过程: [图片] 从上图需要注意的几点: (1)公众平台存储层只会存储新老两个access_token,意味着假设开发者重复调用3次接口,则会导致最早的access_token立刻失效。 (2)虽然请求新的access_token后,老的access_token过期时间会更新为当前时间,但也不会立刻失效,原理请参考 【2.2 access_token 的逐渐失效性】 (3)出于信息安全考虑,公众平台并不会明文存储appsecret,仅存储appid以及appsecret的哈希值。因此开发者要妥善保管appsecret。当appsecret疑似泄露时,需要及时登录mp.weixin.qq.com重置appsecret。 2.2 access_token 的逐渐失效性 从【access_token的时效性】了解到,当开发者请求获取新的access_token时,老的access_token过期时间会被更新为当前时间,但此时不会立刻失效,因为公众平台会提供【5分钟的新老access_token交替缓冲时间】,因此也称为access_token 的逐渐失效性。 实现的原理是: 1. 由于老的access_token过期时间戳已被刷新,所以在API接口请求期间,带上的access_token解开后,过期时间戳会加上5分钟,然后和当前设备时间进行比对,若超过当前设备时间,判断为失效。 2. 公众平台的设备会保持时钟同步,但设备之间仍然可能会存在1-2分钟的时间差异,所以【5分钟】并非绝对的时间值。当开发者获取到新的access_token后应该尽快切换到新的access_token。 [图片] 从上图需要注意的几点: (1)由于存在设备时间同步的差异,可能会导致开发者遇到拿着老的access_token请求API接口,部分请求成功,部分请求失败的情况,建议开发者获取到新的access_token后尽快使用。 (2)通过理解两个图示,对开发者来说,access_token是相当关键且不能乱调的接口,建议开发者统一管理access_token,以免造成多次请求导致access_token失效。
2021-05-11 - 云函数设置TZ 为 Asia/Shanghai无效的解决方案(云函数时区问题)
[图片] 官方文档表明Node10以及Node15+版本可用环境变量:TZ 为 Asia/Shanghai,方式获取北京时间。其他Node版本需要借助第三方包,现给出第三方包方案。 云函数: //1.安装 moment-timezone npm install moment-timezone //2.编写云函数 const cloud = require('wx-server-sdk') const moment = require("moment-timezone") exports.main = async (event, context) => { let formatTime=moment().tz("Asia/Shanghai").format()//北京时间 2022-01-24T14:14:50+08:00 let hour=moment().tz("Asia/Shanghai").get('hour') //获取北京时间小时数 let minute=moment().tz("Asia/Shanghai").get('minute') //获取北京时间分数 let second=moment().tz("Asia/Shanghai").get('second') //获取北京时间秒数 return formatTime //result:{} 小程序中以result为载体 } //构建完毕重新部署上传云函数 js: //调用云函数 wx.cloud.callFunction({ name: "云函数名称", }).then(res => { //res 是云函数调用成功后return出来的值 console.log('云函数调用', res) }).catch(res => { console.log(res) })
2022-01-24 - Behavior的methods在Page和Component里的表现有差异?
我有一个behavior的themeChanged()方法,需要传给wx.themeChanged事件,用作回调。 在Page页面里使用这个behavior,这样做是没问题的。调试时可以看到this.themeChanged()方法是bound了this的: [图片] 但,在使用Componet构造的页面里发现,this.themeChanged()没有bind this,导致事件回调时因this问题报错。 [图片] 在代码中自己判断没有bind时自己bind一下也能解决问题。 所以,这不算是太大问题,只是想了解一下为何会有这个差异,以便更深入理解小程序框架,以后遇到小坑能灵活闪避-;)
2021-05-24 - 前端架构之路:小程序 Log 日志
前言 后续我会在 [代码]github[代码] 开放源码,并打包至 [代码]npm[代码] ,开发者后续可自行 [代码]install[代码] 调用。 后续 源码地址 及 npm安装方法 将会在该页面更新。 开放时间基于大家需求而定。 通常情况下,日志系统是开发中重要的一环。 但出于种种原因,在前端开发中做日志打印和上报系统却不常见。 但有些特定情况下,日志系统往往有奇效。 比如一个聊天系统中遇到了以下问题: 语音通话中,用户听不到声音 即时通讯中,部分场景用户反馈,消息发送不出去 即时通讯中, A 回复 B 消息时,偶尔对话框不显示 即时通讯中, A 给 B 连续发送两条消息后, B 接收不到第二条的提示 即时通讯中,发送语音消息发送时,用户以为语音已经发送,但实际上录音还在继续。这时用户以为是网络卡了,最后发现自己和其他人说话的声音被录制进去 但是以上几种错误,在后台接口中并没有体现。再加上部分用户手机型号的问题,导致问题很难被定位。 如果我们这里有 [代码]log[代码] ,我们就能很快定位到出问题的代码。 如果不是代码问题,也更有底气回复用户不是我们系统的问题。 如何使用小程序 Log 日志系统 小程序侧提供了两种小程序 Log 日志接口: LogManager ( 普通日志 ) RealtimeLogManager ( 实时日志 ) 官方并没有介绍两者的具体区别,只是强调了 Realtime 的实时性质。 在我看来他们的最大区别就是: [代码]LogManager[代码] 可以让用户有种心安的感觉,因为 [代码]LogManager[代码] 是用户手动反馈的问题。 [代码]RealtimeLogManager[代码] 则对开发者更友好,可以在用户不知情的情况下收集到问题信息,并在用户无感的情况下对问题进行修复。 LogManager 小程序提供的 [代码]Log[代码] 日志接口,通过 [代码]wx.getLogManager()[代码] 获取实例。 注意: 最多保存5M的日志内容,超过5M后,旧的日志内容会被删除。 对于 小程序 ,用户可以通过使用 [代码]button[代码] 组件的 [代码]open-type="feedback"[代码] 来上传打印的日志。 对于 小游戏 ,用户可以通过使用 [代码]wx.createFeedbackButton[代码] 来创建上传打印的日志的按钮。 开发者可以通过小程序管理后台左侧菜单 反馈管理 页面查看相关打印日志。 创建 LogManager 实例 你可以通过 [代码]wx.getLogManager()[代码] 获取日志实例。 括号中可以传参 [代码]{ level: 0 | 1 }[代码] 来决定是否写入 [代码]Page[代码] 的生命周期函数, [代码]wx[代码] 命名空间下的函数日志。 0: 写入 1: 不写入 [代码]const logger = wx.getLogManager({ level: 0 }) [代码] 使用 LogManager 实例 [代码]const logger = wx.getLogManager({ level: 0 }) logger.log({str: 'hello world'}, 'basic log', 100, [1, 2, 3]) logger.info({str: 'hello world'}, 'info log', 100, [1, 2, 3]) logger.debug({str: 'hello world'}, 'debug log', 100, [1, 2, 3]) logger.warn({str: 'hello world'}, 'warn log', 100, [1, 2, 3]) [代码] 用户反馈上传 LogManager 记录的日志 当日志记录后, 用户可以在小程序的 [代码]profile[代码] 页面,单击 反馈与投诉 ,在点击 功能异常 进行日志上传。 开发者处理用户反馈及和用户沟通 开发者可以在小程序后台 管理 -> 用户反馈 -> 功能异常 查看用户反馈的信息。 开发者可以在 功能 -> 客服 下绑定客服微信,绑定后可以在 48小时 内通过微信和反馈用户沟通。 注:沟通需要用户反馈时勾选:允许开发者在 48 小时内通过客服消息联系我。 RealtimeLogManager 小程序提供的 [代码]实时Log[代码] 日志接口,通过 [代码]wx.getRealtimeLogManager()[代码] 获取实例。 注意: [代码]wx.getRealtimeLogManager()[代码] 基础库 2.7.1 开始支持 官方给出实时日志每条的容量上限是 [代码]5kb[代码] 官方对每条日志的定义:在一个页面 onShow -> onHide 之间,会聚合成一条日志上报 开发者可从小程序管理后台: 开发 -> 运维中心 -> 实时日志 进入小程序端日志查询页面 为了定位问题方便,日志是按页面划分的,某一个页面,在onShow到onHide(切换到其它页面、右上角圆点退到后台)之间打的日志,会聚合成一条日志上报,并且在小程序管理后台上可以根据页面路径搜索出该条日志 创建 RealtimeLogManager 实例 你可以通过 [代码]wx.getRealtimeLogManager()[代码] 获取实时日志实例。 [代码]const logger = wx.getRealtimeLogManager() [代码] 使用 RealtimeLogManager 实例 [代码]const logger = wx.getRealtimeLogManager() logger.debug({str: 'hello world'}, 'debug log', 100, [1, 2, 3]) logger.info({str: 'hello world'}, 'info log', 100, [1, 2, 3]) logger.error({str: 'hello world'}, 'error log', 100, [1, 2, 3]) logger.warn({str: 'hello world'}, 'warn log', 100, [1, 2, 3]) [代码] 查看实时日志 与普通日志不同的是,实时日志不再需要用户反馈,可以直接通过以下方式查看实例。 登录小程序后台 通过路径 开发 -> 开发管理 -> 运维中心 -> 实时日志 查看实时日志 如何搭建小程序 Log 日志系统 上面我们知道了小程序的 [代码]Log[代码] 日志怎么使用,我们当然可以不进行封装直接使用。 但是我们直接使用起来会感觉到十分的别扭,因为这不符合我们程序员单点调用的习惯。 那么接下来让我们对这套 Log 系统进行初步的封装以及全局的方法的日志注入。 后续我会在 github 开放源码,并打包至 npm ,需要的开发者可自行 install 调用。 封装小程序 Log 方法 封装 Log 方法前,我们需要整理该方法需要考虑什么内容: 打印格式:统一打印格式有助于我们更快的定位问题 版本号:方便我们清晰的知道当前用户使用的小程序版本,避免出现旧版本问题在新代码中找不到问题 兼容性:我们需要考虑用户小程序版本不足以支持 [代码]getLogManager[代码] 、 [代码]getRealtimeLogManager[代码] 的情况 类型:我们需要兼容 [代码]debug[代码] 、 [代码]log[代码] 、 [代码]error[代码] 类型的 [代码]log日志[代码] 版本问题 我们需要一个常量用以定义版本号,以便于我们定位出问题的代码版本。 如果遇到版本问题,我们可以更好的引导用户 [代码]const VERSION = "1.0.0" const logger = wx.getLogManager({ level: 0 }) logger.log(VERSION, info) [代码] 打印格式 我们可以通过 [代码][version] file | content[代码] 的统一格式来更快的定位内容。 [代码]const VERSION = "1.0.0" const logger = wx.getLogManager({ level: 0 }) logger.log(`[${VERSION}] ${file} | `, ...args) [代码] 兼容性 我们需要考虑用户小程序版本不足以支持 [代码]getLogManager[代码] 、 [代码]getRealtimeLogManager[代码] 的情况 [代码]const VERSION = "0.0.18"; const canIUseLogManage = wx.canIUse("getLogManager"); const logger = canIUseLogManage ? wx.getLogManager({ level: 0 }) : null; const realtimeLogger = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null; export function RUN(file, ...args) { console.log(`[${VERSION}]`, file, " | ", ...args); if (canIUseLogManage) { logger.log(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } [代码] 类型 我们需要兼容 [代码]debug[代码] 、 [代码]log[代码] 、 [代码]error[代码] 类型的 [代码]log日志[代码] [代码]export function RUN(file, ...args) { ... } export function DEBUG(file, ...args) { ... } export function ERROR(file, ...args) { ... } export function getLogger(fileName) { return { DEBUG: function (...args) { DEBUG(fileName, ...args) }, RUN: function (...args) { RUN(fileName, ...args) }, ERROR: function (...args) { ERROR(fileName, ...args) } } } [代码] 完整代码 以上都做到了,就完成了一套 [代码]Log[代码] 系统的基本封装。 [代码]const VERSION = "0.0.18"; const canIUseLogManage = wx.canIUse("getLogManager"); const logger = canIUseLogManage ? wx.getLogManager({ level: 0 }) : null; const realtimeLogger = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null; export function DEBUG(file, ...args) { console.debug(`[${VERSION}] ${file} | `, ...args); if (canIUseLogManage) { logger.debug(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } export function RUN(file, ...args) { console.log(`[${VERSION}]`, file, " | ", ...args); if (canIUseLogManage) { logger.log(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } export function ERROR(file, ...args) { console.error(`[${VERSION}]`, file, " | ", ...args); if (canIUseLogManage) { logger.error(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.error(`[${VERSION}]`, file, " | ", ...args); } export function getLogger(fileName) { return { DEBUG: function (...args) { DEBUG(fileName, ...args) }, RUN: function (...args) { RUN(fileName, ...args) }, ERROR: function (...args) { ERROR(fileName, ...args) } } } [代码] 全局注入 Log 通过该章节的名称,我们就可以知道全局注入。 全局注入的意思就是,不通过手动调用的形式,在方法写完后自动注入 [代码]log[代码] ,你只需要在更细节的地方考虑打印 [代码]log[代码] 即可。 为什么要全局注入 虽然我们实现了全局 [代码]log[代码] 的封装,但是很多情况下,一些新同学没有好的打 [代码]log[代码] 的习惯,尤其是前端同学(我也一样)。 所以我们需要做一个全局注入,以方便我们的代码书写,也避免掉手动打 [代码]log[代码] 会出现遗漏的问题。 如何进行全局注入 小程序提供了 [代码]behaviors[代码] 参数,用以让多个页面拥有相同的数据字段和方法。 需要注意的是, [代码]page[代码] 级别的 [代码]behaviors[代码] 在 2.9.2 之后开始支持 我们可以通过封装一个通用的 [代码]behaviors[代码] ,然后在需要 [代码]log[代码] 的页面进行引入即可。 [代码]import * as Log from "./log-test"; export default Behavior({ definitionFilter(defFields) { console.log(defFields); Object.keys(defFields.methods || {}).forEach(methodName => { const originMethod = defFields.methods[methodName]; defFields.methods[methodName] = function (ev, ...args) { if (ev && ev.target && ev.currentTarget && ev.currentTarget.dataset) { Log.RUN(defFields.data.PAGE_NAME, `${methodName} invoke, event dataset = `, ev.currentTarget.dataset, "params = ", ...args); } else { Log.RUN(defFields.data.PAGE_NAME, `${methodName} invoke, params = `, ev, ...args); } originMethod.call(this, ev, ...args) } }) } }) [代码] 总结 连着开发带整理,林林总总的也有了 [代码]2000+[代码] 字,耗费了三天的时间,整体感觉还是比较值得的,希望可以带给大家一些帮助。 也希望大家更重视前端的 [代码]log[代码] 一点。这基于我自身的感觉,尤其是移动端用户。 在很多时候由于 手机型号 、 弱网环境 等导致的问题。 在没有 [代码]log[代码] 时,找不到问题的着力点,导致问题难以被及时解决。
2022-01-17 - 论公众号搜一搜机制
一.搜一搜在哪里? 首先,我们打开微信在聊天页面上面即可进入搜一搜。 '搜一搜'是帮助用户快速查找内容或服务的工具。众所周知搜索项目包含朋友圈、文章、公众号、外部网页、小说和表情等;当然本身的公众号 文章 小程序会展示到前面,因为是自家产品,这个入口不只是应用内搜索,最后可能会成为整个移动互联网的入口,有网友说,这就像在微信植入了百度,现在的微信用户多少亿,这块的流量肯定要抓住!对做这块的玩家来说,如何抓住流量就是学问了 二.排名的影响因素 1.关键词的匹配程度 一般来说匹配度越贴和排名就越高,所以你的名称最好要包含关键词甚至就是关键词(亲身体会,有个好名称涨粉的速度胜过精心运营) 2.微信认证 这是平台本身就有的功能,氪金总比空手扒拉好 3.粉丝活跃 阅读量和阅读率的高低 不仅仅是粉丝数,粉丝活跃度、阅读量和阅读率的高低也会影响到权重高低,所以不但要增加粉丝数,还要提高公众号的质量,多创作质量高的文章,多和粉丝互动,这些数据也往往反应了公众号的质量,微信明显要扶持高质量公众号。 4.粉丝互动 微信本身就是社交软件,肯定要鼓励社交属性,粉丝不活跃证明你账号不够优质,看能否用手段来增加和粉丝之间的互动 5.注册时间 这个具体就不用细说了,其他平台甚至域名年限都有权限,多少有些影响 6.粉丝增长量 粉丝增长越快权重越高,可能很多人觉得粉丝到了就行,其实这个过程也是个重要的影响因素,不过这项权重在整个排名里影响可能不是很大 7.功能介绍 要含有关键词,能和名称 文章内容匹配上。用户搜索时也会搜索到,虽然影响效果比较小,但如果都是很冷门的词,多少还是会影响些 8.推送频率 公众号本身就是发文推送,如果长期不推送或者推送时间不定会被平台判定僵尸号或者运营不走心,所以一定要制定好内容和时间频率,让平台判断机制认为你是精心运营玩家(优质内容和时间频率)
2022-01-10 - 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - 如何自己在小程序内做埋点数据统计
如何在自己小程序内做数据埋点 小程序后台已经有了较为完善的数据统计和基础的分析,但是功能还是比较基础的,通常我们对数据分析有较高的要求时,就需要自己做数据收集了。那小程序内如何做自动的数据埋点和手动埋点呢。 自动埋点 启动时间 (onLaunch) 系统信息 (systemInfo) 停留时长 (Page onHide - Page onShow) 来源 (query 参数) PV (Page onShow) UV (结合用户筛选PV) 预设点击数据收集 (onTap 等) 手动埋点 自定义点击收集数据 改造小程序生命周期 自动埋点是需要集成到底层内,不能对业务进行侵入,所以,我们需要改造小程序生命周期,在不同的生命周期内进行预设收集数据的功能。 对 [代码]App[代码] 进行重写 [代码]const oldApp = App; // 我们需要重写的方法 const appFn = ['onLaunch', 'onShow', 'onHide']; App = function (options) { let oldFuncs = {}; appFn.forEach((item) => { oldFuncs[item] = options[item] }) appFn.forEach((item) => { options[item] = function (options) { // todo 做各类数据收集 oldFuncs[item].apply(this, arguments) } }) oldApp.apply(this, arguments); }; [代码] 对 [代码]Page[代码] 重写 [代码]const oldPage = Page; const pageFn = ['onLoad', 'onShow', 'onHide', 'onUnload', 'onShareAppMessage', 'onAddToFavorites'] Page = function (options) { let oldFuncs = {}; pageFn.forEach((item) => { if (options[item]) { oldFuncs[item] = options[item] } }) pageFn.forEach((item) => { if (options[item]) { options[item] = function () { console.log('Page', item, ); // 收集各类数据 oldFuncs[item].apply(this, arguments) } } }) // 以下代码则是对除生命周期类的方法进行重写,做预设点击事件收集数据 const methods = getMethods(options); if (!!methods) { for (var i = 0, len = methods.length; i < len; i++) { clickProxy(options, methods[i]); } } oldPage.apply(this, arguments); } [代码] 对 [代码]Component[代码] 重写 [代码]const oldComponent = Component; Component = function (options) { // 对组建内 methods 进行重写预设点击事件埋点收集 Object.keys(options.methods).forEach((method) => { clickProxy(options.methods, method) }) oldComponent.apply(this, arguments); } [代码] 以上对 [代码]App[代码] [代码]Page[代码] [代码]Component[代码] 进行重写之后,在必要的地方,加入自己的上报代码. 以下完整代码 [代码] const mpHook = { data: 1, onLoad: 1, onShow: 1, onReady: 1, onPullDownRefresh: 1, onReachBottom: 1, onShareAppMessage: 1, onPageScroll: 1, onResize: 1, onTabItemTap: 1, onHide: 1, onUnload: 1, }; const oldApp = App; const oldPage = Page; const oldComponent = Component; const appFn = ['onLaunch', 'onShow', 'onHide'] App = function (options) { let oldFuncs = {}; appFn.forEach((item) => { oldFuncs[item] = options[item] }) appFn.forEach((item) => { options[item] = function (options) { console.log('App', item); // 收集各类数据 oldFuncs[item].apply(this, arguments) } }) oldApp.apply(this, arguments); }; const pageFn = ['onLoad', 'onShow', 'onHide', 'onUnload', 'onShareAppMessage', 'onAddToFavorites'] Page = function (options) { let oldFuncs = {}; pageFn.forEach((item) => { if (options[item]) { oldFuncs[item] = options[item] } }) pageFn.forEach((item) => { if (options[item]) { options[item] = function () { console.log('Page', item, ); // 收集各类数据 oldFuncs[item].apply(this, arguments) } } }) const methods = getMethods(options); if (!!methods) { for (var i = 0, len = methods.length; i < len; i++) { clickProxy(options, methods[i]); } } oldPage.apply(this, arguments); } Component = function (options) { Object.keys(options.methods).forEach((method) => { clickProxy(options.methods, method) }) oldComponent.apply(this, arguments); } function clickProxy(options, method) { const oldFunc = options[method]; options[method] = function () { const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const pageQuery = currentPage.options || {}; const pagePath = currentPage.route; const res = oldFunc.apply(this, arguments); let prop = {}, type = ""; if (isObject(arguments[0])) { const current_target = arguments[0].currentTarget || {}; const dataset = current_target.dataset || {}; type = arguments[0]["type"]; prop["$event_type"] = type; prop["$event_timestamp"] = Date.now(); prop["$element_id"] = current_target.id; prop["$element_type"] = dataset["type"]; prop["$element_content"] = dataset["content"]; prop["$element_name"] = dataset["name"]; prop["$page_path"] = pagePath; prop["$page_quey"] = pageQuery; if (isObject(arguments[0].event_prop)) { prop = Object.assign(prop, arguments[0].event_prop); } } console.log('type', type) if (type) { // 可以对不同事件类型进行筛选是否需要收集 post(prop) } console.log(res); return res; }; } const getMethods = function (options) { let methods = []; for (let m in options) { if (typeof options[m] === "function" && !mpHook[m]) { methods.push(m); } } return methods; }; const isObject = function (obj) { if (obj === undefined || obj === null) { return false; } else { return toString.call(obj) == "[object Object]"; } }; const post = function (data) { console.log('data', data) // 提交数据时,可以在组合下 systeminfo 用户信息等相关信息 wx.request({ url: 'https://www.example.php', method: 'post', data }) } // 手动埋点的部分,自己在需要收集的地方调用相关方法收集数据 [代码]
2021-06-24 - 如何保证小程序的每个页面,在执行页面周期时,都是已登录(解决方案)
1. 实现效果: 不管用户第一个访问的页面是:首页、详情页、购物车、个人中心...任意页面,保障该页面周期onLoad、onShow、onReady运行时,都是处于已登录(登录态)。 2. 遇到的问题: 由于js是异步执行,直接把登录写在onLaunch,在执行页面onLoad时,可能会因为登录接口未返回,页面onLoad拿不到登录信息,导致异常。 要么每个页面都需要加登录判断,维护难度很大。 3. 解决思路 挟持Page并使用发布订阅模式,可保障任意页面执行onLoad、onShow时,自动执行:先判断当前是否已登录,未登录先订阅,已登录则执行onLoad。 4. 代码实现 // app.js // 引入login-sdk(几十行代码),并在登录后触发登录事件,即可实现所有页面登录。 import { publisher } from "./utils/login-sdk"; App({ onLaunch() { this.toLogin(); }, async toLogin() { // 模拟openid静默登录 let { code } = await wx.login(); setTimeout(() => { publisher.emit("login"); }, 50); }, }); 5. 代码片段 https://developers.weixin.qq.com/s/bBkO2Umv7Avd 6. 挟持Page稳定性? 目前我们应用在电商小程序里,已有2年,服务累计3000万用户,亲测没遇到什么问题。
2021-12-29 - 混小程序圈子,这十大技能你掌握几个?
在微信小程序圈子里,有着众多规则或应用技能!下面汇总了比较实用的十条供入门不深者查阅,以备不时之需。或当业内茶余饭后之谈资也不妨不可! 一、多个小程序来回切换。 长按小程序右上角关闭圈圈,即可弹出近期所打开的小程序供你切换。 [图片] 二、小程序圈子必收藏的10个小程序: 1、小程序数据助手(也有PC端) 说明:小程序数据助手,支持小程序相关的开发和运营人员在手机端更方便、及时地查看运营数据。 2、小游戏数据助手 说明:小游戏数据助手,支持小游戏相关的开发和运营人员在手机端更方便、及时地查看运营数据。 3、小程序助手 说明:帮助开发者管理小程序的移动管理平台;成员管理、审核版本、性能分析;无小程序官方后台可以通过小程序助手找回(先准备一个新邮箱)。 4、微信支付商家助手 说明:商家微信支付后台手机版。 5、微信收款商业版 说明:商家微信支付后台手机版(加强版,要在微信支付PC后台申请)。 6、微信指数 说明:微信官方提供的基于微信大数据分析的移动端指数。(温馨提醒:不仅仅是搜索数据!) 7、酒店指数榜单 说明:酒店客栈民宿公寓指数排行榜单,用于反映一段时期内的行业品牌影响力(搜索指数、媒体指数、舆情指数、运营指数),为酒店相关从业者提供参考。 8、微信服务平台 说明:提供小程序/公众号相关的开发服务、内容、API服务等,实现服务商与商户、开发者在平台的合作与沟通。 9、微信公开课+ 说明:“微信公开课+”是微信官方与行业伙伴沟通的载体,为线下微信公开课的免费授课提供线上的相关服务,包括查阅活动、参与互动以及回放等。 10、小程序服务商助手 说明:小程序服务商查看提审状态、综合数据查看、成员管理等。 为减少篇幅,二维码和截图就不一一上了;直接搜索名称即可。除了这10个小程序,你还有其它常用小程序可以在评论区回复一下。 三、不想让别人知道你的邮箱,怎么发账号密码给别人登陆公众号或小程序? 其实除了邮箱,也可以用 原始ID+密码 登录。 原始ID获取路径:公众号后台>设置与开发>公众号设置>注册信息>原始ID [图片] 四、非技术如何获取小程序页面地址? 请查看来一间往期文章: https://developers.weixin.qq.com/community/develop/article/doc/0008627017cf104da879c3dd25b813 做公众号菜单跳转小程序具体页面会经常用到这个功能。 五、非技术如何获取某小程序二维码? 请查看来一间往期文章: https://developers.weixin.qq.com/community/develop/article/doc/000a8c12ca46707a9f7965ae85b013 六、如何通过发送短信或邮件让准客户直接点开小程序? 1、登录小程序后台。 2、进入设置页面: [图片] [图片] 可以查看官方文档了解更多: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-scheme.html 七、如何知道公司主体名下有多少个小程序?分别是哪些? 请查看来一间往期文章: https://developers.weixin.qq.com/community/develop/article/doc/000eea28a08f00e6158aa566951413 八、公众号自动回复,如何回复小程序? 请查看来一间往期文章: https://developers.weixin.qq.com/community/develop/article/doc/00022268e2c8902d56aae394156013 九、一个手机号注册两个微信。 这个功能没记错的话,也是今年才有。但不要高兴太早,如果你已经有小号了。这个注册入口是不显示的! 步骤如下: 我 > 设置 > 切换账号 > 添加账号 > 注册一个新的账号 > 通过当前微信号辅助注册。 [图片] 十、小程序管理员离职联系不上,企业如何找回账号? 请查看来一间往期文章: https://developers.weixin.qq.com/community/develop/article/doc/0006a436574b4871f87a2b07156413 汇总了以上小程序圈子十大技能,你会多少个?评论区回复一下。也大大欢迎补充指正!
2021-12-28 - [bug反馈]wxml 中 style 指向的文件路径 在不同设备中解析不同
// 模拟器中有效, 真机无效 style="background-image: url('/images/testUseCard.png');" // 真机中有效, 模拟器无效 style="background-image: url('images/testUseCard.png');"
2021-03-12 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15 - 基于云开发的答题活动小程序v2.0-完整项目分享(附源码)
简介答题活动小程序v2.0,是一个微信小程序答题软件,它基于微信原生小程序+云开发实现。 它使用了最新的前端技术栈,具有原生APP体验服务的小程序框架,小程序视图层描述语言 WXML 和 WXSS,以及基于 JavaScript 的逻辑层框架,响应的数据绑定,提供了丰富的基础组件和API。 提炼了典型的业务模型,它可以帮助你快速搭建各种形式的答题软件产品。相信不管你的需求是什么,本项目都能帮助到你。 [图片] [图片] 目录结构小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件(app.js、app.json、app.wxss)组成,必须放在项目的根目录。目录结构如下: [图片] 一个小程序页面由这四个文件组成,你可以留意到这个项目里边有不同类型的文件: .json 后缀的 JSON 配置文件;.wxml 后缀的 WXML 模板文件;.wxss 后缀的 WXSS 样式文件;.js 后缀的 JS 脚本逻辑文件; 功能主要包含六大功能模块页面,首页、答题页、结果页、活动规则页、答题记录页、排行榜页。适用于交通安全答题、 消防安全知识宣传、 安全生产知识学习、百年历史知识答题活动、学法守法有奖答题等。 - 首页 - 微信授权登录 - 获取微信头像和昵称等 - 活动规则页 - 答题页 - 实现用云开发实现查询题库功能 - 题库随机抽题 - 实现动态题目数据绑定 - 答题交互逻辑 - 切换下一题 - 提交答卷保存到云数据库集合 - 系统自动判分 - 结果页 - 答题结果页从云数据库查询答题成绩 - 实现转发分享答题成绩功能 - 答题记录页 - 查询历史成绩 - 排行榜页 - 成绩由高到低进行排名 - 实现页面间跳转功能 - 路由 - 界面交互 - 消息提示框 - loading 提示框 源码地址uem/答题活动小程序v2 https://gitee.com/uemeng/answer-activity-applet-v2.0 作品体验[图片]
2021-12-20 - 如何查看小商店/小程序页面路径?
01如何获取小商店/小程序的url? 小商店获取URL 1.首先需要要注册一个公众号。 2.在公众号后台打开一篇文章,选择上方插入小程序。 [图片] 3.选择需要关联的小程序,可以根据名字或者appid搜索,之后会被记住了显示在常用小程序列表中。 [图片] 4.在小程序页面选择需要关联的微信小商店 点击获取更多页面路径,然后在右侧填入微信号,点击开启。 此时该微信小商店对该用户已开启获取页面路径权限,其他用户不可查看页面路径,不必担心。 [图片] 5.在手机上打开该小商店,点击你需要获取url路径的页面,点击右上角三个点,底部会弹出菜单,点击【复制页面链接】即可。 [图片] 6.拿到链接地址后可以嵌入到公众号、菜单栏、自己的小程序等。 小程序获取URL 1.登陆小程序后台,点击右上角工具,点击生成小程序码,输入小程序appid搜索,点击下一步,之后会被记住了显示在常用小程序列表中。 [图片] 2.点击获取更多页面路径,然后在右侧填入微信号,点击开启。 此时该微信小程序对该用户已开启获取页面路径权限,其他用户不可查看页面路径,不必担心。 [图片] 3.在手机上打开该小商店,点击你需要获取url路径的页面,点击右上角三个点,底部会弹出菜单,点击【复制页面链接】即可。 [图片] 02常用URL,可以直接复制使用 1、微信小商店首页: pages/index/index 2、优惠券页面: plugin-private://wx34345ae5855f892d/pages/couponDetail/couponDetail?couponId=18041086 //18041086为优惠券id 3、小商店商品: __plugin__/wx34345ae5855f892d/pages/productDetail/productDetail?productId=678236 //678236为商品ID 4、小商店带货商品: __plugin__/wx34345ae5855f892d/pages/cpsProductDetail/cpsProductDetail?productId=6201078 //6201078为带货商品ID 5、小商店订单页: __plugin__/wx34345ae5855f892d/pages/orderList/orderList?tabId=all //全部all,待付款pendingPay,待发货/待收货状态的订单pendingRecevied,所有售后单afterSale 6、小商店购物车 __plugin__/wx34345ae5855f892d/pages/shoppingCart/shoppingCart 7、【组件版】小商店商品: plugin-private://wx34345ae5855f892d/pages/productDetail/productDetail?productId=2575804 /*2575804为商品ID*/ 8、【组件版】优惠券页面: plugin-private://wx34345ae5855f892d/pages/couponDetail/couponDetail?couponId=17816280 /*17816280为优惠券id*/ 9、【组件版】小商店订单页: plugin-private://wx34345ae5855f892d/pages/orderList/orderList?tabId=all //全部all,待付款pendingPay,待发货/待收货状态的订单pendingRecevied,所有售后单afterSale 10、【组件版】小商店购物车: plugin-private://wx34345ae5855f892d/pages/shoppingCart/shoppingCart 11、如何查看商品的SpuId(商品编号) 微信搜索并打开【小商店助手】小程序,选择对应的小商店,点击带货收入->我的的货,进入商品列表每个商品左上角编号为该商品ID。 [图片] [图片] [图片] [图片] 03常见问题Q&A 1、为什么提供小程序更多页面路径获取的方式? a、默认显示的小程序首页路径不一定满足用户需求。 b、用户获取小程序其他页面路径的成本较高。 2、给指定的微信号“开启入口”失败怎么办? a、指定的微信号尚未关注该公众号,请先关注公众号。 b、如果已经关注公众号,请查看微信的隐私设置(在手机微信的"我-设置-隐私-添加我的方式"中),并开启"可通过以下方式找到我"的"微信号",否则可能无法复制小程序页面路径。 3、为什么微信号设置成功,却在小程序中找不到“复制页面路径”的菜单? a、“复制页面路径”仅出现在指定微信号(开启入口的微信号)中。 b、只有在第一步选择的小程序中才会出现“复制页面路径”的菜单。 c、复制功能仅保留10分钟,10分钟后功能消失。 d、微信版本较低,请确保手机微信版本为6.7.2及以上。
2021-01-04 - 简析诗词大会对战答题小程序
简析诗词大会对战答题 ~ 诗词大会和一站到底是平时在央视和江苏卫视的两个节目,这两个界面均存在对战PK的模式,今天分析的小程序就是诗词大会,当然本诗词大会小程序并不是央视出品,但是不妨碍我们去学习 总的来说,诗词大会的界面真的算上乘,可以给我在开发对战答题带来很多参考 1 [图片] 1 [图片] 1 [图片] 1 这是进入对战答题之前的两个界面,相比较我目前 开发的逻辑,增加了一个匹配成功后的等待页,我觉得这一步可以省略,匹配完成,直接进入答题即可 1 [图片] 1 [图片] 1 [图片] 1 下面列几个小细节 1 [图片] 答题过程,如果有人退出房间,报该话术 1 [图片] 好友已离开房间是在对战结果页,点击再来一局的提示话术 1
2021-02-22 - 简单实现签到日历效果
wxml: <view class="box"> <view class="section"> <picker mode="date" value="{{date}}" fields="month" start="2010-01-01" end="{{cy+'-'+cm}}" bindchange="bindDateChange"> <view class="picker">{{cur_year || "--"}} 年 {{cur_month || "--"}} 月</view> </picker> </view> <view> <!-- 显示星期 --> <view class="week color9b"> <view wx:for="{{weeks_ch}}" wx:key="unique">{{item}}</view> </view> <view class='days'> <!-- 行 --> <view class="rows" wx:for="{{days.length/7}}" wx:for-index="i" wx:key="unique"> <!-- 列 --> <view class="columns" wx:for="{{7}}" wx:for-index="k" wx:key="unique"> <!-- 每个月份的空的单元格 --> <view class='cell' wx:if="{{days[7*i+k].date == null}}"> <text decode="{{true}}"> </text> </view> <!-- 每个月份的有数字的单元格 --> <view class='cell' wx:else> <!-- 当前日期已签到 --> <view wx:if="{{days[7*i+k].isSign == true}}" class='qianbg'> <text class="colorff">{{days[7*i+k].date}}</text> <text class="sourse">+{{days[7*i+k].Score}}</text> </view> <!-- 当前日期未签到 --> <view wx:else> <text>{{days[7*i+k].date}}</text> </view> </view> </view> </view> </view> </view> </view> 简单提下思路,首先默认确定当前年月,cy cm, 初始化:获取days遍历日历的格子,通过获取当前月第一天是星期几来判断前面有几个空格,放入days,再当月天数放入days,然后进行渲染,再通接口去拿签到信息,签到成功的突出显示。这里签到初始化时我默认给了标识isSign,将已签到列表和当前年月日进行比较,符合条件则更新签到状态。切换选择日期,这里我用的是选择器,当然可以写成点击左侧按钮上一月,右侧按钮下一月那种,重新选择日期后,initdata(e) 传入年月,就是当前选择年月的数据。将星期日作为第一日的我也备注上去了。样式根据自己的喜好改就行了,最后看看我写的两个项目效果: [图片][图片] 写了个demo:https://developers.weixin.qq.com/s/SSlwjGmb7Wm9
08-14 - 云开发新增素材怎样调用customerServiceMessage.uploadTempMedia?
想做个客服自动回复图片,但是看了文档一脸懵逼。图片路径在哪?没有路径怎么上传?buffer缓冲什么意思?我是小白搞不懂,求大神指点!!度娘找不到答案,估计很少有人用,官方大佬出来解答一下吧!谢谢! [图片]
2019-09-24 - 如何提升你的云函数性能
在使用云开发一段时间后,你一定会遇见一个问题:虽然云函数非常的方便,但我的云函数似乎性能不够好,为什么我的云函数每次加载都需 2 ~ 3 秒种,时间太长了!。 这篇文章,就来告诉你,应该如何提升你的云函数性能。 如何了解云函数运行情况? 在了解如何优化云函数的运行情况之前, 我们需要先了解,如何查看当前的云函数运行情况,这样才能有个对比。 [图片] 打开小程序开发者工具,并打开你的项目 进入到你要调试的页面,打开调试器 调用云函数,并在调试器中切换到 Network 页面,找到你的请求。 点击你的请求,然后切换到 Timing 页面,查看具体的情况。 在这个页面中,你可以理解其中的 Waiting(TTFB) 是你发起请求到你接收到返回结果的第一个字节的时间,简单的来说,就是服务器计算结果需要花费的时间。而下方的 Content Download 则是下载内容所需的时间,你可以理解是表现出网络速度快慢的数据。 总结来说,就是如果 Waiting TTFB 的值比较大,你就去优化云函数性能。如果 Content Download 的数值毕竟大,你就需要优化网络情况 优化 Waiting TTFB 云函数的运行机制 Waiting TTFB 的优化是云函数性能端的优化,那么在优化之前,我们就需要先来了解一下云函数的运行机制,以便帮助我们了解应该如何去进行性能优化。 [图片] 在蕴含运行时,具体的顺序是这样的 用户发起请求,请求发送到云开发的后台 云开发后台的调度器将请求分发给下方的执行的 worker 、容器 容器创建环境、下载代码 执行代码 在这个过程中,发起请求到云开发、调度器调度速度、调度器传递信息到容器、函数调用等,都是可以优化的,但是我们在具体的使用过程中。这些大都需要由云开发的工作人员来完成,对于我们自己来说,只能去尽可能的优化容器内部到代码层面的东西。 接下来,我们可以看看更细致的调用逻辑。 [图片] 在云开发中,我们可以将调用分为三种类型: 冷启动:图中的红色阶段,需要重新创建容器、下载代码,耗时最长 温启动:图中的黄色阶段,需要下载代码,耗时较长 热启动:图中的蓝色阶段,不需要下载代码,耗时最短 我们可以看到,最快的,是热启动,函数不需要创建容器,不需要启动函数就可以完成执行,显然比要创建容器或要下载代码的温启动和冷启动速度更快。这样,我们就得到了优化云函数性能的第一个方法 1. 让你的云函数每次调用都走热启动 当我们可以让我们的云函数的每一次调用都走热启动,少了容器的创建和函数的部署,请求的速度理所当然的要比冷启动和温启动更快。 我们可以测试一下,我设置每秒调用一次云函数,看看 TTFB 的变化。 [代码]setInterval(()=>{wx.cloud.callFunction({name:'profile'})},1000) [代码] 函数内代码是默认创建的云函数代码。 则对应的执行效果如下 [图片] 可以看到,函数的执行时间从第一次的 1.2s 降低到了 200ms左右,性能提升了 80%,我们仅仅是简单的提升了函数的调用频次,就可以实现提升函数的调用性能,这就是热启动带给我们的价值。 实施方案 如果你需要足够高的性能,不妨借助云开发的定时器,定期唤起你的容器,从而为你的容器保活,确保你的函数时刻被热启动。 2. 缩小你的函数大小 在前面我们曾介绍过,云函数在启动过程中,会创建容器和下载代码。创建容器的过程对于开发者来说不可控,不过我们可以使用一些方法,缩小我们的代码,提升代码的下载速度,比如说,缩小我们的函数代码。 这里我们可以做个测试,这里我创建了两个函数,两个函数的代码完全一致,不同的是,在实验组的函数中,我加入了一个 temp 变量的声明,这个变量的值是一个非常长的字符串,从而使得两个函数的大小分别是 68K 和 4K。 接下来,我们看看二者的执行时间。 [图片] 我们会发现,几乎没有差距的代码,因为加入了变量声明的因素,在性能上会略慢几毫秒,后续随着容器的不断复用,函数的之间的差距也越来越小,几乎可以忽视。 实施方案 对于你的代码,要尽可能的精炼,减少无用的代码,减少代码下载所需时间。 3. 削减不需要的 Package 除了下载代码以外,还需要下载 Node 环境运行所需的依赖包,虽然云开发可能针对 Node Modules 已经做了缓存,但依然存在下载的时间差区别,这里我也做了一个实验。 空包:什么都没装,把 wx-server-sdk 都卸载掉了。 复杂包:装了 Mongoose、sequelize、sails 等依赖的包。 函数逻辑上也相差无几,都是返回 Event ,则结果如下 [图片] 我们发现,前三次可能是因为涉及到依赖包的下载问题,所以前三次的时长大小对比特别的明显,而从第四次开始,二者的区别就不大了,可能是因为依赖已经完成了缓存,所以可以直接使用缓存来完成函数的执行。 实施方案 你可以选择看看你的 package.json ,看看其中是否有你不需要的依赖,将其删除,仅保留有需要的依赖,可以有效提升你的代码执行速度。 优化 Content Download 如果你想要优化 Content Download ,核心需要优化的是两个点: 手机到服务端的节点的距离和速度 内容的大小 前者一般来说,你可以通过切换不同的网络环境来实现优化,比如从 3G 切换到 4G ,从 4G 升级到 5G,这些都可以提升你的手机到服务端节点之间的速度。 此外,还可以借助内容分发网络 CDN 能力来完成缩小你到服务端节点之间的距离,不过对于云函数来说,因为你不可控,无法控制,所以这一点不再谈。 这里补充一句,云开发的文件存储都是有 CDN 的,因此,你通过云存储下载的文件才会比别人更快。 后者则一般通过调整代码来完成,比如只返回必须的资源,对于不需要的内容,不再返回,或压缩返回。 总结 最后,我们回顾一下这篇文章中介绍的优化云函数的方法: 函数下载性能优化 保持函数容器的热启动,提升函数启动性能 缩小函数大小,提升代码下载速度 削减不必要的包,减少依赖大小 网络优化 使用更好的网络,比如 Wi-Fi 云函数中仅返回所需要的内容,减少下载时间。 以上这些方法,你都在你的函数中试过么?有没有其他的优化方法?欢迎你与我分享。
2019-12-08 - 小程序的智能裁剪接口应该怎么用
在小程序的服务端接口中,有一类是图像处理接口,其中有一个接口是让大家觉得用起来很头疼的,就是 aiCrop — 图片智能裁剪这个接口。 这个接口根据官方的描述,其能力是 “本接口提供基于小程序的图片智能裁剪能力。”,但是,根据文档中给出的结果,似乎也并没有返回图片的 Buffer 流,那么这个接口真正应该怎么用呢?背后又有什么坑呢?今天我就给你讲一讲。 前置条件 你需要已经注册好小程序,并开通小程序云开发(本次演示将基于小程序云开发制作) 业务流程 [图片] 流程说明 用户侧选择图片,并生成临时文件路径(如果是网络图片,需要下载到本地,并修改云函数,改为直接传递 网络图片地址) 将图片上传的云存储中,并拿到 FileID 将 FileID 传递到云函数中,云函数获取到对应的临时 URL 将临时文件 URL 传递到微信的 AI 剪切接口 AI 接口将裁剪结果返回到云函数 云函数将裁剪结果返回到小程序 小程序基于返回结果进行渲染。 服务端代码 这里我们创建一个云函数来完成图片的裁剪,你需要创建一个新的云函数,其中[代码]index.js[代码]的代码如下 [代码]// index.js const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { let fileId = event.file; // 获取文件的临时连接 let tempUrl = await cloud.getTempFileURL({ fileList: [fileId] }) let newUrl = tempUrl.fileList[0].tempFileURL; // 对图片进行裁剪 let cropResult = await cloud.openapi.img.aiCrop({ imgUrl:newUrl, ratios:'1,2.35,0.5,0.25,3.25'//裁剪比例 }) return cropResult } [代码] 以及在该函数目录下创建一个 [代码]config.json[代码] 文件,内容如下 [代码]{ "permissions": { "openapi": [ "img.aiCrop" ] } } [代码] 这样就完成了云函数部分的内容。 上面这段代码帮助我们获取 FileID 对应的文件临时路径,并将其传递给微信接口进行调用。 小程序端调用代码 在小程序端,我们主要是选择文件,将其上传到云端,并调用云函数进行裁剪,在取得返回值后在小程序端进行渲染。 小程序的页面 JS 代码如下 [代码]Page({ /** * 由于此数据仅在逻辑层使用,因此定义一个tempData 进行存储 */ tempData:{ path:null, }, onClick() { /** * 选择文件 */ wx.chooseImage({ success: res => { /** * 获取文件路径,并传递给 tempData */ let file = res.tempFiles[0].path this.tempData.path = file console.log("[info]:开始上传文件") /** * 上传文件到云存储 */ wx.cloud.uploadFile({ filePath: file, cloudPath: "test.jpg" }).then(res => { /** * 调用云函数 */ console.log("[info]:开始调用云端裁剪") wx.cloud.callFunction({ name: "aicrop", data: { file: res.fileID } }).then(res => { /** * 调用裁剪 */ console.log("[info]:云端裁剪成功 ", res) this.crop(res.result); }).catch(err => { console.error("[error]:函数调用错误", err) }) }).catch(err => { console.error("[error]:文件上传错误", err) }) }, fail: err => { console.error("[error]:文件选择错误", err) } }) }, crop(cropOps) { /** * 获取 Context */ let ctx = wx.createCanvasContext('aiCrop', this); /** * 判断是否成功裁剪 */ if (cropOps.results.length == 0) { return } /** * 计算裁剪的值 */ let crop = cropOps.results[0]; let width = crop.cropRight - crop.cropLeft let height = crop.cropBottom - crop.cropTop /** * 绘制图像 */ ctx.drawImage(this.tempData.path, crop.cropLeft, crop.cropTop, width, height, 0, 0, 300, 300); ctx.draw() } }) [代码] 对应页面的 WXML 页面结构如下 [代码]<button bindtap="onClick">Crop MY IMAGE</button> <canvas canvas-id="aiCrop" style="width:300px;height:300px;"></canvas> [代码]
2019-12-22 - 云函数接收公众号消息推送
公众号的消息推送和处理,也可以云开发来做了,不需要搭建自己的服务器了。 具体步骤如下: 1、将小程序的云环境共享给公众号的appid。 操作如下:小程序开发工具--云开发--更多--环境共享--添加共享--添加公众号appid。共享成功; 2、配置公众号云开发 打开开发工具--回到初始页项目管理页--左则项目类型栏选择“公众号网页”--云开发--弹出框里填入公众号appid--进入云控制台 3、配置公众号消息推送 以公众号appid进入云控制台后--更多--环境共享--消息推送--添加消息推送--选择event--选择subscribe_and_unsubscribe--选择接收消息推送的云环境--选择接收消息推送的云函数--结束 4、配置完成,接收消息推送 在云函数里写处理消息推送的代码。 在云函数里: console.log(event)//获取消息包JSON数据 console.log(wxContext)//获取公众号用户的openid和unionid。 5、结束。
2021-10-21 - 为什么图片链接可正常访问但image组件加载不出来图片?
因为 image 控件的图片拉取本质上是 web 上的 backgroundImage,很多时候是由于图片不规范(content-type / length / 是否302跳转等 )导致拉取不成功,最终表现为加载不出图片。关于这一块我们在持续优化中
2021-12-17 - 用云开发Webify,5分钟上线新网站!
用最简单的方式,带你上线自己的网站!大家好,我是鱼皮。 相信每位学编程的同学都想要拥有一个自己的网站,比如个人博客,可以拿来记录自己的学习过程、分享自己的文章、展示作品等,从而激励自己持续学习和总结。 那么今天这篇文章,目标很简单,我要用 新技术 带 所有同学 从 0 到 1 快速上线一个自己的网站! 给我 5 分钟,我给你全世界。 上线网站极简教程让我们先来了解下传统的上线网站流程。 传统方式假如我们要上线个人博客网站,供其他同学访问,那么需要经历如下步骤: 准备一份个人博客网站的源代码购买一台有公网 IP 的服务器把网站文件放到服务器上,并安装 web 服务器软件提供网页访问能力购买一个域名配置 DNS 解析,把域名指向服务器的 IP 地址如果要提高网站访问速度,自行购买 CDN流程图如下: [图片] 听起来就觉得麻烦,而且这一套流程下来最少也要 1 个小时。这也是为啥很多同学只是有上线个人网站的想法,却从未实现。 但是,昨天我却只用 5 分钟,就上线了自己的网站,怎么做到的呢? 下面引出今天的主角 [代码]Webify[代码] 。 WebifyWebify 是腾讯云提供的 一站式 Web 应用托管服务,帮助大家极速开发、部署、上线网站项目。 什么是一站式呢? 就是一条龙服务,只要你有一套网页代码,无论是静态、动态网站还是其他类型的 web 应用,都能使用 Webify 傻瓜式部署。由它提供服务器、默认域名、自定义域名、HTTPS、CDN 加速,提升 Web 应用的性能和安全性。 换言之,如果使用 Webify 上线个人博客,你只需要: 准备一份个人博客网站的源代码进入 Webify 控制台,选择源码和配置一键发布流程大大精简了! [图片] 此外,Webify 还提供基于 Git 工作流的 DevOps 流程,每次修改代码都能自动重新构建部署,不用再登录服务器自己操作了! 听起来挺爽,下面我们一起试着用 Webify 上线个人博客。 Webify 实战地址:https://cloud.tencent.com/product/webify首先进入 Web 应用托管平台,新建一个应用。 一个应用对应一个网站项目,这里提供两种新建应用的方式:Git 导入和从模板创建。 [图片] Git 导入创建应用Git 导入适用于已有网站源代码的方式,只要你的代码存在于 Git 托管平台,就能直接在 Webify 导入。 比如我们想要上线个人博客,先要有一套博客源代码。可以自己写代码;也可以直接使用一些现成的站点生成器,比如 Hugo、Hexo 等(后面详细介绍),自动生成源代码;当然还可以下载、克隆别人的项目代码。搞到代码后,把它上传到 GitHub 或 Gitee 等代码托管平台就可以被 Webify 导入了。 导入之后需要根据应用的技术栈和类型,填写构建命令等配置。这里可以直接选择预设配置,比如你的项目用到了 Vue.js,可以直接选择对应的预设,不用填写就能自动配置: [图片] 从模板创建应用如果我们啥代码都没有,也搞不来代码,咋办? 也没有关系,Webify 内置了一些项目模板,直接选择需要的应用创建即可。比如我们要做个人博客,可以选择 Docusaurus 2 这款主流的站点生成器: [图片] 选中模板后,系统会自动把代码模板复制到新的 Git 仓库,和应用关联。 [图片] Webify 会自动给 Git 仓库配置 Webhooks,后续每当仓库的代码发生变更(push)时,都会自动触发应用的重新部署,无需再跑到服务器上改代码了! [图片] 点击下一步,进入应用配置,由于我们使用的是系统预设模板,什么都不用改,用默认配置就行了。 [图片] 点击部署按钮,稍等几分钟,应用就创建成功了! 应用详情可以在应用列表和部署记录中查看到新建完成的应用: [图片] 点击新建的应用,进入应用详情页: [图片] 可以查看到应用的详细信息,比如系统为我们提供的默认项目域名,点击之后就能访问到已上线的博客网站啦! [图片] 应用详情中还有一个所属环境信息,那是啥呢? 其实在部署过程中,系统会自动创建一个 [代码]云开发[代码] 环境,根据配置的命令自动构建项目,将构建产物放到 [代码]静态网站托管[代码] 上。 可以在云开发控制台看到已经上传到服务器上的文件: [图片] 在静态网站托管页面,可以修改已上传的文件,修改 CDN 缓存设置等: [图片] 想要了解什么是云开发?欢迎阅读我之前的文章:我和云开发 。进入应用详情的设置页,可以给项目添加自定义域名、修改应用构建配置、删除应用等: [图片] 持续发布下面让我们给自己的博客网站添加一篇文章,进入到应用对应的 Git 仓库,在 [代码]docs/tutorial-basics[代码] 路径下新建一个 [代码].md[代码] 后缀文件,输入博客标题和内容。 [图片] 点击 [代码]commit[代码] 按钮,本次代码改动将自动 push 到主分支: [图片] 当然,更好的方式是把代码仓库拉取到本地,在本地修改后再 push 到远程。可以先 push 到 dev 分支,确认代码没问题后再合并到 master 分支。 代码 push 之后,事件会通过 Webhooks 传递给 Webify,自动触发重新部署,等一段时间后就可以看到新的部署记录: [图片] 再次访问网站地址,就能够看到新增的博客啦! [图片] 如果没有立即看到更新,可能是由于 CDN 的缓存(默认 2 分钟),导致没有拉取到最新的资源,等个几分钟就好了。OK,从 0 到 1 上线网站成就达成。后面大家可以参考 Docusaurus 站点生成器的官方文档,更改代码和配置,进一步定制自己的博客。 使用感受其实这个东西并不算新技术了,产品形态和体验上类似 Vercel 和 Github Pages。不过优点是 Webify 在国内,提供了高速 CDN;还能够和其他云产品打通、形成体系。 使用 Webify 上线网站还是很爽的,整个流程非常简单、易上手,着实省去了很多自己上线网站的琐碎流程。无论是对想快速上线自己网站的同学、还是 web 开发爱好者,都是不错的选择。 还有重要的一点要提醒大家,世上没有免费的午餐,Webify 依托于云开发,也是要收费的(提供 1 个月的免费体验),但相对于自己购买服务器(即使是学生机),性价比也是更高的。 技术交流 [图片]
2021-07-27 - wxml-to-canvas使用笔记
wxml-to-canvas使用笔记。小程序内通过静态模板和样式绘制 canvas ,导出图片,可用于生成分享图,生成小程序分享海报等场景。 在已经创建好的项目中进行安装 wxml-to-canvas 1.详情-本地设置-使用npm; [图片] 2,在工具中的终端或者是电脑终端。新建终端命令 [图片] 3,初始化npm 输入 npm init 大概需要点击8次回车。才能初始化完成。最后会出现OK,再回车一次 [图片] 4,安装wxml-to-canvas插件 wxml-to-canvas插件介绍地址:https://developers.weixin.qq.com/miniprogram/dev/extended/component-plus/wxml-to-canvas.html [图片] 5,构建npm [图片] 6,生成海报 海报的样式,支持 view、text、image 三种标签。按文档的要求进行拼装。文字和图片定位,我个人用position属性来定位的。 [图片] 完!
2021-07-26 - 云开发短信跳小程序(自定义开发版)教程
写在前面如果你想要自主开发,但没有云开发相关经验,可以采用演示视频来学习本教程: [视频] 一、能力介绍境内非个人主体的认证的小程序,开通静态网站后,可以免鉴权下发支持跳转到相应小程序的短信。短信中会包含支持在微信内或微信外打开的静态网站链接,用户打开页面后可一键跳转至你的小程序。 这个链接的网页在外部浏览器是通过 URL Scheme 的方式来拉起微信打开主体小程序的。 总之,短信跳转能力的实现分为两个步骤,「配置拉起网页」和「发送短信」。本教程将介绍如何执行操作完成短信跳转小程序的能力。 如果你想要无需写代码就能完成短信跳转小程序的能力,可以参照无代码版教程进行逐步实现。 二、操作指引1、网页创建首先我们需要构建一个基础的网页应用,在任何代码编辑器创建一个 html 文件,在教程这里命名为 index.html 在这个 html 文件中输入如下代码,并根据注释提示更换自己的信息: window.onload = function(){ window.web2weapp.init({ appId: 'wx999999', //替换为自己小程序的AppID gh_ID: 'gh_999999',//替换为自己小程序的原始ID env_ID: 'tcb-env',//替换小程序底下云开发环境ID function: { name:'openMini',//提供UrlScheme服务的云函数名称 data:{} //向这个云函数中传入的自定义参数 }, path: 'pages/index/index.html' //打开小程序时的路径 }) } 以上引入的 web2weapp.js 文件是教程封装的有关拉起微信小程序的极简应用,我们直接引用即可轻松使用。 如果你想进一步学习和修改其中的一些WEB展示信息,可以前往 github 获取源码并做修改。 有关于网页拉起小程序的更多信息可以访问官方文档 如果你只想体验短信跳转功能,在执行完上述文件创建操作后,继续以下步骤。 2、创建服务云函数在上面创建网页的过程中,需要填写一个UrlScheme服务云函数。这个云函数主要用来调用微信服务端能力,获取对应的Scheme信息返回给调用前端。 我们在示例中填写的是 openMini 这个命名的云函数。 我们前往微信开发者工具,定位对应的云开发环境,创建一个云函数,名称叫做 openMini 。 在云函数目录中 index.js 文件替换输入以下代码: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { return cloud.openapi.urlscheme.generate({ jumpWxa: { path: '', // 打开小程序时访问路径,为空则会进入主页 query: '',// 可以使用 event 传入的数据制作特定参数,无需求则为空 }, isExpire: true, //是否到期失效,如果为true需要填写到期时间,默认false expire_time: Math.round(new Date().getTime()/1000) + 3600 //我们设置为当前时间3600秒后,也就是1小时后失效 //无需求可以去掉这两个参数(isExpire,expire_time) }) } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 接下来,我们需要开启云函数的未登录访问权限。进入小程序云开发控制台,转到设置-权限设置,找到下方未登录,选择上几步我们统一操作的那个云开发环境(注意:第一步配置的云开发环境和云函数所在的环境,还有此步操作的环境要一致),勾选打开未登录 [图片] 接下来,前往云函数控制台,点击云函数权限,安全规则最后的修改,在弹出框中按如下配置: [图片] 3、本地测试我们在本地浏览器打开第一步创建的 index.html ;唤出控制台,如果效果如下图则证明成功! 需要注意,此处本地打开需要时HTTP协议,建议使用live server等扩展打开。不要直接在资源管理器打开到浏览器,会有跨域的问题! [图片] 4、上传本地创建好的 index.html 至静态网站托管将本地创建好的 index.html 上传至静态网站托管,在这里静态托管需要是小程序本身的云开发环境里的静态托管。 如果你上传至其他静态托管或者是服务器,你仍然可以使用外部浏览器拉起小程序的能力,但会丧失在微信浏览器用开放标签拉起小程序的功能,也不会享受到云开发短信发送跳转链接的能力。 如果你的目标小程序底下有多个云开发环境,则不需要保证云函数和静态托管在一个环境中,无所谓。 比如你有A、B两个环境,A部署了上述的云函数,但是把 index.html 部署到B的环境静态托管中了,这个是没问题的,符合各项能力要求。只需要保证第一步 index.html 网页中的云开发环境配置是云函数所在环境即可。 部署成功后,你便可以访问静态托管的所在地址了,可以通过手机外部浏览器以及微信内部浏览器测试打开小程序的能力了。 5、短信发送云函数的配置在上面创建 openMini 云函数的环境中再来一个云函数,名字叫 sendsms 。 在此云函数 index.js 中配置如下代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { try { const config = { env: event.env, content: event.content ? event.content : '发布了短信跳转小程序的新能力', path: event.path, phoneNumberList: event.number } const result = await cloud.openapi.cloudbase.sendSms(config) return result } catch (err) { return err } } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 6、测试短信发送能力在小程序代码中,在 app.js 初始化云开发后,调用云函数,示例代码如下: App({ onLaunch: function () { wx.cloud.init({ env:"tcb-env", //短信云函数所在环境ID traceUser: true }) wx.cloud.callFunction({ name:'sendsms', data:{ "env": "tcb-env",//网页上传的静态托管的环境ID "path":"/index.html",//上传的网页相对根目录的地址,如果是根目录则为/index.html "number":[ "+8616599997777" //你要发送短信的目标手机,前面需要添加「+86」 ] },success(res){ console.log(res) } }) } }) 重新编译运行后,在控制台中看到如下输出,即为测试成功: [图片] 你会在发送的目标手机中收到短信,因为短信中包含「退订回复T」字段,可能会触发手机的自动拦截机制,需要手动在拦截短信中查看。 需要注意:你可以把短信云函数和URLScheme云函数分别放置在不同云开发环境中,但必须保证所放置的云开发环境属于你操作的小程序 另外,出于防止滥用考虑,短信发送的云调用能力需要真实小程序用户访问才可以生效,你不能使用云端测试、云开发JS-SDK以及其他非wx.cloud调用方式(微信侧WEB-SDK除外),会提示如下错误: [图片] 如果你想在其他处使用此能力,可以使用服务端API来做正常HTTP调用,具体访问官方文档 7、查看短信监控图表进入 云开发控制台 > 运营分析 > 监控图表 > 短信监控,即可查看短信监控曲线图、短信发送记录。 [图片] 三、总结短信跳转小程序核心是静态网站中配置的可跳转网页,外部浏览器通过URL Scheme 来实现的,这个方式不适用于微信浏览器,需要使用开放标签才可以URL Scheme的生成是云调用能力,需要是目标小程序的云开发环境的云函数中使用才可以。并且生成的URL Scheme只能是自己小程序的打开链接,不能是任意小程序(和开放标签的任意不一致)短信发送能力的体验是每个有免费配额的环境首月100条,如有超过额度的需求可前往开发者工具-云开发控制台-对应按量付费环境-资源包-短信资源包,进行购买。如当前资源包无法满足需求也可通过云开发 工单 提交申请[图片]短信发送也是云调用能力,需要真实小程序用户调用才可以正常触发,其他方式均报错返回参数错误,出于防止滥用考虑云函数和网页的放置可以不在同一个环境中,只需要保证所属小程序一致即可。(需要保证对应环境ID都能接通)如果你不需要短信能力,可以忽略最后两个步骤CMS配置渠道投放、数据统计可参考官方文档
2021-04-07 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个star咯 https://github.com/lucaszhu2zgf/mp-progress 环形进度条由灰色底圈+渐变不确定圆弧+双色纽扣组成,首先先把页面结构写好: .canvas{ position: absolute; top: 0; left: 0; width: 400rpx; height: 400rpx; } 因为进度条需要盖在文字上面,所以采用了绝对定位。接下来先把灰色底圈给画上: const context = wx.createContext(); // 打底灰色曲线 context.beginPath(); context.arc(this.convert_length(200), this.convert_length(200), r, 0, 2*Math.PI); context.setLineWidth(12); context.setStrokeStyle('#f0f0f0'); context.stroke(); wx.drawCanvas({ canvasId: 'progress', actions: context.getActions() }); 效果如下: [图片] 接下来就要画绿色的进度条,渐变暂时先不考虑 // 圆弧角度 const deg = ((remain/total).toFixed(2))*2*Math.PI; // 画渐变曲线 context.beginPath(); // 由于外层大小是400,所以圆弧圆心坐标是200,200 context.arc(this.convert_length(200), this.convert_length(200), r, 0, deg); context.setLineWidth(12); context.setStrokeStyle('#56B37F'); context.stroke(); // 辅助函数,用于转换小程序中的rpx convert_length(length) { return Math.round(wx.getSystemInfoSync().windowWidth * length / 750); } [图片] 似乎完成了一大部分,先自测看看不是满圆的情况是啥样子,比如现在剩余车位是120个 [图片] 因为圆弧函数arc默认的起点在3点钟方向,而设计想要的圆弧的起点从12点钟方向开始,现在这样是没法达到预期效果。是不是可以使用css让canvas自己旋转-90deg就好了呢?于是我在上面的canvas样式中新增以下规则: .canvas{ transform: rotate(-90deg); } 但是在真机上并不起作用,于是我把新增的样式放到包裹canvas的外层元素上,发现外层元素已经旋转,可是圆弧还是从3点钟方向开始的,唯一能解释这个现象的是官方说:小程序中的canvas使用的是原生组件,所以这样设置css并不能达到我们想要的效果 [图片] 所以必须要在canvas画图的时候把坐标原点移动到弧形圆心,并且在画布内旋转-90deg [图片] // 更换原点 context.translate(this.convert_length(200), this.convert_length(200)); // arc原点默认为3点钟方向,需要调整到12点 context.rotate(-90 * Math.PI / 180); // 需要注意的是,原点变换之后圆弧arc原点也变成了0,0 真机预览效果达成预期 [图片] 接下来添加环形渐变效果,但是canvas原本提供的渐变类型只有两种: 1、LinearGradient线性渐变 [图片] 2、CircularGradient圆形渐变 [图片] 两种渐变中离设计效果最近的是线性渐变,至于为什么能够形成似乎是随圆形弧度增加而颜色变深的效果也只是控制坐标开始和结束的坐标位置罢了 const grd = context.createLinearGradient(0, 0, 100, 90); grd.addColorStop(0, '#56B37F'); grd.addColorStop(1, '#c0e674'); // 画渐变曲线 context.beginPath(); context.arc(0, 0, r, 0, deg); context.setLineWidth(12); context.setStrokeStyle(grd); context.stroke(); 来看一下真机预览效果: [图片] 非常棒,最后就剩下跟随进度条的纽扣效果了 [图片] 根据三角函数,已知三角形夹角根据公式radian = 2*Math.PI/360*deg,再利用cos和sin函数可以x、y,从而计算出纽扣在各部分半圆的坐标 const mathDeg = ((remain/total).toFixed(2))*360; // 计算弧度 let radian = ''; // 圆圈半径 const r = +this.convert_length(170); // 三角函数cos=y/r,sin=x/r,分别得到小点的x、y坐标 let x = 0; let y = 0; if (mathDeg <= 90) { // 求弧度 radian = 2*Math.PI/360*mathDeg; x = Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 90 && mathDeg <= 180) { // 求弧度 radian = 2*Math.PI/360*(180 - mathDeg); x = -Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 180 && mathDeg <= 270) { // 求弧度 radian = 2*Math.PI/360*(mathDeg - 180); x = -Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } else{ // 求弧度 radian = 2*Math.PI/360*(360 - mathDeg); x = Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } [图片] 有了纽扣的圆形坐标,最后一步就是按照设计绘制样式了 // 画纽扣 context.beginPath(); context.arc(x, y, this.convert_length(24), 0, 2 * Math.PI); context.setFillStyle('#ffffff'); context.setShadow(0, 0, this.convert_length(10), 'rgba(86,179,127,0.5)'); context.fill(); // 画绿点 context.beginPath(); context.arc(x, y, this.convert_length(12), 0, 2 * Math.PI); context.setFillStyle('#56B37F'); context.fill(); 来看一下最终效果 [图片] 最后我重新review了整个代码逻辑,并且已经将代码开源到https://github.com/lucaszhu2zgf/mp-progress,欢迎大家使用
2020-05-27 - 你知道流量主的ECPM吗?扫盲篇
ECPM:千次曝光收益。他不是指单纯的曝光1000次的收益。具体是指1000次曝光里,有多少人点击产生的收益,才叫千次曝光收益。 也就是说ECPM和点击率有强关联性。(不单单指小程序流量主,是针对所有平台都是这么计算) 假设1:你的流量主曝光了1000次,一个人都没点击,你的收益是0,即ECPM=0 假设2:你的流量主曝光了1000次,只有一个人点击,再假设每个点击均为1元(具体数值取决于广告主投放的价格)。那么你这1000次曝光给你收益就是1元,即ECPM=1 假设3:你的流量主曝光了1000次,有100人点击,再假设每个点击均为1元(具体数值取决于广告主投放的价格)。那么你这1000次曝光给你收益就是100元,即ECPM=100 假设4:你的流量主曝光了1000次,只有一个人点击,这个用户实质性的转化为广告主的用户(比如购买商品、充值、关注公众号等),那么这个用户可能给你带来的收益是30,那么你这1000次曝光给你收益就是30元,即ECPM=30 假设5:你的流量主曝光了1000次,有100人点击,这100个用户实质性的转化为广告主的用户(比如购买商品、充值、关注公众号等),那么这100个用户可能给你带来的收益是3000,那么你这1000次曝光给你收益就是3000元,即ECPM=3000(这种情况几乎不会出现) 以上就是ECPM的扫盲。下面凑个原创文章多说一些,看破不说破,还能做朋友。 1、流量主的收益取决于广告主的广告内容、广告投放方式、受众群体、投放城市等有关。 2、正常1个人,1天从你这看5个广告,点击2次,那么这个是正常的,点击率在这个用户上是40%,你可以拿100%的流量主费用。但一个用户点击很多次,接近于100%点击必然会有问题,会在你最终收入中扣除一定比例。 3、你看到的流量主收益日报是基于昨天的单用户统计+总用户统计,微信做出最终的收益计算。 4、正常用户正常看广告,点击率大概在3%左右(常见数值),点击率高、超过大盘均值必然有问题(常见情况:流量主以不正当方式制造虚假或无效曝光量、点击量,轻则1-30天关闭广告展示,重则永久关闭) 5、如果有一天流量主点击特别高了,超出预期也别着急,可能投广告的人铺设的地域,和你的用户地域非常吻合。 6、针对一些互点群、刷广告等,不要做幻想靠广告发大财,这是硬生生从微信嘴里刨食啊,微信能坐视不管?正常运营流量主,你赚你的,广告主赚广告主的,微信赚微信的,这叫生态平衡,广告主和微信都不傻。 7、好好经营小程序,给身边的朋友多宣传宣传自己的小程序,指数自然增长,不出2年,肯定能日广告费赚百元以上稳稳的。 共建微信生态,造福你我他。
2021-04-08 - 微信开放平台应用进行了转移,unionid改变了,怎样把旧的unionid更新成新的?
微信开放平台应用进行了转移,unionid改变了,怎样把旧的unionid更新成新的?前提是不想让用户再重新调起微信授权
2021-07-19 - 解决弹框滑动时,页面跟随滑动问题(弹框内容过长,有滚动内容时,同样适用)
1 使用背景 小程序开发过程中,如果使用弹框功能,弹框滚动时,经常会出现小程序页面跟随弹框滚动而滚动,产品经理一般希望能做到弹框滚动时,页面不要滚动。 2 效果演示 解决前效果.gif 解决后效果.gif 3 解决技术点 弹框内容使用scroll-view组件 4 代码片段 https://developers.weixin.qq.com/s/dKcg2Kmh75rd 5 温馨提示: 不定期分享文章,欢迎关注哈。
2021-07-12 - 未完成微信认证的组织类型小程序怎么注销?
目前小程序组织类型的帐号仅支持有对公账户的注销,用户需要继续完成认证后操作注销。 更多小程序注销相关内容(包含个人类型、组织类型、政府无对公账户类型)可参考:https://kf.qq.com/faq/181226yieaEv181226r6nuMr.html
2019-10-25 - 调用微信sdk接口方法,能实现实时语音转译文字么?
如何使用语音接口实现实时语音转译
2021-07-02 - 微信小程序之自定义退出小程序事件 - 点击页面某个功能按钮退出小程序!
今天遇到个需求,之前没有接触过,用户在提交某个信息之后,直接退出小程序,查阅了官方文档,有这个解决方案,有相关指令和api来进行实现:官方文档:https://developers.weixin.qq.com/miniprogram/dev/component/navigator.html 方案一:不需要js代码,直接在wxml里面加上如下代码即可,这个主要实现方法就是target=“miniProgram”,在页面跳转中加上这个属性就可以了: <navigator class="close" target="miniProgram" open-type="exit"> 退出当前小程序 </navigator> 方案二:如果我们需要点击了之后执行某一个js逻辑后再退出怎么实现呢,这个知道的可以欢迎留言,我现在还暂时没有用到,等项目需要的时候我在研究一下,感觉以后应该会用到。
2021-06-23 - 自定义组件可以使用createIntersectionObserver去监听子组件元素出现在视窗么?
现在做了一个自定义组件列表,列表中的元素也是自定义组件,想在列表中监听列表元素出现在视窗,然后触发元素的曝光事件,这个能做么? 还是只能每个元素自己监听自己? 我自己试了下 不太行,不知道是不是写的有问题,回调不触发 https://developers.weixin.qq.com/s/mFF11Tmx7wp4 observeItems(){ const observerObj = wx.createIntersectionObserver(this,{ observeAll:true }).relativeToViewport({bottom: 0}); observerObj.observe('.item',function(res){ console.error(res) }); }, 已解决 [图片]
2021-04-28 - canvas里面插入的图片可以是网络图片吗?
canvas里面插入的图片可以是网络图片吗?我使用了网络图片,但是当输出时,图片却不显示, 试例:仅供参考,不是真实代码 !!! export default { data() { return { bg: 'https://...', }; }, methods: { copyFn(src) { .select('#sss') .boundingClientRect(data => { const avatarWidth = data.width; const avatarHeight = data.height; var ctx = uni.createCanvasContext('myCanvas'); ctx.drawImage(this.bg, 0, 0, avatarWidth, avatarWidth); ctx.draw(); } } } }
2021-04-21 - 小程序中使用rxjs,是真的舒服!
rxjs-mp 在小程序中使用RxJs。(RxJs for miniprogram) 1. 安装依赖: [代码]npm install --save rxjs-mp[代码] [代码]npm install --save-dev rxjs[代码] 安装完成后使用小程序开发者工具构建npm,然后就可以在项目中使用rxjs了。此外,在vscode里还能享受rxjs的语法提示。 2. 如何使用: 为了演示在小程序中如何使用rxjs,现在举一个用rjxs封装http请求的例子。相对于Promise的封装,rxjs可随时取消已经发出的请求,这一点是Promise很难实现的。我们在utils目录下建一个http.js的文件。 2.1 封装utils/http.js [代码]const Rx = require('rxjs-mp'); /** * Request 方法 * @param {string} url * @param {any} data * @param {'GET'|'POST'|'PUT'|'DELETE'} method * @param {{header?: object}} options */ function request(url, data = {}, method = "GET", { header }={}) { return new Rx.Observable(ob => { const requestTask = wx.request({ url, data, method, header: { ...(header || {}) Token: wx.getStorageSync('token') }, success: res => { if (res.statusCode == 200) { ob.next(res); ob.complete(); } else { ob.error(res); } }, fail: err => { console.error(err); ob.error(err); } }); return () => requestTask.abort(); }); } /** * GET 方法 * @param {string} url * @param {{header?: object}} options */ function get(url, options) { return request(url, null, 'GET', options); } /** * POST 方法 * @param {string} url * @param {any} data * @param {{header?: object}} options */ function post(url, data, options) { return request(url, data, 'POST', options); } /** * PUT 方法 * @param {string} url * @param {any} data * @param {{header?: object}} options */ function put(url, data, options) { return request(url, data, 'PUT', options); } /** * GET 方法 * @param {string} url * @param {{header?: object}} options */ function delete_(url, options) { return request(url, null, 'DELETE', options); } module.exports = { http: { get, post, put, delete: delete_, request } } [代码] 2.2 调用封装,pages/your/page.js 管理rxjs订阅可以使用subsink2订阅管理工具(安装:[代码]npm install subsink2[代码]). [代码]const Rx = require('rxjs-mp'); const { SubSink } = require('subsink2'); const http = require('../uitls/http.js'); const subs = new SubSink(); Page({ data: { todoList: [], }, onLoad() { // 发起请求 this.httpRequest01(); this.httpRequest02(); this.httpRequest03(); }, httpRequest01() { // 请求1,用subsink的id方法标识请求,可以防重得请求,还可以随时取消 subs.id('sub01').sink = http.get('htts://some-api-url').pipe( Rx.operators.map(res => res && res.data || {}) ).subscribe(res => { console.log(res); }, err => { console.error(err); }); }, httpRequest02() { // 请求2,直接把请求加入订阅池 subs.sink = http.post('htts://some-api-url', {}).pipe( Rx.operators.map(res => res && res.data || {}) ).subscribe(res => { console.log(res); }, err => { console.error(err); }); }, httpRequest03() { // 请求2,把请求加入订阅池的另一种方法 subs.add(http.get('htts://some-api-url').pipe( Rx.operators.map(res => res && res.data || {}) ).subscribe(res => { console.log(res); }, err => { console.error(err); })); }, bindUnSubRequest01() { // 取消request01的请求 subs.id('sub01').unsubscribe(); }, onUnload() { // 销毁时,取消所有请求 subs.unsubscribe(); }, }); [代码] 2.3 你还可使用rxjs其它强大的功能 比如消息发布和订阅,rxjs基于流的响应式编程帮你逃出Promise的回调地狱。 [代码]// ================================================ // utils/message.js // ================================================ const Rx = require('rxjs-mp'); module.exports = { onSomeThingChange$: new Rx.Subject(), onOtherThingChange$: new Rx.BehaviorSubject(false), } // ================================================ // pages/your/page.js // ================================================ const Rx = require('rxjs-mp'); const { onSomeThingChange$, onOtherThingChange$ } = require('../../utils/message.js'); onSomeThingChange$.pipe( Rx.operators.first() ).subscribe(status=> { console.log(status) // doSomeThing }); onOtherThingChange$.subscribe(status => { console.log(status) // doSomeThing }); onSomeThingChange$.next(true); onOtherThingChange$.next(true); [代码]
2021-03-31 - 优秀开源项目推荐-基于云开发的英文单词对战小程序
在我开始之前,我首先要声明我并不是这个开源项目的开发者/维护者,因此,大家不要太信任我的观点。我确实非常深入地研究了这个项目的代码实现,但是无论如何我也不能保证能跟开发者保持一致。话虽如此,我已经用源码来支持我的观点,并尝试着使我的论点尽可能的真实。 本文背景 是这样的,我最近在开发双人对战答题,在参考git上一些好的开源项目的时候发现了这个小程序,目前这个小程序完全开源的,如果对这个对战模式感兴趣可以学习下。 本文内容 本文介绍一款优秀的开源项目推荐优秀的开源项目推荐基于云开发的英文单词对战小程序 项目介绍 https://juejin.im/post/6844904136215887880 项目地址 https://github.com/arleyGuoLei/wx-words-pk 一下内容摘自开源项目readme 单词天天斗微信小程序云开发实现的单词PK小程序,支持好友对战、随机匹配、人机模式,完整代码,可以直接部署阅览 ~ UI可以披靡市场上所有同类型小程序,体验也是一流的哦 ~ 目前已经有同学在QQ小程序、阿里小程序部署;也有同学修改成了[代码]公务员题库[代码] ~ 期待看到各类优秀产品上线哦 ~ 部署文档: 源码目录下 - 部署文档.md 如果觉得这个文档比较长,可以查看源码目录下 - 精简核心文档.md 上线说明: 源码开源,但上线需要经过作者许可哦 ~ 开发不易、创作不易。需要支付RMB [代码]66+[代码]方可上线,保障作者著作权益 ~ 如果你觉得项目对你有所帮助 ~ 期待得到你的打赏哦 在线体验[图片] UI截图[图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] 需求概述[图片] 单词对战模式 对战业务需求解析单词对战的游戏核心为:随机生成一定数量的单词列表的单选题类型题目,题目文本为该单词,有 4 个随机中文释义的选项,其中仅有一个为正确释义,双方用户一起选择释义,正确率高且速度快的用户获得对战胜利。 单词对战游戏分为好友对战、随机匹配、人机对战三种对战的形式,均通过上述游戏核心的方式进行对战。 对战设置用户还可以对以下对战信息进行自定义设置 对战的单词书,用户可以选择自己想要背诵的单词类型,包含四级核心词、四级大纲词、六级核心词、六级大纲词、考研真题核心词、考研大纲词、小学必备词、中考大纲词、高考大纲词、雅思大纲词、商务词汇等多种单词书,亦可以选择随机单词书模式,则将从所有的单词中进行随机抽取;设置每一局对战的单词数目为以下任意一种:8、 10(默认)、 12、 15、 20设置切换下一题是否自动播放单词发音设置错词是否加入到生词本开始和错词的时候是否震动设置默认是否播放背景音乐,游戏中也可以随时关闭/开启背景音乐 其他细节优化加入[代码]正在对战过程中[代码]、[代码]对战已结束[代码]、[代码]房间已满[代码]等非正常类型房间,做出相应的交互提示,然后跳转至首页在对战过程中任意用户退出游戏或掉线,则结束本局游戏,进行对战结算对战结束后,房主可以选择再来一局,当房主创建好再来一局的房间后,另外一个用户可以选择再来一局,加入继续对战在对战过程中,选择错误的单词或使用提示卡选择的单词,自动加入到用户生词本,用户可以在生词本中进行复习加入倒计时机制,每一个单词的对战周期为 10s,超时则判断为错选 完整对战流程图[图片] 词汇挑战模式 词汇挑战模式业务解析词汇挑战的核心为:获取随机的一个单词作为单选题题目文本,包含四个中文释义选项,其中一个为正确答案,选择错误则失败,选择正确再获取随机单词,循环下去。 挑战复活机制在词汇挑战的过程中,如果选择错误,可以有两次复活机会 首次复活:通过分享小程序获得复活机会第二次复活:通过观看一个 15s 之内的广告获得复活机会当第三次选择错误,显示再来一局,从零开始记录分数 其他词汇挑战每正确一个词,得分增加 100 分当挑战失败的时候,如果挑战分数高于历史最高分数,则修改历史最高分数为当前分数,用于排行榜排行可以使用提示卡进行选择 完整挑战流程图[图片] 其他功能 生词本用户可以在生词本中查看在单词对战模式、词汇挑战模式中选择错误的单词可以查看单词及单词释义、播放单词发音、删词生词在设置中可以一键清空所有生词 学习打卡当在单词对战模式中,当天对战局数超过 5 局且胜利局数超过 2 局,则打卡成功可以在在打卡页面查看当日进度,可以查看历史的打卡日历 排行榜排行榜包含词力值、词汇挑战分数、签到天数等排名信息每类排行版显示前 20 名的排名头像和昵称以及分数显示自己当前类目下的排名以及分数 用户相关数据库应记录的用户数据包含:昵称、头像、对战局数、胜利局数、选择的单词本、词力值词力值机制:在单词对战模式、单词挑战模式中,每局对战都可以获得相应的词力值分数,作为用户的经验值 其他建议反馈:用户可以在小程序中,反馈意见,然后再后台可以查看用户留言打赏作者:用户可以在小程序中,通过扫码的形式,对小程序进行打赏小程序友情链接:可通过当前小程序跳转至作者的其他小程序中小程序中加入部分广告,不影响用户体验 团队组成整个项目的产品方案、UI 设计、开发、测试、上线运营等皆一个人独立完成。 技术方案 设计设置使用sketch完成,设计稿上传至[代码]蓝湖[代码],作为数据标注。 蓝湖链接链接:https://lanhuapp.com/url/qe2Dl 密码: ydIX 设计图源文件[图片] [图片] [图片] [图片] 下载链接: https://pan.baidu.com/s/1KsZjvlTUbtyYFDcVCy91lg 密码:vylm 开发技术栈前端:原生微信小程序服务端:微信小程序云开发 其他工具ESLintGit + GithubvscodeElectronNodeJSPython 系统架构 项目文件简介├── cloudfunctions # 云开发_云函数目录 | ├── model_auto_sign_trigger # 自动签到定时触发器 | ├── model_book_changeBook # 改变单词书 | ├── model_userWords_clear # 清除用户生词 | ├── model_userWords_get # 获取用户生词 | └── model_user_getInfo # 获取用户信息 ├── db # 数据整理的脚本 ├── design # 设计稿文件、素材文件 | └── words-pk-re.sketch # 设计稿 ├── docs # 项目文档 ├── miniprogram # 小程序前端目录 | ├── app.js # 小程序全局入口 | ├── app.json # 全局配置 | ├── app.wxss # 全局样式 | ├── audios # 选词正确错误的发音 | | ├── correct.mp3 | | └── wrong.mp3 | ├── components # 全局组件 | | ├── header # header组件 | | ├── loading # 全局loading | | └── message # 全局弹窗 | ├── images | | ├── ... 图片素材 | ├── miniprogram_npm # 小程序npm目录 | | └── wxapp-animate # 动画库 | ├── model # 所有的数据库操作 | | ├── base.js # 基类,所有集合继承该基类 | | ├── book.js # 单词书集合 | | ├── index.js # 导出所有数据库操作 | | ├── room.js # 房间集合 | | ├── sign.js # 签到集合 | | ├── user.js # 用户集合 | | ├── userWord.js # 生词表集合 | | └── word.js # 单词集合 | ├── pages # 页面 | | ├── combat # 对战页 | | ├── home # 首页 | | ├── ranking # 排行榜 | | ├── setting # 设置页 | | ├── sign # 签到页 | | ├── userWords # 生词表页 | | └── wordChallenge # 单词挑战 | └── utils | ├── Tool.js # 全局工具类,放了加载、全局store等 | ├── ad.js # 广告 | ├── log.js # 日志上报 | ├── router.js # 全局路由 | ├── setting.js # 全局设置 | └── util.js # 全局工具函数 ├── package.json └── project.config.json # IDE设置、开发设置 云开发数据交互的 Model 层设计在该项目中,将所有的服务端交互、数据库的读取、云函数的调用都放到了 model 目录下,对该目录结构深入解析。 (1) Base.jsbase 基类,所有其他数据集合都继承该类,在构造函数中,用来做数据集合初始化和生命一些可能所需用到的变量。 import $ from './../utils/Tool' const DB_PREFIX = 'pk_' export default class { constructor(collectionName) { const env = $.store.get('env') const db = wx.cloud.database({ env }) this.model = db.collection(`${DB_PREFIX}${collectionName}`) this._ = db.command this.db = db this.env = env } get date() { return wx.cloud.database({ env: this.env }).serverDate() } /** * 取服务器偏移量后的时间 * @param {Number} offset 时间偏移,单位为ms 可+可- */ serverDate(offset = 0) { return wx.cloud.database({ env: this.env }).serverDate({ offset }) } } (2)其他集合文件 (model 目录下,除了 base 和 index 之外的文件)在这些文件中,对应和文件名同名的集合的所有数据操作,比如 book.js 中,包含了所有对 pk_book 集合的所有数据增删改查操作。 import Base from './base' import $ from './../utils/Tool' const collectionName = 'book' /** * 权限: 所有用户可读 */ class BookModel extends Base { constructor() { super(collectionName) } async getInfo() { const { data } = await this.model.get() return data } async changeBook(bookId, oldBookId, bookName, bookDesc) { if (bookId !== oldBookId) { const { result: bookList } = await $.callCloud('model_book_changeBook', { bookId, oldBookId, bookName, bookDesc }) return bookList } } } export default new BookModel() (3)index.js在该文件中,对所有的数据集合操作文件进行引入,然后又导出,之后在其他文件中的的调用,就只需要引入该文件即可,就可以实现调用不同的集合操作。 import userModel from './user' import bookModel from './book' import wordModel from './word' import roomModel from './room' import userWordModel from './userWord' import signModel from './sign' export { userModel, bookModel, wordModel, roomModel, userWordModel, signModel } 环境区分在小程序初始化的时候,对云开发环境进行了全局的初始化,区别开发环境和正式环境。 // app.js initEnv() { const envVersion = __wxConfig.envVersion const env = envVersion === 'develop' ? 'dev-lkupx' : 'prod-words-pk' // 'prod-words-pk' // ['develop', 'trial', 'release'] wx.cloud.init({ env, traceUser: true }) this.store.env = env }, onLaunch() { this.initEnv() this.initUiGlobal() }, 难点解析 难点 1:单词数据 1. 抓包分析和代码实现本课题中使用 MacOS 系统、Charles 抓包软件、安卓手机作为抓包的基本环境。首先在电脑上安装 Charles,然后开启 Proxy 抓包代理,同局域网下配置手机 WiFi 代理实现抓取手机包。 2. 单词数据整理通过爬虫下来的单词数据如下,对于该课题的项目单词数据相对复杂,所以我们对单词数据结构进行简化,只提取项目中需要的字段,以单词 yum 为例: 优化前: {"wordRank":63,"headWord":"yum","content":{"word":{"wordHead":"yum","wordId":"PEPXiaoXue4_2_63","content":{"usphone":"jʌm","ukphone":"jʌm","ukspeech":"yum&type=1","usspeech":"yum&type=2","trans":[{"tranCn":"味道好","descCn":"中释"}]}}},"bookId":"PEPXiaoXue4_2"} 优化后: {"rank":286,"word":"yum","bookId":"primary","_id":"primary_286","usphone":"jʌm","trans":[{"tranCn":"味道好"}]} 通过 NodeJS 编写批量格式整理的程序,整理后导出 JSON 文件 [图片] 3. 数据文件批量导入(传入数据库)由于微信小程序云开发控制台不支持数据文件的批量导入数据库,所以开发了一个支持云开发数据集合批量导入的程序 [图片] [图片] [图片] 数据库批量导入程序更多解析:https://juejin.im/post/5e2bf3e4f265da3e4244ea7f程序代码开源:https://github.com/arleyGuoLei/wxcloud-databases-import 难点 2:单词对战模式本节详细解析单词对战模式的实现,将从创建房间(生成随机词汇、新增房间数据)、对战监听、对战过程(好友对战、随机匹配、人机对战)、对战结算的角度进行分析。 创建对战房间对战房间的创建,分为触发创建房间事件、获取当前选择的单词书、获取单词对战每一局的词汇数量、从数据库 pk_word 集合读取随机单词、格式化获取的随机单词列表、创建房间(使用生成的单词列表、是否好友对战条件)、根据房间的 roomId(主键)跳转至对战页等多个步骤流程组成。 [图片] 房间数据监听单词对战模式中,对 room 数据集合的监听是对战的核心要点,进入对战页面后,调用数据集合的 WatchAPI 对 room 集合中的当前房间记录进行监听,在当前房间记录数据发生变化的时候,将会调用 watch 函数的回调,执行相应的业务,详细流程如下: [图片] 好友对战的实现有了前面创建好的对战房间,也建立好了对当前房间的数据监听,接下来就可以实现有趣的对战交互了。游戏会监听好友用户准备,更新 room 集合中的 right.openid 字段,触发 watch,通知房主可以开始对战;房主点击开始对战,会更新 room 集合中的 state 字段为 PK,watch 回调通知双方开始对战,显示第一道题目,双方用户选择释义的时候,会把选择结果和得分更新至 left/right 中的 grades 和 gradeSum 字段,在 watch 的回调中对双方的选择结果进行显示;当对战到达最后一道题目,且双方都选择完毕,进入结算流程,将房间 state 更新至 finish;如果在对战过程中,有任意用户离开对战,将修改房间 state 为 leave;对战结束之后,房主可以选择再来一局,进行创建房间,更新上一个房间的 nextRoomId 字段,在 watch 回调中通知非房主用户可以加入新的房间,进行再来一局的对战。 [图片] 随机匹配的实现随机匹配对战相对于好友对战的区别在于:好友对战是通过房主将房间链接(roomId)分享到微信好友/微信群,当用户点击分享卡片之后,会跳转至对战页面且房间 Id 为当前分享的房间 roomId,用户进入房间之后就进行上述的监听操作和准备、开始对战等。然而随机匹配的实现原理为,当用户触发随机匹配操作之后,会先在数据库检索有没有符合自己所选择的单词书、目前房主在等待的房间,如果有则加入该房间,如果没有则创建新的随机匹配房间,等待其他用户进入。用户进入之后会自动触发准备操作,房主在 watch 中监听到有用户准备,然后自动触发开始对战操作,后续对战、结算、再来一局流程则和好友对战流程一致。 [图片] 人机对战的实现人机对战的核心思想为:房主用户端随机取一名人机用户,房主端触发人机的自动准备,房主端也自动开始对战,在对战过程中,房主端通过页面 UI 用户手动选词,人机将在 2~5s 或房主选词之后随机完成选词操作,正确率为 75%。 后期可以对正确率进行优化,根据用户的历史正确率进行自动化推算,实现更智能的人机用户,提供更好的用户体验。 [图片] 最后通过 3 个月的开发、功能迭代和运营,目前拥有2600 多的用户量,小程序用户打分为5.0 满分。创建房间且完成对战12000 多局,收录词汇25960个,收录了用户65000多个生词,十分感谢这个项目带给我的成就感。
2020-09-02 - 微信小程序通过websocket实现实时语音识别
之前在研究百度的实时语音识别,并应用到了微信小程序中,写篇文章分享一下。 先看看完成的效果吧 [图片] 前置条件 申请百度实时语音识别key 百度AI接入指南 创建小程序 设置小程序录音参数 在index.js中输入 [代码] const recorderManager = wx.getRecorderManager() const recorderConfig = { duration: 600000, frameSize: 5, //指定当录音大小达到5KB时触发onFrameRecorded format: 'PCM', //文档中没写这个参数也可以触发onFrameRecorded的回调,不过楼主亲测可以使用 sampleRate: 16000, encodeBitRate: 96000, numberOfChannels: 1 } [代码] 使用websocket连接 [代码] linkSocket() { let _this = this //这里的sn是百度实时语音用于排查日志,这里我图方便就用时间戳了 let sn = new Date().getTime() wx.showLoading({ title: '识别中...' }) recorderManager.start(recorderConfig) //开启链接 wx.connectSocket({ url: 'wss://vop.baidu.com/realtime_asr?sn=' + sn, protocols: ['websocket'], success() { console.log('连接成功') _this.initEventHandle() } }) }, //监听websocket返回的数据 initEventHandle() { let _this = this wx.onSocketMessage((res) => { let result = JSON.parse(res.data.replace('\n','')) if(result.type == 'MID_TEXT'){ _this.setData({ textDis: 'none', value: result.result, }) } if(result.type == 'FIN_TEXT'){ let value = _this.data.text let tranStr = value + result.result _this.setData({ value: '', valueEn: '', textDis: 'block', text: tranStr, }) } }) wx.onSocketOpen(() => //发送数据帧 _this.wsStart() console.log('WebSocket连接打开') }) wx.onSocketError(function (res) { console.log('WebSocket连接打开失败') }) wx.onSocketClose(function (res) { console.log('WebSocket 已关闭!') }) }, [代码] 发送开始、音频数据、结束帧 [代码] wsStart() { let config = { type: "START", data: { appid: XXXXXXXXX,//百度实时语音识别appid appkey: "XXXXXXXXXXXXXXXXXX",//百度实时语音识别key dev_pid: 15372, cuid: "cuid-1", format: "pcm", sample: 16000 } } wx.sendSocketMessage({ data:JSON.stringify(config), success(res){ console.log('发送开始帧成功') } }) }, wsSend(data){ wx.sendSocketMessage({ data:data, success(res){ console.log('发送数据帧成功') } }) }, wsStop(){ let _this = this this.setData({ click: true, }) let config = { type: "FINISH" } wx.hideLoading() recorderManager.stop() wx.sendSocketMessage({ data:JSON.stringify(config), success(res){ console.log('发送结束帧成功') } }) }, [代码] 小程序录音回调 [代码] onShow: function () { let _this = this recorderManager.onFrameRecorded(function (res){ let data = res.frameBuffer _this.wsSend(data) }) recorderManager.onInterruptionBegin(function (res){ console.log('录音中断') _this.wsStopForAcc() }) recorderManager.onStop(function (res){ console.log('录音停止') }) }, wsStopForAcc(){ let _this = this this.setData({ click: true, }) let config = { type: "FINISH" } wx.sendSocketMessage({ data:JSON.stringify(config), success(res){ wx.hideLoading() console.log('发送结束帧成功') } }) }, [代码]
2020-08-20 - JavaScript 浮点数运算的精度问题
问题描述在 JavaScript 中整数和浮点数都属于 [代码]Number[代码] 数据类型,所有数字都是以 64 位浮点数形式储存,即便整数也是如此。 所以我们在打印 [代码]1.00[代码] 这样的浮点数的结果是 [代码]1[代码] 而非 [代码]1.00[代码] 。在一些特殊的数值表示中,例如金额,这样看上去有点变扭,但是至少值是正确了。然而要命的是,当浮点数做数学运算的时候,你经常会发现一些问题,举几个例子: JavaScript 代码: // 加法 ===================== // 0.1 + 0.2 = 0.30000000000000004 // 0.7 + 0.1 = 0.7999999999999999 // 0.2 + 0.4 = 0.6000000000000001 // 2.22 + 0.1 = 2.3200000000000003 // 减法 ===================== // 1.5 - 1.2 = 0.30000000000000004 // 0.3 - 0.2 = 0.09999999999999998 // 乘法 ===================== // 19.9 * 100 = 1989.9999999999998 // 19.9 * 10 * 10 = 1990 // 1306377.64 * 100 = 130637763.99999999 // 1306377.64 * 10 * 10 = 130637763.99999999 // 0.7 * 180 = 125.99999999999999 // 9.7 * 100 = 969.9999999999999 // 39.7 * 100 = 3970.0000000000005 // 除法 ===================== // 0.3 / 0.1 = 2.9999999999999996 // 0.69 / 10 = 0.06899999999999999 计算过程比如在 JavaScript 中计算 [代码]0.1 + 0.2[代码]时,到底发生了什么呢? 首先,十进制的[代码]0.1[代码]和[代码]0.2[代码]都会被转换成二进制,但由于浮点数用二进制表达时是无穷的,例如。 JavaScript 代码: 0.1 -> 0.0001100110011001...(无限) 0.2 -> 0.0011001100110011...(无限) IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,所以两者相加之后得到二进制为: JavaScript 代码: 0.0100110011001100110011001100110011001100110011001100 因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了 [代码]0.30000000000000004[代码]。所以在进行算术计算时会产生误差。 整数的精度问题在 Javascript 中,整数精度同样存在问题,先来看看问题: JavaScript 代码: console.log(19571992547450991); //=> 19571992547450990 console.log(19571992547450991===19571992547450992); //=> true 同样的原因,在 JavaScript 中 [代码]Number[代码]类型统一按浮点数处理,整数是按最大54位来算最大([代码]253- 1[代码],[代码]Number.MAX_SAFE_INTEGER[代码],[代码]9007199254740991[代码]) 和最小([代码]-(253[代码] [代码]- 1)[代码],[代码]Number.MIN_SAFE_INTEGER[代码],[代码]-9007199254740991[代码]) 安全整数范围的。所以只要超过这个范围,就会存在被舍去的精度问题。 当然这个问题并不只是在 Javascript 中才会出现,几乎所有的编程语言都采用了 IEEE-745 浮点数表示法,任何使用二进制浮点数的编程语言都会有这个问题,只不过在很多其他语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。 解决方案上面说了这么多问题和原因,这里给出一些解决方案。 类库通常这种对精度要求高的计算都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算问题。前端也有几个不错的类库: Math.jsMath.js 是专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型 像数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。 官网:http://mathjs.org/ GitHub:https://github.com/josdejong/mathjs decimal.js为 JavaScript 提供十进制类型的任意精度数值。 官网:http://mikemcl.github.io/decimal.js/ GitHub:https://github.com/MikeMcl/decimal.js big.js官网:http://mikemcl.github.io/big.js GitHub:https://github.com/MikeMcl/big.js/ 这几个类库帮我们解决很多这类问题,不过通常我们前端做这类运算通常只用于表现层,应用并不是很多。所以很多时候,一个函数能解决的问题不需要引用一个类库来解决。 下面介绍各个更加简单的解决方案。 整数表示对于整数,我们可以通过用[代码]String[代码]类型的表示来取值或传值,否则会丧失精度。 格式化数字、金额、保留几位小数等如果只是格式化数字、金额、保留几位小数等可以查看这里 https://www.html.cn/archives/7324 浮点数运算toFixed() 方法浮点数运算的解决方案有很多,这里给出一种目前常用的解决方案, 在判断浮点数运算结果前对计算结果进行精度缩小,因为在精度缩小的过程总会自动四舍五入。 toFixed() 方法使用定点表示法来格式化一个数,会对结果进行四舍五入。语法为: JavaScript 代码: numObj.toFixed(digits) 参数 [代码]digits[代码] 表示小数点后数字的个数;介于 0 到 20 (包括)之间,实现环境可能支持更大范围。如果忽略该参数,则默认为 0。 返回一个数值的字符串表现形式,不使用指数记数法,而是在小数点后有 [代码]digits[代码] 位数字。该数值在必要时进行四舍五入,另外在必要时会用 0 来填充小数部分,以便小数部分有指定的位数。 如果数值大于 [代码]1e+21[代码],该方法会简单调用 [代码]Number.prototype.toString()[代码]并返回一个指数记数法格式的字符串。 特别注意:toFixed() 返回一个数值的字符串表现形式。具体可以查看 MDN中的说明,那么我们可以这样解决精度问题: JavaScript 代码: parseFloat((数学表达式).toFixed(digits)); // toFixed() 精度参数须在 0 与20 之间 // 运行 parseFloat((1.0 - 0.9).toFixed(10)) // 结果为 0.1 parseFloat((0.3 / 0.1).toFixed(10)) // 结果为 3 parseFloat((9.7 * 100).toFixed(10)) // 结果为 970 parseFloat((2.22 + 0.1).toFixed(10)) // 结果为 2.32
2020-08-20 - 一文全面了解CRMEB多商户商城系统
打在文档上的字,一遍遍的出现,又一遍遍的删除,内心非常的忐忑和不安。 因为,历时一年半研发周期的CRMEB多商户商城系统,终于决定于七月中旬发布,尽管这款产品凝聚了所有众邦人的心血,尽管我们对每一个细节和流程都做到了严苛,尽管研发部、设计部、测试部都在加班加点的在做最后的测试和功能流程校验,但我们还是怕辜负了大家对CRMEB的信任。因为,我们知道您和我们一样有一个平台梦,我们担忧的是辜负了这梦想,所以我们才会如此的惶恐和不安。CRMEB打通版商城从正式开源到商业版再到CRMEB Pro版,历时四载,十六个季节的更迭。一路走来,是你们见证了CRMEB的成长蜕变,如今多商户系统将要发布,这背后都有大家的一份力量,几年来大家与CRMEB共同成长,携手奋斗,每每看到大家殷切期盼的言语,让我们倍感肩上的责任更重,也让我们做好产品的信心更足,这小小成就的背后是大家长久以来对我们的产品鞭挞和督促的结果,是你们让我们不断的自省和完善,更是得益于大家长久以来无私的鼓励和支持,才让CRMEB发展的更加从容,因为你们的不离不弃,我们才会一路向上,身后有您,让我们倍感温暖。 [图片] 很多跟随CRMEB一起成长的伙伴,或多或少都对多商户商城系统有期待,也常常咨询我们何时发布,甚至更有部分老客户因为时间原因高价购买了别家的产品,在这里我们真诚的向大家道歉,是我们的责任让大家付出了那么高的代价。但专注原创研发和精益求精的原则我们不能打破,不抢时间,不抢客户,为的是打磨出优质的产品回报给大家,探索并解决传统企业转型发展难题的初心不能更改,坚持通过互联网工具,为合作伙伴事业的长远发展贡献力量的理想不能放弃。 长久以来crmeb始终以开放共享的理念助力程序员、个人开发者及传统企业发展,秉承完全开放源代码,包终身升级,进VIP会员群,开论坛会员账户的完善售后体系,帮助大家用最小的付出换取最大的回报,更希望大家能携手CRMEB团队一起奋斗,一起成长,因为看到现在的你们仿佛看到曾经的我们,我们感同身受,我们懂一个人孤军奋斗的不 易,我们更懂你在某个深夜开发某个功能的艰辛,我们也能体会一个老板为企业发展夜不能寐的煎熬。 [图片]crmeb多商户 多商户系统不同于单商户系统,它对程序性能的要求更高,对开发人员的技术要求也更高,对平台的实际运行效率、稳定性、容灾性等各个方面的要求都非常高。因此,在功能上我们对比单商户商城城产品做了减法,一方面是为了确保多商户商城系统的稳定性和安全性,另一方面是后期会根据使用者的需求多少去集成更多的功能,确保每一个功能都是必须,不再让系统功能那么冗余和难以使用。 CRMEB多商户商城系统亮点产品特点: 1、B2B2C商城系统自营+招商多模式运营 多商户的核心是B2B2C,支持企业自营模式,联营模式及商家入驻模式等; 商家可整合行业资源,联系商户入驻商城,实现商城产品和服务类目的多样性,打造综合购物商圈,轻松拥有专属自己的“京东商城” 2、打通分销系统,构建移动社交电商 用户购买礼包成为推广员后,可通过微信邀请好友下单赚取佣金,降低人工营销宣传成本,实现人人分享赚钱的乐趣,促进商城社交化推广 3、多种盈利模式 B2B2C商城平台手续费、自营收入、资金沉淀、广告资源等多种平台盈利模式 4、界面美观的商城UI 优秀的UI设计让商城系统变得有个性有品味,舒适、简单的体验感提高用户对商城平台的认可 5、强大稳定的后台管理系统 后台采用最新的TP框架,系统高效稳定;后台的功能操作简单快捷,企业及个人运营可快速上手使用 产品功能:商户管理:平台可添加及管理商户,设置手续费、商品审核等商品管理:单规格、多规格商品管理,品牌管理、商品分类管理、商品评价订单管理:能够完成用户的订单管理(发货、订单详情、修改订单、订单备注、订单记录、订单退款) 、售后服务 (评论的回复与删除)用户管理:对公众号、小程序、H5的会员进行管理,可通过筛选给用户发送优惠券、消息通知等内容管理:文章管理、用户反馈、素材管理营销管理:优惠券管理分销管理:分销员管理、分销礼包管理、分销配置财务管理:提现管理、充值管理、商户对账应用管理:公众号配置、自动回复、图文管理,小程序订阅消息设置:可对商城的基础性功能进行设置,如移动端界面、支付、文件上传、小票打印等;可设置充值方案、短信、运费物流等;可对商城的后台菜单、管理员身份及权限进行设置维护:可灵活设置组合数据,配置商城数据;可随时对商城数据库进行备份技术特点thinkphp6 + swoole4、高性能、快速打开、表单生成、长链接、异步任务、任务队列、定时任务、前后端分离、uniapp,一套代码多端适配
2020-08-21 - IconFont的高阶用法,提高开发效率。
介绍 适用于各平台的小程序的icon自定义组件,结合iconfont使用,更加方便易扩展。关于iconfont的使用方法参考前面的文章《如何更优雅的使用IconFont你应该知道》 安装 通过npm安装 需要注意的是 package.json 和 node_modules 必须在 miniprogram 目录下 [代码]npm install miniprogram_icon [代码] 构建npm包 打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件 [图片] 使用 引入组件 只需要在app.json或index.json中配置 Icon 对应的路径即可。如果你是通过下载源代码的方式使用 miniprogram_icon,请将路径修改为项目中 miniprogram_icon 所在的目录。 [代码]// 全局引入 // app.json "usingComponents": { "l-icon": "miniprogram_icon/icon/index" } // 单页面引入 // index.json "usingComponents": { "l-icon": "miniprogram_icon/icon/index" } [代码] 使用组件 引入组件后,可以在 wxml 中直接使用组件 [代码]<l-icon name="icon-zuanshi_o"></l-icon> [代码] 代码演示 这里有一份微信小程序的代码片段,下载开发工具打开即可预览,打开代码片段 基础用法 Icon的[代码]name[代码]属性传入图标名称 [代码]// 线性图标 <l-icon name="icon-sousuo_o" /> // 加粗图标 <l-icon name="icon-sousuo" /> [代码] 图片代替 Icon的[代码]src[代码]属性传入图片地址 [代码]// 线性图标 <l-icon src="/assets/icons/shouye_o.svg" /> // 加粗图标 <l-icon src="/assets/icons/shouye.svg" /> [代码] 徽标提示 设置[代码]dot[代码]属性后,会在图标右上角展示一个小红点。设置[代码]info[代码]属性后,会在图标右上角展示相应的徽标 [代码]// 圆点 <l-icon name="icon-tongzhizhongxin_o" dot /> // 数字 <l-icon name="icon-tongzhizhongxin_o" info="1" /> // 自定义数字 <l-icon name="icon-tongzhizhongxin_o" info="99" /> // 超长数字 <l-icon name="icon-tongzhizhongxin_o" info="99+" /> [代码] 图标颜色 设置[代码]color[代码]属性来控制图标颜色,设置[代码]dot-bg[代码]属性来控制圆点颜色 [代码]// 自定义颜色 <l-icon name="icon-zuanshi_o" color="blue" /> // 16进制颜色 <l-icon name="icon-zuanshi_o" color="#20bf64" /> // 圆点颜色 <l-icon name="icon-xiaoxi_o" color="#db524a" dot dot-bg="#fba929" /> // 数字背景色 <l-icon name="icon-xiaoxi_o" info="123" dot-bg="#fba929" /> [代码] 图标大小 设置[代码]size[代码]属性来控制图标大小 [代码]// 支持像素单位px/rpx/em <l-icon name="icon-yinliang_o" size="40rpx" /> <l-icon name="icon-yinliang_o" size="50rpx" /> <l-icon name="icon-yinliang_o" size="60rpx" /> <l-icon name="icon-yinliang_o" size="70rpx" /> [代码] 加载图标 Icon的[代码]loading[代码]属性可旋转图标 [代码]// 好像iconfont的图标有点问题,旋转的时候有点晃动,可用图片来代替 <l-icon name="icon-jiazai_dan_o" loading /> <l-icon name="icon-jiazai_shuang_o" loading /> [代码] API Props 参数 说明 类型 name 图标名称 string src 图片地址 string dot 是否显示图标右上角小红点 boolean dotBg 圆点颜色,或者文字提示背景色 string info 图标右上角文字提示 string/number color 图标颜色 string size 图标大小,如 20px,20rpx,默认单位为rpx string loading 是否使用加载属性 boolean Event 事件名 说明 参数 bind:click 点击图标时触发 - 其他 如何使用其他iconfont图标,可用找到[代码]miniprogram_npm[代码]目录下的[代码]miniprogram_icon[代码]目录找到[代码]icon/iconfont.wxss[代码],替换即可。注意[代码]index.wxss[代码]里的[代码]@import[代码]路径是否正确。
2020-07-26 - 关于云开发的一次性订阅消息
前段时间看到了这位老哥的一篇关于订阅消息的文章:https://developers.weixin.qq.com/community/develop/article/doc/0008802e8381e0eeabb92c9975b013 这篇文章对于程序员来说非常直观的说明了一次性订阅消息的逻辑:订阅1次,可以收到订阅消息一次,订阅10次,可以收到订阅消息10次。 但是我觉得这个方案对于一个普通用户来说,并不够友好,如果我是一个不懂订阅消息的普通用户,我根本不会花时间去点这样一个点1加1的订阅消息。我觉得对于开发者来说,用户能够点一次允许并且勾上不在询问就已经很不错了,剩下的完全可以交给程序来处理。下面是我的方案。让一次性订阅消息达到长期订阅的效果。 首先明确以下逻辑: 通过 wx.getSetting({ withSubscriptions: true }) 的 success 回调 res 可以得出订阅消息的以下5种状态[图片]当用户勾选了“不在询问”之后,不管你后面怎么调 wx.requestSubscribeMessage ,订阅消息的弹窗都是不会弹起的wx.requestSubscribeMessage 需要用户手动点击触发当得到以上几种状态之后,接下来就可以根据需要做自己想要做的操作 如我的小程序首页是一个版本列表 [图片] 我在列表的头部设计了一个跟小程序同风格的授权卡片,这样不会显得突兀同时告诉用户点击授权并且勾选“不在询问”,并告诉用户这样做的目的是什么。 然后根据上面得到的不同状态来显示不同的提示语: 总开关关闭了: [图片] 勾选了“不在询问”并且选项是取消 [图片] 接下来就是实现订阅消息+1的步骤,上面提到了当用户勾选了“不在询问”之后,不管你后面怎么调 wx.requestSubscribeMessage ,订阅消息的弹窗都是不会弹起的 这时在用户点击你应用中必点的操作时,比如知乎微博的点击列表进入详情,或者我这个小程序点击版本列表进入版本详情时就可以根据以上得到的状态来判断:当授权状态是“选择了不在询问并且选项是允许” 时,直接调用 wx.requestSubscribeMessage ,这时 wx.requestSubscribeMessage 的回调必定是success,而且不会出现授权弹窗,自然也就实现了+1效果。 最后把订阅次数+1记录到数据库,推送时推送订阅次数大于0的就ok了 [图片] 这样一个普通用户需要做的操作就只有点击授权-勾选不在询问-允许 这样一个步骤,同时就实现了无形中增加订阅次数的效果,替代让用户手动去点+1增加订阅消息的操作。 另外不用担心这种操作会使用户感觉像垃圾广告一样一直被推送,因为不管是在服务通知页面,还是在设置页面,用户都是可以很轻松的一键关闭通知。 [图片] 然后说下订阅消息的几个特殊情况: 1.当你的账号在开发者工具上面点过允许或取消的时候,wx.getSetting({ withSubscriptions: true }) 的 success 回调结果是这样的 [图片] 手机上的设置界面是这样的 [图片] 回调的itemSettings属性消失了,界面上有订阅消息的开关,但是订阅消息的选项却没了,正常情况应该是这样的 [图片] 这样的 [图片] 2.当用户点了“不在询问并允许”但是又手动通过服务通知页面,或者设置页面关闭了消息通知,这时就算该用户之前已经订阅过了很多次,都会被系统自动清0,这时你的数据库可能存的该用户还有比如5次订阅消息,但是通过cloud.openapi.subscribeMessage.send推送消息的时候,会进catch,errCode是43101。 3.当用户手速过快连续点击了授权按钮触发wx.requestSubscribeMessage时,会进入fail回调,errMsg是 requestSubscribeMessage:fail last call,这个文档是没写的。 最后可以扫码体验一下: [图片]
2020-07-14 - 小程序canvas绘制海报
2020年第一篇文章,年初忙着复习刷题一直没空去写东西,书看的越多感觉越技不如人,始终徘徊在小菜鸡的行列中,最近项目里正好有一个canvas的业务,突然又燃起了我一个UI前端的火种,记下了踩坑和思考🤔。 踩坑💥 问题1:为什么在canvas上画图片模糊? 在canvas上绘制图片/文字的时候,我们设定canvas:375*667的宽高,会发现绘制出来的图片很模糊,感觉像是一张分辨率很差的图片,文字看起来也会有叠影。 [图片] 注意:物理像素是指手机屏幕上显示的最小单元,而设备独立像素(逻辑像素)计算机设备中的一个点,css 中设置的像素指的就是该像素。 原因:在前端开发中我们知道一个属性叫[代码]devicePixelRatio(设备像素比)[代码],该属性决定了在渲染界面时会用几个(通常是2个)物理像素来渲染一个设备独立像素。 举个例,一张100*100像素大小的图片,在retina屏幕下,会用2个像素点去渲染图片的一个像素点,相当于图片放大了一倍,因此图片会变得模糊,这也是1px在retina 屏上变粗的原因。 [图片] 解决: 将canvas-width和canvas-height都放大2倍,在通过style将canvas的显示width,height缩小2 倍. 例如: [代码]<canvas width="320" height="180" style="width:160px;height:90px;"></canvas> [代码] 问题2:如何处理px和rpx的转换? rpx是小程序里特有的尺寸单位,可以根据屏幕的宽度进行自适应,而在iphone6/iphonex上,1rpx等于不同的px。所以很可能会导致在不同手机下,你的canvas展示不一致。 在绘制海报的之前,我们拿到的设计稿一般都是基于iphone6的2倍图。而且从上一个问题的解决,我们知道canvas的大小也是2倍的,所以我们可以直接量取2倍图的设计稿直接绘制canvas,而尺寸需要注意一下rpxtoPx. [代码]/** * * @param {*} rpx * @param {*} int //是否变成整数 factor => 0.5 //iphone6 pixelRatio => 2 像素比 */ toPx(rpx, int) { if (int) { return parseInt(rpx * this.factor * this.pixelRatio) } return rpx * this.factor * this.pixelRatio } [代码] 问题3:关于canvasContext.measureText计算纯数字的时候手机上为0 在小程序中提供[代码]this.ctx.measureText(text).width[代码]去计算文本的长度,但是如果你全[代码]数字[代码] 的话,你会发现该API永远都计算成0.所以,最后采用模拟measureText方法去计算文本长度。 [代码]measureText(text, fontSize = 10) { text = String(text) text = text.split('') let width = 0 text.forEach(function(item) { if (/[a-zA-Z]/.test(item)) { width += 7 } else if (/[0-9]/.test(item)) { width += 5.5 } else if (/\./.test(item)) { width += 2.7 } else if (/-/.test(item)) { width += 3.25 } else if (/[\u4e00-\u9fa5]/.test(item)) { // 中文匹配 width += 10 } else if (/\(|\)/.test(item)) { width += 3.73 } else if (/\s/.test(item)) { width += 2.5 } else if (/%/.test(item)) { width += 8 } else { width += 10 } }) return width * fontSize / 10 } [代码] 问题4:如何保证一行字体的居中展示?多行呢? 字体的如果过长,会超出canvas画布,造成绘制难看,这个时候我们就应该让超出的部分变成[代码]...[代码] 你可以设置一个width并且循环计算计算出文本的宽度,如果超出则利用substring截取后补充[代码]...[代码]即可。 [代码]let fillText='' let width = 350 for (let i = 0; i <= text.length - 1; i++) { // 将文字转为数组,一行文字一个元素 fillText = fillText + text[i] // 判断截断的位置 if (this.measureText(fillText, this.toPx(fontSize, true)) >= width) { if (line === lineNum) { if (i !== text.length - 1) { fillText = fillText.substring(0, fillText.length - 1) + '...' } } if (line <= lineNum) { textArr.push(fillText) } fillText = '' line++ } else { if (line <= lineNum) { if (i === text.length - 1) { textArr.push(fillText) } } } } [代码] 文字剧中展示计算公式: 居中在canvas中可以用(canvas的宽度-文字宽度)/2 + x (x为字体的x轴的推移) [代码]let w = this.measureText(text, this.toPx(fontSize, true)) this.ctx.fillText(text, this.toPx((this.canvas.width - w) / 2 + x), this.toPx(y + (lineHeight || fontSize) * index)) [代码] 问题5:在小程序中如何处理网络图? 关于在小程序里使用网络图片,比如cdn上的图片,是需要down到微信本地进行 LRU 管理,让后续绘制同样图片时,节省下载时间。所以首先需要你在微信小程序的后台配置downloadFile合法域名,其次你可以在canvas绘制之前,最好提前去down图片,等待图片下载好了,再开始绘制,以避免一些绘制失败的问题。 问题6:在 IDE 中可设置 base64 的图片数据进行绘制,但真机上无用? 先把 base64 转成 [代码]Uint8ClampedArray[代码] 格式。然后再通过 [代码]wx.canvasPutImageData(OBJECT, this)[代码] 绘制到画布上,然后把画布导出为图片。 <!–### 问题6:如何画一个圆角图片?–> 问题7:关于wx.canvasToTempFilePath 使用 Canvas 绘图成功后,直接调用该方法生成图片,在IDE上没有问题,但在真机上会出现生成的图片不完整的情况,可以使用一个setTimeout来解决这个问题。 [代码]this.ctx.draw(false, () => { setTimeout(() => { Taro.canvasToTempFilePath({ canvasId: 'canvasid', success: async(res) => { this.props.onSavePoster(res.tempFilePath)//回调事件 // 清空画布 this.ctx.clearRect(0, 0, canvas_width, canvas_height) }, fail: (err) => { console.log(err) } }, this.$scope) }, time) }) [代码] 问题8:关于canvasContext.font fontsize 不能使用小数 如果设置 font 中字体大小部分包含小数,则会导致整个 font 设置无效。 问题9:安卓下字体渲染错位? [图片] 这个问题出现在安卓手机上,ios表现正常。一开始看到这个问题,摸不着头脑,为什么有的正常居中有的却往前了很多。后面发现是安卓下[代码]this.ctx.setTextAlign(textAlign)[代码] 默认是为center,所以导致了错乱,改成left后就正常了。 问题10:绘制一个折线图 [图片] 利用canvas绘制一个简单的折线图,只需要利用[代码]lineTo[代码]和[代码]moveTo[代码]俩个API将点连接即可。利用[代码]createLinearGradient[代码]绘制阴影。 思考💡 思考1:用json配置表生成海报的局限 现在的海报生成只需要按照设计稿去量取尺寸就可以,但是量取的过程还是很繁琐的,在设计稿量不到的地方还需要手动微调一下。 后续还可以做一个web端使用拖拽的方式去完成设计稿的事情,自动生成json应用到小程序的海报上。 思考2:后端生成海报的局限 海报一开始是后端同学生成的,优点是不需要前端绘制时间,也不需要去踩微信API的坑,接口返回拿到url即可展示,但是在后端生成出来的效果不佳,毕竟这种工作更加前端一些。 思考3:前端生成海报的局限 前端生成海报的时候我发现耗时更长,包括图片的下载本地而且还需要给安卓一个特意写一个setTimeout去确保绘制正常。各种兼容性问题、手机的dpr、安卓和ios等不间断彩蛋踩到你头秃~ 哈哈哈哈~ 彩蛋 采用了最新的canvas-2d背景图确无法绘制全部? 在canvas开发的过程中,小程序里一直有一束微光提醒我。 [图片] 我也试了试最新的canvas2d的api,的确同步了web端,写法也更流畅,在开发者工具中看是一切正常,跑在手机上则,只显示宽度的一半在各种机型下测试也是一样。 [图片] 后面改成原始的canvas就又好了。。。具体原因也还没有在微信社区里找到,后续迭代升级的时候再研究阿吧啊吧啊吧。 [图片]
2020-07-09 - 小程序导航栏出现返回首页按钮
目前返回首页按钮出现的条件为(需同时满足): 1. 使用了默认导航栏样式(非 custom) 2. 不是首页或 tabbar 页面(在 app.json 中定义的) 3. 是页面栈最底层页面 如果是开发者自己手写的 tabbar 导致的问题,需要在页面的 onShow 中调用 wx.hideHomeButton() https://developers.weixin.qq.com/miniprogram/dev/api/ui/navigation-bar/wx.hideHomeButton.html手动隐藏返回首页按钮。
2019-09-27 - 关于 swiper 中 数据过多时 滑动卡顿掉帧的解决办法
[图片] 做小程序的时候 使用到了 swiper, 大概15个 <swiper-item> 每个<swiper-item>中是一个展示列表大概100条数据. 发现滑动的时候非常的卡顿掉帧, 苹果手机稍微好点但是也掉帧. 我的解决方式是: 定义2个数组 第一个数组存放所有数据 allList 第二个数组 存放 15个空数组 用于 <swiper-item>循环 list: [[],[],[],[]...] 然后获取当前页 下一页: 当前页+1, 上一页:当前页-1 如果当前页的索引为 0 (说明是第一页) 则上一页为最后一页 = list.length -1 如果当前页的索引为 list.length-1 (说明是最后一页) 则下一页为第一页 = 0 这样判断, 就可以拿到了 上一页 当前页 下一页的索引 然后每次切换页面 动态改变list { list [上一页索引] = allList [上一页索引] list [当前页索引] = allList [当前页索引] list [下一页索引] = allList [下一页索引] 可选: 可以选择清空之前的数据 例如 清空上上页的数据 这样就可以一直维持 只有3个数组中有数据 swiper就不会卡顿了, 也可以保留达到缓存的效果 } 这样就每次渲染其实只有3个数组中有数据 别的 <swiper-item>为空 就会流畅很多 如果表达不清晰 见谅
2020-06-15 - 已解决。小程序获取手机号时,checkSession通过但是获取手机号解密失败
一开始我的处理方式是在页面直接用checkSession,我的session_key是在index.js登录的时候保存到storage,这里check回调的是“success”。 但是把此时storage里面的session_key结合授权按钮的参数去进行解密是失败的,需要在当前的Page再登陆一次才能成功。 不推荐把session_key存放在缓存。所以以上做法直接跳过。 最后参考了一个朋友的做法,在Page onLoad的时候执行一次wx.login(),然后拿到新的session_key,再用此时的新key去解密就通了。或者改为请求解密之前执行一次登录,据说出问题的概率还是很大 结尾补充:最后一种方法还有个问题要考虑,就是最好执行获取手机号之前再checkSession一下(尽管没啥用)。 问题源头,由于这个函数在校验session_key的时候,无论是过期的key还是新的key都是success,所以有了之后一些列的问题,session_key的状态没法把控 [代码]Page({ data: { currentSessionKey: null }, onLoad: function(options) { /* do something*/ const here = this; // 执行登录确保session_key在线 wx.login({ success(res) { if (res.code) { // call()是我自己基于wx.request封装的一个请求函数工具,这里通过后端发送登录请求获得openid const data = call(userLogin, { code: res.code }); data.then(obj => { if (!obj.error) { here.setData({ currentSessionKey: obj.result.session_key }) } }); } }, fail(error) { throw error; } }); }, // 点击按钮获取手机号权限并解析<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" bindtap='doMyAction'>获取手机号</button> getPhoneNumber: function (e) { const { encryptedData, iv } = e.detail; const options = { encryptedData: encryptedData, iv: iv, sessionKey: this.data.currentSessionKey }; here.doGetPhone(options); }, doMyAction: function() { // 还可以做一些事情 }, doGetPhone: function (options) { const { sessionKey, encryptedData, iv } = options; const here = this; // 向服务器请求解密 wx.request({ // 这里是解密用的接口 url: 'https://xxx.com/python/decrypt', method: 'POST', data: { sessionKey: sessionKey, encryptedData: encryptedData, iv: iv }, success(res) { // 最终获取到用户数据,国家代号前缀、不带前缀的手机号。默认是不带前缀 const { countryCode, purePhoneNumber } = res.data; here.pageForward(countryCode, purePhoneNumber); }, fail(error) { console.log(error); here.pageForward(); } }) }, pageForward: function(countryCode, purePhoneNumber) { // 获取成功后我是跳转到另一个页面 wx.navigateTo({ url: `/pages/person/index?phone=${purePhoneNumber}` }) } }) [代码]
2020-09-15 - 「分享」在微信小程序中快速的创建一个滚动卡片
最近做项目遇到的一个功能要求,没在网上找到类似代码,所以自己写了一个,整理分享出来,有需要的拿走。 手指上下滑动,可以滚动这个卡片;点击卡片顶部可以快速的展开或收起卡片。 [图片] 目前许多手机应用都使用了这种设计,使用 ScrollCard 的代码可以在微信小程序中快速的创建一个类似交互的卡片。 ScrollCard 主要使用了微信小程序的 scroll-view 组件,然后结合 CSS 的位置控制,从而实现视觉上的效果,几乎没有用到 Javascript(如果不需要点击卡片顶部快速展开或收起卡片,那么就完全不需要用到 Javascript)。 源码:https://github.com/impony/miniprogram-scrollcard
2020-06-10 - 小程序快速接入 ECharts 图表插件功能
[图片] ECharts 图表组件插件可以让小程序轻松使用 ECharts 图表功能,不用担心接入成本以及占用较大体积的问题,以及较为复杂的接入使用问题。详细接入文档:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx1db9e5ab1149ea03 接入步骤1、插件申请首先要在小程序管理后台的“设置-第三方服务-插件管理”中添加插件。开发者可登录小程序管理后台,通过 [代码]wx1db9e5ab1149ea03[代码] 查找插件并添加。通过申请后,方可在小程序中使用相应的插件 2、引入插件配置在 app.json 中插件如下代码,注意 AppId 为 [代码]wx1db9e5ab1149ea03[代码] { "plugins": { "echarts": { "version": "1.0.2", "provider": "wx1db9e5ab1149ea03" } } } 3、使用方式在使用的页面中的 json 配置文件中,插件如下代码: { "usingComponents": { "chart": "plugin://echarts/chart" } } 在 wxml 中需要展示图表的位置,插入如下代码: <chart chart-class="chart" option="{{ option }}" bindinstance="onInstance" /> 其中 chart-class 为样式类,option 为 ECharts 中的 option 配置对象,bindinstance 会回调 ECharts 实例对象,如果需要操作 ECharts 实例对象可以实现此方法 4、效果展示[图片] 注意事项如果需要兼容低于 2.6.1 的小程序,在更新 option 的时候需要手动 setOption,具体代码如下: // 支持 observers 的版本可以直接修改 option if (echarts.isSupportObservers()) { this.setData({ option, }); } else if (this.instance) { // 在不支持 observers 的小程序版本中需要手动通过 ECharts 实例更新 option 配置 this.instance.setOption(option, true); } 如果你的小程序最低支持版本大于 2.6.1,或者你不会动态改变 option 配置,那么你不需要关系这段代码,也不需要处理 bindinstance 回调。 关于使用中遇到任何问题可以添加微信反馈,添加微信时可以备注 ECharts。 [图片]
2020-06-16 - 【笔记】云开发通过客服消息实现自动回复进群,同时兼容客服小助手
小程序不具备小程序内扫描二维码的能力,因此如果要实现关注公众号或加用户群功能大家一般都利用微信客服功能的自动回复来实现。此时如果自己去实现微信客服自动回复,客服小助手就不能用了,很令人纠结。经过我的研究,借助云开发找到了一个方案,可以实现当用户想获取微信群走自动回复的接口,真正咨询时,直接到客服小助手进行回复。 效果如下 [图片] 原理解析 云开发在做消息推送配置的时候可以配置消息类型,这个时候如果我们只配置一种类型(小程序卡片),那么就只有卡片才会被云函数接管做自动回复,其他消息类型(图片、文字)则正常走小程序客服的通道。 实现步骤 1.小程序端设置按钮属性open-type="contact",用于用户点击时带上定义的卡片跳到客服消息界面。 申请加入 2.新建云端的函数,设置config.json定义权限,如下config.json { "permissions": { "openapi": [ "customerServiceMessage.send", "customerServiceMessage.uploadTempMedia", "customerServiceMessage.setTyping" ] } } 3.写云函数端代码,如下 if (event.Title == "我要进用户群"||event.Title =="关注公众号"||event) { //设置输入状态 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'Typing' }) //从云储存中拉取图片数据 const qunimg = await cloud.downloadFile({ fileID: "cloud://pm-hsfip.706d-pm-hsfip-1259751853/img/qun.png", }) //上传图片素材到媒体库,并获取到图片id const qunmedia = await cloud.openapi.customerServiceMessage.uploadTempMedia({ type: 'image', media: { contentType: 'image/png', value: qunimg.fileContent } }) //向用户发送群二维码 await cloud.openapi.customerServiceMessage.send({ touser: OPENID, msgtype: 'image', image: { mediaId: qunmedia.mediaId, } }) //取消状态设置 cloud.openapi.customerServiceMessage.setTyping({ touser: OPENID, command: 'CancelTyping' }) } 4.设置消息推送,路径如下 云开发-设置-全局设置-云函数接收消息推送 中添加消息类型为miniprogrampage,绑定云函数为新建的云函数。 [图片] 5.微信公众平台绑定客服[图片] 注意事项 如果按照本教程 客服小助手无法收到消息 或 无法自动回复,可以先将以上消息推送配置删除,然后再微信后台绑定客服后,再重新进行消息推送配置。
2020-07-29 - 【笔记】解决用户头像过期无法显示问题
小根据官方规则,用户如果修改了头像,那么一段时间之后,用户原始的头像链接会失效。而因为我们一般用户资料储存的时候只储存了链接,就会造成失效,因此需要把用户头像转换成base64直接存数据库中,这样就不怕失效了。 云开发代码 /** * 插入用户数据 */ function addUserData(openid, userInfo) { if (!userInfo) { console.log('无用户信息,更新失败') } // 将头像图片转换为base64 http.get(userInfo.avatarUrl.replace("https", "http"), function (res) { let chunks = []; //用于保存不断加载的缓冲数据 let size = 0; //保存缓冲数据的总长度 res.on('data', function (chunk) { chunks.push(chunk); //把接受到的数据逐段保存在缓冲区(Buffer size += chunk.length;//累加缓冲数据的长度 }); res.on('end', function () { var data = Buffer.concat(chunks, size);//Buffer.concat将chunks数组中的缓冲数据拼接起来 if (Buffer.isBuffer(data)) { //如果为Buffer转换为base64并赋值给avatarImg var base64Img = 'data:image/png;base64,' + data.toString('base64'); userInfo.avatarImg = base64Img } db.collection('user').doc(openid).set({ data: userInfo }).then(e => { console.log('用户数据更新成功', e) }) }); }); } 小程序端直接渲染 <!-- 直接渲染到页面 page.wxml --> <view style="background-image:url({{detail.avatarImg||detail.avatarUrl}});"></view> 小程序端将图片保存到本地 //如果需要将头像转成图片保存,如cavans绘图场景 page.js const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(src) || []; if (format) { const filePath = `${wx.env.USER_DATA_PATH}/tmp_base64src.${format}`; // console.log(filePath) // const buffer = wx.base64ToArrayBuffer(bodyData); FileSystemManager.writeFile({ filePath, data: bodyData, encoding: 'base64', success() { console.log(filePath) }, fail() { console.log (new Error('ERROR_BASE64SRC_WRITE')); }, }); } 小程序端 已授权用户进入时自动更新 //进入小程序时,自动更新授权用户的信息到云端 app.js onLaunch: function () { this.getUserAuth(); } getUserAuth: function () { wx.getSetting({ success: res => { res.authSetting['scope.userInfo'] && wx.getUserInfo({ success: res => { wx.cloud.callFunction({ name: 'user', data: { userData: res.userInfo, } }) } }) } }) },
2020-07-07 - 小程序使用原生的camera组件拍照压缩上传示例
wxml文件: <view> <!–相机组件–> <camera device-position=’{{status}}’ model=‘normal’ flash=‘off’ style=‘width:{{w}}px;height:{{h}}px’ binderror=‘error’> [代码]<cover-view style="position:absolute" hidden='{{hideTakeButton}}' bindtap='test'> <button type='primary' plain='true'>测试</button> </cover-view> <!--头像框--> <cover-view style="position:absolute;width:100%;height:{{h}}px;z-index:-99" hidden='{{hideMask}}'> <cover-image src='{{config.imgBasePath}}/img/face_mask.png'></cover-image> </cover-view> <!--拍摄按钮--> <cover-view style="position:absolute;bottom:17%;left:{{(w-66)/2}}px;" hidden='{{hideTakeButton}}' bindtap='takePhoto'> <cover-image src='{{config.imgBasePath}}/img/takephoto_take.png'style="width:132rpx;"></cover-image> </cover-view> <!--定格图片--> <cover-view style='position:absolute;width:100%;height:{{h}}px;z-index:-1' hidden='{{hideCoverImage}}'> <cover-image src="{{showPath}}"></cover-image> </cover-view> <!--重拍按钮--> <cover-view class="complete" hidden='{{hideCoverButton}}'> <button class="reset" bindtap='takeAgain'>重拍</button> <button class="submit" bindtap='confirm'>完成</button> </cover-view> [代码] </camera> <!-- <view style=“position:fixed;top:999999999999999999999rpx;”> <canvas style=“width:{{cw}}px;height:{{ch}}px;” canvas-id=‘firstCanvas’> </canvas> </view> --> </view> js相关: /** 页面的初始数据 */ data: { filePath: ‘’, showPath:’’, status: ‘front’, w: app.globalData.winWidth, h: app.globalData.screenHeight, cw: 300, ch: 200, hideTakeButton: false, hideCoverImage: true, hideCoverButton: true, hideMask:false }, 压缩方法: //处理页面绑定事件 takePhoto() { let that = this; const ctx = wx.createCameraContext() ctx.takePhoto({ quality: ‘high’, success: (res) => { let tempPath = res.tempImagePath that.setData({ showPath : tempPath }) let zipedPath = ‘’; //直接压缩开始 wx.compressImage({ src: tempPath, quality:60, success:function®{ zipedPath= r.tempFilePath //console.log(“压缩后:” + zipedPath); that.setData({ filePath: zipedPath, hideTakeButton: true, hideMask:true, hideCoverButton: false, hideCoverImage: false }) } }) //直接压缩结束 }, fail: function() { wx.showToast({ title: ‘照片拍摄失败,请检查摄像头’, icon: ‘none’ }) } }) }, //点击确定才开始上传文件 confirm: function() { let that = this; that.setData({ hideCoverButton: true }); wx.showLoading({ title: ‘图片上传中…’, mask: true }) [代码]//调用文件处理方法 //that.zipImage(); //上传压缩过的文件 let uploadfilePath = that.data.filePath; console.log("获取上传图片:"+uploadfilePath) that.uploadzipBase64(uploadfilePath); [代码] }, //重拍按钮 takeAgain: function() { let that = this; that.setData({ hideTakeButton: false, hideMask:false, hideCoverImage: true, hideCoverButton: true, filePath: ‘’ }); }, //图片压缩后直接上传BASE64格式 zipImage: function() { let that = this; let filePath = that.data.filePath; var uploadFile = ‘’; //文件压缩 //获得原始图片大小 wx.getImageInfo({ src: filePath, success(res) { var originWidth = res.width; var originHeight = res.height; console.log(“原始宽高比” + originWidth + “:” + originHeight); //设置压缩比例,最大尺寸限度 var maxWidth = 1200; var maxHeight = 600; //目标尺寸 var targetWidth = originWidth; var targetHeight = originHeight; //等比例压缩,如果宽度大于高度,则宽度优先,否则高度优先 if (originWidth > maxWidth || originHeight > maxHeight) { if (originWidth / originHeight > maxWidth / maxHeight) { //宽度*原生图片比例=新图片尺寸 targetWidth = maxWidth; targetHeight = Math.round(maxWidth * (originHeight / originWidth)); } else { targetHeight = maxHeight; targetWidth = Math.round(maxHeight * (originWidth / originHeight)); } } //尝试压缩文件,创建canvas var ctx = wx.createCanvasContext(‘firstCanvas’); ctx.clearRect(0, 0, targetWidth, targetHeight); ctx.drawImage(filePath, 0, 0, targetWidth, targetHeight); //canvas压缩后图片处理要写在draw方法的回调里 ctx.draw(false, function callback() { [代码] //更新canvas大小 that.setData({ cw: targetWidth, ch: targetHeight }); //保存图片 setTimeout(function() { wx.canvasToTempFilePath({ canvasId: 'firstCanvas', fileType: 'jpg', success: (res) => { uploadFile = res.tempFilePath; //上传 that.uploadzipBase64(uploadFile); } }, this) }, 500); }); }, fail() { wx.showToast({ title: '获取图片失败', icon: 'none' }) } }) [代码] }, //转BASE64并上传 uploadzipBase64(uploadFile) { let that = this; //通过文件系统管理器重新编码文件 const fs = wx.getFileSystemManager(); fs.readFile({ filePath: uploadFile, encoding: “base64”, success: function(res) { var base64Data = res.data; console.log(“开始上传”) //调用你的ajax请求上传后台 ajaxUpload(); }, fail: function() { wx.showToast({ title: ‘读取图片失败’, icon: ‘none’ }) } }); }, //绑定出错事件 error(e) { wx.showToast({ title: ‘照片拍摄失败,请检查摄像头’, icon: ‘none’ }) } 总结: 以上示例,图片压缩可以通过2种方式: 1.官方的api:wx.compressImage({}) 2.利用canvas进行压缩重绘 转化为BASE64编码方式:通过文件系统管理器,读文件转化 const fs = wx.getFileSystemManager();
2020-03-10 - 只有三行代码的神奇云函数的功能之四:获取电话号码
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之四:获取电话号码: 还是这三行代码,获取用户的电话号码。 wxml: <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" >{{mobile||"获得电话号码"}}</button> js: getPhoneNumber: function (e) { wx.cloud.callFunction({ name: 'login', data: {weRunData: wx.cloud.CloudID(e.detail.cloudID)} }).then(res => { this.setData({ mobile: res.result.weRunData.data.phoneNumber }) }) } 其他功能: 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 神奇功能之三:100%成功获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/00066a967c4e384949f93fe1151413 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, https://developers.weixin.qq.com/community/develop/article/doc/000ea802c00f70894cf9fe72556013 [图片]
2020-12-16 - view内部存在button与input,单击view中的任何位置都会触发button事件?
<view class="i-class i-cell i-input" wx:for="{{telephone}}"> <view class="i-cell-hd i-input-title">电话</view> <input wx:if="{{ item.show }}" type="number" value="{{ item.disnum }}" style="width:30px" bindinput="phone" id="{{ item.id }}"/> <view wx:if="{{ item.show }}">-</view> <input type="number" value="{{ item.phone }}" class="i-input-input i-cell-bd" style="margin-left:10px" placeholder-class="i-input-placeholder" id="{{ item.id }}" bindinput="myphone"/> <button bindtap="phoneadd" class="i-but" id="{{ item.id }}"><cover-image style="width:20px;height:20px" src="{{ item.img }}"/></button> </view> 在一行内放入输入框和button,单击view内任何位置都会触发button事件,在开发者工具上没问题,实机iPhone xr出了问题,求解
2020-05-13 - 小程序搜索优化指南(SEO)
2019年上半年微信发布了基于小程序页面的搜索,为了让我们更好地发现及理解小程序的页面,结合过去一段时间来我们遇到的各种情况,我们强烈建议各位开发者花一些宝贵的时间认真阅读本文:) 爬虫访问小程序内页面时,会携带特定的 user-agent "mpcrawler" 及场景值:1129 1. 小程序里跳转的页面 (url) 可被直接打开。 小程序页面内的跳转url是我们爬虫发现页面的重要来源,且搜索引擎召回的结果页面 (url) 是必须能直接打开,不依赖上下文状态的。特别的:建议页面所需的参数都包含在url 2. 页面跳转优先采用navigator组件。 小程序提供了两种页面路由方式: a.navigator 组件 b. 路由 API,包括 navigateTo / redirectTo / switchTab / navigateBack / reLaunch 建议使用 navigator 组件,若不得不使用API,可在爬虫访问时屏蔽针对点击设置的时间锁或变量锁。 3.清晰简洁的页面参数。 结构清晰、简洁、参数有含义的 querystring 对抓取以及后续的分析都有很大帮助,但是将 JSON 数据作为参数的方式是比较糟糕的实现。 4. 必要的时候才请求用户进行授权、登录、绑定手机号等。 建议在必须的时候才要求用户授权(比如阅读文章可以匿名,而发表评论需要留名)。 5. 我们不收录 web-view 中的任何内容。 我们暂时做不到这一点,长期来看,我们可能也做不到。 6. 利用 sitemap 配置引导爬虫抓取,同时屏蔽无搜索价值的路径。 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html 7. 设置一个清晰的标题和页面缩略图。 页面标题和缩略图对于我们理解页面和提高曝光转化有重要的作用。 通过wx.setNavigationBarTitle或 自定义转发内容onShareAppMessage对页面的标题和缩略图设置,另外也为 video、audio 组件补齐 poster /poster-for-crawler属性。 8. 使用页面路径推送能力 可极大丰富微信可以收录的内容,进而提高小程序内容的曝光机会。请参考: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/search/search.submitPages.html
2020-01-14 - 小程序的视频内容流自动播放
小程序的视频内容流自动播放 啊啊啊,又解决一个问题 0、起因 这个需求产生的起因,是在做内容流(包含文本,图片,视频)的时候,需要如果流里面有视频,则滚动到一定位置时自动播放视频,类似朋友圈、微博等等的自动播放效果。 [图片] 1、第一版尝试 第一版的思路是: 收集当前所有内容流相对于页面头部的高度,做成一个Array 滚动过程中,监听页面滚动事件,当达到某个高度要求,则播放对应的索引视频 这个操作缺点太多了,捡几个主要的说 缺点: 内容流是一个个的组件,获取距离顶部高度不方便,也不太准。并且组件内需要通过事件传播到列表页,在列表页进行高度Array整理、事件监听、切换索引等等(如果有几种列表页,就要写几遍,很麻烦) 监听滚动事件本身就消耗性能,做了节流也不是那么优秀 2、第二版尝试 突然,就发现了[代码]wx.createIntersectionObserver[代码]这个属性,它的作用是:返回[代码]intersectionObserver[代码]对象,用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见(创建一个目标元素,根据目标元素和视窗的相交距离来判断当前页面滚动的情况。通常这个方案也用于页面图片的懒加载)。参考https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.html 怎么解释呢,就是可以理解为,做一个监听,如果当前被监听的元素,进入了你规定的视界或者离开你规定的视界,就触发。 那么,怎么做到监听呢,参考如下代码: [代码]/** 监控视频是否需要播放 */ let {screenWidth, screenHeight} = this.extData.systemInfo //获取屏幕高度 let topBottomPadding = (screenHeight - 80)/2 //取屏幕中间80的高度作为播放触发区域,然后计算上下视窗的高度 topBottomPadding // 80这个高度可以根据UI样式调整,我这边基本两个视频间隔高度在100左右,超过了两个视频之间的间隔,就会冲突,两个视频会同时播放,不建议过大 const videoObserve = wx.createIntersectionObserver() videoObserve.relativeToViewport({bottom: -topBottomPadding, top: -topBottomPadding}) .observe(`#emotion${this.data.randomId}`, (res) => { let {intersectionRatio} = res if(intersectionRatio === 0) { //离开视界,因为视窗占比为0,停止播放 this.setData({ playstart: false }) }else{ //进入视界,开始播放 this.setData({ playstart: true }) } }) [代码] 其中,[代码]observe[代码] 是对应你需要监听的视频(也就是滚动进入视窗的元素) 那么,为什么选择[代码]relativeToViewport[代码]呢,是因为我们需要对它进入某一个视窗进行监听,而不是对进入整个屏幕视窗监听(因为可能整个视窗里会有多个视频)。 以上,就是整个逻辑思路。 最开始用的[代码]relativeTo[代码]监听视频进入某个元素(如[代码].view-port[代码]),但是后来发现每个页面都要写这个元素,太麻烦,并且容易遮盖操作区域 [代码]// 太麻烦,后来舍弃了这个方案 <view class="view-port" style="height: 100rpx; position: fixed; z-index: 1;width: 100%;letf:0;top:50%;transform: translateY(-50%);"></view> [代码]
2019-12-01 - 关于在微信小程序使用WebglCanvas和ThreeJs开发的二三事(二)
书接上文,在解决了canvas后让我们来正式进入ThreeJs部分。这个部分大致分为: ThreeJs的一些基础使用和注意事项gltf模型贴图加载与渲染控制模型的旋转平移一 ThreeJs的一些基础使用和注意事项官方已经出了ThreeJs小程序 WebGL 的适配版本,按照官方的教程操作即可。 https://developers.weixin.qq.com/miniprogram/dev/extended/utils/threejs.html 先安装npm,在项目目录下执行 npm init npm install --save threejs-miniprogram [图片] 后续按照官方教程操作,接着我们来使用ThreeJs来创建一个小球场景。 let THREE = createScopedThreejs(canvas); let renderer, scene, camera; renderer = new THREE.WebGLRenderer(); renderer.setClearColor(0x000000, 1); camera = new THREE.PerspectiveCamera(43, 1, 0.01, 1000); scene = new THREE.Scene(); let geometry = new THREE.SphereGeometry(0.1, 32, 32); let material = new THREE.MeshPhongMaterial({ color: 0xffff00 }); let sphere = new THREE.Mesh(geometry, material); sphere.position.set(0, 0, -1); scene.add(sphere); let directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight.position.set(1, 1, 1); scene.add(directionalLight); let light = new THREE.AmbientLight(0x404040); // soft white light scene.add(light); renderer.render(scene, camera); [图片] [图片][图片] 也许有人会注意到我在创建renderer的时候没有指定canvas,这里不用担心,因为微信里并不支持dom操作,不指定canvas不会生成一个新的canvas,而是会自动获得已创建的canvas。 在第一回最后我提到过最后初始画布大小和调整后的的画布大小差别不宜过大,接下来我们来看看为什么,首先我们将初始大小设为10*10 然后修改为300*300看看效果 [图片][图片] 通过打印ctx发现其drawingBuffer尺寸还是10*10,且在开发工具上这是个只读属性。 最后我们来说说这个渲染效果的问题,首先把渲染的画面在放大点,在真机上你看这个球它又大又圆,在真机上你看着球的边它又毛又乱。 [图片][图片][图片][图片] 这里一般的处理就是开启抗锯齿但是效果不佳,在百度大神的帮助发现setPixelRatio可以较好的解决这个问题。 wx.getSystemInfo({ success: function (res) { renderer.setPixelRatio(res.devicePixelRatio); } }) [图片] 获取系统的devicePixelRatio传递给renderer,在真机上就可以看到较好的抗锯齿效果了。 [图片][图片]
2020-04-30 - 小程序AR识别,三行代码实现Camera数据毫秒级转base64图片
关键词:小程序AR 图片 base64 相机 Camera onCameraFrame Canvas ArrayBuffer Uint8Array Uint8ClampedArray upng-js 核心步骤: 1 相机原始图像数据frame.data,即ArrayBuffer数组,转成Uint8Array数组 2 Uint8Array数组转成Uint8ClampedArray数组 3 wx.canvasPutImageData(Uint8ClampedArray) 详细流程如下: 最近因为项目需求,需要上传base64去做AR识别功能,和大家一起分享讨论下具体的实现方式。 首先说下实现原理,通过Camera的onCameraFrame获取实时帧数据,将实时帧数据添加到Canvas上,然后将Canvas保存为临时图片,再将临时图片转换为base64。 贴上核心实现代码: wxml: js: var nCounter = 0; openCamera: function (res) { var that = this var camera_ctx = wx.createCameraContext() listener = camera_ctx.onCameraFrame((frame) => { // nCounter等于30 是因为一开始相机会有一个对焦的过程,如果一开始获取数据,就是模糊的图片 if (nCounter == 30) { console.log(frame.data instanceof ArrayBuffer, frame.width, frame.height) var data = new Uint8Array(frame.data); var clamped = new Uint8ClampedArray(data); // 实时帧数据添加到Canvas上 wx.canvasPutImageData({ canvasId: 'myCanvas', x: 0, y: 0, width: frame.width, height: frame.height, data: clamped, success(res) { // 转换临时文件 wx.canvasToTempFilePath({ x: 0, y: 0, width: frame.width, height: frame.height, canvasId: 'myCanvas', fileType: 'jpg', destWidth: frame.width, destHeight: frame.height, // 精度修改 quality: 0.8, success(res) { // 临时文件转base64 wx.getFileSystemManager().readFile({ filePath: res.tempFilePath, //选择图片返回的相对路径 encoding: 'base64', //编码格式 success: res => { // 保存base64 that.data.mybase64 = res.data; } }) }, fail(res) { console.log(res); } }, that) } }) } nCounter++ // console.log(nCounter); if (nCounter >= 100) { nCounter = 0 } }) listener.start() } 目前网上有两种转换方式并对比下: 1:upng-js等第三方转码js库,将相机流转换成base64,一般需要1-2s左右 [图片] 2.使用canvas将相机流转变base64,都是使用js或者小程序官方的api进行转换,一般转换时间在1秒以下: [图片] 重点说明下: 如何使用wx.canvasPutImageData()将相机流添加canvas,我们查看该官方api,添加的data类型为:Uint8ClampedArray [图片] 而我们通过onCameraFrame获取的data类型为:ArrayBuffer [图片] 所有两者类型不一致,就需要转换,将ArrayBuffer=>Uint8Array=>Uint8ClampedArray var data = new Uint8Array(frame.data); var clamped = new Uint8ClampedArray(data); 成功的把onCameraFrame获取实时帧数据转换并canvasPutImageData在canvas上,并通过canvasToTempFilePath获取临时文件,如何获取临时文件getFileSystemManager转换为base64,传入云端进行AR识别,就大功告成! 技术分享来自于:北京晞翼科技有限公司 技术作者:le3d618、xiaoz0816 微信商务联系:le3d618
2020-04-30 - 用开发者工具控制开发环境和生产环境
最近开发过程中遇到一个尴尬的情况,发版前调试一下,API地址改到了测试环境,改完OK发版。。。结果尴尬了,测试地址忘了改回来。审核等待前功尽弃。。。。 这种事情对于匆匆忙忙写bug,匆匆忙忙发版的程序猿来说肯定会发生第二次,第三次。遇到问题,解决问题。 开始翻看找小程序的api文档,于是找到了 wx.getSystemInfo 但是这个API只能区分是pc还是移动端并没有其他有用的信息 接下来去开发社区 https://developers.weixin.qq.com/community/develop/article/doc/000ec87cdd8070c3ba89fe00051813 突然看到这篇文章很有道理,虽然是想直接把作者的方法拿来用,但是想到要在运行时环境通过一个try cath判断控制变量心里有点发虚,再想想。 不过作者提到了灵感来自于node,那是不是可以直接写一个node脚本进行预编译呢。想想小程序的开发者工具还是很open的,兴许可以呢。毕竟还提供了命令行调用,可以考虑一下从这里入手(这太麻烦了。。。。) https://developers.weixin.qq.com/miniprogram/dev/devtools/cli.html 又玩弄了一番开发者工具,终于发现了一个有趣的功能,开发者工具竟然提供了自定义处理命令(编译前的钩子) [图片] 在执行相应的操作前钩子可以执行shell命令,那不是说预览和上传之前都可以调用node脚本了吗。 流程 :预览,上传前》执行调用node脚本命令,带上环境参数》node脚本运行 获取命令行参数》根据参数生成不同环境中使用的配置代码》重新写入api配置文件》预览,上传 [图片] api.template.js 用作config文件模板,需要根据开发环境动态生成的变量可以用占位符的形式写在里面 [代码]var WxApiRoot = '%%API_URL%%'; module.exports=WxApiRoot; [代码] env.config.js 用于配置不同开发环境的配置文件 [代码]const envConfig = { DEV: { API_URL: 'https://dev.com', }, PROD: { API_URL: 'https://pro.com', }, }; module.exports = envConfig; [代码] 接下来是node脚本的入口文件: 首先在项目根目录安装: [代码]cnpm install node-notifier --save-dev [代码] 选择安装,目的是为了编译之后提醒开发者配置文件预编译结果 脚本内容: [代码]const fs = require('fs'); const exec = require('child_process').exec; const envConfig = require('./config/env.config'); const _arguments = process.argv.splice(2); const path = require('path'); const [MINIENV = 'DEV'] = _arguments; const notifier = require('node-notifier'); const templatePath = './config/api.template.js'; const configPath = './config/api.js'; const config = envConfig[MINIENV]; //读取配置文件模板 let templateConfig = fs.readFileSync(templatePath, 'utf8'); //拿到那些变量需要动态编译 const configKeys = Object.keys(config); //通过遍历替换的方式替换模板中的占位符 configKeys.forEach((key) => { const regText = `%%${key}%%`; const regObj = new RegExp(regText, 'g'); templateConfig = templateConfig.replace(regObj, config[key]); toast(`编译环境:${MINIENV}--${key}`, config[key]); }); //把替换后的模板字符串写入api文件中 fs.writeFileSync(configPath, templateConfig); /** * @description 警告提示,方便开发者看到编译过后的配置文件的变量信息 * @author songs * @date 2020-04-27 * @param {*} title * @param {*} content */ function toast(title, content) { console.log(path.join(__dirname, './', 'login-bg.png')); notifier.notify({ title: title, message: content, icon: path.join(__dirname, './', 'login-bg.png'), }); } [代码] 这样 每次预览或者上传之前都可以执行一次node脚本了,config文件预编译完成,再上传。虽然还是没有从根本上解决问题,但是如果配上一个上线流程规范,应该可以应付一大半的环境区分问题吧。 预编译用到的文件不需要上传,可以再project.config里设置忽略上传 [代码] "packOptions": { "ignore": [ { "type": "file", "value": "config/api.template.js" }, { "type": "file", "value": "config/env.config.js" }, { "type": "file", "value": "login-bg.png" } ] }, [代码] 结束!!
2020-04-29 - 微信小程序开发-76种动画 animate.css
1、微信小程序动画有自己的方法:官方链接 但需要自己去写动画效果,比较麻烦。 2、本文介绍的是把animate.css这个非常棒的css库引入到小程序内使用。 animate.css包含76种动画,使用非常简单。animate.css官网 : https://daneden.github.io/animate.css/ 3、由于小程序对代码大小限制比较大,所以删除了animate.css中 所有@-webkit-部分css,减少了一半体积 再把文件后缀名改为wxss,以后出来的百度小程序、支付宝小程序、今日头条小程序估计修改对应的后缀名就可以直接使用了。 下载地址:http://nodejs999.com/animate.wxss 4、放到小程序代码中,然后@import到app.wxss文件中。 我项目是把animate.wxss文件放在utils文件夹下。 所以在app.wxss中加入 @import './utils/animate.wxss'; 即可。 就可以像animate.css一样使用了。
2018-11-01